JavaScript逆向实战:怎么在Node.js环境中复现浏览器加密逻辑

作者:安全边界时间:2026-10-30 18:35:10浏览:555

本文详细讲解如何在Node.js服务端环境中脱离浏览器运行并复现前端JavaScript加密逻辑,涵盖断点定位、代码提取、模块化改造、环境补桩及结果对齐的完整实战流程。

前端向后端发起关键业务请求时,常会在请求头或请求体中附加由 JavaScript 动态计算出的签名(如 sign、token 等加密参数)。在服务端或者爬虫场景下,要自动化复现这一计算过程,最可靠的方法通常不是用 Python 等其他语言重写算法,而是直接提取页面中运行的原生 JS 代码,并在 Node.js 环境中模拟执行。复现的核心路径在于定位加密入口、剥离非核心逻辑、补齐运行依赖的宿主对象,最终确保两端在相同输入下产生完全一致的密文。

JavaScript逆向实战:怎么在Node.js环境中复现浏览器加密逻辑
图 1:从浏览器逆向定位到 Node.js 复现的五个阶段

一、快速定位浏览器中的加密入口

前端加密逻辑并不脱离网络发送的生命周期。第一步是在浏览器开发者工具中锁定触发加密的代码位置,避免在大体积打包文件中盲目检索。

打开浏览器的 Network 面板,触发目标操作,找到带有加密参数的 API 请求。检查该请求的 Initiator(发起程序)列,鼠标悬停可以查看调用栈。通常最底层的调用是原生 fetch 或 XMLHttpRequest.send,沿调用栈向上逐层回溯 2 到 4 层,就能发现诸如 getSignature、encryptPayload 或 data: e.sign(t) 等具体赋值语句。

若调用栈被异步封装或 Promise 链打断,可在 Sources 面板的 XHR/fetch Breakpoints 中添加目标 URL 的路径关键词。当请求构造完成即将发送前,页面脚本会自动暂停在断点处。此时在 Scope(作用域)窗口中逐级排查已计算完成的对象属性,就能精准定位参与运算的原始入参及加密函数定义。

二、提取代码与处理打包加载机制

现代前端工程绝大多数采用 Webpack、Vite 或 Rollup 打包。定位到函数后,往往不能简单复制几行算法,因为函数内部会引用外层闭包变量、常量字典或其他模块(如标准加解密库)。针对打包结构,最稳妥的扣取方式是保留其模块调度器。

JavaScript逆向实战:怎么在Node.js环境中复现浏览器加密逻辑
图 2:Webpack 模块加载器解构与全局调用链导出改造

在以 Webpack 为代表的代码结构中,整个打包结果包裹在一个立即执行函数(IIFE)内。要让提取的代码独立运作,通常采取如下步骤:

  1. 将外层加载器函数(包含 __webpack_require__ 定义)保留,将其赋值给一个模块作用域变量,例如 var myWebpackRequire;。
  2. 在加载器内部加入 myWebpackRequire = __webpack_require__;,让加载能力逃逸到全局或外层。
  3. 裁剪不必要的页面路由和视图模块,仅保留加载器调用的加密模块 ID 以及其递归引用的子模块。
  4. 在脚本底部通过 myWebpackRequire(目标模块ID) 取得加密函数,并将其赋给 module.exports。

三、在 Node.js 中搭建复现运行环境

完成代码提取后,将其保存为本地 JavaScript 文件(如 bundle_decrypt.js)。在 Node.js 中执行该代码时,最常见的阻碍是浏览器宿主对象的缺失。Node.js 的顶层对象是 global,而浏览器是 window。

JavaScript逆向实战:怎么在Node.js环境中复现浏览器加密逻辑
图 3:浏览器与 Node.js 上下文差异及代理补环境模型

1. 基础环境打桩

在加载提取出的业务代码前,先初始化最基础的全局别名与必要对象:

// env_setup.js
// 1. 将 global 别名赋给 window
global.window = global;

// 2. 补齐常见的浏览器只读信息
global.navigator = {
  userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  platform: "Win32",
  language: "zh-CN",
  languages: ["zh-CN", "zh"]
};

// 3. 补齐最简 document 桩
global.document = {
  cookie: "",
  createElement: function(tag) {
    return {
      setAttribute: function() {},
      getElementsByTagName: function() { return []; }
    };
  },
  documentElement: {},
  body: {}
};

