1. 调试环境
OS
Windows 10
Firefox_Setup_59.0.exe
SHA1: 294460F0287BCF5601193DCA0A90DB8FE740487C
Xul.dll
SHA1: E93D1E5AF21EB90DC8804F0503483F39D5B184A9
2. 补丁信息
Mozilla对该漏洞的补丁为Bug 1446062。
在本次pwn2own 2018中使用的漏洞对应的CVE为CVE-2018-5146。
从安全公告中能看出漏洞出现在libvorbis这一第三方多媒体库中,下面就先介绍一下与这个第三方多媒体库的相关信息。
3. Ogg 及 Vorbis
3.1. Ogg
Ogg是一个自由且开放标准的多媒体文件格式,由Xiph.org基金会所维护。
在格式上,它主要由下面两个特点:
1. 一个Ogg文件主要由若干个Ogg Page组成;
2. Ogg Page分为Ogg Header 与 Segment Table两个部分。
Ogg Page 的结构如下图所示:
图1 Ogg Page结构
3.2. Vorbis
Vorbis是一种有损音讯压缩格式,同样由Xiph.org基金会所维护。
在一个Ogg文件中,Vorbis相关数据会被封装在各个Ogg Page的Segment Table中,具体的封装步骤可以参考MIT的相关文档。
3.2.1. Vorbis Header
在Vorbis标准中,一共有三个种类的Vorbis Header,对于同一个Vorbis bitstream而言,三个头部都必须要出现,缺一不可。这三个Vorbis Header分别是:
Vorbis Identification Header
主要定义了Ogg文件中所包含的bitstream为Vorbis格式。其中还包含了Vorbis版本、对应的bitstream的基础音频信息,如channel数量、码率等。
Vorbis Comment Header
主要包含了一些用户文本注释,比如对应bitstream的提供者等文本信息。
Vorbis Setup Header
主要包含了一下用于设置编码解码器的信息,如完整的向量以及解码所需的霍夫曼码表等。
从标准中能够看得出与漏洞相关的信息肯定主要存在于Vorbis Identification Header
与Vorbis Setup Header中。下面简单地介绍一下两个头部的内部结构。
3.2.2. Vorbis Identification Header
Vorbis Identification Header内部结构相对简单,如下图所示:
图2 Vorbis Identification Header 结构
3.2.3. Vorbis Setup 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 结构
3.2.3.1. Vorbis CodeBook
根据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结构。
3.2.3.2. Vorbis Time
在当前的Vorbis标准中这一数据结构只是充当一个占位符的作用,其中的每一个TimeBackend结构中的数据应全为0。
3.2.3.3. Vorbis Floor
在当前标准中,定义了两种不同的FloorBackend结构,但是因为其于实际的漏洞关系不大,就不做展开介绍了。
3.2.3.4. Vorbis Residue
在当前的标准中,定义了三种不同的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结构同实际的漏洞关系不大,也不做展开介绍了。
4. 补丁分析
4.1. Patched Function
在补丁当中一共在三个不同的函数中对循环的条件增加了限制,对应到上文所描述到的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 计算得出来的。
4.2. Buffer – a
通过对代码进行回溯,能看到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堆中的。
4.3. Buffer – t
通过对代码的回溯,能够看到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堆中。
4.4. Cola Time
所以现在,能够确认在漏洞发生的时候,涉及到的两个buffer以及循环控制变量如下:
a
位于mozJemalloc堆;
大小可控。
t
位于mozJemalloc堆;
内容可控。
book->dim
内容可控。
结合漏洞,相当于在mozJemalloc堆中,可以进行一个内容可控、偏移可控的写操作。
那么a的大小可控又能存在怎样的利用点呢?这就需要我们再对mozJemalloc的实现进行探讨。
5. mozJemalloc
mozJemalloc是Mozilla基于Jemalloc二次开发的堆管理器。
可以通过下列的全局变量访问到相关的数据结构:
gArenas
mDefaultArena
mArenas
mPrivateArenas
gChunkBySize
gChunkByAddress
gChunkRTress
在mozJemalloc中,堆中的内存首先会被划分为若干个Chunk,而这些Chunk会被不同的Arena给组织、管理起来。用户所申请到的内存块必然是位于某一个Chunk当中,这一些内存块被称为Region。
每一个Chunk中又会细分为不同的大小的Run,每一个Run通过一个bitmap结构来记录、管理其内部的Region的使用状态。
5.1. Arena
在mozJemalloc中,每一个Arena都会分配到一个id,在之后的内存使用中,可以通过id快速地获取到对应的Arena结构。
在Arena中还存在一个特殊的结构mBin,它是一个数组结构,其中的每一项都对应着一个arena_bin_t结构,而这一结构又管理着一个特定大小的内存块使用情况,大小范围从0x10到0x800的内存块均通过这一结构来进行快速地使用。
mBin中所使用的Run在内存中并不一定是连续的,其内部通过一个红黑树来管理其所使用的Run。
需要注意的是在JS::Nursery堆中也存在Arena的概念,但是两者是不同的。
5.2. Run
每一个Run除了第一个Region用于存储对应的Run管理信息之外,余下的所有Region均是可以被申请使用的,并且同一个Run中所有的Region大小相同。
在向Run申请Region的时候,它会返回最为靠近Run头部的第一个可用Region。
5.3. Arena Partition
在当前的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)。
Exploit
下面介绍一下在上述的基础上,如何对该漏洞进行利用。
6.1. 构建Ogg文件
首先需要依据标准,构建出一个能够触发漏洞的Ogg文件,在本文中使用的Ogg文件部分数据截图如下:
图4 恶意Ogg文件部分数据
能够看到在这里使用的codebook->dim的大小为0x48。
6.2. Heap Spary
首先通过在JavaScript中大量地申请合适大小的Array,将mozJemalloc中对应的mBin中的可用内存耗尽,迫使mozJemalloc为对应的mBin申请新的Run。
然后将这些Array交错地进行释放,这样在对应的mBin中就会存在许多的hole。但是因为无法对原始的mBin的内存布局进行预判,加之在释放过程之中也会存在其他对象的申请、释放,所以在释放完成之后,hole未必是交错的分布在mBin中,有可能会存在连续的hole。这样依照mozJemalloc的分配原则,会导致申请到的内存块之后仍然是一个hole。
为了避免这一情况,在进行完交错释放之后,还需要对mBin进行一些补偿操作来使得mBin中的内存布局更为可靠。
6.3. 修改Array长度
在完成了堆喷的工作之后,再在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 ]
6.4. Cola time again
整理一下现在所能够控制的对象,以及能够进行的操作:
Array_length_modified
越界读
越界写
ArrayBuffer_contents_modified
合法读
合法写
如果我们尝试通过Array_length_modified来直接进行内存数据的泄露的话,因为SpiderMonkey中tagged value的使用,导致我们读出的非法值会在JavaScript中修正为NaN。
但是,通过Array_length_modified来对ArrayBuffer_contents_modified进行赋值,而后使用ArrayBuffer_contents_modified来进行读写的话,就能够对任意的JavaScript对象的引用指针进行泄露。
6.5. Fake JSObject
通过对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链,就能够获取到执行任意代码的执行能力了。
6.6. Pop Calc?
最后来可执行内存时,相关的Context如下所示:
图6 到达任意可执行代码
相关的内存信息如下所示:
图7 相关内存地址信息
但是因为Firefox release 版本已经启用了Sandbox,所以在运行Shell Code的时候直接通过CreateProcess去创建calc.exe的进程的话,会被Sandbox给拦下来。
所以最后没能够弹计算器,算是一个小遗憾。
7. 参考文献
Firefox Source Code
OR’LYEH? The Shadow over Firefox by argp
Exploiting the jemalloc Memory Allocator: Owning Firefox’s Heap by argp,haku
QUICKLY PWNED, QUICKLY PATCHED: DETAILS OF THE MOZILLA PWN2OWN EXPLOIT by thezdi