找回密码
 立即注册
首页 业界区 业界 实战还原 V8 bytenode 保护 JS(V8 字节码分析记录) ...

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

魁睥 昨天 23:50
实战还原 V8 bytenode 保护 JS(V8 字节码分析记录)

V8 字节码分析,简单写写在前辈们的基础上,又遇到些什么问题(绝对不是我水不出很长的文章的问题)
(如有错漏,敬请指正,因为是在弄完后过了很久才写的)
我的blog:blog.dorimu.cn
0x00 前言

拿到一个需要逆向分析的 JS  start.js。
目标环境:

  • Node.js:16.14.0
  • 对应 V8:9.4.146.24-node.20(flag hash ed0ab240)
核心代码如下:
  1. const vm = require('vm');
  2. const v8 = require('v8');
  3. const zlib = require('zlib');
  4. const fs = require('fs');
  5. const path = require('path');
  6. const Module = require('module');
  7. v8.setFlagsFromString('--no-lazy');
  8. v8.setFlagsFromString('--no-flush-bytecode');
  9. global.generateScript=function(cachedData, filename) {
  10.    cachedData = zlib.brotliDecompressSync(cachedData);
  11.    fixBytecode(cachedData);
  12.    const length = readSourceHash(cachedData);
  13.    let dummyCode = '';
  14.    if (length > 1) {
  15.          dummyCode = '"' + '\u200b'.repeat(length - 2) + '"';
  16.    }
  17.    const script = new vm.Script(dummyCode, {
  18.          cachedData,
  19.          filename
  20.    });
  21.    if (script.cachedDataRejected) {
  22.          throw new Error('');
  23.    }
  24.    return script;
  25. }
  26. global.compileCode = function(javascriptCode, compress) {
  27.    const script = new vm.Script(javascriptCode, {
  28.          produceCachedData: true
  29.    });
  30.    let bytecodeBuffer = (script.createCachedData && script.createCachedData.call) ?
  31.          script.createCachedData() :
  32.          script.cachedData;
  33.    if (compress) bytecodeBuffer = zlib.brotliCompressSync(bytecodeBuffer);
  34.    return bytecodeBuffer;
  35. };
  36. global.fixBytecode = function(bytecodeBuffer) {
  37.    const dummyBytecode = compileCode('');
  38.    dummyBytecode.subarray(12, 16).copy(bytecodeBuffer, 12);
  39. };
  40. global.readSourceHash = function(bytecodeBuffer) {
  41.    return bytecodeBuffer.subarray(8, 12).reduce((sum, number, power) => sum += number * Math.pow(256, power), 0);
  42. };
  43. try {
  44.    Module._extensions['.jsc'] = function(fileModule, filename) {
  45.          const data = fs.readFileSync(filename, 'utf8')
  46.          const bytecodeBuffer = Buffer.from(data, 'base64');
  47.          const script = generateScript(bytecodeBuffer, filename);
  48.          function require(id) {
  49.             return fileModule.require(id);
  50.          }
  51.          require.resolve = function(request, options) {
  52.             return Module._resolveFilename(request, fileModule, false, options);
  53.          };
  54.          if (process.main) {
  55.             require.main = process.main;
  56.          }
  57.          require.extensions = Module._extensions;
  58.          require.cache = Module._cache;
  59.          const compiledWrapper = script.runInThisContext({
  60.             filename: filename,
  61.             lineOffset: 0,
  62.             columnOffset: 0,
  63.             displayErrors: true
  64.          });
  65.          const dirname = path.dirname(filename);
  66.          const args = [
  67.             fileModule.exports, require, fileModule, filename, dirname, process, global
  68.          ];
  69.          return compiledWrapper.apply(fileModule.exports, args);
  70.    };
  71. } catch (ex) {
  72.    console.error('xrequire:' + ex.message);
  73. }
  74. require("${codeScript}")
复制代码
经过搜索资料发现:
这就是V8 cachedData / bytenode 方案。
0x01 第一次尝试:View8

定位到 bytecode 后,我先上了 View8:
https://github.com/suleram/View8
然后配套 9.4.146.24.exe 去跑反编译。
发现问题:

  • 输出一点代码后自动崩溃退出。
  • View8 因 d8 崩溃只导出 23 个外围函数,关键的函数基本丢失。
