JS 逆向代码片段
整理自 snippet.js,包含两个部分: 监控一个对象的所有属性读取和写入,输出到 console。 用法: 适用场景:快速定位哪段代码在读写某个对象的属性。 当页面通过 JS 动态创建 <script> 标签加载外部脚本时触发断点。 方式一:defineProperty 方式二:Proxy 代理整个原型(更全面) 适用场景:网站动态插入第三方脚本(风控 JS、指纹采集 SDK),定位其加载时机和来源。 完整的 Node.js 补环境方案,在本地运行浏览器加密 JS 时使用。 功能列表: 对任意对象/函数递归包裹 Proxy,追踪所有操作并记录访问路
整理自 snippet.js,包含两个部分:
- 工具函数:独立可用的小片段,直接粘贴到 DevTools Console 使用
- 环境 Hook 模块:完整的补环境 + 全局代理方案,用于 Node.js 本地调试
一、工具函数
1.1 watch() — Proxy 对象监控
监控一个对象的所有属性读取和写入,输出到 console。
function watch(obj, name) {
const handler = {
get: function (target, prop) {
console.log('get:', `${name}.${prop}`, '===>', Reflect.get(...arguments));
return Reflect.get(...arguments);
},
set: function (target, prop, value, receiver) {
console.log('set:', `${name}.${prop}`, '===>', value);
return Reflect.set(target, prop, value, receiver);
}
};
return new Proxy(obj, handler);
}
用法:
// 把 window.navigator 替换为被监控的代理对象
window.navigator = watch(window.navigator, 'navigator');
// 监控自定义对象
const config = watch({ token: '', uid: 0 }, 'config');
config.token = 'abc'; // 控制台输出: set: config.token ===> abc
适用场景:快速定位哪段代码在读写某个对象的属性。
1.2 HTMLScriptElement.src Hook — 监控动态脚本注入
当页面通过 JS 动态创建 <script> 标签加载外部脚本时触发断点。
方式一:defineProperty
// 拦截所有 script 元素的 src 赋值
Object.defineProperty(HTMLScriptElement.prototype, 'src', {
set: function (value) {
console.log('[script.src]', value);
debugger; // 触发断点,查看调用栈定位加载来源
}
});
方式二:Proxy 代理整个原型(更全面)
// 拦截原型上的所有属性赋值,不仅限于 src
HTMLScriptElement.prototype = new Proxy(HTMLScriptElement.prototype, {
set: function (target, prop, value) {
console.log('[HTMLScriptElement]', prop, '=', value);
debugger;
return Reflect.set(target, prop, value);
}
});
适用场景:网站动态插入第三方脚本(风控 JS、指纹采集 SDK),定位其加载时机和来源。
注意:两种方式不要同时使用,会产生冲突(片段中有重复定义)。推荐用方式一定向监控 src,或用方式二监控所有属性。
二、环境 Hook 模块(补环境 + 全局代理)
完整的 Node.js 补环境方案,在本地运行浏览器加密 JS 时使用。
功能列表:
| 功能 | 说明 |
|---|---|
| 深度 Proxy | 对任意对象/函数做递归代理,追踪 get/set/has/call |
| 浏览器环境模拟 | 补全 window/navigator/document/localStorage |
| 定时器拦截 | 记录 setTimeout/setInterval 调用 |
| 全局对象代理 | 对 window/document/navigator 等批量挂载 Proxy |
| 防重复安装 | 通过 __envHookInstalled 标志防止重复执行 |
2.1 配置项
const CONFIG = {
enableStack: true, // 是否在日志中输出调用栈
deepProxy: true, // 是否对 get 返回值递归代理(深度追踪)
logGet: true, // 记录属性读取
logSet: true, // 记录属性写入
logHas: false, // 记录 in 操作符检测(通常不需要)
logCall: true // 记录函数调用
};
2.2 敏感 Key 列表
const SENSITIVE_KEYS = new Set([
"webdriver", // 自动化检测
"userAgent", // UA 指纹
"platform", // 系统平台
"languages", // 语言设置
"plugins", // 浏览器插件列表
"mimeTypes", // MIME 类型
"cookie", // Cookie
"localStorage", // 本地存储
"sessionStorage" // 会话存储
]);
2.3 核心代理 createProxy()
对任意对象/函数递归包裹 Proxy,追踪所有操作并记录访问路径。
function createProxy(target, path = "") {
// 非对象/函数直接返回
if (!isObject(target) && typeof target !== "function") return target;
// 缓存避免重复代理同一对象
if (proxyCache.has(target)) return proxyCache.get(target);
const handler = {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
if (CONFIG.logGet) {
console.log({ type: "get", path, property, valueType: typeof value, stack: safeStack() });
}
// deepProxy: 对返回值也代理,可以追踪链式调用
if (CONFIG.deepProxy) return createProxy(value, `${path}.${String(property)}`);
return value;
},
set(target, property, value, receiver) {
if (CONFIG.logSet) {
console.log({ type: "set", path, property, value });
}
return Reflect.set(target, property, value, receiver);
},
has(target, property) {
if (CONFIG.logHas) console.log({ type: "has", path, property });
return Reflect.has(target, property);
},
apply(target, thisArg, args) {
if (CONFIG.logCall) {
console.log({ type: "call", path, args, stack: safeStack() });
}
return Reflect.apply(target, thisArg, args);
}
};
const proxy = new Proxy(target, handler);
proxyCache.set(target, proxy);
return proxy;
}
2.4 浏览器环境模拟 patchBrowserEnv()
在 Node.js 中补全缺失的浏览器全局对象:
function patchBrowserEnv() {
if (!global.window) global.window = global;
if (!global.navigator) {
global.navigator = {
userAgent: "Mozilla/5.0",
platform: "Win32",
languages: ["zh-CN", "zh"],
webdriver: false // 关键:不能为 true,否则触发反爬检测
};
}
if (!global.document) {
global.document = {
cookie: "",
createElement: () => ({})
};
}
if (!global.localStorage) {
const store = {};
global.localStorage = {
getItem: k => store[k],
setItem: (k, v) => store[k] = v,
removeItem: k => delete store[k]
};
}
}
2.5 定时器拦截 patchTimers()
拦截 setTimeout/setInterval,记录调用延迟(可用于定位无限 debugger 来源):
function patchTimers() {
const _setTimeout = global.setTimeout;
const _setInterval = global.setInterval;
global.setTimeout = function (fn, delay, ...args) {
console.log("setTimeout intercepted", delay);
return _setTimeout(fn, delay, ...args);
};
global.setInterval = function (fn, delay, ...args) {
console.log("setInterval intercepted", delay);
return _setInterval(fn, delay, ...args);
};
}
2.6 全局对象批量代理 hookGlobals()
对列出的全局变量挂载 defineProperty getter,访问时返回 Proxy:
function hookGlobals(list) {
for (const name of list) {
if (!(name in global)) continue;
const original = global[name];
Object.defineProperty(global, name, {
configurable: true,
enumerable: true,
get() {
return createProxy(original, name);
}
});
}
}
// 启动时代理的全局变量列表
hookGlobals([
"window", "document", "navigator", "location",
"history", "screen", "localStorage", "sessionStorage"
]);
2.7 完整代码
(function () {
if (global.__envHookInstalled) return;
global.__envHookInstalled = true;
const CONFIG = {
enableStack: true,
deepProxy: true,
logGet: true,
logSet: true,
logHas: false,
logCall: true
};
const SENSITIVE_KEYS = new Set([
"webdriver", "userAgent", "platform", "languages",
"plugins", "mimeTypes", "cookie", "localStorage", "sessionStorage"
]);
const proxyCache = new WeakMap();
function safeStack() {
if (!CONFIG.enableStack) return null;
return new Error().stack;
}
function isObject(v) {
return typeof v === "object" && v !== null;
}
function createProxy(target, path = "") {
if (!isObject(target) && typeof target !== "function") return target;
if (proxyCache.has(target)) return proxyCache.get(target);
const handler = {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
if (CONFIG.logGet) {
console.log({ type: "get", path, property, valueType: typeof value, stack: safeStack() });
}
if (CONFIG.deepProxy) return createProxy(value, `${path}.${String(property)}`);
return value;
},
set(target, property, value, receiver) {
if (CONFIG.logSet) console.log({ type: "set", path, property, value });
return Reflect.set(target, property, value, receiver);
},
has(target, property) {
if (CONFIG.logHas) console.log({ type: "has", path, property });
return Reflect.has(target, property);
},
apply(target, thisArg, args) {
if (CONFIG.logCall) console.log({ type: "call", path, args, stack: safeStack() });
return Reflect.apply(target, thisArg, args);
}
};
const proxy = new Proxy(target, handler);
proxyCache.set(target, proxy);
return proxy;
}
function patchBrowserEnv() {
if (!global.window) global.window = global;
if (!global.navigator) {
global.navigator = {
userAgent: "Mozilla/5.0", platform: "Win32",
languages: ["zh-CN", "zh"], webdriver: false
};
}
if (!global.document) {
global.document = { cookie: "", createElement: () => ({}) };
}
if (!global.localStorage) {
const store = {};
global.localStorage = {
getItem: k => store[k],
setItem: (k, v) => store[k] = v,
removeItem: k => delete store[k]
};
}
}
function patchTimers() {
const _setTimeout = global.setTimeout;
const _setInterval = global.setInterval;
global.setTimeout = (fn, delay, ...args) => { console.log("setTimeout", delay); return _setTimeout(fn, delay, ...args); };
global.setInterval = (fn, delay, ...args) => { console.log("setInterval", delay); return _setInterval(fn, delay, ...args); };
}
function hookGlobals(list) {
for (const name of list) {
if (!(name in global)) continue;
const original = global[name];
Object.defineProperty(global, name, {
configurable: true, enumerable: true,
get() { return createProxy(original, name); }
});
}
}
patchBrowserEnv();
patchTimers();
hookGlobals(["window", "document", "navigator", "location", "history", "screen", "localStorage", "sessionStorage"]);
console.log("Environment hook installed");
})();
三、使用建议
在 Node.js 补环境场景下
- 将 2.7 完整代码 放在目标 JS 文件最顶部,在
require或eval之前执行 - 运行后观察 console 输出,找到目标网站 JS 实际访问了哪些
navigator/document字段 - 根据输出补全
patchBrowserEnv()中缺失的字段
在 DevTools Console 场景下
- 1.1 watch() 可直接粘贴使用,然后
window.xxx = watch(window.xxx, 'xxx')替换目标对象 - 1.2 script.src hook 页面加载初期(Network 面板 → 点击
Preserve log)注入,可捕获动态脚本
关闭深度代理(避免日志刷屏)
当 deepProxy: true 时,链式属性访问会产生大量日志。可以先关掉:
CONFIG.deepProxy = false;
CONFIG.logGet = false; // 只看 set 和 call
最佳实践
先禁用 deepProxy 再缩小范围:全局代理开启后日志量极大,CONFIG.deepProxy = false; CONFIG.logGet = false; 先关掉深层追踪,只看 set 和 call,定位到可疑操作后再按需打开。
用 watch() 替代手动 Object.defineProperty:本文件提供的 watch(obj, 'key') 工具函数已经封装了 getter/setter Hook,比每次手写 defineProperty 少出错,且自动打印调用栈。
// 监控 config 对象的 token 属性何时被赋值
watch(config, 'token');
deepClone 用于打印快照:在 Hook 回调里用 deepClone(obj) 代替直接 console.log(obj),避免 Chrome Console 延迟求值导致看到的是修改后的值。
环境 Hook 模块统一在入口处初始化:把 initEnvHooks() 放到 Node.js 脚本的第一行,确保所有模拟环境在目标代码 require 前已就绪,否则目标代码在模块加载时就会读到真实 Node.js 环境。
// 正确:最顶部初始化
require('./snippet'); // 先加载环境模拟
const target = require('./target');
// 错误:先加载目标,环境模拟太晚
const target = require('./target'); // 目标已在 require 时读取 navigator 等
用 CONFIG.blacklist 过滤噪音属性:document.title、location.href 等属性访问极频繁,把它们加入 CONFIG.blacklist 避免刷屏,专注于真正有价值的属性访问日志。
常见陷阱
陷阱:deepProxy: true 导致循环引用报错
现象: 开启深层 Proxy 后,控制台报 Maximum call stack size exceeded 或 Converting circular structure to JSON。
原因: document、window 等对象存在循环引用(window.window === window),深层代理递归到循环引用时栈溢出。
解决: 在 Proxy get 拦截器里维护一个 WeakSet 记录已代理对象,遇到已代理的直接返回原值。本文件的实现已处理常见情况;如遇新问题,手动把循环引用属性加入 CONFIG.blacklist。
陷阱:Node.js 下 window 未定义导致目标代码崩溃
现象: 在 Node.js 里运行逆向目标代码,报 ReferenceError: window is not defined。
原因: 浏览器代码依赖全局 window 对象,Node.js 环境没有。
解决: 在入口最顶部执行 global.window = global; 并加载本文件提供的环境模拟模块,它会补充 navigator、document、location 等常见全局变量。
global.window = global;
global.navigator = { userAgent: 'Mozilla/5.0 ...' };
// ... 其他补环境
陷阱:watch() 监控不到通过原型链赋值的属性
现象: watch(obj, 'key') 注册后,赋值操作没有触发 Hook。
原因: watch 在 obj 自身上定义 defineProperty,如果赋值发生在原型对象上(Object.getPrototypeOf(obj).key = val),不会触发 obj 上的 setter。
解决: 对原型链赋值,改为 watch(Object.getPrototypeOf(obj), 'key');或改用 Proxy 拦截整个原型链。