Return-to-dl-resolve原理及利用
About GOT and PLT
为了了解GOT和PLT,首先要知道关于PIC的知识,Position Independent Code(PIC)是为了是为了重定位动态链接库的symbols,现代操作系统不允许修改代码段,只能修改数据段,而使用了动态链接库后函数地址只有在执行时才能确定,所以程序内调用的库中的函数地址在编译时不知道,所以,编译时将函数调用返回.data段,而包含PIC的程序在运行时需要更改.data段中的GOT和PLT来重定位全局变量。
Global Offset Table,也就是GOT表为每个全局变量保存了入口地址,在调用全局变量时,会直接调用对应GOT表条目中保存的地址,而不调用绝对地址。
Procedural Linkage Table,也就是PLT是过程链接表,为每个全局变量保存了一段代码,第一次调用一个函数会调用形如function@PLT的函数,这就是跳到了函数对应的PLT表开头执行,会解析出函数真正的地址填入GOT表中,以后调用时会从GOT表中取出函数真正的起始地址执行,下面给一张我自己做的调用流程图。

Environment
测试代码:
编译链接:
Something about .dynamic
ELF的.dynamic section里包含了和重定位有关的很多信息,完整的.dynamic段:
GOT表分成两部分.got和.got.plt,前一个保存全局变量引用位置,后一个保存函数引用位置,通常说的GOT指后面一个,下文GOT即代表.got.plt。
GOT表的起始地址:
GOT表的前三项有特殊含义:
第一项是.dynamic段的地址,第二个是link_map的地址,第三个是_dl_runtime_resolve函数的地址,第四项开始就是函数的GOT表了,第一项就是printf条目:
PLTRELSZ指定了.rel.plt大小,RELENT指定每一项大小,PLTREL指定条目类型为REL,JMPREL对应.rel.plt地址,保存了重定位表,保存的是结构体信息:
REL的数据结构为:
r_offset就是对应函数GOT表地址,看看.rel.plt第一项和第二项:
再看:
分别和printf与free对应,0x0804a000处就是printf的GOT表地址。
根据宏定义,由r_info=0x107可以知道ELF32_R_TYPE(r_info)=7,对应于R_386_JUMP_SLOT;其symbol index则为RLF32_R_SYM(r_info)=1
还有一个需要注意的就是字符串表,保存了一些符号表,在重定位时会用到:
查看:
How ELF relocation works
在第一次call 0x8048340 <printf@plt>时会跳到PLT段中,第一句会跳到GOT条目指向的地址:
第一次调用函数时,GOT表中的地址为PLT表的第二句地址:
先push reloc_offset,这里是0,再push link_map,也就是GOT表的第二项,再调用_dl_runtime_resolve函数。
_dl_runtime_resolve根据reloc_offset找到.rel.plt段中的结构体:
r_info为0x107。
然后根据ELF32_R_SYM(r_info)找到.dynsym中对应的结构体:
.dynsym有关的信息为:
其实地址为0x80481cc,每个结构体大小为16bytes,
结构体为:
所以SYMTAB[1]为0x80481cc+16:
然后根据sym->st_name=0x29在.dynstr中,也就是STRTAB找到函数对应的字符串:
根据函数名字找到对应的地址,填入GOT表对应的位置,跳到函数起始地址执行,执行完后,printf对应的GOT表处已经填上了函数真正的地址:
Python Libraries
可借用一些库来生成伪造的符号信息
roputils
32位构造使用方法:
以前被人们使用频率最高的是roputils库,他的ROP里面的子方法dl_resolve_data与dl_resolve_call,可以生成伪造符号信息与call时的reloc偏移
使用方法:
dl_resolve_data有两个参数,base_addr它声明了生成的dl_resolve_data数据的地址,因为我们接下来就要想办法将生成的dl_resolve_data数据写入这个地址。call_name_str是你想要调用函数名的字符串。
dl_resolve_call就是生成劫持地址plt[0],及reloc参数,和call参数一个plt_call_gadget,它的第一个参数是dl_resolve_data数据的地址,第二个参数是你想调用函数的参数的地址。
缺陷:roputils库没能很好的设置reloc->r_info,这会使得ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff偏大,在有些情况下会导致version = &l->l_versions[ndx]出现访存错误,下面介绍的pwndbg库解决了这个问题,它伪造的reloc->r_info,使得ndx为0,即vernum[reloc->r_info]为0。
64位构造使用方法:
由于64位程序的利用需要往link_map+0x1c8的位置写0,所以直接避免了了32位程序用roputils库构造的r_info缺陷。
构造的话和之前的脚本代码一样,roputils.ROP方法自动识别程序架构,唯一不同的是dl_resolve_call函数的参数只有一个,没有调用函数的参数,因为调用函数的参数需要你用寄存器传递。
早期有人说这种利用方法很鸡肋:“使得l->l_info[VERSYMIDX (DT_VERSYM)] != NULL这句话不成立来绕过该段代码,即使得l->l_info[VERSYMIDX (DT_VERSYM)]等于NULL,即使得(link_map + 0x1c8) 处为 NULL。这就使问题变成了往link_map写空值,由于link_map在ld.so中,还需要通过got表泄露其地址。因此实现64位的上述方法的ret2dl_resolve,需要泄露与地址写两个漏洞,如果有这两个漏洞我们应该可以使用更轻松的方法来get shell,因此价值不大。”
但是那些人忘了,在完全很冷门glibc的远程环境下,你能泄露地址你也不一定得出一些有用信息,你也猜不出函数偏移,所以我们只能利用这一种方式来getshell。
pwn_debug
这是raycp师傅根据pwntools写的一个库(此处膜拜),它不仅提供了32位完美的dl_resolve数据伪造,还提供了64位程序ret2dl_resolve攻击的link_map伪造。
32位构造使用方法:
下面解释一下,这个库好像必须调用run方法才能使用ret2dl_resolve().....这里先不管能用就行......
dl=pdbg.ret2dl_resolve()是初始化一个对象,可以去研究一下源码,功能是识别binary的架构。
dl.build_normal_resolve( base_addr , call_name_str , resolve_target)关键是这串代码,第一个参数是一个base地址,build_normal_resolve方法根据这个地址微调,这是为了构造ndx=0,寻找正确的symbol_index,最后返回一个被修正后的correct_addr。它的返回值有三个,后两个都是构造好的数据。
我们最后使用的时候,要将得到的resolve_data数据写入到被修正后的correct_addr地址中,它返回的resovle_call是一个plt_call_gadget 但他不同于roputils生成的,因为他没有把参数封装进去,我们需要自己设置call的参数。
另外build_normal_resolve方法的第三个参数resolve_target是程序成功执行dl-resolve执行完我们想要call的函数后,这个地址会被写入 被执行函数的真实地址(虽然普通pwn下没什么用...)。
可见使用这个库一切都变得controllable
64位构造使用方法:
在这之前先说说64位下的dl_runtime_resolve利用,因为64位程序bss段的位置离符号信息段较远,构造的数据一般都是在bss段,如0x601000-0x602000,导致其相对于.dynsym的地址0x400000-0x401000很大,使得reloc->r_info也很大,最后使得访问ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;时程序访存出错,导致程序崩溃,且不可避免。
可以看到该段代码还有一个条件if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0),我们是否可构造sym->st_other使它不为空,从而绕过该段代码,我们看假设sym->st_other使它不为空,dl_fixup的代码流程:
可以看到当sym->st_other不为0时,会调用DL_FIXUP_MAKE_VALUE,根据代码的注释,该代码认为这个符号已经解析过,直接调用DL_FIXUP_MAKE_VALUE函数赋值。DL_FIXUP_MAKE_VALUE函数的定义如下,直接将l->l_addr + sym->st_value赋值给value:#define DL_FIXUP_MAKE_VALUE(map, addr) (addr)
也可以看到sym等都是从link_map中取出来的,如果我们将控制的目标不设定为reloc_arg,而是伪造第一个参数link_map。如果我们可以控制sym->st_value指向got表中的地址如libc_start_main的got,而l->l_addr为目标地址如system到libc_start_main的偏移,则最终得到的value会是l->l_addr + sym->st_value即system地址,从而实现无需leak地址的利用,也可执行libc中的任意gadgets。
所以在利用中我们控制的不再是reloc_arg,而是struct link_map *l,假设我们可以覆盖got+4,即link_map的值,指向我们可控的目标。
下面就来说说利用的脚本了:
首先我们需要知道远程服务器glibc版本,没有的话也可以猜猜碰运气,然后算出我们想要执行的gadget距离__libc_start_main的偏移,build_link_map方法的参数意思也很明显了,fake_addr是我们要基于这个位置生成fake_link_map数据,reloc_index是我们之后执行plt_call_gadget对应的reloc
最后利用的时候,我们要将生成的fake_link_map数据写入fake_addr地址处,还将要程序link_map_got表(got+4)中的link_map地址写成fake_addr地址,最后跳去plt表的push reloc指令地址处执行,之前要构造好参数,最后进入dl_runtime_resolve函数,根据link_map解析到调用函数,getshell.
把pwn_debug里面的fake_link_map功能函数抠出来,可单独使用,脚本如下:
Related Links
如何在32位系统中使用ROP+Return-to-dl来绕过ASLR+DEP
Last updated
Was this helpful?