深入解读补丁分析发现的linux内核提权漏洞(CVE-2017–1000405)

 作者:rain
预估稿费:400
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

 

1. 前言

近日(2017.11.30)国外安全团队Bindecy爆出名为大脏牛(Huge Dirty Cow)的内核提权漏洞,编号为CVE-2017–1000405。包含linux内核(2.6.38~4.14)的服务器,桌面,移动等众多设备将面临严重挑战,数以百万计的用户受到安全威胁。该漏洞是由于对之前的内核提权漏洞(cve-2016-5195)修补不完整引发,因此为了便于理解该漏洞,我们对CVE-2016-5195做一个回顾。

2. Dirty Cow(CVE-2016-5195)回顾

该漏洞是内核函数get_user_pages(mm/gup.c)在处理COW(copy-on-write)过程中,由条件竞争引发未授权进程向只读内存区域写入数据导致的。

2.1 主要数据结构说明

get_user_pages被用来通过虚拟地址查询页面获取物理页面,主要代码如框2.1所示:

框2.1:__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)
{
    /* ... snip ... */

    do {
        /* ... snip ... */
retry:
        cond_resched(); /* please rescheule me!!! */
        page = follow_page_mask(vma, start, foll_flags, &page_mask);
        if (!page) {
            int ret;
            ret = faultin_page(tsk, vma, start, &foll_flags,
                    nonblocking);
            switch (ret) {
            case 0:
                goto retry;
            case -EFAULT:
            case -ENOMEM:
            case -EHWPOISON:
                return i ? i : ret;
            case -EBUSY:
                return i;
            case -ENOENT:
                goto next_page;
            }
            BUG();
        } 
        if (pages) {
            pages[i] = page;
            flush_anon_page(vma, page, start);
            flush_dcache_page(page);
            page_mask = 0;
        }
        /* ... snip ... */
    }
    /* ... snip ... */
}

其中重要的数据结构说明如下:

  • con_resched函数作用是主动放权,等待下一次被调度。
  • follow_page_mask函数的作用是查询页表获取虚拟地址对应的物理页,它将按照linux页表的四级结构(如图1)所示依次向下调用四层函数(follow_page_mask/follow_p4d_mask/follow_pud_mask/follow_pmd_mask/follow_page_pte)进行解析,特别需要注意是:(a)页表中不存在物理页即缺页,(b)访问语义标志foll_flags对应的权限违反内存页的权限时,follow_page_mask返回值为NULL,会触发对faultin_page的调用。

图2.1

  • faultin_page函数的作用是处理页故障(page fault),分析它的函数体会发现,实际上是调用handle_mm_fault按照foll_flags进行处理。(a)当发生缺页时,它会从磁盘中调入页面,(b)当想要以只读权限获取可写页面时,会发生COW,即复制原来只读(read_only)内存页,并将新的内存页标记为只读(read_only),私有(private)和脏的(dirty),整个过程如框2所示:

框2.2 page_fault处理COW结构:

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
  • 访问语义foll_flags的几个主要相关标志说明:
  • FOLL_WRITE:请求可写pte(页表项)。
  • FOLL_FORCE:请求读写权限的pte(页表项)。

框2.3:faultin_page:

 

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;

    unsigned int fault_flags = 0;

    int ret;

    /* mlock all present pages, but do not fault in new pages */

    if ((*flags & (FOLL_POPULATE | FOLL_MLOCK)) == FOLL_MLOCK)

        return -ENOENT;

    /* For mm_populate(), just skip the stack guard page. */

    if ((*flags & FOLL_POPULATE) &&

            (stack_guard_page_start(vma, address) ||

             stack_guard_page_end(vma, address + PAGE_SIZE)))

        return -ENOENT;

    if (*flags & FOLL_WRITE)

        fault_flags |= FAULT_FLAG_WRITE;

    if (*flags & FOLL_REMOTE)

        fault_flags |= FAULT_FLAG_REMOTE;

    if (nonblocking)

        fault_flags |= FAULT_FLAG_ALLOW_RETRY;

    if (*flags & FOLL_NOWAIT)

        fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT;

    if (*flags & FOLL_TRIED) {

        VM_WARN_ON_ONCE(fault_flags & FAULT_FLAG_ALLOW_RETRY);

        fault_flags |= FAULT_FLAG_TRIED;

    }

    ret = handle_mm_fault(mm, vma, address, fault_flags);

    if (ret & VM_FAULT_ERROR) {

        if (ret & VM_FAULT_OOM)

            return -ENOMEM;

        if (ret & (VM_FAULT_HWPOISON | VM_FAULT_HWPOISON_LARGE))

            return *flags & FOLL_HWPOISON ? -EHWPOISON : -EFAULT;

        if (ret & (VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV))

            return -EFAULT;

        BUG();

    }

    if (tsk) {

        if (ret & VM_FAULT_MAJOR)

            tsk->maj_flt++;

        else

            tsk->min_flt++;

    }

    if (ret & VM_FAULT_RETRY) {

        if (nonblocking)

            *nonblocking = 0;

        return -EBUSY;

    }

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

        *flags &= ~FOLL_WRITE;

    return 0;

}

