找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3894

积分

0

好友

521

主题
发表于 1 小时前 | 查看: 4| 回复: 0

最近遇到一个需要分析的目标 JS 文件 start.js。在对这个应用进行逆向时,我发现它使用了 V8 的缓存字节码机制来保护源码。这篇文章将记录我尝试反编译并还原其原始 JavaScript 代码的完整过程。

目标环境信息如下:

  • Node.js: 16.14.0
  • 对应的 V8 版本: 9.4.146.24-node.20 (flag hash ed0ab240)

文件的核心加载代码是这样的:

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 保护方案。它本质上是通过 Node.js 的 vm.Script 模块将 JavaScript 源码编译成 V8 内部的字节码格式并序列化保存。执行时,直接加载并反序列化这份字节码数据,跳过了源码解析和编译阶段,从而达到隐藏源码的目的。

第一次尝试:View8

锁定字节码文件后,我的第一反应是使用现成的工具。于是找到了 View8 项目:https://github.com/suleram/View8

我准备了对应版本的 V8 可执行文件 9.4.146.24.exe,用它配合 View8 进行反编译。然而问题很快出现了:

  • 程序输出一点代码后会自动崩溃退出。
  • View8 因为底层的 d8 崩溃,只导出了大约23个外围函数,关键的核心函数基本全部丢失。

这让我感觉可能是工具太旧,于是继续寻找其他方案。在像云栈社区这样的开发者论坛里,经常能发现针对此类具体技术难题的讨论和工具推荐。

第二次尝试:jsc2js

接着又看到了 jsc2js 项目:https://github.com/xqy2006/jsc2js

这个仓库看起来维护得更勤快一些,还有配套的 patch 和 CI 体系。我把 patch 打到 v8 9.4.146.24 的源码上重新编译,结果和第一轮差不多,工具依然不稳定。这时候心态有点小崩——字节码本身就不好 hook,现成工具还不给力。

为了搞清楚问题,我去查阅了一些关于 V8 字节码逆向的资料:

通过学习了解到,V8 bytecode 是 V8 引擎内部序列化的一种数据。要想稳定地获得反编译结果,必须回到 V8 源码层面去修改其输出逻辑。而且不同版本的 V8 在字节码层(如操作码、参数语义、寄存器布局)差异巨大,通用的补丁往往难以覆盖所有情况。

第三次尝试:拉取 V8 源码并修改

既然现成工具都不行,那就只能自己动手了。我决定拉取对应版本的 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/v8.git@9.4.146.24", >> .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

打补丁与编译参数

源码拉下来之后,先应用已有的补丁(如果有的话),然后单独构建我们需要的 d8 可执行文件:

cd /d <dir>\v8_941\v8
python ..\..\apply_patches_v8_94.py .
gn gen out/release
ninja -C out/release d8

我使用的构建参数(args.gn)如下:

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 = true

修改源码解决关键问题

现成的补丁效果不理想,那就自己动手分析问题所在并进行修改。下面是我遇到的主要问题和解决思路。

真男人就要硬刚V8源码,部分diff就不全贴了,主要把问题和思路分享一下。

问题一: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 中注释掉魔数检查,并增加了一个调试输出,便于发现未知的序列化字节码:

@@ Deserializer<IsolateT>::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);

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

这里就是之前 View8 等工具打印不全甚至崩溃的主要原因。问题的调用链形成了一个递归循环:

  1. BytecodeArray::Disassemble 开始反汇编。
  2. 打印到常量池(Constant Pool)中的对象。
  3. 命中了一个 SharedFunctionInfo 对象。
  4. 调用 SharedFunctionInfoPrint 打印它。
  5. 打印函数又会尝试调用 Disassemble 来显示其字节码。
  6. 深度不断叠加,最终导致调用栈溢出。

为了解决这个递归问题,我采用了 TLS (线程本地存储) guard + SEH (结构化异常处理) 的组合方案。

src/diagnostics/objects-printer.cc 中增加守卫变量并修改打印逻辑:

+thread_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.cc 中,当处于反汇编上下文中时,避免深入打印 SharedFunctionInfo

