查看原文
其他

用VBoxDbg调试并理解单线程版脏牛(CVE-2016-5195)

OxLucifer 看雪学院 2019-05-26


1、序言


脏牛是前几年比较流行的一个漏洞,网上关于该漏洞的分析也比较多。但是对于像UP主一样的新手而言,始终未能看到漏洞触发的第一现场,比如像OOB/UAF等漏洞可以直接通过调试器观察内存看到漏洞确实可以触发成功。但脏牛却不是这样,主要因为双机调试Linux的时候,KGDB无法对页表(物理内存)进行读写。再加上多线程Race也提升了调试的难度,所以一直没能感觉很好的看到脏牛触发的第一现场到底发生了什么,直到我遇到了VBoxDbg这个调试器。


2、VboxDbg说明


在Windows平台下,使用Windbg可以对物理内存和一些特殊寄存器进行读写。但是在Linux内核调试的时候,就不那么方便,因为(严格来说)KGDB+Linux内核没有实现这样的功能。即便可以通过编写proc伪文件系统模块来对物理内存进行读写,但还是没有Windbg那种方式感觉方便。于是UP主找了各种资料,最后终于发现一款比较好用的调试器VboxDbg。

VBoxDbg是VirtualBox内建的一款调试器,主要用于调试在VirtualBox内运行的Guest主机。VBoxDbg也可以查看Guest机器的物理内存和GDTR/IDTR等信息。


3、单线程版DirtyCow


脏牛能触发的一个必要原因是:双线程竞争执行、对第四级页表中的PTE进行读写操作。

为了更好的凸显脏牛的Race的问题,我把网上的公开POC修改为一个单线程版本的脏牛poc,具体如下:

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdint.h>
 
void *map;
int f;
struct stat st;
char *name;
 
void *worker_write(void *arg)
{
       char *str;
       str=(char*)arg;
       int f=open("/proc/self/mem",O_RDWR);
       int i,c=0;
       lseek(f,(uintptr_t) map,SEEK_SET);
       write(f,str,strlen(str));
       printf("procselfmem %d\n\n", c);
}
int main(int argc,char *argv[])
{
       if (argc<3)
       {
               (void)fprintf(stderr, "%s\n","usage: dirtycowtest target_file new_content");
               return 1;
       }
       f=open(argv[1],O_RDONLY);        //这里由于打开的是root权限文件,所以理论上来说使用普通用户权限是无法对root文件进行修改的
       fstat(f,&st);
       name=argv[1];
       map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
       printf("mmap %zx\n\n",(uintptr_t) map);
       worker_write(argv[2]);        //这里尝试进行了写操作  
       return 0;
}

可以看到,代码中只有一个线程,且该线程仅仅执行了一个write动作。我们等下会用VBoxDbg来模拟第二个线程原本的动作(madvise)


4、调试单线程版DirtyCow


调试的总体思路为:

  1. 使用GDB+KGDB在follow_page_mask函数调用返回后的第一条内核指令、以及faultin_page函数内部调用handle_mm_fault函数返回后的第一条内核指令处分别下内存访问断点;

  2. 使用GDB+KGDB监控用户态调用mmap后返回地址处的内存信息;

  3. 在第二次调用handle_mm_fault后借助VBoxDbg手动修改PTE信息并让程序运行起来,观察结果。

由于Dirty Cow与ASLR/KASLR无关,所以在本实验中为了方便调试已经手动关闭了ASLR

首先直接运行修改过的dirtycowtest(运行结果如图1),可以发现,运行后sensitive.txt文件信息并未发生改变,同时记录下mmap返回地址:0x7ffff7ff7000


图1 直接运行dirtycowtest发现root权限文件未被修改

然后再次运行dirtycowtest程序,同时使用GDB+KGDB观察在__get_user_pages函数内部的执行情况,情况如下(如图2):


图2 第一次调用follow_page_mask后断下

可以看到,当前处于刚刚执行完follow_page_mask的时刻(图2),并且此时由于页面未被调入物理内存中,所以用户态地址0x7ffff7ff7000处于无法访问的状态。且此时执行follow_page_mask的返回结果也为0(表示返回错误,未获取到struct page信息)。接下来继续执行,进入到faultin_page函数内部,在handle_mm_fault函数执行完毕后断下:


图3 第一次调用handle_mm_fault后断下

此时(图3)可以看到,地址0x7ffff7ff7000可以被访问到了(页表中PTE状态为present),并且结果为0x000a363534333231(sensitive.txt的文件内容的二进制形式表示)。然后继续执行:


图4 第二次调用follow_page_mask后断下

当再次执行完follow_page_mask函数时(图4),发现虽然地址0x7ffff7ff7000可以被访问了,但follow_page_mask返回值page依然为0。这是由于foll_flags与页表PTE记录的权限信息不符,因此触发了一个写异常。继续执行:


图5 第二次调用handle_mm_fault后断下

此时(图5)是第二次执行完handle_mm_fault函数,也就是此时内核完成了COW页面的生成过程(但由于是同一进程访问自身内存,所以实际上只有一个物理页面)。然后就需要使用VBoxDbg进行后续工作了:

首先通过已知的用户态地址0x7ffff7ff7000,可以计算出在x86_64环境中,其四级页表的每级偏移分别为:0x7F8、0xFF8、0xDF8、0xFB8。这里我们要使用到VBoxDbg的四个命令,分别为:

 命令

描述

stop 

让VirtualBox彻底暂停,此时再操作GDB+KGDB并不会看到回显

 dpta

 可以解析指定物理PTE信息

 eq

 以8字节为单位长度,修改指定地址内容

 g

 让虚拟机继续运行,相当于stop命令的反操作


结合cr3寄存器信息,和上述已知四个页表的偏移信息,可以计算出位于第四级页表中的PTE信息。然后手动对该PTE清零,即可模拟原本Dirty Cow漏洞exploit中的第二个madvise线程工作,具体操作记录如图6:


图6 使用VBoxDbg修改PTE

随后禁用GDB+KGDB环境中的所有断点继续执行,即可从虚拟机中看到执行结果。对比图1中的结果可以发现,虽然dirtycowtest程序没有发生变化,但是确实完成了对sensitive.txt文件的修改(已修改为111111),结果如图7:


图7 利用dirtycowtest成功使用低权限用户修改root权限文件内容

至此,完成了DirtyCow单线程版本的调试。在进行页表或物理内存调试等工作时,VBoxDbg有着GDB/KGDB无法比拟的优势。VBoxDbg还有很多功能,本文无法一一介绍。总体来说,VBoxDbg是一款可以与GDB/KGDB互补的简单好用的调试工具。


5、参考文档


  1. Dirty Cow(CVE-2016-5195)

  2. VirtualBox User Manual




看雪ID:OxLucifer   

bbs.pediy.com/user-733628



本文由看雪论坛 OxLucifer  原创

转载请注明来自看雪社区







戳原文,看看大家都是怎么说的?

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

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