其中handle_mm_fault的返回值ret代表了具体的处理情形,VM_FAULT_WRITE表示发生了COW,标红的为漏洞相关代码。

 2.2 COW正常流程

(a)调用follow_page_mask请求获取可写(FOLL_WRITE)内存页,发生缺页中断,返回值为NULL,调用faultin_page从磁盘中调入内存页,返回值为0。

(b)随着goto entry再次调用follow_page_mask,请求可写(FOLL_WRITE)内存页,由于内存页没有可写权限,返回值为NULL,调用fault_page复制只读内存页并去掉FOLL_WRITE标志(框2.3红色代码),返回值为0。

(c)随着goto entry再次调用follow_page_mask,请求获取虚拟地址对应内存页(无FOLL_WRITE),返回page。

 2.3 竞争条件引发的漏洞异常流程

(a)调用follow_page_mask请求获取可写(FOLL_WRITE)内存页,发生缺页中断,返回值为NULL,调用faultin_page从磁盘中调入内存页,返回值为0。

(b)随着goto entry再次调用follow_page_mask,请求可写(FOLL_WRITE)内存页,由于内存页没有可写权限,返回值为NULL,调用fault_page复制只读内存页并去掉FOLL_WRITE标志(框2.3红色代码),返回值为0。

(a)(b)与正常流程一致

(c)随着goto entry 再次调用follow由于cond_resched会主动放权,引起系统调度其他程序,另一个程序使用madvise(MADV_DONTNEED)换出内存页。

madvise的作用是给系统对于内存的使用一些建议,MADV_DONTNEED告诉系统换出对应内存页。

(d)程序再次被调度执行,调用follow_page_mask请求获取可写(FOLL_WRITE)内存页,发生缺页中断,返回值为NULL,调用faultin_page从磁盘中调入内存页,返回值为0。

(e)随着goto entry再次调用follow_page_mask,请求获取虚拟地址对应内存页(无FOLL_WRITE),返回page。

(f)后续进行写入操作,当内存数据同步到磁盘时,只读文件被改写(触发漏洞)。

 2.4 POC描述

   POC链接如下:

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

主体思路为:

(a)启动procselfmemThread线程负责写入数据。

(b)启动madviseThread线程负责利用madvise换出内存页。

主要流程如图2.2所示:

图2.2

 2.5 补丁分析

    修补代码如框2.4所示,该补丁增加了FOLL_COW去表示COW访问语义,以此来替代移去FOLL_WRITE。增加follow_pfn_pte检测是否请求可写页面/是否发生了COW/是否虚拟内存页项被标记为脏的来判断内存页是否可写。

如框2.4:修补代码:

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

3. HugeDirtyCow(CVE-2017-1000405)

3.1 相关基础知识补充

3.1.1 透明大内存页(THP:Transparent Huge Pages)

通常linux的内存页为4kb大小,为了满足系统和程序的特殊需求,linux允许2M和1G大内存页。常规的虚拟地址翻译如图3.1所示,PGD,PMD均作为页表目录,当启用大内存页时,PMD不再表示页表目录而是和PTE合并共同代表页表项(PTE)。

图3.1

THP内存页主要被用于匿名anonymous,shmem,tmpfs三种内存映射,即:

  • anonymous:通过mmap映射到内存是一个匿名文件及不对应任何实际磁盘文件。
  • shmem:共享内存。
  • tmpfs:是一种虚拟文件系统,存在于内存,因此访问速度会很快,当使用tmpfs类型挂载文件系统释放,它会自动创建。。

THP内存页查看:

cat sudo tee /sys/kernel/mm/transparent_hugepage/enabled

若显示alwasy则表示已开启,否则使用如下命令开启:

echo always > /sys/kernel/mm/transparent_hugepage/enabled

3.1.2 零页(zero page)

