Node.js 运行环境

本文专注于逆向还原场景下的 Node.js 使用,包括如何在本地运行扣出的 JS 代码、利用内置模块辅助逆向分析,以及补全浏览器环境。 vm 模块允许在隔离的沙箱上下文中执行代码,是运行不受信任 JS 代码的常用方式。 vm.runInNewContext(code, sandbox, options) vm.Script 适合需要多次执行同一段代码的场景(预编译): vm.createContext(sandbox) 将一个普通对象提升为 vm 上下文(contextify): 逆向场景推荐使用 vm 而非 eval,原因:扣出的浏览器代码通常会访问

分享

官方文档:https://nodejs.org/en/docs/
适用版本:Node.js 20 LTS+(2026-05-08 核实)

本文专注于逆向还原场景下的 Node.js 使用,包括如何在本地运行扣出的 JS 代码、利用内置模块辅助逆向分析,以及补全浏览器环境。


运行扣出的 JS 代码

基本执行

# 直接运行脚本文件
node script.js

# 执行单行代码
node -e "console.log(require('crypto').createHash('md5').update('hello').digest('hex'))"

# 交互式 REPL
node

vm 模块

vm 模块允许在隔离的沙箱上下文中执行代码,是运行不受信任 JS 代码的常用方式。

vm.runInNewContext(code, sandbox, options)

参数 类型 默认值 说明
code string 必填 要执行的 JS 代码字符串
sandbox object {} 沙箱对象,代码中的全局变量来自此对象
options.filename string 'evalmachine.<anonymous>' 错误堆栈中显示的文件名
options.lineOffset number 0 错误堆栈中的行偏移量
options.timeout number 无限制 执行超时(毫秒),超时抛出错误
options.breakOnSigint boolean false Ctrl+C 时是否中断执行
options.contextName string 'VM Context' 上下文名称(调试用)
const vm = require('vm');

const sandbox = {
    window: {},
    result: null
};

vm.runInNewContext(`
    function encrypt(data) {
        return data.split('').reverse().join('');
    }
    result = encrypt('hello');
`, sandbox);

console.log(sandbox.result); // 'olleh'

vm.Script

适合需要多次执行同一段代码的场景(预编译):

参数 类型 默认值 说明
code string 必填 要编译的 JS 代码字符串
options.filename string 'evalmachine.<anonymous>' 错误堆栈文件名
options.lineOffset number 0 行偏移量
options.columnOffset number 0 列偏移量
options.cachedData Buffer 预编译字节码缓存
const vm = require('vm');

const script = new vm.Script(`
    var count = (typeof count === 'undefined') ? 0 : count;
    count++;
`);

const ctx = vm.createContext({ count: 0 });

for (let i = 0; i < 5; i++) {
    script.runInContext(ctx);
}
console.log(ctx.count); // 5

vm.createContext(sandbox)

将一个普通对象提升为 vm 上下文(contextify):

参数 类型 默认值 说明
sandbox object {} 作为全局对象的基础对象,执行后属性会被写入此对象
const vm = require('vm');

const ctx = vm.createContext({
    window: {},
    console: console,    // 允许沙箱内使用 console
    require: require     // 谨慎:允许沙箱访问 require 会破坏隔离性
});

const script = new vm.Script('window.x = 42;');
script.runInContext(ctx);
console.log(ctx.window.x); // 42

vm 与直接 eval 的区别

对比项 eval vm.runInNewContext
作用域 共享当前作用域,可访问外部变量 独立沙箱,无法访问外部变量
全局对象 共享 global 使用传入的 sandbox 对象
错误隔离 无隔离,错误会污染当前进程 相对隔离(但不是完全安全隔离)
适用场景 简单快速执行,信任代码 执行不确定代码,需要控制全局环境
性能 较高 略低(上下文创建有开销)

逆向场景推荐使用 vm 而非 eval,原因:扣出的浏览器代码通常会访问 windowdocument 等全局变量,通过 sandbox 可以精确控制这些变量的值。


模块系统

CommonJS

// 导出
// utils.js
function md5(str) { /* ... */ }
module.exports = { md5 };
// 或
module.exports.md5 = function(str) { /* ... */ };

// 导入
const utils = require('./utils');
utils.md5('hello');

// 导入内置模块
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

ESM

