源码调试脏牛运行过程

源码调试脏牛运行过程

调试代码

把POC阉割一下好调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdint.h>

struct stat st;
int f;
void *map;
char *name;

void *procselfmemThread(void *arg);

int main(int argc, char const *argv[])
{
	if(argc < 3)
	{
		(void)fprintf(stderr, "%s\n", "usage: dirtycow target_file new_content");
		return 1;
	}
	pthread_t pth1,pth2;

	f = open(argv[1], O_RDONLY);
	fstat(f, &st);
	name = argv[1];

	map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0);
	printf("mmap %zx\n", (uintptr_t)map);
	getchar();//为了调试
	procselfmemThread(argv[2]);

	close(f);

	return 0;
}

void *procselfmemThread(void *arg)
{
	char *str;
	str = (char *)arg;

	int f = open("/proc/self/mem", O_RDWR);
	int i = 0;
	lseek(f, (uintptr_t)map, SEEK_SET);
	write(f,str,strlen(str));
	i++;
	
	close(f);
}

mem_write函数的调用链

因为当write往/proc/self/mem写的时候,是调用的mem_write函数

image-20211123111919954

1
2
3
4
5
调用链如下
mem_write ->
	mem_rw ->
		access_remote_vm ->
			__access_remote_vm

调试过程

https://elixir.bootlin.com/linux/v4.7/source/mm/memory.c

主要是先关注__access_remote_vm这函数,主要是关注下面这几行代码

1
2
3
4
5
6
7
8
9
			maddr = kmap(page);//将page映射到内核地址
			if (write) { //因为我们是写操作,所以write是1
				copy_to_user_page(vma, page, addr,      //就会调用copy_to_user_page往page里写buf
						  maddr + offset, buf, bytes);
				set_page_dirty_lock(page);//设置为脏页
			} else {
				copy_from_user_page(vma, page, addr,
						    buf, maddr + offset, bytes);
			}

这个page是怎么得到的呢,可以往上看到第3737行,它是调用get_user_pages_remote去获取了一个page

1
		ret = get_user_pages_remote(tsk, mm, addr, 1, write, 1, &page, &vma);

然后我们跟入进去,它只是__get_user_pages_locked的封装

image-20211124150824176

进入到__get_user_pages_locked,首先write是1,会给flags加上一个FOLL_WRITE,然后调用__get_user_pages函数

image-20211124151105153

第一次调用follow_page_mask(原因是缺页)

进入到__get_user_pages,do前面是对VMA虚拟空间的一些操作,不用看,重点看红框的

首先调用了cond_resched函数,调度别的任务,这样就提供了一个竞态条件

然后调用follow_page_mask函数去寻找一个page,如果没有找到page,就进入下面的if判断,调用faultin_page函数进行缺页异常处理

image-20211124151537641

那我们来看看follow_page_mask是怎么找page的,它的功能主要是从一级目录二级目录等等来寻找页表项的过程,可以通过阉割版的POC来动态调试看看

我们在cond_resched下个断点

1
2
3
4
5
pwndbg> b mm/gup.c:573
Breakpoint 1 at 0xffffffff8114db5d: file mm/gup.c, line 573.
pwndbg> c
Continuing.
然后qemu里面回车,就是我们之前getchar的地方

image-20211124165337035

步进,执行完574行,我们再看看page和mmap地址处的值是多少

image-20211124165604021

可以发现,page的值是0,说明没有找到合适的page,而且mmap处的地址不可访问,说明还没有分配到物理地址

我们来看看page的0是怎么返回的,回到follow_page_mask,上面说过这个函数就是去找页表项的,那么前面的函数我们不看,直接去看最后一级页目录是怎么找页表项PTE的

image-20211124175304913

因为刚开始最后一级页表项肯定是空的,所以直接看87行,直接跳转到no_page,会调用no_page_table,然后返回了NULL

image-20211124175816090

image-20211124175932268

然后退回去,page得到是NULL,进入下面if分支,调用faultin_page进行缺页处理

image-20211124180104142

进入到faultin_page函数,之前flags就设置了FOLL_WRITE的标志,所以会给fault_flags加上FAULT_FLAG_WRITE标志,说明这是因为写操作造成的缺页异常

