Proxy 对象与 Hook
Proxy 是 ES6 引入的元编程机制,允许拦截并自定义对象的基本操作。在 JS 逆向中,Proxy 是比 Object.defineProperty 更强大的 Hook 工具,可以拦截整个对象上的所有操作,包括函数调用、new 运算符、属性枚举等 Object.defineProperty 无法覆盖的行为。 handler 中没有定义的陷阱,操作会直接透传到 target,行为与不使用 Proxy 相同。 各陷阱方法参数说明: get(target, prop, receiver) set(target, prop, value, receiver)
官方文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
适用版本:ES2015+(2026-05-08 核实)
Proxy 是 ES6 引入的元编程机制,允许拦截并自定义对象的基本操作。在 JS 逆向中,Proxy 是比 Object.defineProperty 更强大的 Hook 工具,可以拦截整个对象上的所有操作,包括函数调用、new 运算符、属性枚举等 Object.defineProperty 无法覆盖的行为。
目录
Proxy 基础语法
const proxy = new Proxy(target, handler);
| 参数 | 类型 | 说明 |
|---|---|---|
target |
任意对象 | 被代理的原始对象,可以是普通对象、函数、数组、另一个 Proxy |
handler |
对象 | 包含各种陷阱(trap)方法的处理器对象 |
handler 中没有定义的陷阱,操作会直接透传到 target,行为与不使用 Proxy 相同。
基础示例
const target = { name: 'alice', age: 18 };
const proxy = new Proxy(target, {
get(target, prop, receiver) {
console.log(`[GET] ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`[SET] ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
});
proxy.name; // 打印 [GET] name
proxy.age = 20; // 打印 [SET] age = 20
Handler 陷阱完整参考
| 陷阱方法 | 触发时机 | 对应的 Reflect 方法 |
|---|---|---|
get(target, prop, receiver) |
读取属性:proxy.prop、proxy[prop] |
Reflect.get |
set(target, prop, value, receiver) |
写入属性:proxy.prop = v |
Reflect.set |
has(target, prop) |
in 运算符:prop in proxy |
Reflect.has |
deleteProperty(target, prop) |
delete proxy.prop |
Reflect.deleteProperty |
apply(target, thisArg, argsList) |
函数调用:proxy()、proxy.call()、proxy.apply() |
Reflect.apply |
construct(target, argsList, newTarget) |
new proxy(...) |
Reflect.construct |
getPrototypeOf(target) |
Object.getPrototypeOf(proxy)、proxy instanceof X |
Reflect.getPrototypeOf |
setPrototypeOf(target, proto) |
Object.setPrototypeOf(proxy, proto) |
Reflect.setPrototypeOf |
ownKeys(target) |
Object.keys()、Object.getOwnPropertyNames()、for...in |
Reflect.ownKeys |
getOwnPropertyDescriptor(target, prop) |
Object.getOwnPropertyDescriptor(proxy, prop) |
Reflect.getOwnPropertyDescriptor |
defineProperty(target, prop, desc) |
Object.defineProperty(proxy, prop, desc) |
Reflect.defineProperty |
isExtensible(target) |
Object.isExtensible(proxy) |
Reflect.isExtensible |
preventExtensions(target) |
Object.preventExtensions(proxy) |
Reflect.preventExtensions |
各陷阱方法参数说明:
get(target, prop, receiver)
| 参数 | 说明 |
|---|---|
target |
原始对象 |
prop |
属性名(string 或 Symbol) |
receiver |
实际触发读取的对象(通常是 proxy 本身,继承时可能不同) |
set(target, prop, value, receiver)
| 参数 | 说明 |
|---|---|
target |
原始对象 |
prop |
属性名 |
value |
要写入的值 |
receiver |
实际触发赋值的对象 |
| 返回值 | 必须返回 true(成功)或 false(失败,严格模式下抛 TypeError) |
apply(target, thisArg, argsList)
| 参数 | 说明 |
|---|---|
target |
被代理的函数 |
thisArg |
调用时的 this |
argsList |
参数数组 |
construct(target, argsList, newTarget)
| 参数 | 说明 |
|---|---|
target |
被代理的构造函数 |
argsList |
new 时传入的参数数组 |
newTarget |
new.target,即实际调用 new 的构造函数 |
| 返回值 | 必须返回一个对象 |
Reflect 对象
Reflect 是与 Proxy 配套的静态对象,每个 Proxy 陷阱都对应一个同名的 Reflect 方法。在陷阱中调用 Reflect 方法可以将操作转发到原始对象,保持默认行为。
// 推荐写法:总是在陷阱末尾调用对应 Reflect 方法
const proxy = new Proxy(target, {
get(target, prop, receiver) {
// 做一些额外的事情...
console.log(`[GET] ${prop}`);
// 然后执行默认行为
return Reflect.get(target, prop, receiver);
}
});
为什么用 Reflect 而不是直接操作 target?
Reflect.get(target, prop, receiver)正确处理了 getter 中的this(指向receiver而非target)- 直接写
return target[prop]会导致this绑定错误,某些内置方法会因此崩溃
逆向 Hook 应用
拦截属性读取(监控谁在读某个值)
// 监控谁在读取 navigator.userAgent(检测反爬检测代码)
(function () {
const navProto = Object.getPrototypeOf(navigator);
const desc = Object.getOwnPropertyDescriptor(navProto, 'userAgent');
const originalGet = desc.get;
Object.defineProperty(navProto, 'userAgent', {
configurable: true,
enumerable: true,
get() {
console.log('[READ] navigator.userAgent', new Error().stack);
return originalGet.call(this);
}
});
})();
拦截属性赋值(监控 Cookie 被谁设置)
使用 Proxy 代理 document 对象(注意:document 是特殊宿主对象,直接代理有限制,优先用 defineProperty 方式):
// Proxy 方式:拦截对 document 上任意属性的写入
const docProxy = new Proxy(document, {
set(target, prop, value, receiver) {
if (prop === 'cookie') {
console.log('[cookie SET via proxy]', value);
console.log(new Error().stack);
}
return Reflect.set(target, prop, value, receiver);
}
});
// 若页面代码通过 docProxy.cookie = '...' 设置则会被拦截
// 但无法替换全局 document,通常与补环境一起使用
拦截函数调用(apply trap 监控参数)
// 代理任意函数,监控每次调用的参数和返回值
function hookFunction(originalFn, name) {
return new Proxy(originalFn, {
apply(target, thisArg, argsList) {
console.log(`[CALL] ${name}(`, ...argsList, ')');
console.log(new Error().stack);
const result = Reflect.apply(target, thisArg, argsList);
console.log(`[RETURN] ${name} =>`, result);
return result;
}
});
}
// 使用示例:Hook JSON.stringify
JSON.stringify = hookFunction(JSON.stringify, 'JSON.stringify');
// Hook window.btoa(Base64 编码)
window.btoa = hookFunction(window.btoa, 'btoa');
// Hook encodeURIComponent
window.encodeURIComponent = hookFunction(
window.encodeURIComponent,
'encodeURIComponent'
);
拦截 new 操作(construct trap)
// 监控 XMLHttpRequest 的构造,在创建时打印调用栈
const OriginalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = new Proxy(OriginalXHR, {
construct(target, argsList, newTarget) {
console.log('[new XMLHttpRequest]', new Error().stack);
return Reflect.construct(target, argsList, newTarget);
}
});
全局对象代理(window 的 Proxy)
代理 window 可以监控页面所有全局变量的读写,适合不知道加密变量名时的全量排查:
// 警告:全局代理 window 会产生大量输出,建议加过滤条件
(function () {
const ignoredProps = new Set([
'console', 'document', 'location', 'window',
'undefined', 'NaN', 'Infinity'
]);
const windowProxy = new Proxy(window, {
set(target, prop, value) {
if (!ignoredProps.has(prop)) {
console.log(`[window SET] ${prop} =`, value);
console.log(new Error().stack);
}
target[prop] = value;
return true;
},
get(target, prop, receiver) {
// 只监控感兴趣的属性,避免刷屏
if (prop === 'sign' || prop === '_token' || prop === 'encrypt') {
console.log(`[window GET] ${prop} =`, target[prop]);
}
return Reflect.get(target, prop, receiver);
}
});
// 注意:无法用 window = windowProxy 替换全局 window
// 通常用于补环境场景,将 globalThis 指向 proxy
})();
与 Object.defineProperty Hook 对比
| 能力 | Object.defineProperty | Proxy |
|---|---|---|
| 拦截属性读取(get) | 支持 | 支持 |
| 拦截属性写入(set) | 支持 | 支持 |
| 拦截属性删除(delete) | 不支持 | 支持(deleteProperty trap) |
拦截 in 运算符 |
不支持 | 支持(has trap) |
| 拦截函数调用 | 不支持 | 支持(apply trap) |
拦截 new 操作 |
不支持 | 支持(construct trap) |
| 拦截 Object.keys / for...in | 不支持 | 支持(ownKeys trap) |
| 拦截 getOwnPropertyDescriptor | 不支持 | 支持 |
| 代理整个对象(无需知道属性名) | 不支持(须逐属性定义) | 支持 |
| 代理不存在的属性 | 不支持(属性必须已存在或新建) | 支持 |
| 在旧版浏览器(ES5)中使用 | 支持 | 不支持(无 polyfill) |
被 Object.freeze 阻止 |
是 | 是(冻结对象无法被代理修改) |
为什么 Proxy 无法被 Object.defineProperty 检测到
Object.defineProperty 只能监控自己明确定义了 getter/setter 的那个属性,无法感知"这个对象是否是 Proxy"。检测一个值是否为 Proxy 实例在 JS 标准中没有提供任何 API,typeof、instanceof、Object.prototype.toString 都返回与原始对象相同的结果。
const proxy = new Proxy({}, {});
typeof proxy; // 'object'
proxy instanceof Object; // true
Object.prototype.toString.call(proxy); // '[object Object]'
网站反 Hook 检测 Proxy 的方式
部分高对抗网站会尝试识别 Proxy,常见手段:
手段 1:检测 toString 是否包含 native code
// 检测内置函数是否被替换
if (!/\[native code\]/.test(JSON.stringify.toString())) {
// 认为被 Hook
}
反制:在 apply trap 内伪造 toString:
const proxiedStringify = new Proxy(JSON.stringify, {
apply(target, thisArg, args) {
return Reflect.apply(target, thisArg, args);
}
});
// 伪造 toString
proxiedStringify.toString = () => 'function stringify() { [native code] }';
手段 2:检测属性描述符
// 检测 document.cookie 的 getter 是否是原生函数
const desc = Object.getOwnPropertyDescriptor(document, 'cookie');
if (desc && !/\[native code\]/.test(desc.get?.toString())) {
// 认为被 Hook
}
手段 3:用 iframe 获取干净对象
// 部分网站通过 iframe 拿到未被 Hook 的原生对象进行比对
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const cleanJSON = iframe.contentWindow.JSON;
// 对比 cleanJSON.stringify === JSON.stringify 来检测 Hook
实战 Hook 脚本
监控所有 document.cookie 赋值操作
(function () {
const desc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
const nativeGet = desc.get;
const nativeSet = desc.set;
Object.defineProperty(document, 'cookie', {
configurable: true,
enumerable: true,
get() {
return nativeGet.call(this);
},
set(val) {
console.log('[cookie SET]', val);
console.log(new Error().stack);
// 有特定 cookie 时打断点
if (val.includes('_sign') || val.includes('token')) {
debugger;
}
nativeSet.call(this, val);
}
});
})();
监控 localStorage 读写
(function () {
const _getItem = localStorage.getItem.bind(localStorage);
const _setItem = localStorage.setItem.bind(localStorage);
const _removeItem = localStorage.removeItem.bind(localStorage);
localStorage.getItem = function (key) {
const value = _getItem(key);
console.log(`[localStorage GET] ${key} =`, value);
return value;
};
localStorage.setItem = function (key, value) {
console.log(`[localStorage SET] ${key} =`, value);
console.log(new Error().stack);
return _setItem(key, value);
};
localStorage.removeItem = function (key) {
console.log(`[localStorage REMOVE] ${key}`);
return _removeItem(key);
};
})();
监控 XMLHttpRequest headers 设置
(function () {
const _setRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
const _open = XMLHttpRequest.prototype.open;
const _send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
this._hookUrl = url;
this._hookMethod = method;
return _open.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
console.log(`[XHR setRequestHeader] ${this._hookMethod} ${this._hookUrl}`);
console.log(` ${name}: ${value}`);
return _setRequestHeader.call(this, name, value);
};
XMLHttpRequest.prototype.send = function (body) {
if (body) {
console.log(`[XHR send] ${this._hookMethod} ${this._hookUrl}`, body);
console.log(new Error().stack);
}
return _send.call(this, body);
};
})();
拦截 WebSocket 发送的数据
(function () {
const OriginalWebSocket = window.WebSocket;
window.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const ws = Reflect.construct(target, args);
const _send = ws.send.bind(ws);
ws.send = function (data) {
console.log('[WebSocket send]', data);
console.log(new Error().stack);
// 关键数据打断点
if (typeof data === 'string' && data.includes('sign')) {
debugger;
}
return _send(data);
};
// 同时监控收到的消息
ws.addEventListener('message', function (event) {
console.log('[WebSocket message]', event.data);
});
return ws;
}
});
})();
全局函数调用追踪(打印调用栈)
// 对 window 上所有函数自动注入追踪逻辑
// 用于不知道哪个函数在加密时的全量扫描
(function () {
const skipFns = new Set([
'eval', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
'requestAnimationFrame', 'cancelAnimationFrame',
'console', 'Error', 'Object', 'Array', 'String', 'Number'
]);
function traceFunction(fn, name) {
if (typeof fn !== 'function' || skipFns.has(name)) return fn;
return new Proxy(fn, {
apply(target, thisArg, argsList) {
console.log(`[TRACE] ${name}(`, ...argsList, ')');
return Reflect.apply(target, thisArg, argsList);
}
});
}
// 只追踪用户自定义函数(在 DOMContentLoaded 后执行,让页面先加载完)
window.addEventListener('DOMContentLoaded', () => {
for (const key in window) {
try {
if (typeof window[key] === 'function' && !skipFns.has(key)) {
window[key] = traceFunction(window[key], key);
}
} catch (e) {
// 忽略 configurable: false 的属性
}
}
});
})();
踩坑与注意事项
Proxy 不能代理原始值
原始值(string、number、boolean、null、undefined、Symbol、BigInt)不是对象,无法作为 new Proxy() 的 target。如果需要拦截对原始值的操作,只能通过拦截其容器对象(如拦截函数的参数)来实现。
// 错误:原始值不能被代理
new Proxy('hello', {}); // TypeError: Cannot create proxy with a non-object
// 正确:代理包装对象(注意包装对象不等于原始值)
new Proxy(new String('hello'), {}); // 可以,但通常没必要
某些内置对象需要特殊处理(以 Date 为例)
许多内置对象的方法依赖内部 slot(如 DateValue),这些 slot 只在原始对象上存在,Proxy 不会继承它们。通过 Proxy 调用这类方法会报 TypeError: this is not a Date object。
const dateProxy = new Proxy(new Date(), {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
// 必须绑定到 target,不能用 receiver
if (typeof value === 'function') {
return value.bind(target); // 关键:绑定到原始对象
}
return value;
}
});
dateProxy.getTime(); // 正常,因为 bind(target) 了
同样的问题存在于 Map、Set、WeakMap、Promise、RegExp、TypedArray 等内置对象。
this 绑定问题
在 get 陷阱中返回方法时,如果不绑定 this,方法内的 this 可能指向 Proxy 而非原始对象,导致内部 slot 访问失败:
// 错误写法(Map 方法会因 this 绑定到 proxy 而报错)
const mapProxy = new Proxy(new Map(), {
get(target, prop) {
return target[prop]; // this 绑定到 proxy,Map 内部方法报错
}
});
mapProxy.set('a', 1); // TypeError: Method Map.prototype.set called on incompatible receiver
// 正确写法
const mapProxy = new Proxy(new Map(), {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
return typeof value === 'function' ? value.bind(target) : value;
}
});
mapProxy.set('a', 1); // 正常
Proxy 必须通过代理对象访问才能触发拦截
Proxy 是一个"壳",只有通过代理对象引用才会触发 trap。如果代码中保留了对原始对象的直接引用,拦截不会生效。
const original = { key: 'value' };
const proxy = new Proxy(original, {
get(t, p) { console.log('intercepted'); return t[p]; }
});
proxy.key; // 打印 "intercepted"
original.key; // 不打印,直接访问原始对象
在逆向中替换全局函数时,必须将 proxy 赋值回原变量:
// 必须将 proxy 赋值回去,否则页面原有代码仍使用旧引用
window.JSON = new Proxy(JSON, { /* ... */ });
// 不能只 const p = new Proxy(JSON, ...) 而不赋值
set 陷阱必须返回 true
set 陷阱如果没有返回 true,在严格模式下会抛出 TypeError,非严格模式下赋值会静默失败。
// 错误:没有 return true
new Proxy({}, {
set(target, prop, value) {
target[prop] = value;
// 忘记 return true
}
});
// 正确
new Proxy({}, {
set(target, prop, value, receiver) {
return Reflect.set(target, prop, value, receiver); // 自动返回 true/false
}
});
最佳实践
用 Proxy 探测未知对象的访问模式:在不了解目标代码访问哪些属性时,用全拦截 Proxy 替换目标对象,将所有 get/set/has 操作打印出来,快速绘制出目标代码的访问模式图:
const explorer = new Proxy({}, {
get(t, k) { console.log('[GET]', k); return undefined },
set(t, k, v) { console.log('[SET]', k, '=', v); return true },
has(t, k) { console.log('[HAS]', k); return false },
})
window = explorer // 替换 window 运行目标代码
apply trap 拦截函数调用比直接替换函数更安全:直接替换 fn = new Proxy(fn, { apply(...) }) 保留原函数引用,拦截器可以调用 Reflect.apply 执行原函数,不改变 fn.length、fn.name 等元信息,减少被检测到 Hook 的概率。
补环境时用 Proxy 构建惰性对象:不知道目标代码会访问哪些属性时,用 Proxy 返回另一个 Proxy 代替 undefined,让链式访问(a.b.c.d())不报错:
const lazy = () => new Proxy(() => lazy(), { get: (t, k) => lazy() })
revocable Proxy 用于临时授权:Proxy.revocable(target, handler) 返回可撤销的 Proxy,在特定时间窗口内开放访问,撤销后所有访问报 TypeError,适合控制 Hook 的生命周期。
Proxy 不能透明代理已冻结对象:若目标已被 Object.freeze(),Proxy 的 set trap 无法写入,必须在冻结前代理,或用整体替换的方式。
常见陷阱
陷阱:Proxy handler 的 get 不返回 Reflect.get 导致原型链断裂
现象: 用 Proxy 代理对象后,instanceof、方法调用报 TypeError: Method called on incompatible receiver。
原因: get trap 没有调用 Reflect.get(target, prop, receiver),导致方法体中 this 指向错误或原型方法找不到。
解决: 始终在 get trap 最后调用 Reflect.get(target, prop, receiver) 返回原始值,确保原型链和 this 绑定正确。
陷阱:set trap 返回 undefined 触发 TypeError in strict mode
现象: Proxy 的 set 钩子在严格模式下抛出 TypeError: 'set' on proxy: trap returned falsish。
原因: set trap 必须返回 true(表示设置成功);若函数没有 return 语句,默认返回 undefined(falsy),严格模式抛错。
解决: 所有 set trap 末尾加 return Reflect.set(target, prop, value, receiver) 或显式 return true。
陷阱:Proxy 无法拦截 typeof 和 instanceof
现象: 目标代码用 typeof obj === 'function' 检测 Hook,Proxy 拦截不了。
原因: typeof 是操作符级别的检查,Proxy 没有对应 trap;instanceof 可以通过 Symbol.hasInstance 影响,但不能用 trap 覆盖。
解决: 让 Proxy 代理的是函数(new Proxy(()=>{}, handler)),这样 typeof 返回 'function' 绕过检测;instanceof 检测只能在目标代码层面分析和处理。