+extern 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.cc 中增加加载和遍历字节码的函数:

+void Shell::LoadBytecode(...)
+std::deque<i::Handle<i::SharedFunctionInfo>> queue;
+std::unordered_set<i::Address> seen;
+while (!queue.empty()) { ... }
+global_template->Set(isolate, "loadBytecode",
+                     FunctionTemplate::New(isolate, LoadBytecode));

提升稳定性和输出可读性

解决了核心崩溃问题后,还需要处理一些细节来确保工具稳定运行,并且输出结果易于阅读和分析。

修复 Handle 生命周期和迭代稳定性

在遍历字节码时,需要特别注意 V8 句柄(Handle)的生命周期管理,避免因为垃圾回收(GC)导致指针失效。

-        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::Handle<i::BytecodeArray> handle(
-                reinterpret_cast<i::Address*>(&handle_storage));
-            i::interpreter::BytecodeArrayIterator iterator(handle);
+            // Use hbca directly — it's 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();

增加调试可见性与完善入队条件

为了便于调试,我加入了一些打印信息,并修正了常量池对象入队的条件判断,避免将小整数(Smi)误判为对象。

+    printf("[DBG] root SFI ptr = 0x%p\n", reinterpret_cast<void*>(root->ptr()));
+    printf("[DBG] root HasBytecodeArray = %d\n", root_has_bc);
...
+            printf("[DBG]   cp[%d] raw=0x%p smi=%d\n", cp_index,
+                   reinterpret_cast<void*>(obj.ptr()), obj.IsSmi());
...
-            if (obj.IsSharedFunctionInfo()) {
+            if (!obj.IsSmi() && obj.IsSharedFunctionInfo()) {

增强常量池内容的可读性

原始的常量池打印对于复杂对象(如数组字面量描述、嵌套数组)可读性很差。我增加了一些逻辑来更友好地展示这些内容。

+const int kMaxLiteralElementsToPrint = 1024;
+std::function<void(i::Object, int)> print_compact_obj;
...
+if (value.IsArrayBoilerplateDescription()) { ... }
+if (value.IsFixedArray()) { ... }
+if (value.IsFixedDoubleArray()) { ... }
...
+print_compact_obj(obj, 0);

其他调整:完整打印字符串

为了让还原的代码更完整,我修改了字符串的打印逻辑,取消长度限制,并以 Unicode 转义形式输出所有字符,确保字符串内容不丢失。

src/objects/string.cc

-if (len > kMaxShortPrintLength) {
+// if (len > kMaxShortPrintLength) {
...
-accumulator->Add("%c", c);
+accumulator->Add("\\u%04x", c);

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

经过上述对 V8 源码的修改和编译,我得到了一个能稳定输出目标字节码反汇编结果的定制版 d8。

接下来的步骤就比较常规了:

  • 运行定制版 d8,加载 .jsc 字节码文件,将其反编译输出保存为文本。
  • 将这个输出喂给 jsc2jsView8 的后处理脚本(可能需要根据输出格式微调脚本),将 V8 字节码助记符和常量池初步还原成结构化的 JavaScript 代码。
  • 手动处理一些转换脚本可能遗漏的细节,比如整理常量池引用、修复控制流结构等。

经过这些步骤,原本被 bytenode 保护的、难以阅读的字节码文件,就被成功还原成了可读、可分析的 JavaScript 源代码。这个过程虽然曲折,但深入 V8 内部进行逆向工程和修改,对于理解 JavaScript 引擎的运作机制和代码保护原理,是一次非常有价值的实践。这也体现了在安全分析中,有时不得不深入到编译器和运行时底层去寻找答案。

动漫少女捂嘴表情


本文基于看雪论坛作者 Dorimu 的分享进行梳理和总结,记录了分析 V8 字节码保护的实际过程与解决方案。




上一篇:脑机接口(BCI)核心技术体系:电极、芯片、系统的技术挑战与应用前景
下一篇:Meta AI芯片路线图曝光:MTIA系列四代演进,如何重塑算力底座?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-13 07:34 , Processed in 0.693750 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表