JavaScript Hook 技术实战指南
前端分析、爬虫开发、安全测试经常需要对网页在运行时“做手脚”——拦截函数调用、插入监控代码、甚至修改执行逻辑,JavaScript Hook 就是实现这些目标的核心技巧。本文从原理出发,搭配 Tampermonkey 脚本实战,让你快速掌握这项技术。
1. 什么是 Hook 技术
Hook(钩子)指的是在程序运行时替换或包装原始函数的技术:你可以在函数执行前、执行中、执行后插入自己的代码,同时(可选地)保留原函数的全部或部分功能。
它的常见应用场景
- 分析前端加密:截获登录 token、密码加密前的明文
- 调试前端逻辑:给满足特定条件的函数调用自动添加断点
- 修改页面行为:屏蔽广告、解锁付费内容(仅用于学习测试)
- 监控 API 调用:追踪 fetch / XHR 的请求和响应参数
2. 浏览器端最推荐的 Hook 工具:Tampermonkey
在浏览器环境下做 JS Hook,Tampermonkey(油猴) 是当之无愧的首选:
- 支持 Chrome、Edge、Firefox、Safari 等主流浏览器
- 脚本安装和管理零门槛
- 内置丰富的 GM_* API(跨域请求、存储、DOM 操作等)
- 可以指定脚本的运行时机和生效域名
快速安装
- Chrome/Edge 用户直接在应用商店搜索「Tampermonkey Beta」(Beta 版功能更全)
- Firefox 用户在 AMO 搜索「Tampermonkey」
- 官网备用: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 入门测试脚本已加载');
})();
高频元数据指令速查表
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 前期观察
- 打开网站,按 F12 切到 Network 面板
- 随便输入用户名密码(例如
admin/123456)并点击登录
- 找到
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 分析结果
安装脚本后刷新网站,再次输入密码登录:
- 控制台会打印加密前的明文:
{"username":"admin","password":"123456"}
- 调用栈里可以找到是
login 函数调用了 btoa
- 原来就是将用户名密码 JSON 序列化后直接 Base64 编码
6. 进阶技巧与反 Hook 对抗
6.1 异步函数 Hook(Promise / async-await)
现在的前端 API(比如 fetch、axios.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 保护,常见手段包括:
- 检查 API 是否被修改:例如比对
btoa.toString() 的结果
- 冻结对象 / 属性:使用
Object.freeze()、Object.seal() 或设置 writable: false
- 代码混淆:让你难以定位 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. 最佳实践
- 精准控制作用域:用
@match 只在需要的网站运行脚本
- 最小权限原则:只申请必要的
@grant 权限,例如不需要跨域就设为 none
- 做好错误处理:给 Hook 逻辑加
try-catch,避免脚本崩溃影响原网站
- 性能优先:不要在高频调用的函数(例如
requestAnimationFrame、Canvas 的 draw 方法)里做太多复杂逻辑
- 代码可复用:把常用的 Hook 方法封装成通用函数
8. 总结
本文带你从 0 到 1 掌握了浏览器端 JS Hook 的核心技能:
- 什么是 Hook 以及它的常见用途
- Tampermonkey 的基础使用与脚本开发
- 三种常用的 Hook 模式(简单函数、原型链、条件断点)
- 实战分析 scrape 登录页的 token 生成
- 异步函数、属性访问 Hook 和简单的反 Hook 对抗思路
掌握这些技巧后,你就可以开始分析更复杂的前端逻辑了。请务必仅用于学习测试,不要用于非法用途哦~