0x00 前言拿到一个需要逆向分析的 JSstart.js。目标环境Node.js16.14.0对应 V89.4.146.24-node.20flag hashed0ab240核心代码如下const vm require(vm); const v8 require(v8); const zlib require(zlib); const fs require(fs); const path require(path); const Module require(module); v8.setFlagsFromString(--no-lazy); v8.setFlagsFromString(--no-flush-bytecode); global.generateScriptfunction(cachedData, filename) { cachedData zlib.brotliDecompressSync(cachedData); fixBytecode(cachedData); const length readSourceHash(cachedData); let dummyCode ; if (length 1) { dummyCode \u200b.repeat(length - 2) ; } const script new vm.Script(dummyCode, { cachedData, filename }); if (script.cachedDataRejected) { throw new Error(); } return script; } global.compileCode function(javascriptCode, compress) { const script new vm.Script(javascriptCode, { produceCachedData: true }); let bytecodeBuffer (script.createCachedData script.createCachedData.call) ? script.createCachedData() : script.cachedData; if (compress) bytecodeBuffer zlib.brotliCompressSync(bytecodeBuffer); return bytecodeBuffer; }; global.fixBytecode function(bytecodeBuffer) { const dummyBytecode compileCode(); dummyBytecode.subarray(12, 16).copy(bytecodeBuffer, 12); }; global.readSourceHash function(bytecodeBuffer) { return bytecodeBuffer.subarray(8, 12).reduce((sum, number, power) sum number * Math.pow(256, power), 0); }; try { Module._extensions[.jsc] function(fileModule, filename) { const data fs.readFileSync(filename, utf8) const bytecodeBuffer Buffer.from(data, base64); const script generateScript(bytecodeBuffer, filename); function require(id) { return fileModule.require(id); } require.resolve function(request, options) { return Module._resolveFilename(request, fileModule, false, options); }; if (process.main) { require.main process.main; } require.extensions Module._extensions; require.cache Module._cache; const compiledWrapper script.runInThisContext({ filename: filename, lineOffset: 0, columnOffset: 0, displayErrors: true }); const dirname path.dirname(filename); const args [ fileModule.exports, require, fileModule, filename, dirname, process, global ]; return compiledWrapper.apply(fileModule.exports, args); }; } catch (ex) { console.error(xrequire: ex.message); } require(${codeScript})经过搜索资料发现这就是V8 cachedData / bytenode方案。0x01 第一次尝试View8定位到 bytecode 后我先上了View8https://github.com/suleram/View8然后配套9.4.146.24.exe去跑反编译。发现问题输出一点代码后自动崩溃退出。View8 因 d8 崩溃只导出 23 个外围函数关键的函数基本丢失。当时第一反应是项目太久没维护又去github找别的项目。0x02 第二次尝试jsc2js又看了jsc2jshttps://github.com/xqy2006/jsc2js这个仓库新一些也有 patch CI 体系。把 patch 套到v8 9.4.146.24结果仍然和第一轮差不多。这时候基本就炸毛了先躺一会字节码本来就不太好 hook现成工具又不稳定。于是查阅相关资料https://www.aynakeya.com/articles/ctf/a-quick-guide-to-disassemble-v8-bytecode/https://rce.moe/2025/01/07/v8-bytecode-decompiler/V8 bytecode 就是 V8 自己序列化的一段内部数据。想稳定拿结果必须回到 V8 源码层改输出逻辑。不同 V8 版本在字节码层差异很大尤其是 opcode、参数语义、寄存器布局。0x03 第三次尝试拉 V8 仓库echo off set PATHE:\Dev\SDKs\depot_tools;%PATH% set DEPOT_TOOLS_WIN_TOOLCHAIN0 mkdir v8_941 cd v8_941 echo solutions [{ .gclient echo name: v8, .gclient echo url: https://chromium.googlesource.com/v8/v8.git9.4.146.24, .gclient echo deps_file: DEPS, .gclient echo managed: False, .gclient echo custom_deps: {}, .gclient echo }] .gclient git clone --depth1 --branch 9.4.146.24 https://chromium.googlesource.com/v8/v8.git v8 gclient sync -D --no-history0x04 patch 编译参数先patch再单独构建 d8cd /d dir\v8_941\v8 python ..\..\apply_patches_v8_94.py . gn gen out/release ninja -C out/release d8构建参数dcheck_always_on false is_clang false is_component_build false is_debug false target_cpu x64 use_custom_libcxx false v8_monolithic true v8_use_external_startup_data false v8_static_library true v8_enable_disassembler true v8_enable_object_print true treat_warnings_as_errors false v8_enable_pointer_compression false v8_enable_31bit_smis_on_64bit_arch false v8_enable_lite_mode false v8_enable_i18n_support true v8_enable_webassembly true0x05 改动阶段真男人就要硬刚v8部分diff我就不贴出来了把问题和思路贴一下欸嘿~0x06 问题一cachedData反序列化被拒绝CodeSerializer::Deserialize默认会严检 magic/version/flags/hash/checksum/source hash。如果任何一项没通过它会直接 reject 掉这份缓存返回空对象。src/snapshot/code-serializer.cc SerializedCodeData::SanityCheck - SanityCheckResult result SanityCheckWithoutSource(); - if (result ! CHECK_SUCCESS) return result; - ... - return CHECK_SUCCESS; return SerializedCodeData::SanityCheckResult::CHECK_SUCCESS; SerializedCodeData::SanityCheckWithoutSource - if (this-size_ kHeaderSize) return INVALID_HEADER; - uint32_t magic_number GetMagicNumber(); - if (magic_number ! kMagicNumber) return MAGIC_NUMBER_MISMATCH; - ... - if (Checksum(ChecksummedContent()) ! c) return CHECKSUM_MISMATCH; - return CHECK_SUCCESS; return SerializedCodeData::SanityCheckResult::CHECK_SUCCESS;src/snapshot/deserializer.cc DeserializerIsolateT::Deserializer - CHECK_EQ(magic_number_, SerializedData::kMagicNumber); /* CHECK_EQ(magic_number_, SerializedData::kMagicNumber); */ ReadSingleBytecodeData std::fprintf(stderr, [FATAL] Unknown serializer bytecode: 0x%02x\n, data);0x07 问题二反汇编/打印阶段栈溢出这里就是之前view8打印不出来的主要问题BytecodeArray::Disassemble打常量池命中SharedFunctionInfoSharedFunctionInfoPrint再次Disassemble深度叠加最终栈爆改动TLS guard SEHsrc/diagnostics/objects-printer.ccthread_local int g_in_bytecode_disasm 0; ... g_in_bytecode_disasm; hbc-Disassemble(*(c-os)); --g_in_bytecode_disasm; SharedFunctionInfoPrint - PrintSourceCode(os); // PrintSourceCode(os); int exc SehWrapCall(DoBcDisasm, ctx); if (exc ! 0) { os BytecodeArray Disassemble CRASHED ...; }src/objects/objects.ccextern thread_local int g_in_bytecode_disasm; void SafePrintSharedFunctionInfo(...); void SafePrintFixedArray(...); ... case SHARED_FUNCTION_INFO_TYPE: if (g_in_bytecode_disasm 0) { break; } SafePrintSharedFunctionInfo(shared, os); case FIXED_ARRAY_TYPE: SafePrintFixedArray(FixedArray::cast(*this), os);对应d8 入口改成 BFS 平铺src/d8/d8.ccvoid Shell::LoadBytecode(...) std::dequei::Handlei::SharedFunctionInfo queue; std::unordered_seti::Address seen; while (!queue.empty()) { ... } global_template-Set(isolate, loadBytecode, FunctionTemplate::New(isolate, LoadBytecode));0x08 稳定性修 Handle 生命周期和字节码迭代稳定性- i::HandleScope inner_scope(isolateInternal); // No inner HandleScope here — child handles stored in queue/all_sfis // must survive across iterations. outer_scope keeps them all alive. ... - i::BytecodeArray handle_storage *hbca; - i::Handlei::BytecodeArray handle( - reinterpret_casti::Address*(handle_storage)); - i::interpreter::BytecodeArrayIterator iterator(handle); // Use hbca directly — its a proper Handle rooted in print_scope. i::interpreter::BytecodeArrayIterator iterator(hbca); ... // Re-derive base_address each iteration (GC-safe) i::Address base_address hbca-GetFirstBytecodeAddress();调试可见性 SFI 入队条件 printf([DBG] root SFI ptr 0x%p\n, reinterpret_castvoid*(root-ptr())); printf([DBG] root HasBytecodeArray %d\n, root_has_bc); ... printf([DBG] cp[%d] raw0x%p smi%d\n, cp_index, reinterpret_castvoid*(obj.ptr()), obj.IsSmi()); ... - if (obj.IsSharedFunctionInfo()) { if (!obj.IsSmi() obj.IsSharedFunctionInfo()) {常量池可读性增强const int kMaxLiteralElementsToPrint 1024; std::functionvoid(i::Object, int) print_compact_obj; ... if (value.IsArrayBoilerplateDescription()) { ... } if (value.IsFixedArray()) { ... } if (value.IsFixedDoubleArray()) { ... } ... print_compact_obj(obj, 0);其它src/objects/string.cc-if (len kMaxShortPrintLength) { // if (len kMaxShortPrintLength) { ... -accumulator-Add(%c, c); accumulator-Add(\\u%04x, c);0x09 将反编译结果初步还原成可读js喂给jsc2js/View8这里你可能要手动改一下懒得贴了我记得好像还要处理一下常量池