CVE-2016-5195是一个几乎通杀全版本linux的本地提权的神洞,最近学习了Linux内核的一些调试方法和漏洞利用技术,但到目前为止做过的漏洞还都是比赛中的题目,都是驱动上的漏洞,没有做过实际漏洞的分析,这篇文章作为第一次对实际Linux Kernel漏洞的分析学习。
漏洞复现
为了增加学习的动力和乐趣,下面先对漏洞进行复现,看看它的威力:
我的环境:VirtualBox + Ubuntu 12.04 x86_64
这里的Ubuntu虚拟机是之前介绍VirtualBox调试内核时用到的,自己编译了linux-3.13的内核,因为据说一些发行版已经修复了该漏洞,后面的分析也是基于3.13进行的,不同版本的内核函数名和执行流程可能稍有不同,但原理相同。
先运行两个PoC,dirtyc0w.c和cowroot.c:
1 | $ su |
dirtyc0w.c这个PoC可以实现向任意可读文件写任意内容。
1 | $ gcc -pthread cowroot.c -o cowroot |
cowroot.c可以实现提权。
漏洞分析
CVE-2016-5195是一个内核竞态条件漏洞,影响范围:Linux Kernel > 2.6.22。
学习漏洞需要了解以下知识:
- 写时拷贝(Copy on Write, COW)
- 竞态条件
- 页式内存管理
- 缺页中断处理
该漏洞的成因是get_user_page
内核函数在处理Copy-on-Write
(以下使用COW表示)的过程中,可能发生竞态条件造成COW过程被破坏,导致出现写数据到进程地址空间内只读内存区域的机会。当我们向带有MAP_PRIVATE
标志的只读文件映射区域写数据时,会产生一个映射文件的复制(COW),对此区域的任何修改都不会写回原来的文件,如果上述的竞态条件发生,就能成功的写回原来的文件。比如我们修改su或者passwd程序就可以达到root的目的。
dirtyc0w.c简要分析
前面用到的第一个PoC关键部分伪代码如下:
1 | Main: |
首先打开需要修改的只读文件并使用MAP_PRIVATE
标志映射文件到内存区域,然后启动两个线程:
线程1:向文件映射的内存区域写数据,这时内核采用COW机制。
线程2:使用带MADV_DONTNEED
参数的madvise
系统调用将文件映射内存区域释放,达到干扰线程1的COW过程,产生竞态条件,当竞态条件发生时就能成功写入文件。
漏洞原理分析
PoC代码中有一个写/proc/self/mem
的线程,关键操作是lseek
和write
函数。对应内核中的代码base.c:
1 | static const struct file_operations proc_mem_operations = { |
mem_write函数关键代码流程如下:
1 | mem_write -> mem_rw -> access_remote_vm -> __access_remote_vm |
__access_remote_vm
函数中完成了数据的写操作,将应用层的数据写到目标位置,代码如下:
1 | maddr = kmap(page); |
可以看到,写操作由两步完成:
- 将应用层传进的数据写到目标page中
- 将page设置为脏页
很显然,这个page如何获取是这个漏洞成因的关键。page获取的代码也是在__access_remote_vm
函数中:下面分析一下page获取代码get_user_pages函数的关键代码流程:1
2ret = get_user_pages(tsk, mm, addr, 1,
write, 1, &page, &vma);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15long __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 {
cond_resched(); // 线程调度,产生多线程竞态条件的可能
while (!(page = follow_page_mask(vma, start,
foll_flags, &page_mask))) {
ret = handle_mm_fault(mm, vma, start, fault_flags);
}
} while (nr_pages && start < vma->vm_end);
...
}__get_user_pages
函数中有两个最关键的函数:follow_page_mask和handle_mm_fault函数。follow_page_mask
函数的作用是根据给的应用层地址addr(虚拟地址)去查找该addr映射到的内核内存页,如果查找到就返回该内存页的描述符struct page*
。当应用层地址addr还没有映射到内核内存页时,该函数返回NULL。handle_mm_fault
函数的作用是使应用层地址addr与内核内存页之间建立联系。
PoC中用mmap
去映射文件到内存区域时,使用了MAP_PRIVATE
标志,写文件时会写到COW机制产生的内存区域中,原文件不受影响。其中获取用户进程内存页的过程如下:
- 第一次调用
follow_page_mask
查找虚拟地址对应的page,带有FOLL_WRITE
标志。因为所在page不在内存中,follow_page_mask
返回NULL,会调用handle_mm_fault
进行缺页处理,最终调用__do_fault函数,分配只读匿名内存页并建立映射关系。 - 第二次循环,调用
follow_page_mask
,带有FOLL_WRITE
标志。在权限检查时出错,代码如下。第二次进入handle_mm_fault
函数,最终调用do_wp_page函数分配COW页,并在上级函数中去掉FOLL_WRITE标志。1
2
3
4
5
6struct page *follow_page_mask(...)
{
...
if ((flags & FOLL_WRITE) && !pte_write(pte))
goto unlock;
} - 第三次循环,调用
follow_page_mask
,不带FOLL_WRITE标志,成功得到page,到这里复制过程就算完成了。
产生竞态条件
我们再来梳理一下写时复制的过程中调页的过程:
- 第一次
follow_page_mask
(FOLL_WRITE),因为page不在内存中,进行缺页处理。 - 第二次
follow_page_mask
(FOLL_WRITE),因为page没有写权限,并去掉FOLL_WRITE
。 - 第三次
follow_page_mask
(无FOLL_WRITE),成功。
__get_user_pages
函数中每次查找page前会先调用cond_resched()
线程调度一下,这样就引入了竞态条件的可能性。在第二次分配COW页成功后,FOLL_WRITE
标记已经去掉,如果此时,另一个线程把page释放了,那么第三次由于page不在内存中,又会进行调页处理,由于不带FOLL_WRITE
标记,不会进行COW操作,此时get_user_pages
得到的page带__PAGE_DIRTY
,竞态条件就是这样产生的,流程如下:
- 第一次
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机制。
漏洞修复
该漏洞的patch。现在不再是把FOLL_WRITE
标记去掉,而是添加了一个FOLL_COW
标志来表示获取一个COW分配的页。即使是竞态条件破坏了一次完整的获取页的过程,但是因为FOLL_WRITE
标志还在,所以会重头开始分配一个COW页,从而保证该过程的完整性。
reference
【漏洞分析】11月4日:深入解读脏牛Linux本地提权漏洞(CVE-2016-5195)
【漏洞分析】CVE-2016-5195 Dirtycow: Linux内核提权漏洞分析
庖丁解牛