image-20211124180635284

之后会调用handle_mm_fault

image-20211124181410524

可以在378行下断点看看

1
2
pwndbg> b mm/gup.c:378
Breakpoint 2 at 0xffffffff8114dbb2: file mm/gup.c, line 378.

然后执行完这个函数看看,发现可以访问mmap的地址了,里面的值就是ABCDEFG等等,也就是文件里的内容

image-20211124181933516

现在我们就可以看看handle_mm_fault究竟做了什么

image-20211124182456655

前面不用看,然后再看看__handle_mm_fault,这个函数前面的都跟我们分析没什么关系,主要看最后return返回时调用的函数

image-20211124182721436

handle_pte_fault函数,首先先把*pte给了entry,这里entry是为空的,而且vma是私有映射,最后会调用do_fault函数

image-20211124182809069

然后进入到do_fault看看,因为我们flags有FAULT_FLAG_WRITE没有VM_SHARED,注意if里的取反符号,所以最后调用的是do_cow_fault函数做COW操作,所以之前的mmap能访问到数据,就是因为这里做了COW,做了一个副本

image-20211124183209430

第二次调用follow_page_mask(原因是没有写权限,有FOLL_WRITE标志)

发现第二次调用page还是0,但是我们的mmap是有了实际的物理地址,但为什么page还是为0呢

image-20211124183937371

我们重新进入follow_page_mask,然后进入follow_page_pte查找页表项这个函数,第一次我们pte是空的,所以进入的no_page了,第二次我们pte不为空了,而且flags标志有了FOLL_WRITE,但是pte是不可写的,因为我们的文件是只有root才有写权限,所以最后还是返回的NULL

image-20211124184600581

那么又跑回下面这里了

image-20211124185020321

第二次进入faultin_page缺页处理,然后跟上面一样,一路进到handle_pte_fault里,此时的pte是不为空的,所以调用了3378行代码

image-20211124185336226

do_wp_page函数,这个函数总的来说就是判断有没有做COW,如果有的话就调用wp_page_reuse,使用上一步分配好的副本页

image-20211124185738843

wp_page_reuse函数,这个函数最后会返回VM_FAULT_WRITE

image-20211124185917147

最后这个VM_FAULT_WRITE是返回到了faultin_page的378行

image-20211124190120815

然后到411行有个判断,此时我们的ret是VM_FAULT_WRITE,而且vma不具有写权限,所以flags会把FOLL_WRITE去掉,说明以及做好了COW的副本页,可以往副本页中写了

image-20211124190154047

所以在第二次缺页处理的时候把FOLL_WRITE去掉了

第三次调用follow_page_mask

第三次调用的时候,page已经有地址了,这是一个正常的COW的流程

image-20211124190748151

对比

正常的流程:

  • 第一次follow_page_mask(FOLL_WRITE),因为page不在内存中,进行缺页处理
  • 第二次follow_page_mask(FOLL_WRITE),因为page没有写权限,并去掉FOLL_WRITE
  • 第三次follow_page_mask(无FOLL_WRITE),成功返回page地址

脏牛的流程

  • 第一次follow_page_mask(FOLL_WRITE),因为page不在内存中,进行缺页处理
  • 第二次follow_page_mask(FOLL_WRITE),因为page没有写权限,并去掉FOLL_WRITE
  • 另一个线程释放上一步分配的COW页
  • 第三次follow_page_mask(无FOLL_WRITE),因为page不在内存中,进行缺页处理
  • 第四次follow_page_mask(无FOLL_WRITE),成功返回page,但没有使用COW机制

我们来看看脏牛的第三次follow_page_mask之后进行的缺页处理,进入到faultin_page的时候,是没有FOLL_WRITE的,那么fault_flags也不会有FAULT_FLAG_WRITE的标志

image-20211124192722782

最后到do_fault函数中来,因为flags没有FAULT_FLAG_WRITE标志,就调用do_read_fault

image-20211124191742003

系统以为是一个只读异常,就会返回文件的物理地址,以为你不会写操作,没有做COW副本页,但是返回了这个地址后,最后是要到__access_remote_vm这个函数的,最后还是往这个文件写入了数据。

image-20211124193328509