> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Proxy 对象与 Hook
- URL: https://blog.vercanti.com/proxy-dui-xiang-yu-hook/
- Published: 2026-08-28T14:34:56.000Z
- Updated: 2026-08-28T14:57:41.000Z
- Description: Proxy 是 ES6 引入的元编程机制，允许拦截并自定义对象的基本操作。在 JS 逆向中，Proxy 是比 Object.defineProperty 更强大的 Hook 工具，可以拦截整个对象上的所有操作，包括函数调用、new 运算符、属性枚举等 Object.defineProperty 无法覆盖的行为。 handler 中没有定义的陷阱，操作会直接透传到 target，行为与不使用 Proxy 相同。 各陷阱方法参数说明： get(target, prop, receiver) set(target, prop, value, receiver)
- Author: yellowdog
- Tags: js逆向, JS基础

> 官方文档：<https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Proxy>  
> 适用版本：ES2015+（2026-05-08 核实）

Proxy 是 ES6 引入的元编程机制，允许拦截并自定义对象的基本操作。在 JS 逆向中，Proxy 是比 `Object.defineProperty` 更强大的 Hook 工具，可以拦截整个对象上的所有操作，包括函数调用、`new` 运算符、属性枚举等 `Object.defineProperty` 无法覆盖的行为。

---

## 目录

- [Proxy 基础语法](#proxy-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95)
- [Handler 陷阱完整参考](#handler-%E9%99%B7%E9%98%B1%E5%AE%8C%E6%95%B4%E5%8F%82%E8%80%83)
- [Reflect 对象](#reflect-%E5%AF%B9%E8%B1%A1)
- [逆向 Hook 应用](#%E9%80%86%E5%90%91-hook-%E5%BA%94%E7%94%A8)
- [与 Object.defineProperty Hook 对比](#%E4%B8%8E-objectdefineproperty-hook-%E5%AF%B9%E6%AF%94)
- [实战 Hook 脚本](#%E5%AE%9E%E6%88%98-hook-%E8%84%9A%E6%9C%AC)
- [踩坑与注意事项](#%E8%B8%A9%E5%9D%91%E4%B8%8E%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)

---

## Proxy 基础语法

```js
const proxy = new Proxy(target, handler);

```

| 参数      | 类型   | 说明                               |
| ------- | ---- | -------------------------------- |
| target  | 任意对象 | 被代理的原始对象，可以是普通对象、函数、数组、另一个 Proxy |
| handler | 对象   | 包含各种陷阱（trap）方法的处理器对象             |

`handler` 中没有定义的陷阱，操作会直接透传到 `target`，行为与不使用 Proxy 相同。

### 基础示例

```js
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` 方法可以将操作转发到原始对象，保持默认行为。

```js
// 推荐写法：总是在陷阱末尾调用对应 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 应用

### 拦截属性读取（监控谁在读某个值）

```js
// 监控谁在读取 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` 方式）：

```js
// 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 监控参数）

```js
// 代理任意函数，监控每次调用的参数和返回值
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）

```js
// 监控 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` 可以监控页面所有全局变量的读写，适合不知道加密变量名时的全量排查：

```js
// 警告：全局代理 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` 都返回与原始对象相同的结果。

```js
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**

```js
// 检测内置函数是否被替换
if (!/\[native code\]/.test(JSON.stringify.toString())) {
    // 认为被 Hook
}

```

反制：在 apply trap 内伪造 toString：

```js
const proxiedStringify = new Proxy(JSON.stringify, {
    apply(target, thisArg, args) {
        return Reflect.apply(target, thisArg, args);
    }
});
// 伪造 toString
proxiedStringify.toString = () => 'function stringify() { [native code] }';

```

**手段 2：检测属性描述符**

```js
// 检测 document.cookie 的 getter 是否是原生函数
const desc = Object.getOwnPropertyDescriptor(document, 'cookie');
if (desc && !/\[native code\]/.test(desc.get?.toString())) {
    // 认为被 Hook
}

```

**手段 3：用 iframe 获取干净对象**

```js
// 部分网站通过 iframe 拿到未被 Hook 的原生对象进行比对
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const cleanJSON = iframe.contentWindow.JSON;
// 对比 cleanJSON.stringify === JSON.stringify 来检测 Hook

```

---

## 实战 Hook 脚本

### 监控所有 document.cookie 赋值操作

```js
(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 读写

```js
(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 设置

```js
(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 发送的数据

```js
(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;
        }
    });
})();

```

### 全局函数调用追踪（打印调用栈）

```js
// 对 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`。如果需要拦截对原始值的操作，只能通过拦截其容器对象（如拦截函数的参数）来实现。

```js
// 错误：原始值不能被代理
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`。

```js
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 访问失败：

```js
// 错误写法（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。如果代码中保留了对原始对象的直接引用，拦截不会生效。

```js
const original = { key: 'value' };
const proxy = new Proxy(original, {
    get(t, p) { console.log('intercepted'); return t[p]; }
});

proxy.key;    // 打印 "intercepted"
original.key; // 不打印，直接访问原始对象

```

在逆向中替换全局函数时，必须将 proxy 赋值回原变量：

```js
// 必须将 proxy 赋值回去，否则页面原有代码仍使用旧引用
window.JSON = new Proxy(JSON, { /* ... */ });
// 不能只 const p = new Proxy(JSON, ...) 而不赋值

```

**`set` 陷阱必须返回 true**

`set` 陷阱如果没有返回 `true`，在严格模式下会抛出 `TypeError`，非严格模式下赋值会静默失败。

```js
// 错误：没有 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` 操作打印出来，快速绘制出目标代码的访问模式图：

```javascript
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()`）不报错：

```javascript
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` 检测只能在目标代码层面分析和处理。

---

## 参见

[Object静态方法与Hook](https://blog.vercanti.com/object-jing-tai-fang-fa-yu-hook-shi-yong-chang-jing/)  
[hook](https://blog.vercanti.com/hook-dai-ma-duan/)  
[补环境](https://blog.vercanti.com/bu-huan-jing/)  
[js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)