Node.js 支持 ESM,但有以下限制:

  • 文件扩展名必须为 .mjs,或 package.json 中设置 "type": "module"
  • 不能直接使用 require(需要用 createRequire 代替)
  • 顶层 await 可用
// ESM 导入导出
import { encrypt } from './crypto.mjs';
export function sign(data) { return encrypt(data); }

// 在 ESM 中使用 require
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const fs = require('fs'); // 现在可以用 require 了

动态 require 加载文件

// 动态加载(路径在运行时确定)
const moduleName = 'crypto';
const mod = require(moduleName);

// 加载并执行 JS 文件(常用于加载扣出的 bundle)
const bundle = require('./extracted_bundle.js');

// 强制重新加载(绕过缓存)
delete require.cache[require.resolve('./target.js')];
const fresh = require('./target.js');

逆向还原常用内置模块

fs:读写文件

fs.readFileSync(path, options)

参数 类型 默认值 说明
path string | Buffer | URL 必填 文件路径
options.encoding string | null null 编码格式,如 'utf8';为 null 时返回 Buffer
options.flag string 'r' 文件系统标志

fs.writeFileSync(path, data, options)

参数 类型 默认值 说明
path string | Buffer | URL 必填 文件路径,不存在则创建
data string | Buffer | Uint8Array 必填 要写入的内容
options.encoding string 'utf8' 编码格式
options.flag string 'w' 'w' 覆盖,'a' 追加
options.mode integer 0o666 文件权限
const fs = require('fs');

// 读取扣出的 JS 文件
const code = fs.readFileSync('./bundle.js', 'utf8');

// 读取二进制文件(如 wasm)
const wasmBuf = fs.readFileSync('./target.wasm'); // 返回 Buffer

// 将结果写入文件
fs.writeFileSync('./result.txt', JSON.stringify(results, null, 2), 'utf8');

// 追加日志
fs.writeFileSync('./log.txt', `[${new Date().toISOString()}] ${msg}\n`, { flag: 'a' });

path:路径拼接

const path = require('path');

// 拼接路径(跨平台)
const filePath = path.join(__dirname, 'files', 'bundle.js');

// 解析绝对路径
const absPath = path.resolve('./bundle.js');

// 获取目录名、文件名、扩展名
path.dirname('/home/user/file.js');  // '/home/user'
path.basename('/home/user/file.js'); // 'file.js'
path.extname('/home/user/file.js');  // '.js'

crypto:内置加密模块

crypto.createHash(algorithm)

参数 类型 默认值 说明
algorithm string 必填 算法名,如 'md5', 'sha1', 'sha256', 'sha512'

返回 Hash 对象,调用 .update(data).digest(encoding) 获取结果。

crypto.createHmac(algorithm, key)

参数 类型 默认值 说明
algorithm string 必填 算法名,如 'sha256'
key string | Buffer | KeyObject 必填 HMAC 密钥

crypto.createCipheriv(algorithm, key, iv)

参数 类型 默认值 说明
algorithm string 必填 算法名,如 'aes-128-cbc', 'aes-256-cbc'
key string | Buffer 必填 密钥,长度需与算法匹配
iv string | Buffer | null 必填 初始化向量(ECB 模式传 null
const crypto = require('crypto');

// MD5
const md5 = crypto.createHash('md5').update('hello').digest('hex');

// SHA256
const sha256 = crypto.createHash('sha256').update('hello', 'utf8').digest('hex');

// HMAC-SHA256
const hmac = crypto.createHmac('sha256', 'secret_key')
    .update('data')
    .digest('hex');

// AES-128-CBC 加密
const key = Buffer.from('0123456789abcdef');   // 16 字节
const iv  = Buffer.from('fedcba9876543210');   // 16 字节
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
let encrypted = cipher.update('plaintext', 'utf8', 'hex');
encrypted += cipher.final('hex');

// AES-128-CBC 解密
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');

// 生成随机字节
const randomBytes = crypto.randomBytes(16).toString('hex');

https / http:发请求

const https = require('https');

// GET 请求
function get(url) {
    return new Promise((resolve, reject) => {
        https.get(url, (res) => {
            let data = '';
            res.on('data', chunk => data += chunk);
            res.on('end', () => resolve(JSON.parse(data)));
        }).on('error', reject);
    });
}

// POST 请求(带 headers)
function post(url, body, headers = {}) {
    const bodyStr = JSON.stringify(body);
    const urlObj = new URL(url);
    return new Promise((resolve, reject) => {
        const req = https.request({
            hostname: urlObj.hostname,
            path: urlObj.pathname + urlObj.search,
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(bodyStr),
                ...headers
            }
        }, (res) => {
            let data = '';
            res.on('data', chunk => data += chunk);
            res.on('end', () => resolve(data));
        });
        req.on('error', reject);
        req.write(bodyStr);
        req.end();
    });
}

