CVE-2016-5195 脏牛漏洞分析
复现环境
利用条件:
Linux kernel >= 2.6.22
镜像条件:
$ uname -r -v
3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014
漏洞复现
添加用户
添加用户
lantern@ubuntu:~$ sudo adduser newtest |
- 输入密码后一路回车
查看当前用户信息, 输入id
, 可以看到当前用户是有 sudo
权限的
lantern@ubuntu:~$ id |
切换到刚刚新建的 newtest
用户, 同样输入 id
查看用户信息, 可知newtest
用户是没有 sudo
权限的
lantern@ubuntu:~$ su newtest |
获得sudo权限
下载 poc
wget https://raw.githubusercontent.com/dirtycow/dirtycow.github.io/master/dirtyc0w.c |
编译运行:
gcc -pthread dirtyc0w.c -o dirtyc0w |
原版 POC 有个很长的循环, 实际上不需要那么长时间, 运行后可以直接终端
可以看到此时newtest
拥有了 sudo 权限, 提权成功
直接修改root密码
要求:
A CVE-2016-5195 vulnerable system. |
编译运行:
$ sudo apt install g++ git |
这份 POC 可以直接修改root
密码为dirtyCowFun
漏洞成因
get_user_page
内核函数在处理Copy-on-Write
(以下使用COW表示)的过程中, 可能产出竞态条件造成 COW 过程被破坏, 导致出现写数据到进程地址空间内只读内存区域的机会。当我们向带有MAP_PRIVATE
标记的只读文件映射区域写数据时, 会产生一个映射文件的复制(COW), 对此区域的任何修改都不会写回原来的文件, 如果上述的竞态条件发生, 就能成功的写回原来的文件。比如我们修改su
或者passwd
程序就可以达到root的目的。
Copy On Write机制
如果有多个调用者同时请求相同的资源(如内存或磁盘上的数据存储), 他们会共同获得相同的指针指向相同的资源, 直到某个调用者试图修改资源的内容 时, 系统才会真正复制一份专用副本(private copy)给该调用者, 而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源, 就不会有副本(private copy)被创建, 因此多个调用者只是读取操作时可以共享同一份资源。
在该漏洞中, 当我们用mmap
去映射文件到内存区域时使用了MAP_PRIVATE
标记, 我们写文件时会写到 COW机制 产生的内存区域中, 原文件不受影响。其中获取用户进程内存页的过程如下:
第一次调用
follow_page_mask
查找虚拟地址对应的page, 带有FOLL_WRITE
标记。因为所在page
不在内存中,follow_page_mask
返回NULL, 第一次失败, 进入faultin_page
, 最终进入do_cow_fault
分配不带_PAGE_RW
标记的匿名
内存页, 返回值为0。重新开始循环, 第二次调用
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。重新开始循环, 第三次调用
follow_page_mask
, 不带FOLL_WRITE
标记, 成功得到page。
源码分析
以Linux 4.7源码为例, 解读流程。
首先从mem_write
函数开始看起
mem_write
mem_write -> mem_rw -> access_remote_vm -> __access_remote_vm
__access_remote_vm源码中的部分实现:
// ... |
其中 get_user_pages_remote 调用链为get_user_pages_remote->__get_user_pages_locked
__get_user_pages_locked 部分代码如下:
... |
由于我们执行的是写操作, 因此会有一个写操作标记FOLL_WRITE
跟入__get_user_pages
函数
__get_user_pages
get_user_pages
系列函数用于获取用户进程虚拟地址所在的页(struct page)
, 返回的时page
数组, 该系列函数最终都会调用__get_user_pages
__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) |
通过follow_page_mask
去查找虚拟地址对应的 page, 如果找不到就进入faultin_page
处理。这里可能会重复几次, 直到找到 page 或发生错误为止。另外由于每次循环会先调用cond_resched()
进行线程调度, 所以才会出现多线程的竞态条件的可能。
接着我们继续看follow_page_mask, 在follow_page_mask
进行了缺页寻找follow_page_pte
follow_page_pte
follow_page_pte部分代码如下:
static struct page *follow_page_pte(struct vm_area_struct *vma, unsigned long address, pmd_t *pmd, unsigned int flags) |
主要有两点
- 由于没有
page
而跳转到no_page
, 最终由no_page_table
返回一个NULL
- 由于没有
写权限(FOLL_WRITE存在)
, 而返回一个NULL
最终返回到__get_user_pages
函数, 由于 page == NULL
, 执行缺页处理faultin_page
faultin_page
faultin_page 部分源码如下:
static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma, unsigned long address, unsigned int *flags, int *nonblocking) |
首先进行一系列的错误标记, 然后进入handle_mm_fault
进行错误处理, 且如果是因为没有写权限导致的错误, 则在错误处理后获得返回标记VM_FAULT_WRITE
而去掉FOLL_WRITE
其调用链为:
handle_mm_fault->__handle_mm_fault->handle_pte_fault->do_fault/do_wp_page
首先我们来看do_fault
函数
do_fault
do_fault 部分源码如下:
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) |
do_fault
函数中有两个重要的处理
- 如果是因为没有可写属性, 则会执行
do_read_fault
- 如果是因为有可写属性,但是是私有页, 则执行
do_cow_fault
, 在内存中分配一个只读的匿名页
接着我们来看do_wp_page
do_wp_page
如果是由于没有写权限, 那么就会执行这个错误处理
do_wp_page部分源码如下:
old_page = vm_normal_page(vma, address, orig_pte); /* 得到之前分配的只读页, 该页是匿名的页 */ |
do_wp_page
会先判断是否真的需要复制当前页, 由于上面分配的页是一个匿名页并且只有当前线程在使用, 所以不用复制, 直接使用即可。执行wp_page_reuse
, 而这个函数调用之后会返回VM_FAULT_WRITE
的标志, 最终返回到faultin_page
去掉FOLL_WRITE
流程如下:
POC 分析
- mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset) : 当磁盘上的文件映射到虚拟内存中, 当 flags 的
MAP_PRIVATE
被置为1时, 对 mmap 得到内存映射进行的写操作会使内核触发COW (Copy-on-Write)
操作, 写的时 COW 后的内存, 不会同步到磁盘的文件中 - madvice(caddr_t addr, size_t len, int advice): 当 advice 为
MADV_DONTNEED
时, 此系统调用相当于通知内核 addr - addr + len 的内存在接下来不再使用, 内核将释放掉这一块内存以节省空间, 相应的页表也会被置空 - /proc/self/mem: 利用写
/proc/self/mem
来改写不具有写权限的虚拟内存。原因时/proc/self/mem
是一个文件, 只要进程对该文件具有写权限, 那就可以随便写这个文件了, 只不过对这个文件进行读写的时候需要执行一遍访问内存地址所需要寻页的流程。因为这个文件指向的是虚拟内存。
正常流程
- 第一次 follow_page_mask(FOLL_WRITE), 因为 page 不在内存中, 进行缺页处理, 在这个过程中执行 COW 机制
- 第二次 follow_page_mask(FOLL_WRITE), 因为 page 没有写权限, 并去掉 FLOLL_WRITE
- 第三次 follow_page_mask(无FOLL_WRITE), 成功
POC流程
第一次 follow_page_mask(FOLL_WRITE), 因为 page 不在内存中, 进行缺页处理, 在这个过程中执行 COW 机制获取 COW 页
第二次 follow_page_mask(FOLL_WRITE), 因为 page 没有写权限, 并去掉 FLOLL_WRITE
此时另一个线程释放掉上一步分配的 COW 页(使用madvice)
第三次 follow_page_mask(无 FOLL_WRITE), 因为 page 不在内存中, 进行缺页处理
第四次 follow_page_mask(无 FOLL_WRITE), 成功返回 page, 但是没有使用 COW 机制, 此时我们对副本的修改会直接影响到源文件
POC 代码分析
POC关键部分伪代码:
Main: |
首先打开我们需要修改的只读文件并使用MAP_PRIVATE
标记映射文件到内存区域, 然后启动两个线程
- 线程1 向文件映射
/proc/self/mem
的内存区域写数据, 这时内核采用 COW 机制 - 线程2 使用带
MADV_DONTNEED
参数的madvise
系统调用将文件映射内存区域释放, 达到干扰另一个线程的 COW 过程, 产生竞态条件, 当竞态条件发生时就能成功写入文件
漏洞修复
现在不再是把FOLL_WRITE标记去掉, 而是添加了一个FOLL_COW标志来表示获取一个COW分配的页。即使是竞态条件破坏了一次完整的获取页的过程, 但是因为FOLL_WRITE标志还在, 所以会重头开始分配一个COW页, 从而保证该过程的完整性。
diff --git a/include/linux/mm.h b/include/linux/mm.h |