JavaScript Hook 技术实战指南

前端分析、爬虫开发、安全测试经常需要对网页在运行时“做手脚”——拦截函数调用、插入监控代码、甚至修改执行逻辑,JavaScript Hook 就是实现这些目标的核心技巧。本文从原理出发,搭配 Tampermonkey 脚本实战,让你快速掌握这项技术。


1. 什么是 Hook 技术

Hook(钩子)指的是在程序运行时替换或包装原始函数的技术:你可以在函数执行前、执行中、执行后插入自己的代码,同时(可选地)保留原函数的全部或部分功能。

它的常见应用场景

  1. 分析前端加密:截获登录 token、密码加密前的明文
  2. 调试前端逻辑:给满足特定条件的函数调用自动添加断点
  3. 修改页面行为:屏蔽广告、解锁付费内容(仅用于学习测试)
  4. 监控 API 调用:追踪 fetch / XHR 的请求和响应参数

2. 浏览器端最推荐的 Hook 工具:Tampermonkey

在浏览器环境下做 JS Hook,Tampermonkey(油猴) 是当之无愧的首选:

  • 支持 Chrome、Edge、Firefox、Safari 等主流浏览器
  • 脚本安装和管理零门槛
  • 内置丰富的 GM_* API(跨域请求、存储、DOM 操作等)
  • 可以指定脚本的运行时机和生效域名

快速安装

  1. Chrome/Edge 用户直接在应用商店搜索「Tampermonkey Beta」(Beta 版功能更全)
  2. Firefox 用户在 AMO 搜索「Tampermonkey」
  3. 官网备用:https://www.tampermonkey.net/

3. Tampermonkey 基础脚本开发

油猴脚本本质上是一个自执行函数包裹的普通 JS 代码,再加上一段元数据头部,告诉浏览器脚本的基本信息。

完整的基础模板

// ==UserScript==
// @name         Hook入门测试脚本
// @namespace    https://github.com/yourname/
// @version      1.0.0
// @description  一个演示 console.log Hook 的油猴脚本
// @author       你的名字
// @match        https://*.example.com/*
// @match        https://login1.scrape.center/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict'; // 强制开启严格模式,避免变量污染

    // 这里写你的 Hook 逻辑
    console.log('✅ Hook 入门测试脚本已加载');
})();

高频元数据指令速查表

指令作用常用示例
@match脚本生效的 URL 模式(支持通配符)@match https://*.jd.com/*
@run-at脚本的执行时机document-start(DOM 树前)/ document-end(DOM 树后)
@grant申请的 GM_* API 权限GM_xmlhttpRequest(跨域请求)/ none(无权限)
@require引入外部 JS 库@require https://code.jquery.com/jquery-3.7.1.min.js

4. JS Hook 实战基础:三种常用模式

4.1 简单函数 Hook(替换/包装)

这是最基础的 Hook 方法:先保存原始函数的引用,再用自定义函数替换原位置,自定义函数里调用原始函数并插入代码。

/**
 * 通用简单 Hook 函数
 * @param {object} obj - 原始函数所在的对象(全局函数传 window)
 * @param {string} methodName - 要 Hook 的函数名
 */
function simpleHook(obj, methodName) {
    const originalFn = obj[methodName];
    // 替换原函数
    obj[methodName] = function(...args) {
        // ✅ 执行前逻辑
        console.group(`🔍 Hook 捕获到 ${methodName}`);
        console.log('传入参数:', args);
        
        // 调用原始函数(保留原功能)
        const result = originalFn.apply(this, args);
        
        // ✅ 执行后逻辑
        console.log('返回结果:', result);
        console.groupEnd();
        
        return result; // 返回原函数的结果(可选修改)
    };
}

// 测试:Hook 全局的 console.log
simpleHook(window.console, 'log');

4.2 原型链方法 Hook

很多前端 API(比如 XHR、Canvas)的方法都挂在构造函数的 prototype 上,Hook 原型可以拦截所有实例的调用。

/**
 * 通用原型链 Hook 函数
 * @param {function} constructor - 构造函数(比如 XMLHttpRequest)
 * @param {string} methodName - 要 Hook 的原型方法名
 */
function prototypeHook(constructor, methodName) {
    const originalFn = constructor.prototype[methodName];
    constructor.prototype[methodName] = function(...args) {
        console.group(`🎯 原型链 Hook ${constructor.name}.${methodName}`);
        console.log('当前实例:', this);
        console.log('传入参数:', args);
        
        const result = originalFn.apply(this, args);
        
        console.log('返回结果:', result);
        console.groupEnd();
        
        return result;
    };
}

// 测试:Hook 所有 XMLHttpRequest 的 open 方法
prototypeHook(XMLHttpRequest, 'open');

4.3 条件断点 Hook

给符合特定条件的函数调用自动加 debugger 断点,再也不用手动找位置点断点了!

/**
 * 通用条件断点 Hook 函数
 * @param {object} obj - 原始函数所在的对象
 * @param {string} methodName - 要 Hook 的函数名
 * @param {function} condition - 判断是否触发断点的函数,参数是原函数的参数
 */
function conditionalBreakpointHook(obj, methodName, condition) {
    const originalFn = obj[methodName];
    obj[methodName] = function(...args) {
        // 符合条件就触发断点
        if (condition(...args)) {
            debugger;
            console.warn(`⚠️ 条件断点触发: ${methodName}`);
        }
        return originalFn.apply(this, args);
    };
}

// 测试:当 localStorage.setItem 的 key 是 'token' 或 'password' 时触发断点
conditionalBreakpointHook(
    window.localStorage,
    'setItem',
    (key) => ['token', 'password'].includes(key)
);

5. 实战案例:分析 scrape 登录页的 token 生成