对比 axios:内置 https 模块更底层,无需安装依赖,适合简单请求或生产环境;axios 更简洁,适合开发调试阶段。逆向脚本推荐 axios 提高效率,生产还原脚本用内置模块避免依赖。

Buffer:二进制数据处理

Buffer.from(data, encoding)

参数 类型 默认值 说明
data string | Array | ArrayBuffer 必填 数据来源
encoding string 'utf8' data 为字符串时的编码:'utf8', 'hex', 'base64', 'latin1'

buf.toString(encoding, start, end)

参数 类型 默认值 说明
encoding string 'utf8' 目标编码:'hex', 'base64', 'utf8', 'latin1'
start number 0 开始字节索引
end number buf.length 结束字节索引(不含)
// 字符串 -> Buffer -> hex
const buf = Buffer.from('hello', 'utf8');
console.log(buf.toString('hex'));    // '68656c6c6f'
console.log(buf.toString('base64')); // 'aGVsbG8='

// hex 字符串 -> Buffer -> 原始字节
const hexBuf = Buffer.from('68656c6c6f', 'hex');
console.log(hexBuf.toString('utf8')); // 'hello'

// base64 解码
const b64Buf = Buffer.from('aGVsbG8=', 'base64');
console.log(b64Buf.toString('utf8')); // 'hello'

// 拼接多个 Buffer
const combined = Buffer.concat([buf1, buf2]);

// Buffer 与 Uint8Array 互转
const uint8 = new Uint8Array(buf);
const backToBuf = Buffer.from(uint8.buffer, uint8.byteOffset, uint8.byteLength);

补全浏览器环境

浏览器 JS 代码运行在 Node.js 时,通常会访问 windowdocumentnavigator 等全局对象。需要手动补全,按报错逐步添加。

常用 polyfill 写法

// 最常用的基础补全
global.window = global;
global.self = global;

// location 对象
global.location = {
    href: 'https://www.example.com/',
    origin: 'https://www.example.com',
    protocol: 'https:',
    host: 'www.example.com',
    hostname: 'www.example.com',
    port: '',
    pathname: '/',
    search: '',
    hash: ''
};

// navigator 对象
global.navigator = {
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
    platform: 'Win32',
    language: 'zh-CN',
    languages: ['zh-CN', 'zh', 'en'],
    cookieEnabled: true,
    onLine: true,
    vendor: 'Google Inc.'
};

// document 基础补全
global.document = {
    cookie: '',
    referrer: 'https://www.example.com/',
    title: '',
    URL: 'https://www.example.com/',
    domain: 'www.example.com',
    createElement: function(tag) { return { style: {} }; },
    getElementById: function(id) { return null; },
    querySelector: function(sel) { return null; },
    querySelectorAll: function(sel) { return []; }
};

// screen 对象
global.screen = {
    width: 1920,
    height: 1080,
    colorDepth: 24,
    pixelDepth: 24
};

// atob / btoa(Node.js >= 16 已内置,旧版本需手动补)
if (typeof atob === 'undefined') {
    global.atob = (str) => Buffer.from(str, 'base64').toString('latin1');
    global.btoa = (str) => Buffer.from(str, 'latin1').toString('base64');
}

// performance
global.performance = {
    now: function() { return Date.now(); },
    timing: { navigationStart: Date.now() }
};

// localStorage / sessionStorage(简单 mock)
global.localStorage = (function() {
    const store = {};
    return {
        getItem: (k) => store[k] || null,
        setItem: (k, v) => { store[k] = String(v); },
        removeItem: (k) => { delete store[k]; },
        clear: () => { Object.keys(store).forEach(k => delete store[k]); }
    };
})();

jsdom 快速集成

对于需要更完整 DOM 环境的场景(如代码操作 DOM 元素),直接使用 jsdom。详细用法参见 jsdom完全指南

const { JSDOM } = require('jsdom');

const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
    url: 'https://www.example.com/',
    referrer: 'https://www.example.com/',
    contentType: 'text/html',
    pretendToBeVisual: true
});

