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

84851

稿费:600RMB(不服你也来投稿啊!)

投稿方式:发送邮件至linwei#360.cn,或登陆

0x00 概述

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

0x01 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标记映射文件到内存区域,然后启动两个线程:

其中一个线程向文件映射的内存区域写数据,这时内核采用COW机制。

另一个线程使用带MADV_DONTNEED参数的madvise系统调用将文件映射内存区域释放,达到干扰另一个线程的COW过程,产生竞态条件,当竞态条件发生时就能写入文件成功。

0x02 漏洞原理分析

faultin_page

handle_mm_fault

__handle_mm_fault

handle_pte_fault

do_fault 

do_cow_fault 

alloc_set_pte

maybe_mkwrite(pte_mkdirty(entry), vma) 

# Returns with 0 and retry

follow_page_mask

follow_page_pte

(flags & FOLL_WRITE) && !pte_write(pte) 

faultin_page

handle_mm_fault

__handle_mm_fault

handle_pte_fault

FAULT_FLAG_WRITE && !pte_write

do_wp_page

PageAnon() 

reuse_swap_page 

wp_page_reuse

maybe_mkwrite 

ret = VM_FAULT_WRITE

((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) 

# Returns with 0 and retry as a read fault

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 

do_read_fault 

Copy-on-Write(COW)

当我们用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。

以下代码以liux 4.7([https://www.kernel.org/pub/linux/kernel/v4.x/linux-4.7.tar.xz])的源码为例,具体解读一下流程。首先从关键的获取用户进程内存页的函数函数get_user_pages看起,get_user_pages系列函数用于获取用户进程虚拟地址所在的页(struct page),返回的是page数组,该系列函数最终都会调用__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

该函数用来通过进程虚拟地址沿着pgd、gud、gmd、pte一路查找page。因为是第一次访问映射的内存区域,此时页表是空的,返回NULL,然后外层函数进入faultin_page过程去调页。

struct page *follow_page_mask(

struct vm_area_struct *vma, /* [IN] 虚拟地址所在的vma */

unsigned long address, /* [IN] 待查找的虚拟地址 */

unsigned int flags, /* [IN] 标记 */

unsigned int *page_mask /* [OUT] 返回页大小 */

)

{

...

return no_page_table(vma, flags);

...

}

static struct page *no_page_table(struct vm_area_struct *vma,

unsigned int flags)

{

if ((flags & FOLL_DUMP) && (!vma->vm_ops || !vma->vm_ops->fault))

return ERR_PTR(-EFAULT);

return NULL;

}

faultin_page

该函数完成follow_page_mask找不到page的处理。第一次查找时页还不在内存中,首先设置FAULT_FLAG_WRITE标记,然后沿着handle_mm_fault -> __handle_mm_fault -> handle_pte_fault -> do_fault -> do_cow_fault分配页。

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;

if (*flags & FOLL_WRITE)

fault_flags |= FAULT_FLAG_WRITE; /* 标记失败的原因 WRITE */

...

ret = handle_mm_fault(mm, vma, address, fault_flags); /* 第一次分配page并返回 0 */

...

return 0;

}

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)

{

if (!pte_present(entry))

if (pte_none(entry))

return do_fault(mm, vma, address, pte, pmd, flags, entry); /* page不在内存中,调页 */

}

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)

{

if (!(vma->vm_flags & VM_SHARED)) /* VM_PRIVATE模式,使用写时复制(COW)分配页 */

return do_cow_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)

{

new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); /* 分配一个page */

ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page,

&fault_entry);

do_set_pte(vma, address, new_page, pte, true, true); /* 设置new_page的PTE */

}

static int __do_fault(struct vm_area_struct *vma, unsigned long address,

pgoff_t pgoff, unsigned int flags,

struct page *cow_page, struct page **page,

void **entry)

{

ret = vma->vm_ops->fault(vma, &vmf);

}

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;

flush_icache_page(vma, page);

entry = mk_pte(page, vma->vm_page_prot);

if (write)

entry = maybe_mkwrite(pte_mkdirty(entry), vma); /* 带_RW_DIRTY,不带_PAGE_RW */

if (anon) { /* anon = 1 */

page_add_new_anon_rmap(page, vma, address, false);

} else {

inc_mm_counter_fast(vma->vm_mm, mm_counter_file(page));

page_add_file_rmap(page);

}

set_pte_at(vma->vm_mm, address, pte, entry);

}

static inline pte_t maybe_mkwrite(pte_t pte, struct vm_area_struct *vma)

