Pwn2own 2018 Firefox case study
Author: Hanming Zhang from 360 vulcan team
Mozilla对该漏洞的补丁为Bug 1446062。
在本次pwn2own 2018中使用的漏洞对应的CVE为CVE-2018-5146。
从安全公告中能看出漏洞出现在libvorbis这一第三方多媒体库中,下面就先介绍一下与这个第三方多媒体库的相关信息。
Ogg是一个自由且开放标准的多媒体文件格式,由Xiph.org基金会所维护。
在格式上,它主要由下面两个特点:
1. 一个Ogg文件主要由若干个Ogg Page组成;
2. Ogg Page分为Ogg Header 与 Segment Table两个部分。
Ogg Page 的结构如下图所示:
图1 Ogg Page结构
Vorbis是一种有损音讯压缩格式,同样由Xiph.org基金会所维护。
在一个Ogg文件中,Vorbis相关数据会被封装在各个Ogg Page的Segment Table中,具体的封装步骤可以参考MIT的 相关文档 。
在Vorbis标准中,一共有三个种类的Vorbis Header,对于同一个Vorbis bitstream而言,三个头部都必须要出现,缺一不可。这三个Vorbis Header分别是:
从标准中能够看得出与漏洞相关的信息肯定主要存在于Vorbis Identification Header
与Vorbis Setup Header中。下面简单地介绍一下两个头部的内部结构。
Vorbis Identification Header内部结构相对简单,如下图所示:
图2 Vorbis Identification Header 结构
Vorbis Setup Header的内部结构相对于其他两个头结构来说要相对复杂,其内部包含了多个子结构。
在“vorbis”之后的第一个字节标记着CodeBooks的数量,而后跟着对应数量的CodeBook结构。在最后一个CodeBook结束后的第一个字节,标记着TimeBackends的数量,后续对应的TimeBackend结构。在最后一个TimeBackend结束后的第一个字节,标记着FloorBackends的数量,后续对应的FloorBackend结构。在最后一个FloorBackend结束后的第一个字节,标记着ResiduesBackends的数量,后续对应的ResiduesBackend结构。在最后一个ResiduesBackend结束后的第一个字节,标记着MapBackends的数量,后续对应的MapBackend结构。在最后一个MapBackend结束后的第一个字节,标记着Modes的数量,后续对应的Mode结构。
简单地来说Vorbis Setup Header的总体结构如下图所示:
图 3 Vorbis Setup Header 结构
根据Vorbis标准中所述,CodeBook的结构如下:
byte 0: [ 0 1 0 0 0 0 1 0 ] (0x42) byte 1: [ 0 1 0 0 0 0 1 1 ] (0x43) byte 2: [ 0 1 0 1 0 1 1 0 ] (0x56) byte 3: [ X X X X X X X X ] byte 4: [ X X X X X X X X ] [codebook_dimensions] (16 bit unsigned) byte 5: [ X X X X X X X X ] byte 6: [ X X X X X X X X ] byte 7: [ X X X X X X X X ] [codebook_entries] (24 bit unsigned) byte 8: [ X ] [ordered] (1 bit) byte 8: [ X 1 ] [sparse] flag (1 bit)
在头部结构结束后就是对应于codebook_entries长度的length_table数组,依据不同的flag设置,数组内部元素的长度可能为5 bit或者6 bit。
再接下来是向量相关的结构:
[codebook_lookup_type] 4 bits [codebook_minimum_value] 32 bits [codebook_delta_value] 32 bits [codebook_value_bits] 4 bits and plus one [codebook_sequence_p] 1 bits
而后是长度为codebook_dimensions*codebook_entrues的向量表,表中的元素大小对应于codebood_value_bits。
需要注意的是,codebook_delta_value 及 codebook_minimum_value两个数据会作为float类型对数据进行解析。但是为了对不同的平台进行支持,Vorbis标准中自行定义了一个float格式,再通过对应系统的相关数学函数转换为对应平台上的float数据。在Windows下,该过程会先解释成为double类型再转化为float类型。
以上的所有格式构成了一个完整的CodeBook结构。
在当前的Vorbis标准中这一数据结构只是充当一个占位符的作用,其中的每一个TimeBackend结构中的数据应全为0。
在当前标准中,定义了两种不同的FloorBackend结构,但是因为其于实际的漏洞关系不大,就不做展开介绍了。
在当前的标准中,定义了三种不同的ResidueBackend结构,其中不同的结构在后续的解码过程中会调用不同的解码函数,它的结构如下图所示:
[residue_begin] 24 bits [residue_end] 24 bits [residue_partition_size] 24 bits and plus one [residue_classifications] = 6 bits and plus one [residue_classbook] 8 bits
其中的residue_classbook描述了在这一ResidueBackend在解码过程中所使用的CodeBook结构。
余下的MapBackend与Mode结构同实际的漏洞关系不大,也不做展开介绍了。
在补丁当中一共在三个不同的函数中对循环的条件增加了限制,对应到上文所描述到的Vorbis结构中,三类ResidueBackend对应于三个不同的解码函数,所以可以猜想这一个漏洞应该与ResidueBackend结构有关系。
通过ZDI的Blog,能够知道在Pwn2Own 2018 Firefox项目中所使用的漏洞存在如下述函数中:
/* decode vector / dim granularity gaurding is done in the upper layer */ long vorbis_book_decodev_add(codebook *book, float *a, oggpack_buffer *b, int n) { if (book->used_entries > 0) { int i, j, entry; float *t; if (book->dim > 8) { for (i = 0; i < n;) { entry = decode_packed_entry_number(book, b); if (entry == -1) return (-1); t = book->valuelist + entry * book->dim; for (j = 0; j < book->dim;) a[i++] += t[j++]; } } else { // blablabla } } return (0); }
能够看到在其中一个分支中,存在一个嵌套循环,但是内层循环所使用的循环条件与外层循环没有关系,也没有进行限制,所以导致了在内层循环中导致了循环结束条件的绕过,造成了一个越界写漏洞。
对应到代码中,就是说当book->dim > n 的时候,会导致 a[i++] += t[j++] 中的 i > n ,从而造成了对a的一个越界写。在代码中,能看到a是作为一个参数传入到漏洞函数的,而t则是通过book->valuelist + entry * book->dim 计算得出来的。
通过对代码进行回溯,能看到a如在如下代码进行初始化的:
/* alloc pcm passback storage */ vb->pcmend=ci->blocksizes[vb->W]; vb->pcm=_vorbis_block_alloc(vb,sizeof(*vb->pcm)*vi->channels); for(i=0;ichannels;i++) vb->pcm[i]=_vorbis_block_alloc(vb,vb->pcmend*sizeof(*vb->pcm[i]));
其中的vb->pcm[i] 即为之后作为参数进入漏洞函数的a,并且它是通过_vorbis_block_alloc函数进行的内存分配,分配的内存块大小为vb->pcmend*sizeof(*vb->pcm[i]),其中vb->pcmend又来自于ci->blocksizes[vb->W],而ci->blocksizes在Vorbis Identification Header中定义。所以所分配的内存块的大小为0x8* ci->blocksizes[vb->W],也就是说该内存块的大小是可控的。
再对_vorbis_block_alloc进分析,发现存在如下的调用链 _vorbis_block_alloc -> _ogg_malloc -> CountingMalloc::Malloc -> arena_t::Malloc,所以最终内存分配到的内存块是位于mozJemalloc堆中的。
通过对代码的回溯,能够看到book->valuelist在如下代码进行了赋值:
c->valuelist=_book_unquantize(s,n,sortindex);
而_book_unquantize的内部逻辑如下
float *_book_unquantize(const static_codebook *b, int n, int *sparsemap) { long j, k, count = 0; if (b->maptype == 1 || b->maptype == 2) { int quantvals; float mindel = _float32_unpack(b->q_min); float delta = _float32_unpack(b->q_delta); float *r = _ogg_calloc(n * b->dim, sizeof(*r)); switch (b->maptype) { case 1: quantvals=_book_maptype1_quantvals(b); // do some math work break; case 2: float val=b->quantlist[j*b->dim+k]; // do some math work break; } return (r); } return (NULL); }
所以book->valuelist就是对应的CodeBook结构中的向量进行解码之后的data,同样它也位于mozJemalloc堆中。
所以现在,能够确认在漏洞发生的时候,涉及到的两个buffer以及循环控制变量如下:
结合漏洞,相当于在mozJemalloc堆中,可以进行一个内容可控、偏移可控的写操作。
那么a的大小可控又能存在怎样的利用点呢?这就需要我们再对mozJemalloc的实现进行探讨。
mozJemalloc是Mozilla基于Jemalloc二次开发的堆管理器。
可以通过下列的全局变量访问到相关的数据结构:
在mozJemalloc中,堆中的内存首先会被划分为若干个Chunk,而这些Chunk会被不同的Arena给组织、管理起来。用户所申请到的内存块必然是位于某一个Chunk当中,这一些内存块被称为Region。
每一个Chunk中又会细分为不同的大小的Run,每一个Run通过一个bitmap结构来记录、管理其内部的Region的使用状态。
在mozJemalloc中,每一个Arena都会分配到一个id,在之后的内存使用中,可以通过id快速地获取到对应的Arena结构。
在Arena中还存在一个特殊的结构mBin,它是一个数组结构,其中的每一项都对应着一个arena_bin_t结构,而这一结构又管理着一个特定大小的内存块使用情况,大小范围从0x10到0x800的内存块均通过这一结构来进行快速地使用。
mBin中所使用的Run在内存中并不一定是连续的,其内部通过一个红黑树来管理其所使用的Run。
需要注意的是在JS::Nursery堆中也存在Arena的概念,但是两者是不同的。
每一个Run除了第一个Region用于存储对应的Run管理信息之外,余下的所有Region均是可以被申请使用的,并且同一个Run中所有的Region大小相同。
在向Run申请Region的时候,它会返回最为靠近Run头部的第一个可用Region。
在当前的mozilla-central的代码分支中,在JavaScript中的内存使用均是通过moz_arena_x系列函数来进行的,而这些函数在使用内存的时候,会通过一个全局的id来获取到对应的Arena,现在是固定使用id为1的Arena来作为JavaScript的专用堆。
而在mozJemalloc中还存在着PrivateArena和非PrivateArena两种Arena的类型,其中id为1的Arena会被归类到PrivateArena中,且其他的Arena则会被归类为非PrivateArena中。这样就使得我们在JavaScript中所申请到的内存与其他组件所使用的内存不存在同一个Arena中,这样就造成了一种类似于隔离堆的效果。
但是存在漏洞的windows平台下的Firefox 59.0中,并不存在PrivateArena,这也就使得不同的对象有可能分配到同一个Arena上,为该漏洞的利用提供了前提条件。(笔者在最开始调试的时候使用的是Linux版本下的opt+debug版本,因为Arena Partition的存在,只将该漏洞利用到了info leak)。
下面介绍一下在上述的基础上,如何对该漏洞进行利用。
首先需要依据标准,构建出一个能够触发漏洞的Ogg文件,在本文中使用的Ogg文件部分数据截图如下:
图4 恶意Ogg文件部分数据
能够看到在这里使用的codebook->dim的大小为0x48。
首先通过在JavaScript中大量地申请合适大小的Array,将mozJemalloc中对应的mBin中的可用内存耗尽,迫使mozJemalloc为对应的mBin申请新的Run。
然后将这些Array交错地进行释放,这样在对应的mBin中就会存在许多的hole。但是因为无法对原始的mBin的内存布局进行预判,加之在释放过程之中也会存在其他对象的申请、释放,所以在释放完成之后,hole未必是交错的分布在mBin中,有可能会存在连续的hole。这样依照mozJemalloc的分配原则,会导致申请到的内存块之后仍然是一个hole。
为了避免这一情况,在进行完交错释放之后,还需要对mBin进行一些补偿操作来使得mBin中的内存布局更为可靠。
在完成了堆喷的工作之后,再在mozJemalloc堆上通过_ogg_malloc进行内存申请,就有机会形成如下所示的内存状态:
|———————contiguous memory —————————|
[ hole ][ Array ][ ogg_malloc_buffer ][ Array ][ hole ]
然后再出发越界写的操作,就能够将尚未被释放的某一个Array的长度进行改写,这样我们就有了一个在mozJemalloc堆上的、能够越界读的Array对象。
接来下,再向mozJemalloc堆上申请大量的ArrayBuffer对象,这样就有机会形成如下所示的内存状态:
|——————————-contiguous memory —————————|
[ Array_length_modified ][ something ] … [ something ][ ArrayBuffer_contents ]
在上述的情况下,能够通过Array越界将内容写到某一个ArrayBuffer中,形成如下的内存状态:
|——————————-contiguous memory —————————|
[ Array_length_modified ][ something ] … [ something ][ ArrayBuffer_contents_modified ]
整理一下现在所能够控制的对象,以及能够进行的操作:
如果我们尝试通过Array_length_modified来直接进行内存数据的泄露的话,因为SpiderMonkey中tagged value的使用,导致我们读出的非法值会在JavaScript中修正为NaN。
但是,通过Array_length_modified来对ArrayBuffer_contents_modified进行赋值,而后使用ArrayBuffer_contents_modified来进行读写的话,就能够对任意的JavaScript对象的引用指针进行泄露。
通过对JavaScript对象的引用指针进行泄露与赋值,能够在内存中构造出如下的Fake JSObject,并且通过对这一个对象,能够对一个地址进行写操作。(为了能够更好地观察到这一个现象,在这里之后默认已经关闭了baselineJIT)。
图5 Fake JavaScript Object
然后在JavaScript中申请两个大小相同的ArrayBuffer,使得它们俩在JS::Nursery堆中处于连续的内存中,如下图所示:
|———————contiguous memory —————————|
[ ArrayBuffer_1 ]
[ ArrayBuffer_2 ]
然后通过上面描述的Fake JSObject将ArrayBuffer_1的metadata进行修改,使得内存状态在逻辑上变为下图所示的情况:
|———————contiguous memory —————————|
[ ArrayBuffer_1 ]
[ ArrayBuffer_2 ]
这样在逻辑上,就能够对任意地址进行读写了。
在获取到对任意地址进行读写的能力之后,已经没有太过于困难的事情了。
在xul.dll中构建ROP链,就能够获取到执行任意代码的执行能力了。
最后来可执行内存时,相关的Context如下所示:
图6 到达任意可执行代码
相关的内存信息如下所示:
图7 相关内存地址信息
但是因为Firefox release 版本已经启用了Sandbox,所以在运行Shell Code的时候直接通过CreateProcess去创建calc.exe的进程的话,会被Sandbox给拦下来。
所以最后没能够弹计算器,算是一个小遗憾。