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.propproxy[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);
        }
    });
})();

使用 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,typeofinstanceofObject.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 不能代理原始值

原始值(stringnumberbooleannullundefinedSymbolBigInt)不是对象,无法作为 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) 了

同样的问题存在于 MapSetWeakMapPromiseRegExpTypedArray 等内置对象。

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.lengthfn.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 无法拦截 typeofinstanceof

现象: 目标代码用 typeof obj === 'function' 检测 Hook,Proxy 拦截不了。
原因: typeof 是操作符级别的检查,Proxy 没有对应 trap;instanceof 可以通过 Symbol.hasInstance 影响,但不能用 trap 覆盖。
解决: 让 Proxy 代理的是函数(new Proxy(()=>{}, handler)),这样 typeof 返回 'function' 绕过检测;instanceof 检测只能在目标代码层面分析和处理。


参见

Object静态方法与Hook
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