JS 逆向代码片段

整理自 snippet.js,包含两个部分: 监控一个对象的所有属性读取和写入,输出到 console。 用法: 适用场景:快速定位哪段代码在读写某个对象的属性。 当页面通过 JS 动态创建 <script> 标签加载外部脚本时触发断点。 方式一:defineProperty 方式二:Proxy 代理整个原型(更全面) 适用场景:网站动态插入第三方脚本(风控 JS、指纹采集 SDK),定位其加载时机和来源。 完整的 Node.js 补环境方案,在本地运行浏览器加密 JS 时使用。 功能列表: 对任意对象/函数递归包裹 Proxy,追踪所有操作并记录访问路

分享

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

整理自 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 补环境场景下

  1. 2.7 完整代码 放在目标 JS 文件最顶部,在 requireeval 之前执行
  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 时,链式属性访问会产生大量日志。可以先关掉:

CONFIG.deepProxy = false;
CONFIG.logGet = false;  // 只看 set 和 call

最佳实践

先禁用 deepProxy 再缩小范围:全局代理开启后日志量极大,CONFIG.deepProxy = false; CONFIG.logGet = false; 先关掉深层追踪,只看 setcall,定位到可疑操作后再按需打开。

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.titlelocation.href 等属性访问极频繁,把它们加入 CONFIG.blacklist 避免刷屏,专注于真正有价值的属性访问日志。


常见陷阱

陷阱:deepProxy: true 导致循环引用报错

现象: 开启深层 Proxy 后,控制台报 Maximum call stack size exceededConverting circular structure to JSON

原因: documentwindow 等对象存在循环引用(window.window === window),深层代理递归到循环引用时栈溢出。

解决: 在 Proxy get 拦截器里维护一个 WeakSet 记录已代理对象,遇到已代理的直接返回原值。本文件的实现已处理常见情况;如遇新问题,手动把循环引用属性加入 CONFIG.blacklist

陷阱:Node.js 下 window 未定义导致目标代码崩溃

现象: 在 Node.js 里运行逆向目标代码,报 ReferenceError: window is not defined

原因: 浏览器代码依赖全局 window 对象,Node.js 环境没有。

解决: 在入口最顶部执行 global.window = global; 并加载本文件提供的环境模拟模块,它会补充 navigatordocumentlocation 等常见全局变量。

global.window = global;
global.navigator = { userAgent: 'Mozilla/5.0 ...' };
// ... 其他补环境

陷阱:watch() 监控不到通过原型链赋值的属性

现象: watch(obj, 'key') 注册后,赋值操作没有触发 Hook。

原因: watchobj 自身上定义 defineProperty,如果赋值发生在原型对象上(Object.getPrototypeOf(obj).key = val),不会触发 obj 上的 setter。

解决: 对原型链赋值,改为 watch(Object.getPrototypeOf(obj), 'key');或改用 Proxy 拦截整个原型链。


参见

阅读更多

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