当时第一反应是项目太久没维护,又去github找别的项目。
0x02 第二次尝试: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、参数语义、寄存器布局。
0x03 第三次尝试:拉 V8 仓库
  1. @echo off
  2. set PATH=E:\Dev\SDKs\depot_tools;%PATH%
  3. set DEPOT_TOOLS_WIN_TOOLCHAIN=0
  4. mkdir v8_941
  5. cd v8_941
  6. echo solutions = [{ > .gclient
  7. echo   "name": "v8", >> .gclient
  8. echo   "url": "https://chromium.googlesource.com/v8/v8.git@9.4.146.24", >> .gclient
  9. echo   "deps_file": "DEPS", >> .gclient
  10. echo   "managed": False, >> .gclient
  11. echo   "custom_deps": {}, >> .gclient
  12. echo }] >> .gclient
  13. git clone --depth=1 --branch 9.4.146.24 https://chromium.googlesource.com/v8/v8.git v8
  14. gclient sync -D --no-history
复制代码
0x04 patch + 编译参数

先patch,再单独构建 d8:
  1. cd /d <dir>\v8_941\v8
  2. python ..\..\apply_patches_v8_94.py .
  3. gn gen out/release
  4. ninja -C out/release d8
复制代码
构建参数:
  1. dcheck_always_on = false
  2. is_clang = false
  3. is_component_build = false
  4. is_debug = false
  5. target_cpu = "x64"
  6. use_custom_libcxx = false
  7. v8_monolithic = true
  8. v8_use_external_startup_data = false
  9. v8_static_library = true
  10. v8_enable_disassembler = true
  11. v8_enable_object_print = true
  12. treat_warnings_as_errors = false
  13. v8_enable_pointer_compression = false
  14. v8_enable_31bit_smis_on_64bit_arch = false
  15. v8_enable_lite_mode = false
  16. v8_enable_i18n_support = true
  17. v8_enable_webassembly = true
复制代码
0x05 改动阶段

真男人就要硬刚v8,部分diff我就不贴出来了,把问题和思路贴一下,欸嘿~
0x06 问题一:cachedData 反序列化被拒绝

CodeSerializer:eserialize 默认会严检 magic/version/flags/hash/checksum/source hash。
如果任何一项没通过,它会直接 reject 掉这份缓存,返回空对象。
src/snapshot/code-serializer.cc:
  1. @@ SerializedCodeData::SanityCheck
  2. -  SanityCheckResult result = SanityCheckWithoutSource();
  3. -  if (result != CHECK_SUCCESS) return result;
  4. -  ...
  5. -  return CHECK_SUCCESS;
  6. +  return SerializedCodeData::SanityCheckResult::CHECK_SUCCESS;
  7. @@ SerializedCodeData::SanityCheckWithoutSource
  8. -  if (this->size_ < kHeaderSize) return INVALID_HEADER;
  9. -  uint32_t magic_number = GetMagicNumber();
  10. -  if (magic_number != kMagicNumber) return MAGIC_NUMBER_MISMATCH;
  11. -  ...
  12. -  if (Checksum(ChecksummedContent()) != c) return CHECKSUM_MISMATCH;
  13. -  return CHECK_SUCCESS;
  14. +  return SerializedCodeData::SanityCheckResult::CHECK_SUCCESS;
复制代码
src/snapshot/deserializer.cc:
  1. @@ Deserializer<IsolateT>::Deserializer
  2. -  CHECK_EQ(magic_number_, SerializedData::kMagicNumber);
  3. +  /*
  4. +  CHECK_EQ(magic_number_, SerializedData::kMagicNumber);
  5. +  */
  6. @@ ReadSingleBytecodeData
  7. +  std::fprintf(stderr, "[FATAL] Unknown serializer bytecode: 0x%02x\n", data);
复制代码
0x07 问题二:反汇编/打印阶段栈溢出

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

  • BytecodeArray:isassemble
  • 打常量池
  • 命中 SharedFunctionInfo
  • SharedFunctionInfoPrint
  • 再次 Disassemble
  • 深度叠加,最终栈爆


  • 改动:TLS guard + SEH