// 4. 补齐加解密依赖的全局编码函数
if (!global.btoa) {
  global.btoa = function(str) {
    return Buffer.from(str, "binary").toString("base64");
  };
}
if (!global.atob) {
  global.atob = function(b64) {
    return Buffer.from(b64, "base64").toString("binary");
  };
}

2. 使用 Proxy 动态定位缺失环境

很多混淆脚本会检测非预期的未定义属性(例如读取 window.chrome 或 document.hidden)。如果逐行阅读混淆代码去人工查找,效率极低。使用 ES6 Proxy 对全局对象进行包裹,能够在访问缺失属性的第一时间打出日志:

function watchEnvironment(target, name) {
  return new Proxy(target, {
    get(obj, prop) {
      if (!(prop in obj)) {
        console.warn(`[环境探测] 访问未定义属性: ${name}.${String(prop)}`);
      }
      return obj[prop];
    },
    set(obj, prop, value) {
      console.log(`[环境探测] 写入属性: ${name}.${String(prop)} = ${value}`);
      obj[prop] = value;
      return true;
    }
  });
}

global.window = watchEnvironment(global.window, "window");
global.document = watchEnvironment(global.document, "document");
global.navigator = watchEnvironment(global.navigator, "navigator");

运行脚本时,控制台输出会清晰显示缺少哪些属性。缺什么补什么,能快速把环境补齐到代码刚好顺利执行的最小子集。

注意环境补桩的“度”

环境并非补得越多越好。很多反爬逻辑会检查 window.navigator.webdriver 或通过 Function.prototype.toString 检查函数是否为原生代码([native code])。尽量只伪造报错中断所依赖的必要字段,不要引入无关的原型链重写。

四、调用复现与双端输出严格对齐

当代码在 Node.js 中可以平稳运行不再抛错时,最关键的一步是验证计算结果的准确性。

JavaScript逆向实战:怎么在Node.js环境中复现浏览器加密逻辑
图 4:Node.js 加密复现对齐与排查决策树

1. 隔离动态变量

前端很多签名在拼接字符串时会引入当前毫秒时间戳 Date.now() 或随机数 Math.random()。在对齐测试期间,必须将这些动态源在两端固定:

// 固定时间戳与随机数,确保单次执行具有确定性
Date.now = function() { return 1700000000000; };
Math.random = function() { return 0.123456789; };

在浏览器控制台中执行上述覆盖,同时在 Node.js 中传入相同的输入参数(如相同的请求体字符串和用户 token)。分别打印浏览器和 Node.js 计算出的最终字符串。

2. 常见不一致原因与排查

  • 中文与特殊符号编码:前端算法内部若使用了自定义实现的 utf8Encode,与 Node.js 默认 Buffer 处理不同。若密文包含中文时结果不一致而纯英文时一致,问题必然出在字符转字节的逻辑上。
  • Base64 补位符号:部分自定义加密会去掉 Base64 末尾的 = 填充,或使用 URL 安全的字符集(将 + 替换为 -,/ 替换为 _)。核对两端字符串长度是否完全相等。
  • 隐藏的环境分流:个别网站在加密前会读取 screen.width 或 document.characterSet 拼入哈希。若 Node.js 中未声明这些属性(导致其为 undefined),加密明文实际已发生改变。通过在浏览器中对拼接处打断点,查看传入算法底层的最终明文,即可找出缺失字段。

五、生产封装与维护建议

完成对齐后,应将复现脚本封装为独立、无副作用的函数模块:

// sign_service.js
require("./env_setup"); // 加载最小环境桩
const { calculateSign } = require("./extracted_core");

function getSignHeader(apiPath, postBody) {
  const timestamp = Date.now();
  const signature = calculateSign(apiPath, JSON.stringify(postBody), timestamp);
  return {
    "X-Sign": signature,
    "X-Timestamp": String(timestamp)
  };
}

module.exports = { getSignHeader };

在工程化落地时,建议保持提取脚本的独立性,避免向扣出的代码中随意添加个人业务逻辑。前端脚本一旦版本更新引发加密变动,独立的模块划分可以让开发者直接重新定位提取,快速完成新版本的替换升级。

作者最新文章
相关文章
更多