当程序申请匿名内存页时,linux系统为了节省时间和空间并不会真的申请一块物理内存,而是将所有申请统一映射到一块预先申请好值为零的物理内存页,当程序发生写入操作时,才会真正申请内存页,这一块预先申请好值为零的页即为零页(zero page),且零页是只读的

  • 零页攻击面

由于零页是只读的,如果共享零页的进程A非法写入了特定的值,其他共享零页的进程例如B读入被A篡改的零页的值,那么后续以此为基础的访存等操作就会发生异常,造成进程B crash,形成漏洞。

3.2 CVE-2016-5195修补不完整阐述

对于CVE-2016-5195对于THP内存页修补如下:

框3.1 修补代码:

@@ -783,6 +783,12 @@ struct page *follow_devmap_pmd(struct vm_area_struct *vma, unsigned long addr,
  
  	assert_spin_locked(pmd_lockptr(mm, pmd));
  
 +	/*
 +	 * When we COW a devmap PMD entry, we split it into PTEs, so we should
 +	 * not be in this function with `flags & FOLL_COW` set.
 +	 */
 +	WARN_ONCE(flags & FOLL_COW, "mm: In follow_devmap_pmd with FOLL_COW set");
 +
  	if (flags & FOLL_WRITE && !pmd_write(*pmd))
  		return NULL;
  
 @@ -1128,6 +1134,16 @@ int do_huge_pmd_wp_page(struct vm_fault *vmf, pmd_t orig_pmd)
  	return ret;
  }
  
 +/*
 + * FOLL_FORCE can write to even unwritable pmd's, but only
 + * after we've gone through a COW cycle and they are dirty.
 + */
 +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;

从框3.1的修补代码可以看出,THP内存页修补和普通内存页的修补操作基本一致,但是对于有一点疏忽的是:对于大内存页来说,不经历COW也可以将内存页标记为脏的。

每次调用内核函数get_user_pages获取可读THP内存页时即会调用follow_page_mask,而follow_page_mask会调用touch_pmd,调用路径为:

follow_page_mask/follow_trans_huge_pmd/touch_pmd

touch_pmd代码如框3.2所示,可以直观地看到标红的代码直接将内存页标记为脏的。

框3.2:touch_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);
}

3.2 漏洞流程描述如下:

(a)调用follow_page_mask请求获取可写(FOLL_WRITE)THP内存页,发生缺页中断,返回值为NULL,调用faultin_page从磁盘中调入内存页,返回值为0。

(b)随着goto entry再次调用follow_page_mask,请求可写(FOLL_WRITE)内存页,由于内存页没有可写权限,返回值为NULL,调用fault_page复制只读内存页获得FOLL_COW标志,返回值为0。

(c)随着goto entry 再次调用follow由于cond_resched会主动放权,引起系统调度其他程序,另一个程序B使用madvise(MADV_DONTNEED)换出内存页,同时程序B读内存页,那么则会最终调用touch_pmd,将内存页标记为脏的

(d)程序再次被调度执行,调用follow_page_mask请求获取可写(FOLL_WRITE)内存页,此时满足FOLL_COW和脏的,因此程序获得可写内存页。

(f)后续进行写入操作,只要设置合理THP内存页可以写前面提到的零页(zero pages),其他共享零页的进程读取修改后的零页数据进行相关操作就会发生crash,触发漏洞

3.3 POC描述:

POC链接如下:

https://github.com/bindecy/HugeDirtyCowPOC

主体思路为:

(a)启动write_thread线程负责写入数据。

(b)启动unmap_and_read_thread线程负责利用madvise换出内存页,并且读内存页,以此将内存页标记为脏的。

(c)wait_for_success检查只读零页是否修改成功。

另外POC为了提高速度开了很多线程进行操作。

3.4 测试环境:

Vmware 12.5.2

操作系统ubuntu16.04

内存:3G

处理器:4核

3.5 POC说明:

(a)按照前面提到的方法开启THP内存页。

(b)以-pthead编译poc。

(c)poc运行显示成功后,只是表示修改零页成功,需要运行其他应用程序,当其他应用程序读取零页,进行非法操作时即可触发crash,引爆漏洞。

(d)由于浏览器操作指令复杂,我们选取firefox进行测试,不到一会儿,漏洞来了。。。

3.6 演示截图:

4. 后记

该补丁是linux的创始人linus亲自打上的,这样的天才对自己的“亲儿子”修补都会犯错,何况是一般的程序员写的程序,打的补丁,总结来看漏洞挖掘任重而道远。补丁分析作为这其中的葵花宝典大放异彩,相关技术值得重视和深思。

(完)