0x00 前言
这是Project Zero上的文章,原文为《Taming the wild copy: Parallel Thread Corruption》
链接: http://googleprojectzero.blogspot.com/2015/03/taming-wild-copy-parallel-thread.html
2002年,Apache Web服务器中发现并修复了一个非常有趣的漏洞。服务器在处理分块传输编码时存在缺陷,漏洞将导致memcpy()调用的长度为负值,且目标缓冲区在堆栈上。当然,很快许多人就宣布这一漏洞是不可利用的,只能造成拒绝服务。毕竟memcpy()函数调用时长度字段采用负值,将导致巨大的内存空间拷贝,肯定会命中一个未映射的内存页导致进程终止。由于FreeBSD和其他BSD衍生版本在memcpy()实现上的一些特性,出现了可用的远程代码执行漏洞利用程序,让人感到了惊喜! GIAC文章LINK记录了漏洞利用代码,并分析了它是如何工作的(GIAC文章的附录D中)。
为了清楚的描述这篇文章的目的,我们将wild copy定义为满足下面两个条件的内存拷贝:
-
攻击者对内存拷贝的长度拥有有限的控制权。
-
内存拷贝的长度总是能够导致对未映射页的访问。
在许多其他有趣的情况下,wild copy型漏洞最终可以被利用:
-
针对Windows结构化异常处理程序(SEH)的攻击。
-
不止BSD衍生版本上存在类似memcpy()特性的问题。 2011年,在Ubuntu上eglibc漏洞LINK是由于对memcpy()函数的调用采用了任意的长度字段。memset()变体漏洞LINK通过Chromium漏洞奖励计划被披露。
-
Java运行时环境中安装了非常复杂的崩溃处理程序,当wild copy不可避免地触发访问异常时被调用。在堆状态任意损坏的情况下,如果崩溃处理程序不够健壮,它自身可能会崩溃,导致漏洞更加可控和可利用。
上述所有情况都有一个共同点:wild copy漏洞可利用性由次要问题(secondary issue)或特性导致。
是否有可能利用wild copy漏洞,而不依赖于次要问题?这是Project Zero要解决的一个有趣问题 -- 当我们探索漏洞利用时,我们可以更加了解问题,或者推动利用技术的发展。今天,我们要探索64位Linux上,Chrome的Flash插件的wild copy漏洞。
现在是时候引入问题122和“parallel thread corruption”。问题122是一个Adobe Flash漏洞,2014年11月被修复,这是一个奇怪的错误,在一些平台上G711声音编解码器发生了错误,编解码器在试图解码一些样品时总是导致wild copy。漏洞触发时,会导致memmove()调用使用负值作为长度参数。在现代Linux系统中,memmove()的实现似乎没有问题,而SIGSEGV处理程序干净的清理了进程。现在来看看我们所做的工作!
0x01 选择方法
我们要去尝试“parallel thread corruption”,也就是在线程A中观察和利用线程B的异步内存破坏.线程B的内存破坏总是会导致崩溃,我们需要并行运行两个线程。还需要赢得一个棘手的竞争条件:在线程B奔溃导致线程A退出前获取代码执行.
我们将试图让内存破坏漏洞破坏Vector.\对象的头,增大其长度字段,使其超过对象真实长度,从而获取越界读写的能力,这在Flash漏洞利用中很常见。Vector.\对象中的数据直接分配在对象头之后,在此特定漏洞里,错误的memmove()最终总是将内存中的数据向后移动2048字节。因此,如果我们分配一个足够大的Vector.\,我们就能够用对象的数据部分破坏对象的头:
通过偏移和对齐可以计算出,第508个uint数据元素将放置在对象头部的长度字段。
0x02 控制Flash堆内存
我们太忘乎所以了,需要记住线程的内存破坏发生在memmove()的wild copy中。即使我的笔记本电脑也能够以每秒大约10千兆字节的速度复制内存,所以我们将需要大量的堆内存用于复制,以使我们在复制操作访问未映射页之前,有足够的时间完成漏洞利用。得到一个大而连续的堆映射听起来很容易,但事实并非如此。如果你只是天真地在Flash堆中喷射了512KB的ByteArray缓冲区,你会看到大量的进程映射,它们看起来像这样:
7fd138039000-7fd138679000 rw-p 00000000 00:00 0
7fd138679000-7fd139039000 ---p 00000000 00:00 0
7fd139039000-7fd139fb9000 rw-p 00000000 00:00 0
7fd139fb9000-7fd13a039000 ---p 00000000 00:00 0
7fd13a039000-7fd13ae79000 rw-p 00000000 00:00 0
7fd13ae79000-7fd13b039000 ---p 00000000 00:00 0
[...]
乍一看,作为安全专业人员,我们可能会说“太棒了!保护页!”,不过,我们这里看到的是意外的无效(accidental inefficiency) Flash堆。当试图向前扩展堆的限制时,会因为另一个映射的存在而失败,它将由操作系统决定在哪放置新的映射。默认的Linux启发式行为恰好是“在最后一次成功的映射前”,所以Flash堆将进入不能向前扩展堆的永久循环。“保护页”来自一个事实,即Flash堆(64位)将以16MB的块来保留地址空间,并根据需要以128KB为粒度来提交该区域。一系列提交的请求加起来,提交的区域也不会完全等于16MB,这很正常。这在所述的地址空间中留下了洞。这样的洞会在漏洞利用完成之前导致崩溃!
为了避免这一切,尝试获取一个可向前生长的堆,我们遵循以下步骤:
- 分配1GB大小的对象,这将被放置在当前堆区域之前。
- 在内存中喷射 4KB大小的内存(Flash堆块大小)来填冲现有的空闲块。
- 喷射更多4KB大小的内存导致堆向后延伸,最终在1GB对象之前分配一个新的16MB区域。
- 释放1GB大小的对象。
如果一切顺利,堆将是下面这个样子,绿色代表不间断增长的线性空间:
| committed: 1MB | reserved: 15MB | 1GB hole | committed: ~16MB | ...
0x03 通过堆操作对漏洞利用的对象进行布局
现在的堆状态很好,可以用来在连续的空间里构造重要的对象,就像这样:
红色是我们在步骤1中决定破坏的vector,而绿色是一个对象,我们会刻意释放掉,现在堆已经建立。在开始之前,我们需要提防更多的堆特性:如堆的扩展,堆的元数据也被扩展,以便描述所有新的4KB块。堆的元数据的扩展过程将在堆内部创建空闲的块区域。这不会是一个问题,但对于另一个特性:当一个块返回给堆空闲列表时,它不是以MRU(most recently used)顺序重新分配的。因此,我们通过另一个堆喷射确保所有8KB(2块)的堆块都被分配,使特定的空闲列表为空。然后,我们释放中间的8KB缓冲区(绿色部分),使空闲堆块处于有用的地方。由于在其两侧都分配了8KB缓冲区,释放的块不会被合并,将成为下一个8KB内存分配的可选选项。
在这些堆操纵完成之后,内存中留下了一系列线性排列的整齐的重要对象,以及前面已经释放掉的8KB堆块,它将在下一次分配中被重用。
0x04 触发漏洞
值得一提的是,特定平台上声音子系统如何初始化也存在差异。在某些平台上,play()被第一次调用时,子系统初始化并异步启动。对于Chrome Flash插件,并不是这样。如果你看一下漏洞利用,可以看到我们通过播放不同的声音来初始化子系统,然后返回到主事件循环。这样可以确保第二次调用play()最终触发异步wild copy。
由于堆已经布局好,我们通过播放G711声音文件触发memmove()。wild copy将立即开始,并以异步方式进行,所以我们需要继续进行!G711声音解码器对象,约4424字节,但堆分配粒度在sizes >= 4096时是4096,该对象将被放置在8192大小的块中 -- 特别是如果一切正常的话,它正好落在我们刚刚释放的缓冲区里。大范围的堆损坏将在这个对象内部开始并继续向前,很快破坏掉相邻的线性排列的对象,包括矢量Vector.<uint>
和Vector.<Object>
这一刻已经触发了漏洞,我们在ActionScript循环中等待Vector.\的长度从2000改为0xfee1dead。
0x05 绕过ASLR
当我们可以在Vector.<uint>
对象范围之外读取和写入任意4字节的值时,我们就检测到了损坏的Vector.<uint>
。在步骤3中的堆布局中,与Vector.<uint>
对象缓冲区相邻的对象是Vector.<Object>
,这是很理想的布局,因为它包含了我们要读取的两个重要东西:
-
指向Flash库的一个虚函数表指针的值,这让我们可以定位代码位置。
-
指向自身的指针,这很重要,因为它使我们能够把一个相对读/写转换为绝对读/写。
0x06 无ROP的代码执行
该漏洞利用并不通过ROP链,而是通过重写GOT / PLT中的一些函数指针。(注意,这依赖于RELRO是否存在,在常见的Linux版本和二进制文件中它很少见。如果RELRO出现了,相应的我们很有可能只针对虚函数表的指针进行重写)有了虚函数表的地址,从它开始在某个固定偏移上就能得到GOT / PLT函数指针。有相当优雅和简单的方法来让我们的代码执行:
-
读取memmove()的函数指针,这个我们已经解决了。
-
计算__libc_system的地址,它在我们刚刚读取的__memmove_ssse3_back的某个固定偏移处。
-
将__libc_system的地址写入munmap()的函数指针处。
这种设置的效果是,当程序下一次调用munmap(buffer),它实际上将调用system(buffer)。这是有用的,因为大的ByteArray缓冲区由mmap()/munmap()提供,而且它被释放的时候,我们可以控制缓冲区中的确切内容。因此,我们可以向system()中传递我们想要的任意字符串。
0x07 继续执行与沙箱
为了说明现在我们对进程的控制,我们给system()的有效负载是“killall -STOP chrome; gnome-calculator -e 31337”。这具有两个作用:
-
显示计算器来证明我们执行了任意代码。
-
发送STOP信号给现有的Chrome进程。这将在崩溃前,有机会终止memmove()。
如果我们附加到终止的进程,观察wild copy线程,它已经成功地复制了50MB数据,即使我们试图让攻击尽可能的快和简单。
对于不希望留下任何痕迹的漏洞利用,可以很容易地连接到线程,在复制的过程中停止复制,而无需杀死线程。此外,在漏洞利用过程中发生的内存破坏可以容易地恢复。
当然,沙箱逃逸留下作为练习。该system()库调用将无法在Chrome沙盒中工作;展示利用的时候,我们使用了--no-sandbox标志,以便专注于远程代码执行。沙盒里的漏洞利用可能会通过对函数指针进行重行连接,通过异常消息来攻击IPC通道,或部署ROP链攻击内核。
0x08 结束语
与往常一样,Project Zero建议将所有最初的内存破坏都假定为可利用。正如我们在之前博客中展示的
glibc off-by-one NUL byte corruption
最初看起来无法利用,通过一两招就转化成了可利用的。
wild copy型漏洞被认为不可利用已经有了悠久的历史,但基于我们这里的研究,我们认为在复杂的多线程软件中(浏览器,浏览器插件)wild copy型漏洞可以通过parallel thread corruption的方法进行利用。我们公布这一发现,希望开发者和厂商认识到wild copy型漏洞比以前认为的更严重。