• 深入分析CVE-2016-5195 Dirty Cow


      前面一段时间,这个编号为CVE-2016-5195的漏洞刷爆了各个安全相关的博客和网站,这个漏洞可以对任意可读文件进行写操作从而导致提权,通杀了包括Android在内的绝大多数linux版本,,影响不可为不大,试着分析一下。

    一:漏洞分析

      这个漏洞逻辑并不复杂,分析的最大难点是流程复杂,容易绕晕在代码迷宫里,所以先梳理一下流程,流程如下(初略扫过即可),根据下面分析来查看上面流程):

    //进行写操作
    mem_write
        mem_rw  
         access_remote_vm
                __access_remote_vm        
                    //用于获取页
                    get_user_pages
                        __get_user_pages
                            retry:
                                follow_page_mask(...,flag,...);
                                    //通过内存地址来找到内存页
                                    follow_page_pte(...,flag,...);
                                         //如果获取页表项时要求页表项所指向的内存映射具有写权限,但是页表项所指向的内存并没有写权限。则会返回空
                                        if ((flags & FOLL_WRITE) && !pte_write(pte)) 
                                            return NULL
                                        ////获取页表项的请求不要求内存映射具有写权限的话会返回页表项
                                        return page
                                if (foll_flags & FOLL_WRITE)//要求页表项要具有写权限,所以FOLL_WRITE为1
                                    fault_flags |= FAULT_FLAG_WRITE;
                                //获取页表项
                                if (!page) {
                                    faultin_page(vma,...); //获取失败时会调用这个函数
                                         handle_mm_fault();
                                            __handle_mm_fault()
                                                handle_pte_fault()
                                                    if (!fe->pte) 
                                                        do_fault(fe);
                                                            ////如果不要求目标内存具有写权限时导致缺页,内核不会执行COW操作产生副本,ers
                                                            if (!(fe->flags & FAULT_FLAG_WRITE))
                                                                do_read_fault(fe, pgoff);
                                                                    __do_fault(fe, pgoff, NULL, &fault_page, NULL);
                                                                    ret |= alloc_set_pte(fe, NULL, fault_page)
                                                                        //如果执行了COW,设置页表时会将页面标记为脏,但是不会标记为可写。
                                                                        if (fe->flags & FAULT_FLAG_WRITE)
                                                                            entry = maybe_mkwrite(pte_mkdirty(entry), vma);
                                                            //如果要求目标内存具有写权限时导致缺页,目标内存映射是一个VM_PRIVATE的映射,内核会执行COW操作产生副本
                                                            if (!(vma->vm_flags & VM_SHARED))
                                                                do_cow_fault(fe, pgoff);
                                                                    new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, fe->address);
                                                                    ret = __do_fault(fe, pgoff, new_page, &fault_page, &fault_entry);
                                                                    copy_user_highpage(new_page, fault_page, fe->address, vma);
                                                                    ret |= alloc_set_pte(fe, memcg, new_page);
                                                    if (fe->flags & FAULT_FLAG_WRITE)
                                                        if (!pte_write(entry))
                                                            do_wp_page(fe, entry)//VM_FAULT_WRITE置1
                                                                if ((flags & FAULT_FLAG_WRITE) && reuse_swap_page(page))
                                                                    maybe_mkwrite(pte_mkdirty(entry), vma);
                                                                    if (likely(vma->vm_flags & VM_WRITE))
                                                                        pte_mkwrite(pte);
                                                                    flags &= ~FAULT_FLAG_WRITE;
                                                                    ret |= VM_FAULT_WRITE;
                                    if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
                                        *flags &= ~FOLL_WRITE;
                              ,==0 goto retry        
            if (write)
              copy_to_user_page

      进行逐步分析,并在分析中参考上文代码流程。

      漏洞发生在调用write函数时,经过一系列调用(write->mem_write->mem_rw->access_remote_vm->__access_remote_vm),通过在__access_remote_vm的get_user_pages来获得页,copy_to_user_page来讲内容复制进页中。而漏洞就发生在get_user_pages中。下面来分析get_user_pages的具体流程。

      get_user_pages中主要部分是一个循环,直到正确找到页,其中有两个函数极为重要,follow_page_mask和faultin_page,其中follow_page来找到页,dofault_page在寻页失败的时候建立映射为下次调用follow_page_mask来做准备。

      第一次执行,由于mmap对第一次对映射内存进行操作,所以并不能直接从页表中找到。get_user_page,因为我们要求页表项要具有写权限,所以FOLL_WRITE为1,设置FAULT_FLAG_WRITE然后调用了faultin_page,之后依次调用了handle_mm_fault->__handle_mm_fault->handle_pte_fault->do_fault->do_cow_fault,此时利用COW来生成了页表,建立了映射。

      第二次执行,follow_page_mask会通过flag参数的FOLL_WRITE位是否为1判断要是否需要该页具有写权限,以及通过页表项的VM_WRITE位是否为1来判断该页是否可写。由于Mappedmem是以PROT_READ和MAP_PRIVATE的的形式进行映射的。所以VM_WRITE为0,而FOLL_WRITE为1,返回null,进而调用faultin_page函数,此时由于已经找到了页表,不再调用_do_fault,而是调用了do_wp_page,在do_wp_page中,将FAULT_FLAG_WRITE置0,同时,将ret的VM_FAULT_WRITE置1,表示已经执行过COW,在faultin_page之后的判断中,由于ret中VM_FAULT_WRITE置1,则flag的FOLL_WRITE置0,而FOLL_WRITE置0代表着也页表项不需要写权限。

      第三次执行,此时调用follow_page_mask时可以正确找到页了。由于进行了COW,所以写操作并不会涉及到原始内存。

      但是正如POC,如果madvice发生在get_user_page第二次执行之后,madvice即取消内存的映射关系,那么第三次执行会follow_page_mask函数会失败,进入dofault_page函数,此时的调用流程会和第一次有一定的区别,由于FAULT_FLAG_WRITE置0,所以直接执行do_read_fault。而do_read_fault函数调用了__do_fault,由于标志位的改变,此时直接与文件内存进行映射。

    __do_fault部分代码如下:

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

        

      在第四次执行的时候,follow_page_mask直接获得文件内存页从而对其进行读写。

    二:补丁分析

      这个漏洞是Linux的创始人Linus亲自修复,简略补丁如下:

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

      Linus在这里新添加了一个FOLL_COW的标志位,来表明已经进行了COW。同时在get_follow_mask判定权限的时候同时利用dirty位来判定FOLL_COW是否有效。

    参考文献:

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

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

    https://bugzilla.suse.com/show_bug.cgi?id=1004418#c14

  • 相关阅读:
    CHROME下去掉保存密码后输入框变成黄色背景样式
    AJAX请求遭遇未登录和Session失效的解决方案
    Oracle数据导入导出imp/exp
    缓存技术
    存储过程中引用的常规表,临时表以及表变量是否会导致存储过程的重编译
    给定一张表(列有月份,销售额),要求查询出月份、本月销售额、上月销售额这三个结果,如果当月上个月的销售额不存在就显示为“*”。
    tempdb 数据文件暴涨
    数据库还原成功之后,数据库依然处于还原状态
    CONVERT时间
    sql 2008 链接服务器到 sql 2000
  • 原文地址:https://www.cnblogs.com/0xJDchen/p/6015476.html
Copyright © 2020-2023  润新知