实战还原V8bytenode保护JS(V8字节码分析记录)

admin 2026-03-12 22:07:50 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文记录了逆向还原V8bytenode保护JS字节码的过程。针对现有工具崩溃问题,作者通过修改V8源码解决。核心改动包括绕过反序列化校验、修复递归打印栈溢出、优化Handle生命周期及采用BFS遍历。最终成功还原可读JS代码,为V8字节码逆向提供了深度实战参考。 综合评分: 92 文章分类: 逆向分析,二进制安全,实战经验


cover_image

实战还原 V8 bytenode 保护 JS(V8 字节码分析记录)

Dorimu Dorimu

看雪学苑

2026年3月12日 17:59 上海

拿到一个需要逆向分析的 JSstart.js

目标环境:

  • Node.js:16.14.0
  • 对应 V8:9.4.146.24-node.20(flag 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.generateScript=function(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方案。

第一次尝试:View8

定位到 bytecode 后,我先上了View8:https://github.com/suleram/View8

然后配套9.4.146.24.exe去跑反编译,发现问题:

  • 输出一点代码后自动崩溃退出。
  • View8 因 d8 崩溃只导出 23 个外围函数,关键的函数基本丢失。

当时第一反应是项目太久没维护,又去github找别的项目。

第二次尝试:jsc2js

又看了jsc2js:https://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、参数语义、寄存器布局。

第三次尝试:拉 V8 仓库

@echo off

set PATH=E:\Dev\SDKs\depot_tools;%PATH%

set DEPOT_TOOLS_WIN_TOOLCHAIN=0

mkdir v8_941

cd v8_941

echo solutions = [{ > .gclient

echo   "name": "v8", >> .gclient

echo   "url": "https://chromium.googlesource.com/v8/[email protected]", >> .gclient

echo   "deps_file": "DEPS", >> .gclient

echo   "managed": False, >> .gclient

echo   "custom_deps": {}, >> .gclient

echo }] >> .gclient

git clone --depth=1 --branch 9.4.146.24 https://chromium.googlesource.com/v8/v8.git v8

gclient sync -D --no-history

#

patch + 编译参数

先patch,再单独构建 d8:

cd&nbsp;/d <dir>\v8_941\v8

python ..\..\apply_patches_v8_94.py .

gn gen out/release

ninja -C out/release d8

构建参数:

dcheck_always_on&nbsp;=&nbsp;false

is_clang&nbsp;=&nbsp;false

is_component_build&nbsp;=&nbsp;false

is_debug&nbsp;=&nbsp;false

target_cpu&nbsp;=&nbsp;"x64"

use_custom_libcxx&nbsp;=&nbsp;false

v8_monolithic&nbsp;=&nbsp;true

v8_use_external_startup_data&nbsp;=&nbsp;false

v8_static_library&nbsp;=&nbsp;true

v8_enable_disassembler&nbsp;=&nbsp;true

v8_enable_object_print&nbsp;=&nbsp;true

treat_warnings_as_errors&nbsp;=&nbsp;false

v8_enable_pointer_compression&nbsp;=&nbsp;false

v8_enable_31bit_smis_on_64bit_arch&nbsp;=&nbsp;false

v8_enable_lite_mode&nbsp;=&nbsp;false

v8_enable_i18n_support&nbsp;=&nbsp;true

v8_enable_webassembly&nbsp;=&nbsp;true

#

改动阶段

真男人就要硬刚v8,部分diff我就不贴出来了,把问题和思路贴一下,欸嘿~

#

问题一:cachedData反序列化被拒绝

CodeSerializer::Deserialize默认会严检 magic/version/flags/hash/checksum/source hash。

如果任何一项没通过,它会直接 reject 掉这份缓存,返回空对象。

src/snapshot/code-serializer.cc

@@ SerializedCodeData::SanityCheck

- &nbsp;SanityCheckResult result = SanityCheckWithoutSource();

- &nbsp;if&nbsp;(result != CHECK_SUCCESS)&nbsp;return&nbsp;result;

- &nbsp;...

- &nbsp;return&nbsp;CHECK_SUCCESS;

+ &nbsp;return&nbsp;SerializedCodeData::SanityCheckResult::CHECK_SUCCESS;

@@ SerializedCodeData::SanityCheckWithoutSource

- &nbsp;if&nbsp;(this->size_ < kHeaderSize)&nbsp;return&nbsp;INVALID_HEADER;

- &nbsp;uint32_t magic_number = GetMagicNumber();

- &nbsp;if&nbsp;(magic_number != kMagicNumber)&nbsp;return&nbsp;MAGIC_NUMBER_MISMATCH;

- &nbsp;...

- &nbsp;if&nbsp;(Checksum(ChecksummedContent()) != c)&nbsp;return&nbsp;CHECKSUM_MISMATCH;

- &nbsp;return&nbsp;CHECK_SUCCESS;

+ &nbsp;return&nbsp;SerializedCodeData::SanityCheckResult::CHECK_SUCCESS;

src/snapshot/deserializer.cc

@@ Deserializer<IsolateT>::Deserializer

- &nbsp;CHECK_EQ(magic_number_,&nbsp;SerializedData::kMagicNumber);

+ &nbsp;/*

+ &nbsp;CHECK_EQ(magic_number_, SerializedData::kMagicNumber);

+ &nbsp;*/

@@ ReadSingleBytecodeData

+ &nbsp;std::fprintf(stderr,&nbsp;"[FATAL] Unknown serializer bytecode: 0x%02x\n", data);

#

问题二:反汇编/打印阶段栈溢出

这里就是之前view8打印不出来的主要问题:

  1. BytecodeArray::Disassemble
  2. 打常量池
  3. 命中SharedFunctionInfo
  4. SharedFunctionInfoPrint
  5. 再次Disassemble
  6. 深度叠加,最终栈爆
  • 改动:TLS guard + SEH

src/diagnostics/objects-printer.cc

+thread_local int g_in_bytecode_disasm = 0;

...

+ &nbsp;++g_in_bytecode_disasm;

+ &nbsp;hbc->Disassemble(*(c->os));

+ &nbsp;--g_in_bytecode_disasm;

@@ SharedFunctionInfoPrint

- &nbsp;PrintSourceCode(os);

+ &nbsp;// PrintSourceCode(os);

+ &nbsp;int exc = SehWrapCall(DoBcDisasm, &ctx);

+ &nbsp;if (exc != 0) { os << "<BytecodeArray Disassemble CRASHED ...>"; }

src/objects/objects.cc

+extern&nbsp;thread_local&nbsp;int&nbsp;g_in_bytecode_disasm;

+voidSafePrintSharedFunctionInfo(...);

+voidSafePrintFixedArray(...);

...

case&nbsp;SHARED_FUNCTION_INFO_TYPE:

+ &nbsp;if&nbsp;(g_in_bytecode_disasm >&nbsp;0) {&nbsp;break; }

+ &nbsp;SafePrintSharedFunctionInfo(shared, os);

case&nbsp;FIXED_ARRAY_TYPE:

+ &nbsp;SafePrintFixedArray(FixedArray::cast(*this), os);
  • 对应:d8 入口改成 BFS 平铺

src/d8/d8.cc

+void&nbsp;Shell::LoadBytecode(...)

+std::deque<i::Handle<i::SharedFunctionInfo>> queue;

+std::unordered_set<i::Address> seen;

+while&nbsp;(!queue.empty()) { ... }

+global_template->Set(isolate,&nbsp;"loadBytecode",

+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;FunctionTemplate::New(isolate, LoadBytecode));

#

稳定性

修 Handle 生命周期和字节码迭代稳定性

- &nbsp; &nbsp; &nbsp; &nbsp;i::HandleScope&nbsp;inner_scope(isolateInternal);

+ &nbsp; &nbsp; &nbsp; &nbsp;// No inner HandleScope here — child handles stored in queue/all_sfis

+ &nbsp; &nbsp; &nbsp; &nbsp;// must survive across iterations. outer_scope keeps them all alive.

...

- &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;i::BytecodeArray handle_storage = *hbca;

- &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;i::Handle<i::BytecodeArray>&nbsp;handle(

- &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;reinterpret_cast<i::Address*>(&handle_storage));

- &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;i::interpreter::BytecodeArrayIterator&nbsp;iterator(handle);

+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// Use hbca directly — it's a proper Handle rooted in print_scope.

+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;i::interpreter::BytecodeArrayIterator&nbsp;iterator(hbca);

...

+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// Re-derive base_address each iteration (GC-safe)

+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;i::Address base_address = hbca->GetFirstBytecodeAddress();

调试可见性 + SFI 入队条件

+ &nbsp; &nbsp;printf("[DBG] root SFI ptr = 0x%p\n", reinterpret_cast<void*>(root->ptr()));

+ &nbsp; &nbsp;printf("[DBG] root HasBytecodeArray = %d\n", root_has_bc);

...

+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;printf("[DBG] &nbsp; cp[%d] raw=0x%p smi=%d\n", cp_index,

+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; reinterpret_cast<void*>(obj.ptr()), obj.IsSmi());

...

- &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(obj.IsSharedFunctionInfo()) {

+ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(!obj.IsSmi() && obj.IsSharedFunctionInfo()) {

常量池可读性增强

+constint&nbsp;kMaxLiteralElementsToPrint =&nbsp;1024;

+std::function<void(i::Object,&nbsp;int)> print_compact_obj;

...

+if&nbsp;(value.IsArrayBoilerplateDescription()) { ... }

+if&nbsp;(value.IsFixedArray()) { ... }

+if&nbsp;(value.IsFixedDoubleArray()) { ... }

...

+print_compact_obj(obj,&nbsp;0);

其它

src/objects/string.cc

-if (len > kMaxShortPrintLength) {

+// if (len > kMaxShortPrintLength) {

...

-accumulator->Add("%c", c);

+accumulator->Add("\\u%04x", c);

#

将反编译结果初步还原成可读js

  • 喂给jsc2js/View8(这里你可能要手动改一下,懒得贴了),我记得好像还要处理一下常量池?

#

看雪ID:Dorimu

https://bbs.kanxue.com/user-home-1069502.htm

*本文为看雪论坛优秀文章,由 Dorimu 原创,转载请注明来自看雪社区

往期推荐

从ANGR-CTF项目入手ANGR和符号执行技术

AI时代-逆向工作者该如何用好这一利器

EXIF解析缓冲区溢出漏洞分析与利用

从C到Pwn:栈溢出漏洞利用实战入门

Android-ARM64的VMP分析和还原

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 Dorimu Dorimu《实战还原 V8 bytenode 保护 JS(V8 字节码分析记录)》

评论:0   参与:  0