{

if (likely(vma->vm_flags & VM_WRITE)) /* 因为是只读的,所以pte不带_PAGE_RW标记 */

pte = pte_mkwrite(pte);

return pte;

}

到这里第一次查询和pagefault处理结束,已经在内存中分配好了,该页是只读的匿名页。

第二次查找

在这次的查找中因为flags带有FOLL_WRITE标记,而page是只读的,此时follow_page_mask返回NULL,进入faultin_page。

struct page *follow_page_mask(...)

{

return follow_page_pte(vma, address, pmd, flags);

}

static struct page *follow_page_pte(...)

{

if ((flags & FOLL_WRITE) && !pte_write(pte)) { /* 查找可写的页,但是该页是只读的 */

pte_unmap_unlock(ptep, ptl);

return NULL;

}

}

在处理faultin_page过程中,我们沿着函数调用路径faultin_page -> handle_mm_fault -> __handle_mm_fault -> handle_pte_fault一路找来,在handle_pte_fault中因为没有写访问权限,会进入do_wp_page函数中:

static int handle_pte_fault(...)

{

if (flags & FAULT_FLAG_WRITE) /* faultin_page函数开头设置了该标志 */

if (!pte_write(entry))

return do_wp_page(mm, vma, address, pte, pmd, ptl, entry);

}

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)

{

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

}

}

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)

{

entry = maybe_mkwrite(pte_mkdirty(entry), vma); 带_RW_DIRTY,不带_PAGE_RW

if (ptep_set_access_flags(vma, address, page_table, entry, 1))

update_mmu_cache(vma, address, page_table);

return VM_FAULT_WRITE;

}

这里需要关注的是wp_page_reuse的返回值是VM_FAULT_WRITE,即handle_mm_fault返回VM_FAULT_WRITE,在faultin_page函数中会去掉查找标志FOLL_WRITE,然后返回0。

static int faultin_page(...)

{

ret = handle_mm_fault(mm, vma, address, fault_flags); /* 返回 VM_FAULT_WRITE */

/* 去掉FOLL_WRITE标记, */

if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))

*flags &= ~FOLL_WRITE;

return 0;

}

第三次查找

在上一次处理查找失败的过程中FOLL_WRITE被去掉了,所以这一次的follow_page_mask会成功返回之前分配的page。到这里写时复制过程就算完成了。

madvise(MADV_DONTNEED)

madvise系统调用的作用是给系统对于内存使用的一些建议,MADV_DONTNEED参数告诉系统未来不访问该内存了,内核可以释放内存页了。内核函数madvise_dontneed中会移除指定范围内的用户空间page。

static long madvise_dontneed(struct vm_area_struct *vma,

struct vm_area_struct **prev,

unsigned long start, unsigned long end)

{

...

zap_page_range(vma, start, end - start, NULL);

return 0;

}

void zap_page_range(struct vm_area_struct *vma, unsigned long start,

unsigned long size, struct zap_details *details)

{

...

for ( ; vma && vma->vm_start vm_next)

unmap_single_vma(&tlb, vma, start, end, details);

...

}

产生竞态条件

我们再来梳理一下写时复制的过程中调页的过程:

1. 第一次follow_page_mask(FOLL_WRITE),因为page不在内存中,进行pagefault处理。

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

3. 第三次follow_page_mask(无FOLL_WRITE),成功。

__get_user_pages函数中每次查找page前会先调用cond_resched()线程调度一下,这样就引入了竞态条件的可能性。在第二次分配COW页成功后,FOLL_WRITE标记已经去掉,如果此时,另一个线程把page释放了,那么第三次由于page不在内存中,又会进行调页处理,由于不带FOLL_WRITE标记,不会进行COW操作,此时get_user_pages得到的page带__PAGE_DIRTY,竞态条件就是这样产生的,流程如下:

1. 第一次follow_page_mask(FOLL_WRITE),page不在内存中,进行pagefault处理。

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

3. 另一个线程释放上一步分配的COW页

4. 第三次follow_page_mask(无FOLL_WRITE),page不在内存中,进行pagefault处理。

5. 第四次follow_page_mask(无FOLL_WRITE),成功返回page,但没有使用COW机制。

0x03 漏洞利用

这个是利用/proc/self/mem来修改只读文件的exploit

这个是利用ptrace(PTRACE_POKETEXT)来修改只读文件的exploit

这个是Andriod系统Root的exploit

0x04 漏洞修复

该漏洞patch的链接:[https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619]。现在不再是把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;

}

0x05 参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值