JS逆向实战:一键Hook加密算法原理与实现

发布时间:2026/7/31 9:32:23
JS逆向实战:一键Hook加密算法原理与实现
1. 项目概述为什么我们需要Hook加密算法在Web安全、数据分析和逆向工程领域我们经常会遇到一个棘手的问题目标网站或应用的核心业务逻辑被一层或多层JavaScript加密算法严密保护。无论是登录时的密码加密、请求参数的签名还是关键数据的加解密传输这些算法就像一个个黑盒阻碍了我们理解其内部逻辑、进行安全审计或实现自动化交互。传统的“人肉”逆向即通过阅读混淆后的代码、手动跟踪执行堆栈来推断算法不仅效率低下而且面对日益复杂的代码混淆和动态加载技术时常常力不从心。这时“Hook”技术便成为了一把利器。简单来说Hook钩子就是在程序运行时拦截并修改其正常的函数调用流程。在JS逆向的语境下Hook加密算法意味着我们能够在JavaScript代码执行到关键的加密函数如CryptoJS.AES.encrypt、window.btoa、自定义的sign函数等时自动“捕获”这次调用并记录下其输入参数、输出结果甚至修改其行为。这相当于在算法的“咽喉”位置安装了一个实时监控和调试器。我之所以投入精力研究并实现“一键Hook主流加密算法”是因为在实际项目中重复性的手动定位和Hook操作消耗了大量时间。每次遇到新的目标都需要重新分析代码结构、寻找函数入口、编写调试脚本。这个项目的核心目标就是将这些重复劳动自动化、标准化打造一个能够快速适配常见加密库和自定义加密函数并能提供清晰调试信息的工具集。无论你是安全研究员、爬虫工程师还是前端开发者想深入了解第三方库的行为这套方法都能让你事半功倍。2. 核心思路与方案选型如何设计一个通用的Hook框架要实现“一键Hook”我们不能针对每个网站写死代码而是需要设计一个具备良好扩展性和适应性的框架。我的设计思路主要围绕以下几个核心原则展开2.1 核心原则无侵入、动态性与可配置首先Hook必须是“无侵入”的。我们不能修改目标网站的源代码文件所有操作都应在运行时通过浏览器开发者工具如Chrome DevTools或注入的脚本完成。这保证了我们分析过程的纯净性和可重复性。其次需要“动态性”。网站可能使用eval、Function构造函数或动态加载脚本的方式引入加密逻辑。我们的Hook机制必须能够应对这些情况确保在目标函数被定义或执行的瞬间就能将其捕获。最后强调“可配置性”。不同的加密场景差异很大可能是标准的Web Crypto API也可能是第三方库如CryptoJS、forge或是开发者自己写的匿名函数。我们的框架需要允许用户通过简单的配置比如函数名、对象路径、特征字符串来指定需要Hook的目标。2.2 技术方案选型基于对象属性劫持的Hook在JavaScript中实现Hook主要有几种方式重写函数直接给window.crypto.subtle.encrypt赋值一个新的函数。这是最简单粗暴的方法但容易被检测例如检查函数的toString结果是否被改变。使用Object.defineProperty重新定义对象的属性描述符在getter中返回我们包装过的函数。这种方式更隐蔽可以捕获通过属性访问触发的调用。代理Proxy对象ES6引入的Proxy可以创建一个对象的代理从而拦截并定义该对象的基本操作如属性读取、函数调用。这是功能最强大、最现代的方式。为了兼顾兼容性部分老旧环境可能不支持完整的Proxy和功能的强大我选择了组合方案对于全局对象如window或已知的命名空间对象如CryptoJS优先使用Proxy进行深度拦截对于具体的函数属性使用Object.defineProperty进行精准Hook。同时我们会保存原始函数的引用确保在必要时可以调用原函数避免破坏网站的正常功能。2.3 调试信息输出策略Hook的目的不仅是拦截更是为了调试。我们需要清晰地输出信息。方案是构建一个独立的日志管理器它能够格式化输出将函数名、调用时间、传入的参数进行安全的序列化避免循环引用、返回值、所在的调用栈Stack Trace等信息以结构化的方式如表格输出到控制台。条件过滤可以设置过滤条件例如只记录包含特定参数的调用或者当参数满足某个条件时才记录避免控制台信息爆炸。持久化与导出允许将拦截到的所有调用记录导出为JSON文件便于后续离线分析。基于以上思路整个项目的架构就清晰了一个核心的Hook引擎负责实施拦截一个可配置的目标函数列表一个强大的日志输出系统。3. 核心实现一步步构建Hook引擎理论说再多不如一行代码。下面我将分步骤拆解核心实现。我们将创建一个名为UniversalCryptoHook的类。3.1 基础Hook使用Object.defineProperty拦截函数这是最基础的单元操作。假设我们要Hookwindow.btoa这个Base64编码函数。class UniversalCryptoHook { constructor() { this.hookedFunctions new Map(); // 存储原始函数引用 } /** * 基础函数Hook方法 * param {Object} obj - 包含目标函数的对象如 window, CryptoJS * param {string} funcName - 函数名 * param {Function} callback - 拦截回调函数接收(originalFunc, args, context)参数 */ hookFunction(obj, funcName, callback) { if (!obj || !obj[funcName] || typeof obj[funcName] ! function) { console.warn([Hook] 目标 ${funcName} 不存在或不是函数); return false; } const originalFunc obj[funcName]; this.hookedFunctions.set(${obj}.${funcName}, originalFunc); Object.defineProperty(obj, funcName, { configurable: true, enumerable: true, writable: true, // 注意某些严格环境可能需要设为false用getter/setter更稳妥 value: function(...args) { // 调用回调并传入原始函数、参数和调用上下文(this) const result callback.call(this, originalFunc, args, this); // 回调函数可以返回一个值作为“伪造”的结果如果未返回则调用原函数 if (result ! undefined) { return result; } return originalFunc.apply(this, args); } }); console.log([Hook] 成功Hook函数: ${funcName}); return true; } }注意直接重写value在某些网站可能失效因为目标代码可能在之后重新赋值该属性。更稳健的做法是使用getter和setter来拦截属性的读取和赋值操作。3.2 高级Hook使用Proxy拦截整个对象对于像CryptoJS.AES这样的对象里面有很多方法encrypt,decrypt一个个Hook太麻烦。我们可以用Proxy拦截整个对象。/** * 使用Proxy深度Hook一个对象及其所有方法 * param {Object} target - 目标对象 * param {string} namespace - 对象命名空间用于日志记录 * returns {Proxy} 被代理后的对象 */ createDeepProxy(target, namespace ) { const hook this; return new Proxy(target, { get(obj, prop) { const value obj[prop]; // 如果属性是函数则返回一个包装函数 if (typeof value function) { return function(...args) { const fullName namespace ? ${namespace}.${prop} : prop; console.group([Hook] 调用: ${fullName}); console.table({ 参数: args.map(a (typeof a string ? a.substring(0, 100) ... : a)), 上下文: this }); try { const result value.apply(this, args); console.log(返回结果:, result); console.groupEnd(); return result; } catch (err) { console.error(调用出错:, err); console.groupEnd(); throw err; } }; } // 如果属性是对象则递归创建Proxy if (value typeof value object) { return hook.createDeepProxy(value, namespace ? ${namespace}.${prop} : prop); } // 其他类型属性直接返回 return value; }, set(obj, prop, newValue) { // 可以在这里拦截赋值操作记录谁修改了这个属性 console.log([Hook] 设置属性 ${namespace}.${prop}, newValue); obj[prop] newValue; return true; // 表示设置成功 } }); }使用方式window.CryptoJS this.createDeepProxy(window.CryptoJS, CryptoJS);这样CryptoJS及其所有子对象下的函数调用都会被自动记录。3.3 自动识别与批量Hook“一键”的关键在于自动识别。我们可以预设一个常见加密库和函数的清单。const COMMON_CRYPTO_TARGETS [ { obj: window, path: btoa }, { obj: window, path: atob }, { obj: window, path: crypto.subtle.encrypt }, { obj: window, path: crypto.subtle.decrypt }, { obj: window, path: CryptoJS }, // 整个库用Proxy处理 { obj: window, path: JSEncrypt }, // RSA库 { obj: window, path: md5 }, // 假设有全局md5函数 // 可以添加更多如 forge, sjcl 等 ]; class UniversalCryptoHook { autoHookCommon() { COMMON_CRYPTO_TARGETS.forEach(target { const { obj, path } target; const pathSegments path.split(.); let currentObj obj; let propName pathSegments.pop(); // 最后一个属性名是函数或对象名 // 遍历路径找到目标对象的父对象 for (const seg of pathSegments) { if (currentObj currentObj[seg]) { currentObj currentObj[seg]; } else { console.warn([Hook] 路径 ${path} 不存在跳过); return; } } if (propName currentObj) { // 如果是明确指向一个函数名用hookFunction // 如果是库名如CryptoJS用createDeepProxy if (typeof currentObj[propName] function) { this.hookFunction(currentObj, propName, this.defaultCallback); } else if (currentObj[propName] typeof currentObj[propName] object) { // 替换为Proxy对象 currentObj[propName] this.createDeepProxy(currentObj[propName], propName); } } }); console.log([Hook] 常见加密目标自动Hook完成); } defaultCallback(originalFunc, args, context) { // 默认回调只记录不修改行为 const funcName originalFunc.name || anonymous; console.log([Hook Call] ${funcName} 被调用, { 参数: args, 上下文: context, 原始函数: originalFunc }); // 返回 undefined让原函数继续执行 return undefined; } }现在只需要const hook new UniversalCryptoHook(); hook.autoHookCommon();就能一键Hook所有预设的常见加密函数。4. 实战调试技巧与问题排查工具做好了怎么用才是关键。下面分享几个实战中总结的调试技巧和常见问题的解决方法。4.1 如何定位需要Hook的加密函数“一键Hook”的前提是知道要Hook什么。如果目标函数不在预设清单里你需要手动定位。搜索关键词在开发者工具的Sources面板全局搜索CtrlShiftF诸如encrypt、decrypt、sign、hash、AES、RSA、CryptoJS、MD5等关键词。XHR/Fetch断点在Network面板找到被加密的请求右键选择“Break on” - “XHR/Fetch”。当请求发起时代码会暂停此时查看调用堆栈Call Stack寻找堆栈中与加密相关的函数。堆栈跟踪法在Console中执行debugger;语句或者直接在疑似加密函数的第一行打上断点然后触发加密操作。观察调用堆栈向上追溯找到加密的源头函数。4.2 Hook过程中的常见问题与解决方案问题现象可能原因解决方案Hook后网站功能异常或报错1. Hook函数改变了this上下文。2. 原始函数被多次赋值我们的Hook被覆盖。3. Proxy或defineProperty的副作用影响了对象内部状态。1. 在包装函数中使用originalFunc.apply(this, args)确保this正确。2. 使用MutationObserver监听DOM脚本加载或设置setInterval循环检查并重新Hook防覆盖。3. 简化Hook逻辑只做记录避免修改对象原型链或属性描述符。控制台输出信息过多找不到关键调用目标函数被高频调用如哈希函数在循环中。在回调函数中添加过滤条件例如只有当参数包含特定字符串、参数长度大于某个值、或来自特定的调用栈时才输出日志。无法Hook通过eval或new Function动态生成的函数这些函数在动态代码执行时才创建常规的Hook时机过早。重写window.eval和Function构造函数在代码执行前对其进行字符串层面的匹配和替换注入我们的Hook代码。这是一个更高级的技巧需谨慎使用。网站检测到Hook并反调试网站会检查关键函数是否被重写如Function.prototype.toString的结果、是否存在调试器检查debugger语句执行时间。1. 使用更隐蔽的Hook方式如通过Object.defineProperty的getter拦截保持函数的toString输出不变。2. 在非阻塞模式下运行调试器或使用setTimeout延迟debugger语句来绕过时间检测。3. 考虑在无头浏览器如Puppeteer环境中运行Hook脚本避免被前端检测。4.3 实操心得让Hook更稳健保存原始引用一定要把原始函数保存起来这是“金科玉律”。不仅是为了能调用原功能也是为了在需要时能恢复现场。异步函数处理如果要Hook的函数返回一个Promise如crypto.subtle.encrypt你的包装函数也需要正确处理异步最好也返回一个Promise并在其中记录结果。value: async function(...args) { console.log([Hook] 异步调用开始: ${funcName}); try { const result await originalFunc.apply(this, args); console.log([Hook] 异步调用成功: ${funcName}, result); return result; } catch (err) { console.error([Hook] 异步调用失败: ${funcName}, err); throw err; } }性能考量在生产环境或分析大型应用时过于复杂的Proxy和日志输出可能影响性能。可以提供一个“采样”模式随机记录一部分调用或者只在特定条件下开启详细日志。代码注入时机确保你的Hook脚本在目标加密库加载之后但在加密函数被调用之前执行。最稳妥的方式是将脚本作为浏览器插件的content script设置为在document_idle时运行或者通过调试工具手动在页面加载后注入。5. 案例实战逆向一个模拟登录的加密参数假设我们遇到一个登录页面其密码在提交前被一个名为window.encryptPassword的函数处理。我们的目标是弄清这个函数的算法。5.1 手动定位与初步分析首先在开发者工具中搜索encryptPassword找到其定义。假设它看起来像这样已被混淆function encryptPassword(t) { var e a1b2c3d4; return CryptoJS.MD5(e t).toString(); }很明显它只是做了一个简单的MD5哈希盐值是a1b2c3d4。但对于复杂的、调用了内部其他模块的函数我们就需要Hook来动态观察。5.2 使用我们的工具进行Hook即使我们看到了源码用Hook验证也是好习惯。我们在Console中执行const hook new UniversalCryptoHook(); hook.hookFunction(window, encryptPassword, (originalFunc, args, context) { console.groupCollapsed( encryptPassword 被调用); console.log(输入密码 (明文):, args[0]); const result originalFunc.apply(context, args); // 调用原函数得到结果 console.log(输出密文:, result); console.groupEnd(); // 不修改结果直接返回 return result; });然后在登录框输入密码123456并点击登录。控制台会立即输出 encryptPassword 被调用 输入密码 (明文): 123456 输出密文: e10adc3949ba59abbe56e057f20f883e同时我们也可以一键HookCryptoJS.MD5看看内部是否还有其他变换。if (window.CryptoJS window.CryptoJS.MD5) { window.CryptoJS.MD5 hook.createDeepProxy(window.CryptoJS.MD5, CryptoJS.MD5); }这样任何对CryptoJS.MD5的调用也会被记录我们可以确认盐值拼接的逻辑。5.3 算法还原与模拟通过Hook我们确认了算法是MD5(固定盐值 密码)。那么在Python或其他语言中模拟这个登录请求就非常简单了import hashlib def encrypt_password(password): salt a1b2c3d4 data salt password return hashlib.md5(data.encode(utf-8)).hexdigest() print(encrypt_password(123456)) # 输出应与Hook捕获的一致这个简单的案例演示了从定位、Hook、分析到最终还原算法的完整流程。对于更复杂的、涉及多次加密、随机数、时间戳的算法Hook的价值会更加凸显它能帮你理清调用链和数据流转。6. 安全、伦理与工具边界在分享如此强大的技术时我必须强调其使用边界。JS逆向和Hook是一把双刃剑。仅用于合法授权目的这项技术应仅用于对自己拥有权限的网站进行安全评估、学习研究、或解决合法的自动化需求如数据聚合分析需遵守robots.txt和服务条款。任何未经授权对他人系统进行攻击、窃取数据或绕过付费墙的行为都是非法且不道德的。尊重知识产权分析代码是为了理解原理和解决问题而不是为了抄袭或盗用他人的核心算法和商业逻辑。注意法律风险许多网站的用户协议明确禁止逆向工程。在实际操作前请务必了解相关法律法规和网站政策。工具本身的防护本文讨论的Hook方法同样可能被网站用来检测和反制。在实际对抗中网站可能会检查关键函数是否被代理、是否包含调试语句等。因此更高级的逆向往往需要在无头浏览器、修改过的运行时环境中进行这是一场持续的技术博弈。最后我个人的体会是Hook技术是打开JS黑盒的钥匙但它考验的不仅仅是编程技巧更是耐心、细心和对系统运行原理的深刻理解。每一次成功的逆向都像是完成一次精密的解密游戏。希望这套“一键Hook”的思路和实现能为你提供一条清晰的路径让你在应对JS加密时更加从容。记住最好的工具是你不断积累的调试经验和系统化的思考方式。