src/diagnostics/objects-printer.cc:
  1. +thread_local int g_in_bytecode_disasm = 0;
  2. ...
  3. +  ++g_in_bytecode_disasm;
  4. +  hbc->Disassemble(*(c->os));
  5. +  --g_in_bytecode_disasm;
  6. @@ SharedFunctionInfoPrint
  7. -  PrintSourceCode(os);
  8. +  // PrintSourceCode(os);
  9. +  int exc = SehWrapCall(DoBcDisasm, &ctx);
  10. +  if (exc != 0) { os << "<BytecodeArray Disassemble CRASHED ...>"; }
复制代码

  • 对应:d8 入口改成 BFS 平铺
src/d8/d8.cc:
  1. +extern thread_local int g_in_bytecode_disasm;
  2. +void SafePrintSharedFunctionInfo(...);
  3. +void SafePrintFixedArray(...);
  4. ...
  5. case SHARED_FUNCTION_INFO_TYPE:
  6. +  if (g_in_bytecode_disasm > 0) { break; }
  7. +  SafePrintSharedFunctionInfo(shared, os);
  8. case FIXED_ARRAY_TYPE:
  9. +  SafePrintFixedArray(FixedArray::cast(*this), os);
复制代码
0x08 稳定性

修 Handle 生命周期和字节码迭代稳定性
  1. +void Shell::LoadBytecode(...)
  2. +std::deque<i::Handle<i::SharedFunctionInfo>> queue;
  3. +std::unordered_set<i::Address> seen;
  4. +while (!queue.empty()) { ... }
  5. +global_template->Set(isolate, "loadBytecode",
  6. +                     FunctionTemplate::New(isolate, LoadBytecode));
复制代码
调试可见性 + SFI 入队条件
  1. -        i::HandleScope inner_scope(isolateInternal);
  2. +        // No inner HandleScope here — child handles stored in queue/all_sfis
  3. +        // must survive across iterations. outer_scope keeps them all alive.
  4. ...
  5. -            i::BytecodeArray handle_storage = *hbca;
  6. -            i::Handle<i::BytecodeArray> handle(
  7. -                reinterpret_cast<i::Address*>(&handle_storage));
  8. -            i::interpreter::BytecodeArrayIterator iterator(handle);
  9. +            // Use hbca directly — it's a proper Handle rooted in print_scope.
  10. +            i::interpreter::BytecodeArrayIterator iterator(hbca);
  11. ...
  12. +                // Re-derive base_address each iteration (GC-safe)
  13. +                i::Address base_address = hbca->GetFirstBytecodeAddress();
复制代码
常量池可读性增强
  1. +    printf("[DBG] root SFI ptr = 0x%p\n", reinterpret_cast<void*>(root->ptr()));
  2. +    printf("[DBG] root HasBytecodeArray = %d\n", root_has_bc);
  3. ...
  4. +            printf("[DBG]   cp[%d] raw=0x%p smi=%d\n", cp_index,
  5. +                   reinterpret_cast<void*>(obj.ptr()), obj.IsSmi());
  6. ...
  7. -            if (obj.IsSharedFunctionInfo()) {
  8. +            if (!obj.IsSmi() && obj.IsSharedFunctionInfo()) {
复制代码
其它

src/objects/string.cc:
  1. +const int kMaxLiteralElementsToPrint = 1024;
  2. +std::function<void(i::Object, int)> print_compact_obj;
  3. ...
  4. +if (value.IsArrayBoilerplateDescription()) { ... }
  5. +if (value.IsFixedArray()) { ... }
  6. +if (value.IsFixedDoubleArray()) { ... }
  7. ...
  8. +print_compact_obj(obj, 0);
复制代码
0x09 将反编译结果初步还原成可读js


  • 喂给 jsc2js/View8(这里你可能要手动改一下,懒得贴了),我记得好像还要处理一下常量池?
来来回回折腾了三天,最开始啥都不懂硬生生肝出来了,基本都是需要什么去问什么去学什么 hhhhhhh 现在还真是方便啊
写完跑路

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册