漏洞概述:
这个漏洞并不能提权,它应该属于组合技里面关键的一环,同样问题的开始出现在/proc/$pid/mem
上,如果有写目标内存权限的话,那么是可以在目标用户内存空间为0的虚拟地址写东西的,那么如果再配上一个内核里面的null pointer dereference的洞是有可能制造提权效果的。
漏洞分析:
mem_open
较之之前的CVE-2012-0056并没什么发生明显的变化,只是把一些操作封装起来了,这次出现问题地方比较深,直接来看mem_write
:
static ssize_t mem_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
return mem_rw(file, (char __user*)buf, count, ppos, 1);
}
write 和 read也整合到了一起。
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
...
while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE);
if (write && copy_from_user(page, buf, this_len)) {
copied = -EFAULT;
break;
}
this_len = access_remote_vm(mm, addr, page, this_len, flags);
...
回顾一下之前的CVE,之前的CVE利用点在于mm结构是mem_write
才获取的,那么可以execl
来替换掉/proc/self/mem
,导致了su可以写自己的内存。那么在这里同样是拿su来写,但是写的是其他进程的内存,写其他低权限进程的内存,有什么作用呢?似乎也没什么作用,但是这里竟然能写到目标内存虚拟地址为0的地方上。
这里需要把目光聚集在是如何获取到这个地址0的。接着看access_remote_vm
:
int access_remote_vm(struct mm_struct *mm, unsigned long addr,
void *buf, int len, unsigned int gup_flags)
{
return __access_remote_vm(NULL, mm, addr, buf, len, gup_flags);
}
int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
unsigned long addr, void *buf, int len, unsigned int gup_flags)
{
struct vm_area_struct *vma;
void *old_buf = buf;
int write = gup_flags & FOLL_WRITE;
down_read(&mm->mmap_sem);
/* ignore errors, just check how much was successfully transferred */
while (len) {
int bytes, ret, offset;
void *maddr;
struct page *page = NULL;
ret = get_user_pages_remote(tsk, mm, addr, 1,
gup_flags, &page, &vma, NULL);
...
maddr = kmap(page);
if (write) {
copy_to_user_page(vma, page, addr,
maddr + offset, buf, bytes);
set_page_dirty_lock(page);
} else {
copy_from_user_page(vma, page, addr,
buf, maddr + offset, bytes);
}
kunmap(page);
put_page(page);
...
}
获取目标page的地方并不在这里,但是这里把获取目标page和写page分开了。所以这里只需要重点关注get_user_pages_remote
,接下来的一些过程比较冗余,不想直接拉代码跟记流水账一样,所以这里只会列出一些重点的地方。 :)
tatic 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 {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
vma = find_extend_vma(mm, start);
...
第一次迭代会去初始化vma,什么是vma?就是虚拟内存,如果你去看/proc/$pid/maps
内容,其中每一行就是一个vma块。
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct rb_node *rb_node;
struct vm_area_struct *vma;
...
rb_node = mm->mm_rb.rb_node;
while (rb_node) {
struct vm_area_struct *tmp;
tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
if (tmp->vm_end > addr) {
vma = tmp;
if (tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
vmacache_update(addr, vma);
return vma;
}
这里就是具体找合适vma结构的地方,有一个宗旨addr < vma->end
,mm->mm_rb
是个红黑二叉树结构,不要想的太过于复杂,在结构上就是和普通二叉树数搜索是一样的,小的在左子节点,大的在右子节点,通过vma->vm_start <= addr
判断,然后不断的逼近合适的vma区域。
在这里你是可以发现addr 如果太大,大于高地址的vma->vma_end
那么肯定是会返回NULL的,但比较小的话,小于低地址的vma->vma_start
是会返回这个低地址所对应的vma。
再进一步看拿到vma是怎么处理的:
find_extend_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct *vma;
unsigned long start;
addr &= PAGE_MASK;
vma = find_vma(mm, addr);
if (!vma)
return NULL;
if (vma->vm_start <= addr)
return vma;
if (!(vma->vm_flags & VM_GROWSDOWN))
return NULL;
start = vma->vm_start;
if (expand_stack(vma, addr))
return NULL;
if (vma->vm_flags & VM_LOCKED)
populate_vma_page_range(vma, addr, start, NULL);
return vma;
}
很显然,我们如果说传入的addr是0,即使我们用mmap分配到虚拟地址最低的位置0x10000.这个值可以查看/proc/sys/vm/mmap_min_addr
,也是不在这个vma范围的。但是有趣的来了,如果这个vma的flag设置了VM_GROWSDOWN
是会进行虚拟内存向下扩展的。
但是会进行一项security_mmap_addr
的检查:
int cap_mmap_addr(unsigned long addr)
{
int ret = 0;
if (addr < dac_mmap_min_addr) {
ret = cap_capable(current_cred(), &init_user_ns, CAP_SYS_RAWIO,
SECURITY_CAP_AUDIT);
/* set PF_SUPERPRIV if it turns out we allow the low mmap */
if (ret == 0)
current->flags |= PF_SUPERPRIV;
}
return ret;
}
这里检查很显然已经用su绕过了,current_cred()
取的写/proc/self/mem
的进程。接下来的一步,在这里我就有些不理解了:
prev = vma->vm_prev;
/* Check that both stack segments have the same anon_vma? */
if (prev && !(prev->vm_flags & VM_GROWSDOWN) &&
(prev->vm_flags & (VM_WRITE|VM_READ|VM_EXEC))) {
if (address - prev->vm_end < stack_guard_gap)
return -ENOMEM;
}
按照前面遍历的过程,此时vma拿到的肯定是地址最低的地方,怎么可能还会有更低的地方?这里的检查有什么作用?然后我想了一下整个过程,其实这里有道理的。可能会出现这样一种情况:
--------------|low
|VMA |
--------------|high
| \|/ <-----------addr
--------------
|VMA |
--------------
这里出现stack_guard_gap
为1M,是一种当vma内存增长时保护措施,具体的可以看看这篇文章https://blog.qualys.com/securitylabs/2017/06/19/the-stack-clash
回到本文的主题,显然这里一切正常,绕过了mmap_min_addr的限制向下扩展内存,然后用缺页中断,分配真正物理内存。以至于可以在用户空间0虚拟地址写入构造的数据,这个mmap_min_addr设置的初衷就是为了减少linux kernel里面null pointer dereference的隐患。
也并非不可以在虚拟地址0上写东西,这个mmap_min_addr的是可以直接设置的。
https://cert.360.cn/report/detail?id=58e8387ec4c79693354d4797871536ea 这篇文章的师傅发表了一个观点说,修复的方法似乎并不合理。但我认为这恰恰是最合理的。
笔者以为这样修补没有真正解决问题。这是一个逻辑漏洞,根本原因在于可以通过两个进程绕过security_mmap_addr函数中cap_capable(current_cred()……)的检查逻辑
师傅认为这里的cap_capable检查逻辑存在问题。我感觉这里并没有错,只是用错了地方。
If the process is attempting to map memory below dac_mmap_min_addr they need CAP_SYS_RAWIO. The other parameters to this function are unused by the capability security module. Returns 0 if this mapping should be allowed-EPERM if not.
从上述注释可以看的出来,the process
想要获取目标内存低于dac_mmap_min_addr
的内存映射,必须要有CAP_SYS_RAWIO
的权限。这个地方权限判断不应该放在进程读写这个地方,想要获取的目标地址并不是属于当前进程,security_mmap_addr
应该是用在当前进程下的地址判断。
但是如果说其他地方也用到这个security_mmap_addr
,如果处于进程间的读写话,也是有可能出现问题的。我也搜索了一下存在security_mmap_addr
的函数。只有一个get_unmapped_area
有,这个函数发生在用户进程空间需要映射新的内存时候。这也很难把和多进程的操作联系起来。
所以正如官方修复的那样,直接删掉这个地方不合理的权限判断,扩展低于mmap_min_addr
的地址时直接返回error。
但是这里还是可以通过指定VM_GROWSDOWN
来向下扩展内存。这是比较有趣的地方,虽然不能扩展至mmap_min_addr
一下。接下来就是分析利用这个洞的组合技。:)