> ## 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.

# JS 逆向代码片段
- URL: https://blog.vercanti.com/js-ni-xiang-dai-ma-pian-duan/
- Published: 2026-08-28T14:35:00.000Z
- Updated: 2026-08-28T14:57:50.000Z
- Description: 整理自 snippet.js，包含两个部分： 监控一个对象的所有属性读取和写入，输出到 console。 用法： 适用场景：快速定位哪段代码在读写某个对象的属性。 当页面通过 JS 动态创建 <script> 标签加载外部脚本时触发断点。 方式一：defineProperty 方式二：Proxy 代理整个原型（更全面） 适用场景：网站动态插入第三方脚本（风控 JS、指纹采集 SDK），定位其加载时机和来源。 完整的 Node.js 补环境方案，在本地运行浏览器加密 JS 时使用。 功能列表： 对任意对象/函数递归包裹 Proxy，追踪所有操作并记录访问路
- Author: yellowdog
- Tags: js逆向, 代码段

> 官方文档：<https://developer.mozilla.org/zh-CN/docs/Web/API>

整理自 `snippet.js`，包含两个部分：

- **工具函数**：独立可用的小片段，直接粘贴到 DevTools Console 使用
- **环境 Hook 模块**：完整的补环境 + 全局代理方案，用于 Node.js 本地调试

---

## 一、工具函数

### 1.1 watch() — Proxy 对象监控

监控一个对象的所有属性读取和写入，输出到 console。

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

```

**用法：**

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

```js
// 拦截所有 script 元素的 src 赋值
Object.defineProperty(HTMLScriptElement.prototype, 'src', {
    set: function (value) {
        console.log('[script.src]', value);
        debugger;  // 触发断点，查看调用栈定位加载来源
    }
});

```

**方式二：Proxy 代理整个原型（更全面）**

```js
// 拦截原型上的所有属性赋值，不仅限于 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 配置项

```js
const CONFIG = {
    enableStack: true,   // 是否在日志中输出调用栈
    deepProxy: true,     // 是否对 get 返回值递归代理（深度追踪）
    logGet: true,        // 记录属性读取
    logSet: true,        // 记录属性写入
    logHas: false,       // 记录 in 操作符检测（通常不需要）
    logCall: true        // 记录函数调用
};

```

### 2.2 敏感 Key 列表

```js
const SENSITIVE_KEYS = new Set([
    "webdriver",       // 自动化检测
    "userAgent",       // UA 指纹
    "platform",        // 系统平台
    "languages",       // 语言设置
    "plugins",         // 浏览器插件列表
    "mimeTypes",       // MIME 类型
    "cookie",          // Cookie
    "localStorage",    // 本地存储
    "sessionStorage"   // 会话存储
]);

```

### 2.3 核心代理 createProxy()

对任意对象/函数递归包裹 Proxy，追踪所有操作并记录访问路径。

```js
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 中补全缺失的浏览器全局对象：

```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 来源）：

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

```js
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 完整代码

```js
(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 补环境场景下

1. 将 **2.7 完整代码** 放在目标 JS 文件最顶部，在 `require` 或 `eval` 之前执行
2. 运行后观察 console 输出，找到目标网站 JS 实际访问了哪些 `navigator` / `document` 字段
3. 根据输出补全 `patchBrowserEnv()` 中缺失的字段

### 在 DevTools Console 场景下

- **1.1 watch()** 可直接粘贴使用，然后 `window.xxx = watch(window.xxx, 'xxx')` 替换目标对象
- **1.2 script.src hook** 页面加载初期（Network 面板 → 点击 `Preserve log`）注入，可捕获动态脚本

### 关闭深度代理（避免日志刷屏）

当 `deepProxy: true` 时，链式属性访问会产生大量日志。可以先关掉：

```js
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` 少出错，且自动打印调用栈。

```javascript
// 监控 config 对象的 token 属性何时被赋值
watch(config, 'token');

```

**`deepClone` 用于打印快照**：在 Hook 回调里用 `deepClone(obj)` 代替直接 `console.log(obj)`，避免 Chrome Console 延迟求值导致看到的是修改后的值。

**环境 Hook 模块统一在入口处初始化**：把 `initEnvHooks()` 放到 Node.js 脚本的第一行，确保所有模拟环境在目标代码 `require` 前已就绪，否则目标代码在模块加载时就会读到真实 Node.js 环境。

```javascript
// 正确：最顶部初始化
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` 等常见全局变量。

```javascript
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 拦截整个原型链。

---

## 参见

- [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/)
- [调试辅助](https://blog.vercanti.com/diao-shi-fu-zhu-dai-ma-duan/)
- [代码片段大全](https://blog.vercanti.com/js-ni-xiang-hook-yu-dai-ma-pian-duan-da-quan/)