Linux内核提权——Dirty Copy-on-Write

之前针对Linux提权学习过利用SUID权限进行提权的方法,现在又看到了一种可以利用Linux内核竟态条件去写入文件导致只读文件可以写入的脏牛(Dirty COW)漏洞,学习一下基础原理。

简介

为什么这个漏洞叫脏牛(Dirty COW)漏洞

Linux内核的内存子系统在处理写时拷贝(Copy-on-Write)时存在条件竞争漏洞,导致可以破坏私有只读内存映射。

一个低权限的本地用户能够利用此漏洞获取其他只读内存映射的写权限,有可能进一步导致提权漏洞

漏洞危害

  • 非特权本地用户可利用此缺陷获得对其他只读内存映射的写入访问权限,从而增加其在系统上的权限。
  • 此缺陷允许具有本地系统帐户的攻击者修改磁盘上的二进制文件,从而绕过标准权限机制,该机制可在没有适当权限集的情况下防止修改。

影响范围

Linux内核>=2.6.22(2007年发行)开始就受影响了,直到2016年10月18日才修复。Linux内核版本历史Linux kernel stable tree。影响范围非常大,至今可能有一些系统还是使用较低版本内核。

成因分析

__get_user_pages()的主要逻辑

这个函数能够获取用户进程调用的虚拟地址之后的物理地址,调用者需要声明它想要执行的具体操作(例如写/锁等操作),所以内存管理可以准备相对应的内存页。具体来说,也就是当进行写入私有映射的内存页时,会经过一个COW(写时拷贝)的过程,即复制只读页生成一个带有写权限的新页,原始页可能是私有保护不可写的,但它可以被其他进程映射使用。用户也可以在COW后的新页中修改内容之后重新写入到磁盘中。  

源代码地址:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/snapshot/linux-4.1.14.tar.gz

mm/gup.c line 416

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
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 )
{
//...
do {
struct page *page;
unsigned int foll_flags = gup_flags;
// ...
/* first iteration or cross vma bound */
//
vma = find_extend_vma(mm,start);
// ...

retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (unlikely(fatal_signal_pending(current)))
return i ? i : -ERESTARTSYS;
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &page_mask);
if (!page) {
int ret;
ret = faultin_page(tsk, vma, start, &foll_flags,
nonblocking);
switch (ret) {
case 0:
goto retry;
case -EBUSY:
ret = 0;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
goto out;
case -ENOENT:
goto next_page;
}
}

关键函数为follow_page_mask和faultin_page:

  • follow_page_mask读取页表来获取指定地址的物理页(同时通过PTE允许)或获取不满足需求的请求内容。在follow_page_mask操作中会获取PTE的spinlock,用来保护试图获取内容的物理页不会被释放掉。

  • faultin_page函数申请内存管理的权限(同样有PTE的spinlock保护)来处理目标地址中的错误信息。在成功调用faultin_page后,锁会自动释放,从而保证follow_page_mask能够成功进行下一次尝试。

 

在对read_ONLY的COW页进行写操作时,正常流程是:

  1. follow_page_mask缺页,进页错误处理函数faultin_page,挂载页到物理内存
  2. 再次调用follow_page_mask,发现是一个READ_ONLY的PRIVATE,再次发生页错误,因此faultin_page需要去除FOLL_WRITE标签来让副本可写
  3. 再次调用follow_page_mask,正常返回页

首先缺页,follow_page_mask和faultin_page返回0,同时faultin_page内:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
* necessary, even if maybe_mkwrite decided not to set pte_write. We
* can thus safely do subsequent page lookups as if they were reads.
* But only do so when looping for pte_write is futile: in some cases
* userspace may also be wanting to write to the gotten user page,
* which a read fault here might prevent (a readonly page might get
* reCOWed by userspace write).
*/
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags |= FOLL_COW; //漏洞修复后
//漏洞修复前 *flags &= ~FOLL_WRITE;
return 0;

上面这个判断语句想要表示的是,如果当前VMA中的标志显示当前页不可写,但是用户又执行了页的写操作,那么内核会执行COW操作,并且在处理中会有VM_FAULT_WRITE标志。换句话说在执行了COW操作后,上面的if判断为真,这时就移除了FOLL_WRITE标志。

一般情况下在COW操作后移除FOLL_WRITE标志是没有问题的,因为这时VMA指向的页是刚经过写时拷贝复制的新页,我们是有写权限的,后续不进行写权限检查并不会有问题。

但是,考虑这样一种情况,如果在这个时候用户通过madvise(MADV_DONTNEED)(这页内存之后很可能不再需要,操作系统会适时释放该页,但若后续出现了其他的线程,进程对该页的访问,该访问仍然成功,后续再次请求该页时会从底层映射文件的最新内容(对于共享文件映射、共享匿名映射和基于shmem的技术,如System V共享内存段)重新填充内存内容)将刚刚申请的新页丢弃掉,那这时本来在faultin_page后应该成功的follow_page_mask会再次失败,又会进入faultin_page的逻辑,但是这个时候已经没有FOLL_WRITE的权限检查了,只会检查可读性。这时内核就会将只读页面直接映射到我们的进程空间里,这时VMA指向的页不再是通过COW获得的页,而是文件的原始页,这就获得了任意写文件的能力。  

PoCs

PoCs · dirtycow/dirtycow.github.io Wiki · GitHub


Linux内核提权——Dirty Copy-on-Write
https://chujian521.github.io/blog/2022/12/15/Linux内核提权——Dirty-Copy-on-Write/
作者
Encounter
发布于
2022年12月15日
许可协议