在之前的ret2libc过程中,我们通过延迟绑定实现了libc地址的泄露。在调用libc函数时,首先通过plt表和got表绑定到函数的真实地址上。这样,在第二次调用时就可以直接使用,无需再次绑定。
接下来,我们通过一个具体的题目来逐步分析调试过程,首先我们来看一下write函数。
我们在调用write时,首先跳转到write的plt表,然后将一个0x20推送到栈上,这个推送的值称为reloc_arg,也是dl_runtime_resolve的第一个参数。
接着,我们可以看到又推送了一个名为link_map的值,它是dl_runtime_resolve的第二个参数。
我们来看一下link_map里存储的值。
link_map中的第三个值是.dynamic的地址,通过link_map可以找到.dynamic的地址。而.dynamic中存储着.dynstr、.dynsym和.rel.plt的地址,它们分别在.dynamic+0x44、.dynamic+0x4c和.dynamic+0x84的位置。
因此,.rel.plt的真实地址是.rel.plt + reloc_arg,称为ELF32.Rel的指针,即rel。在ida中也可以验证这一点。
接下来,我们可以通过rel找到r_offest(got表)和r_info。
在ida中验证一下r_offest是否是got表。
验证结果是正确的。那么r_info有什么用呢?我们将r_info >> 8得到的值为6,它是.dynsym中的下标。通过这个下标,我们可以得到函数名的偏移。
偏移为0x4c,再加上.dynstr,就可以找到函数名所在的地址。
ida中也是这样的。
通过找到对应的函数名(st_name),在动态链接库中找到该函数的地址,将其赋值给*rel->r_offset,即完成了一次函数的动态链接。而ret2dlresolve则是通过在这之间伪造来进行getshellde。
在32位NO RELRO情况下,我们可以直接修改.dynamic。我们可以使用工具来gethsell,exp在这里。
这里使用pwntools里面的rop模块创建了一个rop对象,rop.raw()可以往rop链里面填充数据,rop.read()可以调用read函数,rop.chain()可以发送完整的shellcode。我们将.dynamic的地址改成我们bss段上的假地址,然后再调用read的第二条plt指令触发dl_runtime_resolve,然后在特定位置给上参数/bin/sh。
对于32位Partial RELRO的类型,pwntools仍然提供了强大的工具来构造payload。
然而,最好还是弄清楚原理再使用工具。善于利用工具可以少走很多弯路,但是有利有弊。好处是可以快速地做出题,并且能节省下很多时间;坏处是只知道这样可以做出来,但是为什么这样做出来的完全不懂。可以在CTFwiki上找到具体的手工构造payload的方法: https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve
标签:游戏攻略