【漏洞分析】11月4日:深入解读脏牛Linux本地提权漏洞(CVE-2016-5195)

阅读量430199

|评论1

|

发布时间 : 2016-11-04 17:10:33

http://p7.qhimg.com/t0126e67c86002ba604.jpg

作者:elemeta

稿费:600RMB(不服你也来投稿啊!)

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

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 <- 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!

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

该函数通过follow_page_mask去查找虚拟地址对应的page,如果找不到就进入faultin_page处理。这里可能会重复几次,直到找到page或发生错误为止。另外由于每次循环会先调用cond_resched()进行线程调度,所以才会出现多线程的竞态条件的可能。

第一次查找页

follow_page_mask

该函数用来通过进程虚拟地址沿着pgd、gud、gmd、pte一路查找page。因为是第一次访问映射的内存区域,此时页表是空的,返回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;
}

faultin_page

该函数完成follow_page_mask找不到page的处理。第一次查找时页还不在内存中,首先设置FAULT_FLAG_WRITE标记,然后沿着handle_mm_fault -> __handle_mm_fault -> handle_pte_fault -> do_fault -> do_cow_fault分配页。

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; /* 标记失败的原因 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)) /* 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)) /* 因为是只读的,所以pte不带_PAGE_RW标记 */
        pte = pte_mkwrite(pte);
    return pte;
}

到这里第一次查询和pagefault处理结束,已经在内存中分配好了,该页是只读的匿名页。

第二次查找

在这次的查找中因为flags带有FOLL_WRITE标记,而page是只读的,此时follow_page_mask返回NULL,进入faultin_page。

struct page *follow_page_mask(...)
{
    return follow_page_pte(vma, address, pmd, flags);
}

static struct page *follow_page_pte(...)
{
    if ((flags & FOLL_WRITE) && !pte_write(pte)) { /* 查找可写的页,但是该页是只读的 */
        pte_unmap_unlock(ptep, ptl);
        return NULL;
    }
}

在处理faultin_page过程中,我们沿着函数调用路径faultin_page -> handle_mm_fault -> __handle_mm_fault -> handle_pte_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) {
                /*
                 * The page is all ours. Move it to
                 * our anon_vma so the rmap code will
                 * not search our parent or siblings.
                 * Protected against the rmap code by
                 * the page lock.
                 */
                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); 带_RW_DIRTY,不带_PAGE_RW 
    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被去掉了,所以这一次的follow_page_mask会成功返回之前分配的page。到这里写时复制过程就算完成了。

madvise(MADV_DONTNEED)

madvise系统调用的作用是给系统对于内存使用的一些建议,MADV_DONTNEED参数告诉系统未来不访问该内存了,内核可以释放内存页了。内核函数madvise_dontneed中会移除指定范围内的用户空间page。

static long madvise_dontneed(struct vm_area_struct *vma,
                 struct vm_area_struct **prev,
                 unsigned long start, unsigned long end)
{
    ...
    zap_page_range(vma, start, end - start, NULL);
    return 0;
}

void zap_page_range(struct vm_area_struct *vma, unsigned long start,
    unsigned long size, struct zap_details *details)
{
    ...
    for ( ; vma && vma->vm_start < end; vma = vma->vm_next)
        unmap_single_vma(&tlb, vma, start, end, details);
    ...
}

产生竞态条件

我们再来梳理一下写时复制的过程中调页的过程:

1. 第一次follow_page_mask(FOLL_WRITE),因为page不在内存中,进行pagefault处理。

2. 第二次follow_page_mask(FOLL_WRITE),因为page没有写权限,并去掉FOLL_WRITE。

3. 第三次follow_page_mask(无FOLL_WRITE),成功。

__get_user_pages函数中每次查找page前会先调用cond_resched()线程调度一下,这样就引入了竞态条件的可能性。在第二次分配COW页成功后,FOLL_WRITE标记已经去掉,如果此时,另一个线程把page释放了,那么第三次由于page不在内存中,又会进行调页处理,由于不带FOLL_WRITE标记,不会进行COW操作,此时get_user_pages得到的page带__PAGE_DIRTY,竞态条件就是这样产生的,流程如下:

1. 第一次follow_page_mask(FOLL_WRITE),page不在内存中,进行pagefault处理。

2. 第二次follow_page_mask(FOLL_WRITE),page没有写权限,并去掉FOLL_WRITE。

3. 另一个线程释放上一步分配的COW页

4. 第三次follow_page_mask(无FOLL_WRITE),page不在内存中,进行pagefault处理。

5. 第四次follow_page_mask(无FOLL_WRITE),成功返回page,但没有使用COW机制。


0x03 漏洞利用

https://github.com/dirtycow/dirtycow.github.io/blob/master/dirtyc0w.c

这个是利用/proc/self/mem来修改只读文件的exploit

https://github.com/dirtycow/dirtycow.github.io/blob/master/pokemon.c

这个是利用ptrace(PTRACE_POKETEXT)来修改只读文件的exploit

https://github.com/timwr/CVE-2016-5195

这个是Andriod系统Root的exploit


0x04 漏洞修复

该漏洞patch的链接:[https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619]。现在不再是把FOLL_WRITE标记去掉,而是添加了一个FOLL_COW标志来表示获取一个COW分配的页。即使是竞态条件破坏了一次完整的获取页的过程,但是因为FOLL_WRITE标志还在,所以会重头开始分配一个COW页,从而保证该过程的完整性。

diff --git a/include/linux/mm.h b/include/linux/mm.h
index e9caec6..ed85879 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -2232,6 +2232,7 @@ static inline struct page *follow_page(struct vm_area_struct *vma,
 #define FOLL_TRIED    0x800    /* a retry, previous pass started an IO */
 #define FOLL_MLOCK    0x1000    /* lock present pages */
 #define FOLL_REMOTE    0x2000    /* we are working on non-current tsk/mm */
+#define FOLL_COW    0x4000    /* internal GUP flag */

 typedef int (*pte_fn_t)(pte_t *pte, pgtable_t token, unsigned long addr,
            void *data);
diff --git a/mm/gup.c b/mm/gup.c
index 96b2b2f..22cc22e 100644
--- a/mm/gup.c
+++ b/mm/gup.c
@@ -60,6 +60,16 @@ static int follow_pfn_pte(struct vm_area_struct *vma, unsigned long address,
    return -EEXIST;
 }

+/*
+ * FOLL_FORCE can write to even unwritable pte's, but only
+ * after we've gone through a COW cycle and they are dirty.
+ */
+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;
 }

0x05 参考资料

https://dirtycow.ninja/

https://github.com/dirtycow

http://bobao.360.cn/learning/detail/3132.html

http://blog.csdn.net/vanbreaker/article/details/7955713

https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619

https://bugzilla.redhat.com/show_bug.cgi?id=1384344

https://access.redhat.com/security/cve/CVE-2016-5195

https://security-tracker.debian.org/tracker/CVE-2016-5195

http://people.canonical.com/~ubuntu-security/cve/2016/CVE-2016-5195.html

https://www.suse.com/security/cve/CVE-2016-5195.html

本文由qy7tt原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/84851

安全客 - 有思想的安全新媒体

分享到:微信
+15赞
收藏
qy7tt
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66