这篇文章将会探索一下CVE-2016-9066 ,这是Firefox中一个简单却有趣(从实际操作的角度来看)的漏洞,可以利用该漏洞来获取代码执行权限。
一段负责加载脚本标签的代码中的一个整数溢出导致了对mmap结束块的越界写操作。一种利用方法是在缓冲区后放置一个JavaScript堆以便便溢出到它的元数据中来创建一个假的未使用堆单元。然后可以将一个ArrayBuffer实例放置在另一个ArrayBuffer的内联数据中。ArrayBuffer对象内部可以被任意修改,就产生了任意读/写原语。至此,实现代码执行就变的非常简单了。完整的exploit可以在此处 找到,并针对macOS 10.11.6平台上的Firefox 48.0.1进行了测试。bugzilla报告可在此处 找到。
以下代码 用来加载外部脚本标签:
当服务器中的新数据到达时这段代码将被OnIncrementalData
调用。这是一个简单的整数溢出Bug,当服务器发送超过4GB数据的时候即可发生。数据超过4GB的情况下,capacity
将会环回(wrap around),接下来的对mBuffer.reserve
函数的调用并不会修改缓冲区。接下来mDecode->Convert
函数将会把数据写入到8GB缓冲区的尾部(数据在浏览器中以char16_t的形式存储),这部分内存将会通过mmap块备份(对于一个非常大的块这是通用做法)。
修补也是相当简单:
这个漏洞第一眼看上不并没有什么搞头。它有一个必要条件,需要发送和申请多大几个GB的数据。正如我们即将看到的,该漏洞在我的2015 MacBook Pro上相当可靠的被利用,完成整个利用代码只需打开页面用时不到1分钟。我们接下来先来探索一下此漏洞为什么会在macOS上被利用并弹出一个计算器,然后我们来改进一下利用代码让它变的更可靠一些,并且占用更低的带宽(剧透:我们将会使用HTTP压缩)
当超过mmap区的溢出发生时,我们首先关注的是有没有可能在溢出的内存之后可靠地申请一块空间。与一些堆分配器相反,mmap(可以看作是内核提供的内存分配器)是非常具有确定性的:如果没有合适的内存块,调用mmap两次将会导致两次连续的内存映射。你可以用下边的代码来尝试一下。注意,实验的结果的异同取决于代码是运行在Linux系统还是macOS系统上。mmap内存区相较于macOS系统上由低向高增长,在Linux系统上,是由高向低增长。在本篇文章接下来的部分,我们将会专注于macOS。Linux或者Windows上应该也可能存在相似的利用代码。
上边的程序向我们展示了通过简单的映射所有的内存页直到所有已经存在的分页被填满,然后通过mmap再申请一块内存块。为了验证这个过程,我们接下来将会这样做:
为了实现这个过程,我们需要一些浏览器中的JavaScript代码和服务器之间的同步原语。为此,我在python的asyncio库智商写了一个小小的webserver,它包含一个方便的Event对象,用来和协同程序同步。创建两个全局事件可使客户端代码完成当前任务等待webserver进行下一步的操作时通知服务器。/sync
的处理例程如下所示:
客户端中我是用了同步XMLHttpRequests来阻塞脚本的执行,直到服务器完成相关工作:
这样,我们就可以实现上边的场景并且将会看到实际上有一个ArrayBuffer对象的开始处包含了我们的payload字节。不过还有一个小小的限制条件:我们只能通过有效的UTF-16来进行溢出,因为这是Firefox内部使用的。我们必须记住这一点。现在剩下的就是找到一些更有趣的事情,用内存分配来取代对ArrayBuffer的溢出。
因为malloc
(同样的C++中的new
操作)将在某些时候使用mmap请求更多的内存,所以像这些操作分配的内存可能是我们代码所感兴趣的。我走了一条不同的路线。最初我想检测一下是否有可能溢出到JavaScript对象中,比如说使数组的或者其他类似对象的长度腐败。为此,我开始围绕着JavaScript分配器深入发掘,来看JSObject被存储在哪里。Spidermonkey(Firefox中的JavaScript引擎)把JSObjet存储在两个独立的区域中:
对Spidermonkey内部更深入的讨论可以参见phrack上的这篇文章 。
永久堆中存储对象的容器叫做Arenas:
注释已经给出了非常好的总结:Arenas 只是简单容器对象,其中分配了相同大小的JavaScript对象。它们位于容器对象内,这个块结构 本身就是直接使用mmap来分配的。在Arena类中有趣的部分是它的firstFreeSpan
成员:它是Arena对象(因此处于一个映射区的开始处)的第一个成员,并且本质上它指明了Arena中第一个未使用区块的索引。下边就是FreeSpan 的大致结构:
其中的first和last在Arena中都是按字节索引,用来指明未使用区块链的头部。那么这就开辟了一条有趣的道路来利用这个漏洞:通过对Arena中firstFreeSpan
成员对象的溢出,我们有可能在另一个对象中分配一个对象,最好是在某些可访问的内联数据中分配。那么接下来我们就能任意的修改内部分配的对象。
这个技巧有一下几点好处:
事实证明,ArrayBuffer对象中高达96字节的数组会被内联存储在该对象头部之后后。这将会跳过托管过程 并且因此它将会在Arena中被分配。这使得它们成为我们漏洞利用的理想选择。我们会这样做:
不幸的是,这并不是那样简单:为了让Spidermonkey在我们的目标Arena(被破坏)中申请一个对象,那么这个Arena就必须在之前就被(部分)标记为可使用。这意味着,我们需要释放所有的Arena中至少一个存储块。我们可以通过删除每第25个ArrayBuffer(每个Arena有25个)来实现,然后强制进行垃圾回收。
Spidermonkey因为各种各样 的 原因 而触发垃圾回收。诸多方法中似乎使用TOO_MUCH_MALLOC
来触发是最简单的一种:只要通过malloc分配了一定数量的字节,它就会被简单的触发。因此,下边的代码足以用来触发垃圾回收:
在此之后,我们的目标Arena将会被放置在一个未使用标记链表中,随后的覆盖将会破坏该链表。下一次从被破坏的Arena中产生的分配将会返回一个假的处于一个ArrayBuffer对象的内联数据中内存块。
实际上,这有点复杂了。存在一种叫做压缩GC模式的垃圾回收,这种模式将会把多个部分填充的Arena 移动去填满另一个Arena。此举减少了内部碎片,并且协助释放整个内存区域以便系统回收。不管怎样,对于我们来讲,压缩GC着实是个麻烦,因为,它有可能填充了我们之前创建的目标Arena。以下的代码用来决定是否应该运行一个压缩GC:
查看一下代码,应该有方法来阻止压缩GC来运行(比如说,展示一些动画)。看来我们很幸运:上文中提到的gc
函数(译注:用来产生ArrayBuffer的JS代码片段)将会在Spidermonkey中触发下边的代码流程,因此,阻止压缩GC的调用形式将会从GC_SHRINK
变为GC_NORMAL
。
此刻,我们已经拼接了所有的碎片,可以实际动手写利用代码了。一旦我们创建了一个假的自由存储块并在其中创建一个ArrayBuffer对象,就能看见其中一个之前申请的ArrayBuffer中包含了我们的数据。ArrayBuffer对象的结构大致如下:
常量XXX_SLOT
确定对象的起始位置相应值的偏移量。这样一来,数据指针(DATA_SLOT
)将会被存储在addrof(ArrayBuffer) + sizeof(ArrayBuffer)
。
现在,我们就可以构建以下的代码利用原语:
为了避免浏览器进程在下一次垃圾回收中崩溃,我们必须修复一下几点:
以上几点就足以保证浏览器在跑完利用代码之后仍然存活了。
剩下的工作就是以某种方式弹出一个计算器。
一个简单的跑自己代码的方式是嘿咻JIT 了,但是,这个技术(部分)在Firefox中被削弱 了。考虑到我们开发的原语,可以绕过被削弱的部分(比如,使用ROP来转移控制),但对于简单的PoC来说,似乎有点复杂。
有一些其他的Firefox相关的技巧来通过滥用特权的JavaScript获取代码执行权限,但是这些需要对浏览器状态进行不必要的修改(比如, 关闭所有安全性,以便病毒可以接管这台电脑 )。
我最终使用了一些标准的CTF技巧来完成利用代码:寻找对libc中第一个参数为字符串的函数的交叉引用(此例中,选用strcmp),我发现Date.toLocalFormat
的执行并且注意到了该函数将第一个参数从JSString转换为C-String ,它的第一个参数被用来做strcmp 。因此,我们可以简单的strcmp
的GOT换成system
,然后执行data_obj.toLocaleFormat("open -a /Applications/Calculator.app");
。搞定:).
这时,基本的利用代码已经完成了, 接下来我们将会描述如何使它变的更可靠一些以及占用更少的带宽。
此时此刻我们的利用代码知识申请了一些非常大的ArrayBuffer实例(每个1GB)用来填充mmap空间,然后再分配一大堆js :: Arena实例用来溢出。因此,该操作假设浏览器的堆操作在利用期间多多少少是确定的。既然这不一定是这样,我们希望让我们的漏洞更加强大一些。
快速浏览一下mozilla::Vector类(用来保留脚本缓冲区)接下来的操作向我们展示了它使用了realloc
在其需要的时候来倍增自己的空间。由于jemalloc直接使用mmap来申请较大的区块,这就给了我们以下的分配模式:
因为当前区块的大小总是会大于之前所有区块的大小,这将导致我们的最终缓冲区之前存在大量的可用空间。理论上,我们可以计算空闲空间之和,然后申请一个大的ArrayBuffer。实际上,这行不通,因为当服务器发送数据到来时,在浏览器完成解压缩最后一块数据完成之前有一些其他的申请空间的操作。此外jemalloc还保留了一部分被释放的内存以供以后使用。相反,我们会尽快在浏览器中申请被释放的空间,理由如下:
这个简单的算法服务器端代码在这里 ,客户端第一步的代码 在这里。使用这个算法,我们可以通过仅喷射几兆的ArrayBuffer实例而不是多个千兆字节来相当可靠地获得分配在目标缓冲区之后的空间。
目前,我们的利用代码需要通过服务器发送4GB的数据。可以简单的通过下述方法来改进:使用HTTP压缩。zlib有一个好处 ,支持流式压缩,它可以逐步压缩payload。有了这个,我们只需将payload的每个部分添加到在zlib流中,然后调用flush来获取payload的下一个压缩块,并将其发送到服务器(译注:服务器发送到浏览器,笔误?)。服务器(译注:浏览器,笔误?)在收到该块后解压缩该文件,并执行所需要的操作(比如,执行一次realloc操作)。
poc.py 中的construct_payload
方法执行了该过程,并且将payload的大小减少到大约18MB。
至少在理论上,exploit需要非常大量的内存:
不管怎么说,因为许多的缓冲区并没有被写入,所以不一定得消耗如此多的物理空间。更多的,在最终realloc中,只有4GB新的缓冲区会被写入之前释放的旧的缓冲区中,因此真正需要的仅仅是8GB而已。
不过这还是非常的占内存。然而,如果物理内存降低后,还是有一些技术来帮助减少内存占用:
CPU使用率在峰值期间(解压缩数据)也是相当的高。然而可以通过延迟发送较小的块之间的时间(这显然会增加漏洞利用的时间)来进一步降低CPU的压力。这也将给OS更多的时间来压缩和或删除大的重复数据内存缓冲区。
目前的漏洞利用有几个不可靠的因素,主要是处理时机:
从我的经验来看,如果浏览器没有大量的处理任务,这个漏洞利用率非常可靠(>95%)。如果10个以上的其他选项卡是打开的,则漏洞利用仍然有效,但如果大型Web应用程序正在加载,则可能会失败。
虽然从攻击者的角度来看,这个漏洞并不理想,但它仍然可以相当可靠地在低带宽下利用。过程中有趣的是使用的各种技术(压缩,页重复删除。。。)来使得这个漏洞更加简单的被利用。
考虑到如何防止这种错误的可利用性,有几点是我想说明的。一个相当通用的缓解措施是使用保护页面(无论用什么来访问保护页面都会产生段错误)。保护页面必须在每个mmap分配区域之前或之后分配,并且此举将防止对这种线性溢出的利用。但是,它们不会防止非线性溢出,比如说这个漏洞 。另一种可能性是引入内部mmap随机化来分散整个地址空间中的分配区域(可能仅在64位系统上有效)。这最好由内核执行,当然也可以在用户空间中完成。
翻译的时候用的是markdown,这是复制到浏览器之后最好的效果了,没有高亮。。。。。。
result
nsScriptLoadHandler::TryDecodeRawData(const uint8_t* aData,
uint32_t aDataLength,
bool aEndOfStream)
{
int32_t srcLen = aDataLength;
const char* src = reinterpret_cast<const char *>(aData);
int32_t dstLen;
nsresult rv =
mDecoder->GetMaxLength(src, srcLen, &dstLen);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t haveRead = mBuffer.length();
uint32_t capacity = haveRead + dstLen;
if (!mBuffer.reserve(capacity)) {
return NS_ERROR_OUT_OF_MEMORY;
}
rv = mDecoder->Convert(src,
&srcLen,
mBuffer.begin() + haveRead,
&dstLen);
NS_ENSURE_SUCCESS(rv, rv);
haveRead += dstLen;
MOZ_ASSERT(haveRead <= capacity, "mDecoder produced more data than expected");
MOZ_ALWAYS_TRUE(mBuffer.resizeUninitialized(haveRead));
return NS_OK;
} int32_t haveRead = mBuffer.length();
- uint32_t capacity = haveRead + dstLen;
- if (!mBuffer.reserve(capacity)) {
+
+ CheckedInt<uint32_t> capacity = haveRead;
+ capacity += dstLen;
+
+ if (!capacity.isValid() || !mBuffer.reserve(capacity.value())) {
return NS_ERROR_OUT_OF_MEMORY;
} #include <sys/mman.h>
#include <stdio.h>
const size_t MAP_SIZE = 0x100000; // 1 MB
int main()
{
char* chunk1 = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
char* chunk2 = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
printf("chunk1: %p - %p\n", chunk1, chunk1 + MAP_SIZE);
printf("chunk2: %p - %p\n", chunk2, chunk2 + MAP_SIZE);
return 0;
} async def sync(request, response):
script_ready_event.set()
await server_done_event.wait()
server_done_event.clear()
response.send_header(200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': '2'
})
response.write(b'OK')
await response.drain() function synchronize() {
var xhr = new XMLHttpRequest();
xhr.open('GET', location.origin + '/sync', false);
// Server will block until the event has been fired
xhr.send();
} /*
* Arenas are the allocation units of the tenured heap in the GC. An arena
* is 4kiB in size and 4kiB-aligned. It starts with several header fields
* followed by some bytes of padding. The remainder of the arena is filled
* with GC things of a particular AllocKind. The padding ensures that the
* GC thing array ends exactly at the end of the arena:
*
* <----------------------------------------------> = ArenaSize bytes
* +---------------+---------+----+----+-----+----+
* | header fields | padding | T0 | T1 | ... | Tn |
* +---------------+---------+----+----+-----+----+
* <-------------------------> = first thing offset
*/
class Arena
{
static JS_FRIEND_DATA(const uint32_t) ThingSizes[];
static JS_FRIEND_DATA(const uint32_t) FirstThingOffsets[];
static JS_FRIEND_DATA(const uint32_t) ThingsPerArena[];
/*
* The first span of free things in the arena. Most of these spans are
* stored as offsets in free regions of the data array, and most operations
* on FreeSpans take an Arena pointer for safety. However, the FreeSpans
* used for allocation are stored here, at the start of an Arena, and use
* their own address to grab the next span within the same Arena.
*/
FreeSpan firstFreeSpan;
// ... class FreeSpan
{
uint16_t first;
uint16_t last;
// methods following
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
上传的附件: