补环境

将从网站中提取的 JS 加密代码放到 Node.js 中独立运行时,由于缺少浏览器原生对象,通常会报错。补环境就是在 Node.js 中模拟出浏览器的运行环境,让代码能够正常执行。 使用 Proxy 拦截对任意属性的访问,自动打印缺失的环境属性,用于快速定位需要补的内容。 运行后控制台会输出所有访问了但不存在的属性,按需补充即可。 部分网站使用 Canvas 绘图结果作为设备指纹。 对于依赖完整 DOM 环境的代码,使用 jsdom 库更省力。适合页面脚本需要 document、navigator、Cookie 等完整 BOM/DOM 环境的场景。 先用

分享

官方文档:https://nodejs.org/en/docs/
适用场景:将浏览器 JS 加密代码移植到 Node.js 独立运行,补充缺失的浏览器 API

将从网站中提取的 JS 加密代码放到 Node.js 中独立运行时,由于缺少浏览器原生对象,通常会报错。补环境就是在 Node.js 中模拟出浏览器的运行环境,让代码能够正常执行。


环境差异

对象/API 浏览器 Node.js
window 存在(全局对象) 不存在
document 存在 不存在
navigator 存在 不存在
location 存在 不存在
localStorage 存在 不存在
XMLHttpRequest 存在 不存在
canvas 存在 不存在
fetch 存在 Node 18+ 原生支持
crypto window.crypto require('crypto')

基础补环境模板

// env.js - 放在扣出的加密代码之前执行

// 补全全局对象
global.window = global;
global.self = global;

// 补 navigator
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', 'en'],
    cookieEnabled: true,
    javaEnabled: function() { return false; },
    plugins: [],
    appName: 'Netscape',
};

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

// 补 document
global.document = {
    cookie: '',
    domain: 'example.com',
    referrer: '',
    createElement: function(tag) { return {}; },
    getElementById: function() { return null; },
    querySelector: function() { return null; },
};

// 补 localStorage
global.localStorage = (function() {
    var store = {};
    return {
        getItem: function(key) { return store[key] || null; },
        setItem: function(key, val) { store[key] = String(val); },
        removeItem: function(key) { delete store[key]; },
        clear: function() { store = {}; },
    };
})();

// 补 sessionStorage
global.sessionStorage = global.localStorage;

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

// 补 history
global.history = {
    length: 1,
    pushState: function() {},
    replaceState: function() {},
};

Proxy 通杀补环境

使用 Proxy 拦截对任意属性的访问,自动打印缺失的环境属性,用于快速定位需要补的内容。

// 使用 Proxy 检测缺失的环境变量
function createProxyEnv(name, target = {}) {
    return new Proxy(target, {
        get(obj, prop) {
            if (!(prop in obj)) {
                console.log(`[缺失] ${name}.${String(prop)}`);
                return undefined;
            }
            return obj[prop];
        },
        set(obj, prop, value) {
            obj[prop] = value;
            return true;
        }
    });
}

global.window = createProxyEnv('window', global);
global.navigator = createProxyEnv('navigator', {
    userAgent: 'Mozilla/5.0 ...'
});
global.document = createProxyEnv('document', {});

运行后控制台会输出所有访问了但不存在的属性,按需补充即可。


Canvas 指纹补环境

部分网站使用 Canvas 绘图结果作为设备指纹。

// 简单的 canvas 补环境
global.document.createElement = function(tag) {
    if (tag === 'canvas') {
        return {
            width: 0,
            height: 0,
            getContext: function() {
                return {
                    fillStyle: '',
                    fillRect: function() {},
                    fillText: function() {},
                    font: '',
                    textBaseline: '',
                    toDataURL: function() {
                        // 返回固定值或真实 canvas 计算结果
                        return 'data:image/png;base64,iVBORw0KGgo=';
                    }
                };
            },
            toDataURL: function() {
                return 'data:image/png;base64,iVBORw0KGgo=';
            }
        };
    }
    return {};
};

jsdom 方案

对于依赖完整 DOM 环境的代码,使用 jsdom 库更省力。适合页面脚本需要 documentnavigator、Cookie 等完整 BOM/DOM 环境的场景。

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',
    runScripts: 'dangerously',   // 允许执行页面内嵌脚本
    pretendToBeVisual: true,     // 启用 requestAnimationFrame 等视觉 API
    userAgent: 'Mozilla/5.0 ...'
});

global.window    = dom.window;
global.document  = dom.window.document;
global.navigator = dom.window.navigator;
global.location  = dom.window.location;

jsdom 的完整 API 说明(构造选项、VirtualConsole、CookieJar、资源拦截、Proxy 补环境、反检测等)请参考 → jsdom完全指南


常见报错及处理

报错信息 原因 解决方案
window is not defined 缺少 window 对象 global.window = global
document is not defined 缺少 document 补 document 对象
navigator is not defined 缺少 navigator 补 navigator 对象
Cannot read property 'userAgent' of undefined navigator 未正确补全 补 navigator.userAgent
localStorage is not defined 缺少 localStorage 补 localStorage
btoa is not defined Node 较老版本 使用 Buffer.from(str).toString('base64')
atob is not defined Node 较老版本 使用 Buffer.from(str, 'base64').toString()

补环境工作流

1. 将加密函数代码复制到本地 .js 文件
2. 在文件头部引入 env.js(基础环境)
3. 使用 Proxy 包装全局对象,记录缺失属性
4. node 运行,按控制台输出逐步补充缺失的属性/方法
5. 确认加密函数能正常返回结果
6. 在 Python 中通过 subprocess 或 PyExecJS 调用该 Node.js 脚本

Python 调用 Node.js

import subprocess
import json

def call_js_encrypt(data):
    result = subprocess.run(
        ['node', 'encrypt.js', json.dumps(data)],
        capture_output=True,
        text=True
    )
    return result.stdout.strip()

# encrypt.js 最后一行:
# const input = JSON.parse(process.argv[2]);
# console.log(encrypt(input));

最佳实践

先用 Proxy 探测再针对性补:在 global.window = new Proxy({}, { get(t,k){ console.log(k); return undefined } }) 运行目标代码,观察所有访问的属性后,再挑关键属性补充真实实现,避免盲目补充。

补环境代码放在单独文件,与目标 JS 分离env.js 只包含浏览器环境模拟,通过 require('./env') 在目标 JS 前引入,保持目标 JS 不被修改,方便追踪目标代码更新时的变化。

关键 API 优先补:cryptonavigatorlocationdocument:绝大多数加密代码会访问这 4 个对象;crypto.getRandomValuesrequire('crypto').randomFillSync 实现,navigator.userAgent 设为真实 UA 字符串。

vm.runInNewContext 隔离全局变量污染:多段目标 JS 代码独立运行时,用 vm.runInNewContext(code, { window: {...} }) 各自隔离,避免变量名冲突;适合批量并发处理多个请求。

Node.js 调用链加 --timeout 保护:目标 JS 可能含无限循环或死锁的反调试,通过 subprocess 调用 Node.js 时设置超时(timeout=5),避免主进程被卡死。


常见陷阱

陷阱:补了 window.document 但 DOM 操作仍然失败

现象: 补了 document = { querySelector: () => null } 后,代码仍然报 TypeError: document.getElementById is not a function
原因: 补的对象没有覆盖代码所有用到的 DOM API,遇到未补的方法就报错。
解决: 补一个 Proxy 代理替代 document,让所有方法调用默认返回空对象或 null,再逐步替换为真实实现;或用 jsdom 提供完整 DOM 实现。

陷阱:window.location.href 赋值报错

现象: 目标 JS 中有 window.location.href = url 导致 Node.js 报 TypeError: Cannot set property href of [object Object]
原因: 补的 location 对象的 href 是普通属性,部分代码用 setter 赋值跳转,而 Node.js 不需要实际跳转。
解决:Object.defineProperty(location, 'href', { get: () => 'https://target.com', set: () => {} }) 让赋值变成空操作。

陷阱:eval 内的代码引用了外部变量但访问不到

现象: 目标 JS 在 eval 中运行部分代码,但变量是 undefined
原因: eval 的作用域与当前闭包相关,在 Node.js 的模块作用域中直接 eval 不能访问全局变量(需要 global.xxx);或用 vm.runInNewContext 时上下文中缺少该变量。
解决: 将全局变量挂到 global 上(global.xxx = value),或在 vm.runInNewContext 的 context 对象中包含所有需要的变量。


参见

jsdom完全指南
RPC远程调用
Proxy对象与Hook
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