Blogger Information
Blog 142
fans 5
comment 0
visits 130005
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
How to kill a (Fire)fox
php开发大牛
Original
692 people have browsed it

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


Statement of this Website
The copyright of this blog article belongs to the blogger. Please specify the address when reprinting! If there is any infringement or violation of the law, please contact admin@php.cn Report processing!
All comments Speak rationally on civilized internet, please comply with News Comment Service Agreement
0 comments
Author's latest blog post