由MADV_DONTNEED引发的竞态条件, 看完之后直呼过瘾 ,这个漏洞确实牛逼。
贴一份漏洞细节 ,https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails
faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
do_fault <- pte is not present
do_cow_fault <- FAULT_FLAG_WRITE
alloc_set_pte
maybe_mkwrite(pte_mkdirty(entry), vma) <- mark the page dirty
but keep it RO
# Returns with 0 and retry
follow_page_mask
follow_page_pte
(flags & FOLL_WRITE) && !pte_write(pte) <- retry fault
faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
FAULT_FLAG_WRITE && !pte_write
do_wp_page
PageAnon() <- this is CoWed page already
reuse_swap_page <- page is exclusively ours
wp_page_reuse
maybe_mkwrite <- dirty but RO again
ret = VM_FAULT_WRITE
((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) <- we drop FOLL_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 <- pte is not present
do_read_fault <- this is a read fault and we will get pagecache
page!
想要搞明白整个流程是个怎么回事,要围绕一个主题
- COW 机制
COW 实现流程
操作系统为了节约内存,多个进程共用会同一块(共享可读)物理内存,当对内存发生写行为时,内核才分配一块的新的物理内存,并将修改写入到该(私有可写)物理内存,不影响其他进程。
从逻辑上讲, COW 机制由写行为触发 , 当调用写内存时,内核的函数调用如下:
sys_write() => vfs_write() => __vfs_write() => file->f_op->write() => mem_write() => mem_rw() => __access_remote_vm() => get_user_pages_remote() => __get_user_pages_locked() => !重点函数__get_user_pages()
简单理解下这个流程 ,除了接口的一层层的封装,要写一块内存需要索引到内存地址,接口的参数为虚拟地址,需要将其转换为物理地址,再执行写入。
COW 机制就隐藏在 从虚拟地址获取物理地址的流程中, __get_user_pages() 内部。
先进行一些VMA的检查,调用follow_page_mask() https://github.com/torvalds/linux/blob/v4.7/mm/gup.c#L574 查找物理页。
follow_page_mask 第一次调用
返回NULL, 由于该虚拟地址对应的物理页面尚未被分配或加载到内存中。
调用faultin_page => do_cow_fault 分配对应的cow_page 分配cow_page ,并将cow_page 设置为dirty https://github.com/torvalds/linux/blob/v4.7/mm/memory.c#L3092 随后faultin_page返回0 。
通过goto https://github.com/torvalds/linux/blob/v4.7/mm/gup.c#L581 再次调用 follow_page_mask 。
follow_page_mask 第二次调用
对应的pte 为只读https://github.com/torvalds/linux/blob/v4.7/mm/gup.c#L98,再次返回NULL。 调用faultin_page => do_wp_fault 完成物理页的复制 ,faultin_page 返回0 。并移除 FOLL_WRITE
follow_page_mask 第三次调用
由于取消了 FOLL_WRITE 表示 follow_page_mask返回内存。
??
这里有个疑问 一块匿名只读的内存, COW 完成后, 不应该返回的也是 私有只读的内存吗 ,因为may_mkwrite 设置写权限 会判断vma->vm_flags ,此时必定不可写
// 伪代码
pte_t maybe_mkwrite(pte_t pte, struct vm_area_struct *vma) {
if (vma->vm_flags & VM_WRITE) { // 检查 VMA 是否允许写入
// 如果允许,则在 PTE 中设置写权限位
pte = pte_mkwrite(pte);
}
return pte;
}
哦 对应这个情况 https://github.com/torvalds/linux/blob/v4.7/mm/gup.c#L411 去掉了FOLL_WRITE
所以能通过 https://github.com/torvalds/linux/blob/v4.7/mm/gup.c#L103 找到
??
漏洞利用
关键点1 : drop FOLL_WRITE
http://github.com/torvalds/linux/blob/v4.7/mm/gup.c#L411 移除FOLL_WRITE 标志
关键点2:利用madvise_dontneed 导致 !pte_present && pte_none 成立
follow_page_mask 每次retry时 ,调用cond_resched() , 启用调度。
如果在第二次follow_page_mask ,通过 madvise_dontneed page 将不在内存,构成条件
!pte_present && pte_none https://github.com/torvalds/linux/blob/v4.7/mm/gup.c#L87
再次触发faultin_page 由于去掉了FOLL_WRITE https://github.com/torvalds/linux/blob/v4.7/mm/gup.c#L365
导致 https://github.com/torvalds/linux/blob/v4.7/mm/memory.c#L3188 成立
if (!(flags & FAULT_FLAG_WRITE))
return do_read_fault(mm, vma, address, pmd, pgoff, flags,
直接返回了只读内存的page 到 用户空间 , 写入数据将导致dirty ,将修改写入到对应的文件。
https://www.anquanke.com/post/id/84851
https://github.com/dirtycow/dirtycow.github.io/blob/master/dirtyc0w.c
https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails
0 条评论