转载

How to kill a (Fire)fox

Pwn2own 2018 Firefox case study

Author: Hanming Zhang from 360 vulcan team

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 的结构如下图所示:

How to kill a (Fire)fox

图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内部结构相对简单,如下图所示:

How to kill a (Fire)fox

图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的总体结构如下图所示:

How to kill a (Fire)fox

图 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文件部分数据截图如下:

How to kill a (Fire)fox

图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)。

How to kill a (Fire)fox

图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如下所示:

How to kill a (Fire)fox

图6 到达任意可执行代码

相关的内存信息如下所示:

How to kill a (Fire)fox

图7 相关内存地址信息

但是因为Firefox release 版本已经启用了Sandbox,所以在运行Shell Code的时候直接通过CreateProcess去创建calc.exe的进程的话,会被Sandbox给拦下来。

所以最后没能够弹计算器,算是一个小遗憾。

7. 参考文献

  1. Firefox Source Code
  2. OR’LYEH? The Shadow over Firefox by argp
  3. Exploiting the jemalloc Memory Allocator: Owning Firefox’s Heap by argp,haku
  4. QUICKLY PWNED, QUICKLY PATCHED: DETAILS OF THE MOZILLA PWN2OWN EXPLOIT by thezdi
原文  http://blogs.360.cn/blog/how-to-kill-a-firefox/
正文到此结束
Loading...