// 将 jsdom 的 window 挂载到 global
const { window } = dom;
global.window = window;
global.document = window.document;
global.navigator = window.navigator;
global.location = window.location;

// 现在可以运行浏览器代码了
eval(fs.readFileSync('./bundle.js', 'utf8'));

调试技巧

--inspect / --inspect-brk 开启 Chrome DevTools 调试

# 启动调试服务器,等待连接后才执行代码
node --inspect-brk script.js

# 启动调试服务器,不等待,代码立即执行
node --inspect script.js

# 指定端口(默认 9229)
node --inspect-brk=0.0.0.0:9230 script.js

连接方式:

  1. 在 Chrome 地址栏输入 chrome://inspect
  2. 点击 "Configure..." 添加 localhost:9229
  3. 在 "Remote Target" 下点击 "inspect" 打开 DevTools

--inspect-brk 会在第一行代码处暂停,适合调试启动阶段的问题。--inspect 不暂停,适合在代码中手动设置断点(debugger 语句)。

console.log 调试 vs 断点调试

对比项 console.log 断点调试
适用场景 快速验证、批量输出 深入分析执行流程
效率 需要重复运行 可实时查看所有变量
对代码的侵入性 需要修改代码 不修改代码
异步代码 输出顺序可能混乱 可暂停,清晰追踪

逆向建议:先用 console.log 快速确认入参和返回值,再用断点调试定位内部逻辑。

// 包装目标函数,自动打印参数和返回值
function wrapFn(fn, name) {
    return function() {
        const args = Array.from(arguments);
        console.log(`[${name}] 调用参数:`, args);
        const result = fn.apply(this, arguments);
        console.log(`[${name}] 返回值:`, result);
        return result;
    };
}

target.encrypt = wrapFn(target.encrypt, 'encrypt');


最佳实践

--inspect 调试扣出的 JS 代码node --inspect-brk target.js 启动后在 Chrome 打开 chrome://inspect,可以用 DevTools 调试 Node.js 代码,包括断点、变量查看、Call Stack,比 console.log 高效得多。

内置 crypto 模块替代浏览器 crypto.subtle:逆向代码中 crypto.createHash('md5')crypto.createHmac 等是 Node.js 内置 API,无需安装额外包;浏览器的 crypto.subtle 对应 webcrypto,Node 18+ 的 globalThis.crypto 也可用。

vm.runInNewContext 隔离全局状态:多段目标代码需要并行执行时,用 vm.runInNewContext(code, sandbox) 各自隔离全局变量;比在同一个 global 上运行更安全,避免相互污染。

process.argv 传参比环境变量更简单:在 Python 中通过 subprocess.run(['node', 'sign.js', json.dumps(params)]) 调用,Node.js 里 const params = JSON.parse(process.argv[2]) 接收,比用文件或 stdin 更直接。

ES Modules 支持用 .mjs"type": "module":目标代码用 import/export 时,文件改为 .mjs 扩展名或在 package.json"type": "module" 即可运行,不需要 Babel 转译。


常见陷阱

陷阱:require 在 ESM 模块中不可用

现象: .mjs 文件中用 require('crypto')ReferenceError: require is not defined
原因: ES Modules 中没有 require,需要用 import crypto from 'crypto'createRequire(import.meta.url) 兼容。
解决: import { createRequire } from 'module'; const require = createRequire(import.meta.url); 后就可以用 require

陷阱:console.log 输出的对象在某些情况下不完整

现象: console.log(bigObject) 只显示 [Object: null prototype] 或截断了嵌套内容。
原因: Node.js 的 console.log 默认只显示 2 层嵌套,超过层级的显示为 [Object]
解决: console.log(JSON.stringify(bigObject, null, 2)) 完整输出;或 require('util').inspect(bigObject, { depth: null })

陷阱:Node.js 版本不兼容目标代码语法

现象: 运行目标 JS 报 SyntaxError: Unexpected token '?.'(可选链)或 Private fields are not supported
原因: 较老的 Node.js 版本不支持新 JS 语法;Node.js 14 不支持可选链,Node.js 12 不支持逻辑赋值运算符。
解决: 升级到 Node.js 20+ LTS;或用 npx babel target.js --presets @babel/preset-env 将代码降级后运行。


参见

Webpack逆向还原
补环境
jsdom完全指南
js逆向调试技巧

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog