Loading
0

深入解读脏牛Linux本地提权漏洞(CVE-2016-5195)

0x00 概述

该漏洞是Linux的一个本地提权漏洞,发现者是Phil Oester,影响>=2.6.22的所有Linux内核版本,修复时间是2016年10月18号。该漏洞的原因是get_user_page内核函数在处理Copy-on-Write(以下使用COW表示)的过程中,可能产出竞态条件造成COW过程被破坏,导致出现写数据到进程地址空间内只读内存区域的机会。当我们向带有MAP_PRIVATE标记的只读文件映射区域写数据时,会产生一个映射文件的复制(COW),对此区域的任何修改都不会写回原来的文件,如果上述的竞态条件发生,就能成功的写回原来的文件。比如我们修改su或者passwd程序就可以达到root的目的。
0x01 POC分析
POC的地址如下:[https://github.com/dirtycow/dirtycow.github.io/blob/master/dirtyc0w.c], 下面是POC关键部分的伪代码:
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)
首先打开我们需要修改的只读文件并使用MAP\_PRIVATE标记映射文件到内存区域,然后启动两个线程:
其中一个线程向文件映射的内存区域写数据,这时内核采用COW机制。
另一个线程使用带MADV_DONTNEED参数的madvise系统调用将文件映射内存区域释放,达到干扰另一个线程的COW过程,产生竞态条件,当竞态条件发生时就能写入文件成功。
还有一种方法:使用ptrace系统调用的PTRACE_POKETEXT参数来写文件映射的内存区域,参考见[https://github.com/dirtycow/dirtycow.github.io/blob/master/pokemon.c]。
0x02 漏洞原理分析
先附上一份[https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails]中的源代码分析结果:
faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
do_fault
do_cow_fault
alloc_set_pte
maybe_mkwrite(pte_mkdirty(entry), vma)
# Returns with 0 and retry
follow_page_mask
follow_page_pte
(flags & FOLL_WRITE) && !pte_write(pte)

faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
FAULT_FLAG_WRITE && !pte_write
do_wp_page
PageAnon()
reuse_swap_page
wp_page_reuse
maybe_mkwrite
ret = VM_FAULT_WRITE
((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))

# Returns with 0 and retry as a read fault
cond_resched -> different thread will now unmap via madvise
follow_page_mask
!pte_present && pte_none
faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
do_fault
do_read_fault
Copy-on-Write(COW)
当我们用mmap去映射文件到内存区域时使用了MAP\_PRIVATE标记,我们写文件时会写到COW机制产生的内存区域中,原文件不受影响。其中获取用户进程内存页的过程如下:
1. 第一次调用follow_page_mask查找虚拟地址对应的page,带有FOLL_WRITE标记。因为所在page不在内存中,follow_page_mask返回NULL,第一次失败,进入faultin_page,最终进入do_cow_fault分配不带_PAGE_RW标记的匿名内存页,返回值为0。
2. 重新开始循环,第二次调用follow_page_mask,带有FOLL_WRITE标记。由于不满足((flags & FOLL_WRITE) && !pte_write(pte))条件,follow_page_mask返回NULL,第二次失败,进入faultin_page,最终进入do_wp_page函数分配COW页。并在上级函数faultin_page中去掉FOLL_WRITE标记,返回0。
3. 重新开始循环,第三次调用follow_page_mask,不带FOLL_WRITE标记。成功得到page。
以下代码以liux 4.7([https://www.kernel.org/pub/linux/kernel/v4.x/linux-4.7.tar.xz])的源码为例,具体解读一下流程。首先从关键的获取用户进程内存页的函数函数get_user_pages看起,get_user_pages系列函数用于获取用户进程虚拟地址所在的页(struct page),返回的是page数组,该系列函数最终都会调用\__get_user_pages。
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)

分页阅读: 1 2 3 4 5
【声明】:8090安全小组门户(http://www.8090-sec.com)登载此文出于传递更多信息之目的,并不代表本站赞同其观点和对其真实性负责,仅适于网络安全技术爱好者学习研究使用,学习中请遵循国家相关法律法规。如有问题请联系我们:邮箱hack@ddos.kim,我们会在最短的时间内进行处理。