作者:rain
预估稿费:400
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
1. 前言
近日(2017.11.30)国外安全团队Bindecy爆出名为大脏牛(Huge Dirty Cow)的内核提权漏洞,编号为CVE-2017–1000405。包含linux内核(2.6.38~4.14)的服务器,桌面,移动等众多设备将面临严重挑战,数以百万计的用户受到安全威胁。该漏洞是由于对之前的内核提权漏洞(cve-2016-5195)修补不完整引发,因此为了便于理解该漏洞,我们对CVE-2016-5195做一个回顾。
2. Dirty Cow(CVE-2016-5195)回顾
该漏洞是内核函数get_user_pages(mm/gup.c)在处理COW(copy-on-write)过程中,由条件竞争引发未授权进程向只读内存区域写入数据导致的。
2.1 主要数据结构说明
get_user_pages被用来通过虚拟地址查询页面获取物理页面,主要代码如框2.1所示:
框2.1:__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)
{
/* ... snip ... */
do {
/* ... snip ... */
retry:
cond_resched(); /* please rescheule me!!! */
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 -EFAULT:
case -ENOMEM:
case -EHWPOISON:
return i ? i : ret;
case -EBUSY:
return i;
case -ENOENT:
goto next_page;
}
BUG();
}
if (pages) {
pages[i] = page;
flush_anon_page(vma, page, start);
flush_dcache_page(page);
page_mask = 0;
}
/* ... snip ... */
}
/* ... snip ... */
}
其中重要的数据结构说明如下:
- con_resched函数作用是主动放权,等待下一次被调度。
- follow_page_mask函数的作用是查询页表获取虚拟地址对应的物理页,它将按照linux页表的四级结构(如图1)所示依次向下调用四层函数(follow_page_mask/follow_p4d_mask/follow_pud_mask/follow_pmd_mask/follow_page_pte)进行解析,特别需要注意是:(a)页表中不存在物理页即缺页,(b)访问语义标志foll_flags对应的权限违反内存页的权限时,follow_page_mask返回值为NULL,会触发对faultin_page的调用。
图2.1
- faultin_page函数的作用是处理页故障(page fault),分析它的函数体会发现,实际上是调用handle_mm_fault按照foll_flags进行处理。(a)当发生缺页时,它会从磁盘中调入页面,(b)当想要以只读权限获取可写页面时,会发生COW,即复制原来只读(read_only)内存页,并将新的内存页标记为只读(read_only),私有(private)和脏的(dirty),整个过程如框2所示:
框2.2 page_fault处理COW结构:
faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
FAULT_FLAG_WRITE && !pte_write
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
- 访问语义foll_flags的几个主要相关标志说明:
- FOLL_WRITE:请求可写pte(页表项)。
- FOLL_FORCE:请求读写权限的pte(页表项)。
框2.3: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;
/* mlock all present pages, but do not fault in new pages */
if ((*flags & (FOLL_POPULATE | FOLL_MLOCK)) == FOLL_MLOCK)
return -ENOENT;
/* For mm_populate(), just skip the stack guard page. */
if ((*flags & FOLL_POPULATE) &&
(stack_guard_page_start(vma, address) ||
stack_guard_page_end(vma, address + PAGE_SIZE)))
return -ENOENT;
if (*flags & FOLL_WRITE)
fault_flags |= FAULT_FLAG_WRITE;
if (*flags & FOLL_REMOTE)
fault_flags |= FAULT_FLAG_REMOTE;
if (nonblocking)
fault_flags |= FAULT_FLAG_ALLOW_RETRY;
if (*flags & FOLL_NOWAIT)
fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT;
if (*flags & FOLL_TRIED) {
VM_WARN_ON_ONCE(fault_flags & FAULT_FLAG_ALLOW_RETRY);
fault_flags |= FAULT_FLAG_TRIED;
}
ret = handle_mm_fault(mm, vma, address, fault_flags);
if (ret & VM_FAULT_ERROR) {
if (ret & VM_FAULT_OOM)
return -ENOMEM;
if (ret & (VM_FAULT_HWPOISON | VM_FAULT_HWPOISON_LARGE))
return *flags & FOLL_HWPOISON ? -EHWPOISON : -EFAULT;
if (ret & (VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV))
return -EFAULT;
BUG();
}
if (tsk) {
if (ret & VM_FAULT_MAJOR)
tsk->maj_flt++;
else
tsk->min_flt++;
}
if (ret & VM_FAULT_RETRY) {
if (nonblocking)
*nonblocking = 0;
return -EBUSY;
}
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
return 0;
}
其中handle_mm_fault的返回值ret代表了具体的处理情形,VM_FAULT_WRITE表示发生了COW,标红的为漏洞相关代码。
2.2 COW正常流程
(a)调用follow_page_mask请求获取可写(FOLL_WRITE)内存页,发生缺页中断,返回值为NULL,调用faultin_page从磁盘中调入内存页,返回值为0。
(b)随着goto entry再次调用follow_page_mask,请求可写(FOLL_WRITE)内存页,由于内存页没有可写权限,返回值为NULL,调用fault_page复制只读内存页并去掉FOLL_WRITE标志(框2.3红色代码),返回值为0。
(c)随着goto entry再次调用follow_page_mask,请求获取虚拟地址对应内存页(无FOLL_WRITE),返回page。
2.3 竞争条件引发的漏洞异常流程
(a)调用follow_page_mask请求获取可写(FOLL_WRITE)内存页,发生缺页中断,返回值为NULL,调用faultin_page从磁盘中调入内存页,返回值为0。
(b)随着goto entry再次调用follow_page_mask,请求可写(FOLL_WRITE)内存页,由于内存页没有可写权限,返回值为NULL,调用fault_page复制只读内存页并去掉FOLL_WRITE标志(框2.3红色代码),返回值为0。
(a)(b)与正常流程一致
(c)随着goto entry 再次调用follow由于cond_resched会主动放权,引起系统调度其他程序,另一个程序使用madvise(MADV_DONTNEED)换出内存页。
madvise的作用是给系统对于内存的使用一些建议,MADV_DONTNEED告诉系统换出对应内存页。
(d)程序再次被调度执行,调用follow_page_mask请求获取可写(FOLL_WRITE)内存页,发生缺页中断,返回值为NULL,调用faultin_page从磁盘中调入内存页,返回值为0。
(e)随着goto entry再次调用follow_page_mask,请求获取虚拟地址对应内存页(无FOLL_WRITE),返回page。
(f)后续进行写入操作,当内存数据同步到磁盘时,只读文件被改写(触发漏洞)。
2.4 POC描述
POC链接如下:
https://github.com/dirtycow/dirtycow.github.io/blob/master/dirtyc0w.c
主体思路为:
(a)启动procselfmemThread线程负责写入数据。
(b)启动madviseThread线程负责利用madvise换出内存页。
主要流程如图2.2所示:
图2.2
2.5 补丁分析
修补代码如框2.4所示,该补丁增加了FOLL_COW去表示COW访问语义,以此来替代移去FOLL_WRITE。增加follow_pfn_pte检测是否请求可写页面/是否发生了COW/是否虚拟内存页项被标记为脏的来判断内存页是否可写。
如框2.4:修补代码:
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;
}
3. HugeDirtyCow(CVE-2017-1000405)
3.1 相关基础知识补充
3.1.1 透明大内存页(THP:Transparent Huge Pages)
通常linux的内存页为4kb大小,为了满足系统和程序的特殊需求,linux允许2M和1G大内存页。常规的虚拟地址翻译如图3.1所示,PGD,PMD均作为页表目录,当启用大内存页时,PMD不再表示页表目录而是和PTE合并共同代表页表项(PTE)。
图3.1
THP内存页主要被用于匿名anonymous,shmem,tmpfs三种内存映射,即:
- anonymous:通过mmap映射到内存是一个匿名文件及不对应任何实际磁盘文件。
- shmem:共享内存。
- tmpfs:是一种虚拟文件系统,存在于内存,因此访问速度会很快,当使用tmpfs类型挂载文件系统释放,它会自动创建。。
THP内存页查看:
cat sudo tee /sys/kernel/mm/transparent_hugepage/enabled
若显示alwasy则表示已开启,否则使用如下命令开启:
echo always > /sys/kernel/mm/transparent_hugepage/enabled
3.1.2 零页(zero page)
当程序申请匿名内存页时,linux系统为了节省时间和空间并不会真的申请一块物理内存,而是将所有申请统一映射到一块预先申请好值为零的物理内存页,当程序发生写入操作时,才会真正申请内存页,这一块预先申请好值为零的页即为零页(zero page),且零页是只读的。
- 零页攻击面
由于零页是只读的,如果共享零页的进程A非法写入了特定的值,其他共享零页的进程例如B读入被A篡改的零页的值,那么后续以此为基础的访存等操作就会发生异常,造成进程B crash,形成漏洞。
3.2 CVE-2016-5195修补不完整阐述
对于CVE-2016-5195对于THP内存页修补如下:
框3.1 修补代码:
@@ -783,6 +783,12 @@ struct page *follow_devmap_pmd(struct vm_area_struct *vma, unsigned long addr,
assert_spin_locked(pmd_lockptr(mm, pmd));
+ /*
+ * When we COW a devmap PMD entry, we split it into PTEs, so we should
+ * not be in this function with `flags & FOLL_COW` set.
+ */
+ WARN_ONCE(flags & FOLL_COW, "mm: In follow_devmap_pmd with FOLL_COW set");
+
if (flags & FOLL_WRITE && !pmd_write(*pmd))
return NULL;
@@ -1128,6 +1134,16 @@ int do_huge_pmd_wp_page(struct vm_fault *vmf, pmd_t orig_pmd)
return ret;
}
+/*
+ * FOLL_FORCE can write to even unwritable pmd's, but only
+ * after we've gone through a COW cycle and they are dirty.
+ */
+static inline bool can_follow_write_pmd(pmd_t pmd, unsigned int flags)
+{
+ return pmd_write(pmd) ||
+ ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pmd_dirty(pmd));
+}
+
struct page *follow_trans_huge_pmd(struct vm_area_struct *vma,
unsigned long addr,
pmd_t *pmd,
@@ -1138,7 +1154,7 @@ struct page *follow_trans_huge_pmd(struct vm_area_struct *vma,
assert_spin_locked(pmd_lockptr(mm, pmd));
- if (flags & FOLL_WRITE && !pmd_write(*pmd))
+ if (flags & FOLL_WRITE && !can_follow_write_pmd(*pmd, flags))
goto out;
从框3.1的修补代码可以看出,THP内存页修补和普通内存页的修补操作基本一致,但是对于有一点疏忽的是:对于大内存页来说,不经历COW也可以将内存页标记为脏的。
每次调用内核函数get_user_pages获取可读THP内存页时即会调用follow_page_mask,而follow_page_mask会调用touch_pmd,调用路径为:
follow_page_mask/follow_trans_huge_pmd/touch_pmd
touch_pmd代码如框3.2所示,可以直观地看到标红的代码直接将内存页标记为脏的。
框3.2:touch_pmd
static void touch_pmd(struct vm_area_struct *vma, unsigned long addr,
pmd_t *pmd)
{
pmd_t _pmd;
/*
* We should set the dirty bit only for FOLL_WRITE but for now
* the dirty bit in the pmd is meaningless. And if the dirty
* bit will become meaningful and we'll only set it with
* FOLL_WRITE, an atomic set_bit will be required on the pmd to
* set the young bit, instead of the current set_pmd_at.
*/
_pmd = pmd_mkyoung(pmd_mkdirty(*pmd));
if (pmdp_set_access_flags(vma, addr & HPAGE_PMD_MASK,
pmd, _pmd, 1))
update_mmu_cache_pmd(vma, addr, pmd);
}
3.2 漏洞流程描述如下:
(a)调用follow_page_mask请求获取可写(FOLL_WRITE)THP内存页,发生缺页中断,返回值为NULL,调用faultin_page从磁盘中调入内存页,返回值为0。
(b)随着goto entry再次调用follow_page_mask,请求可写(FOLL_WRITE)内存页,由于内存页没有可写权限,返回值为NULL,调用fault_page复制只读内存页获得FOLL_COW标志,返回值为0。
(c)随着goto entry 再次调用follow由于cond_resched会主动放权,引起系统调度其他程序,另一个程序B使用madvise(MADV_DONTNEED)换出内存页,同时程序B读内存页,那么则会最终调用touch_pmd,将内存页标记为脏的。
(d)程序再次被调度执行,调用follow_page_mask请求获取可写(FOLL_WRITE)内存页,此时满足FOLL_COW和脏的,因此程序获得可写内存页。
(f)后续进行写入操作,只要设置合理THP内存页可以写前面提到的零页(zero pages),其他共享零页的进程读取修改后的零页数据进行相关操作就会发生crash,触发漏洞。
3.3 POC描述:
POC链接如下:
https://github.com/bindecy/HugeDirtyCowPOC
主体思路为:
(a)启动write_thread线程负责写入数据。
(b)启动unmap_and_read_thread线程负责利用madvise换出内存页,并且读内存页,以此将内存页标记为脏的。
(c)wait_for_success检查只读零页是否修改成功。
另外POC为了提高速度开了很多线程进行操作。
3.4 测试环境:
Vmware 12.5.2
操作系统ubuntu16.04
内存:3G
处理器:4核
3.5 POC说明:
(a)按照前面提到的方法开启THP内存页。
(b)以-pthead编译poc。
(c)poc运行显示成功后,只是表示修改零页成功,需要运行其他应用程序,当其他应用程序读取零页,进行非法操作时即可触发crash,引爆漏洞。
(d)由于浏览器操作指令复杂,我们选取firefox进行测试,不到一会儿,漏洞来了。。。
3.6 演示截图:
4. 后记
该补丁是linux的创始人linus亲自打上的,这样的天才对自己的“亲儿子”修补都会犯错,何况是一般的程序员写的程序,打的补丁,总结来看漏洞挖掘任重而道远。补丁分析作为这其中的葵花宝典大放异彩,相关技术值得重视和深思。