我们以 **https://login1.scrape.center/**(崔庆才老师的爬虫练习站)为例,看看怎么用 Hook 找到登录 token 的生成逻辑。

5.1 前期观察

  1. 打开网站,按 F12 切到 Network 面板
  2. 随便输入用户名密码(例如 admin/123456)并点击登录
  3. 找到 login 请求,发现 Form Data 里只有一个加密的 token 字段,没有明文密码

5.2 推测与 Hook

  • 加密后的 token 看起来像 Base64 编码
  • 浏览器端做 Base64 的常用 API 是 window.btoa()
  • 直接 Hook btoa,并打印调用栈,找到调用它的函数

5.3 完整脚本

// ==UserScript==
// @name         Scrape登录Token Hook
// @namespace    https://github.com/yourname/
// @version      1.0.0
// @description  分析 scrape.center 登录 token 的生成
// @author       你的名字
// @match        https://login1.scrape.center/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 保存原始 btoa
    const originalBtoa = window.btoa;
    // 替换 btoa
    window.btoa = function(input) {
        console.group('🔑 btoa Token Hook');
        console.log('加密前的明文:', input);
        console.trace('调用栈(往上翻找到加密逻辑!)');
        debugger; // 自动触发断点,方便调试
        
        const result = originalBtoa(input);
        
        console.log('加密后的 token:', result);
        console.groupEnd();
        
        return result;
    };
})();

5.4 分析结果

安装脚本后刷新网站,再次输入密码登录:

  1. 控制台会打印加密前的明文:{"username":"admin","password":"123456"}
  2. 调用栈里可以找到是 login 函数调用了 btoa
  3. 原来就是将用户名密码 JSON 序列化后直接 Base64 编码

6. 进阶技巧与反 Hook 对抗

6.1 异步函数 Hook(Promise / async-await)

现在的前端 API(比如 fetchaxios.get)大多是异步的,Hook 异步函数时需要注意错误捕获返回 Promise 的处理

/**
 * 通用异步函数 Hook
 * @param {object} obj - 原始函数所在的对象
 * @param {string} methodName - 要 Hook 的异步函数名
 */
function asyncHook(obj, methodName) {
    const originalFn = obj[methodName];
    obj[methodName] = async function(...args) {
        const startTime = performance.now();
        console.group(`⚡ 异步 Hook ${methodName}`);
        console.log('传入参数:', args);
        
        try {
            const result = await originalFn.apply(this, args);
            console.log('✅ 执行成功,耗时:', (performance.now() - startTime).toFixed(2), 'ms');
            console.log('返回结果:', result);
            return result;
        } catch (err) {
            console.error('❌ 执行失败:', err);
            throw err; // 抛出错误,不影响原逻辑的错误处理
        } finally {
            console.groupEnd();
        }
    };
}

// 测试:Hook 全局的 fetch API
asyncHook(window, 'fetch');

6.2 属性访问 Hook(getter / setter)

如果想监控某个对象的属性被赋值或读取,不能用函数 Hook,而要用 Object.defineProperty 重写 getter 和 setter。

/**
 * 通用属性访问 Hook
 * @param {object} obj - 属性所在的对象
 * @param {string} propName - 要 Hook 的属性名
 */
function propertyHook(obj, propName) {
    let internalValue = obj[propName]; // 用内部变量存储属性值
    Object.defineProperty(obj, propName, {
        get() {
            console.log(`📖 读取属性 ${propName}:`, internalValue);
            return internalValue;
        },
        set(newValue) {
            console.log(`✏️ 修改属性 ${propName}:`, newValue);
            internalValue = newValue;
        },
        configurable: true, // 必须设为 true,否则无法再次修改属性
        enumerable: true // 保持原属性的可枚举性
    });
}

// 测试:Hook window.location.href
propertyHook(window.location, 'href');

6.3 简单的反 Hook 对抗

现在很多网站会做反 Hook 保护,常见手段包括:

  1. 检查 API 是否被修改:例如比对 btoa.toString() 的结果
  2. 冻结对象 / 属性:使用 Object.freeze()Object.seal() 或设置 writable: false
  3. 代码混淆:让你难以定位 Hook 点

简单应对策略

// 1. 从 iframe 里获取“干净”的原始 API(如果网站有 iframe 的话)
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
const cleanBtoa = iframe.contentWindow.btoa;
// 使用 cleanBtoa...

// 2. 绕过 writable: false 的限制(重新 defineProperty)
function bypassWritable(obj, propName, newFn) {
    Object.defineProperty(obj, propName, {
        value: newFn,
        writable: true,
        configurable: true,
        enumerable: true
    });
}

注意:以上技术仅用于学习研究,请勿在未授权的情况下对线上服务进行干扰或攻击。


7. 最佳实践

  1. 精准控制作用域:用 @match 只在需要的网站运行脚本
  2. 最小权限原则:只申请必要的 @grant 权限,例如不需要跨域就设为 none
  3. 做好错误处理:给 Hook 逻辑加 try-catch,避免脚本崩溃影响原网站
  4. 性能优先:不要在高频调用的函数(例如 requestAnimationFrame、Canvas 的 draw 方法)里做太多复杂逻辑
  5. 代码可复用:把常用的 Hook 方法封装成通用函数

8. 总结

本文带你从 0 到 1 掌握了浏览器端 JS Hook 的核心技能:

  • 什么是 Hook 以及它的常见用途
  • Tampermonkey 的基础使用与脚本开发
  • 三种常用的 Hook 模式(简单函数、原型链、条件断点)
  • 实战分析 scrape 登录页的 token 生成
  • 异步函数、属性访问 Hook 和简单的反 Hook 对抗思路

掌握这些技巧后,你就可以开始分析更复杂的前端逻辑了。请务必仅用于学习测试,不要用于非法用途哦~