通用JS加密库Hook方案:动态监控CryptoJS等主流加密库的调用
1. 项目概述为什么我们需要一个通用Hook方案在Web安全、数据分析和逆向工程领域JavaScript加密库是横亘在开发者与目标数据之间的一道高墙。无论是分析某个电商网站的商品价格加密逻辑还是研究一个社交平台的API请求签名机制你总会遇到诸如CryptoJS、sjcl、或者各种自定义的AES、RSA实现。传统的逆向方法是“一案一析”找到加密函数下断点跟栈一点点抠出核心逻辑和密钥。这个过程耗时耗力且一旦对方更新了代码或混淆方式所有工作可能都要推倒重来。“一键Hook主流加密库”这个想法正是为了解决这种重复劳动和脆弱性而生的。它的核心目标不是去破解某一个特定的加密算法而是构建一个通用的“监听器”或“探针”能够自动识别页面中加载的常见加密库如CryptoJS, forge, asmCrypto等并在其关键函数如encrypt,decrypt,update,finalize被调用时自动捕获其输入参数、输出结果以及可能用到的密钥、IV初始化向量等信息。这相当于在加密库的“喉咙”上装了一个麦克风无论应用层代码如何千变万化只要它最终调用了这些底层库我们就能清晰地听到“明文进密文出”的全过程。这个方案的价值在于其主动性和通用性。它改变了我们与加密逻辑的交互方式——从被动的、事后的静态分析转变为主动的、实时的动态监控。对于安全研究人员可以快速评估前端加密的强度对于数据分析师可以绕过复杂的加密直接获取原始数据对于爬虫开发者则能极大简化逆向加密参数的过程。接下来我将拆解实现这一方案的核心思路、技术细节以及我在实战中积累的避坑经验。2. 核心思路与架构设计实现通用Hook的关键在于理解JavaScript的动态特性。在浏览器环境中一切皆对象函数也是对象。Hook的本质就是替换目标函数对象在调用原函数之前或之后插入我们自己的监听逻辑。2.1 方案选型为什么选择基于原型的Hook实现Hook有多种技术路径比如直接重写全局函数、劫持Function.prototype.call/apply、或者使用ES6的Proxy。经过多次实践对比我最终选择了基于原型链的Hook方案作为核心。原因如下兼容性最好无论是老旧的ES5代码还是使用class语法糖的ES6代码其方法最终都挂载在构造函数的prototype上。Hookprototype能确保所有实例都受到影响覆盖最全面。侵入性相对可控相比于劫持Function.prototype这种“核弹级”操作会影响页面所有函数极易导致页面崩溃Hook特定库的原型方法目标更精确副作用更小。实现简单稳定利用闭包保存原函数引用然后替换为新函数。这个模式非常经典在各大浏览器的开发者工具控制台中都能稳定运行。我们的架构设计可以概括为“识别-挂载-监听”三步流水线识别通过检查全局对象window或globalThis是否存在特定属性如CryptoJS、sjcl来判断加密库是否已加载。挂载一旦识别立即遍历该库的核心类如CryptoJS.AES、CryptoJS.MD5找到其prototype上的目标方法。监听使用我们的Hook函数替换原方法。Hook函数负责记录参数、调用原函数、记录结果最后将原函数的结果返回确保不影响页面正常功能。2.2 目标库的确定与特征分析“主流加密库”是一个相对概念。我们的方案需要有一个目标库列表。根据我的经验以下库的覆盖率最高CryptoJS最流行没有之一。特征全局对象CryptoJS包含AES,DES,TripleDES,Rabbit,RC4,MD5,SHA1,SHA256等子模块。Stanford JavaScript Crypto Library (sjcl)学术和注重安全的项目常用。特征全局对象sjcl密码学操作通过sjcl.cipher、sjcl.hash等命名空间进行。Forge功能强大常用于处理TLS/SSL相关格式。特征全局对象forge包含forge.cipher,forge.md等。asmCrypto追求性能的WebAssembly实现。特征全局对象asmCryptoAPI风格偏底层。Web Crypto API浏览器原生API标准且高效。但它不是第三方库Hook方式不同通常通过重写window.crypto.subtle上的方法来实现。注意除了这些“有名有姓”的库更多的情况是网站自己实现的或经过深度混淆的加密函数。对于这类情况通用Hook方案可能失效需要结合函数特征如函数名包含encrypt、sign或参数中包含key、iv进行模糊匹配这属于更高级的范畴本文会简要提及思路。3. 核心代码实现与逐行解析理论讲完我们直接上干货。下面是一个高度精简但功能完整的通用Hook实现核心代码我将逐段解析其设计意图和关键细节。3.1 Hook工具函数的实现这是整个方案的引擎一个通用的函数Hook工具。/** * 通用函数Hook工具 * param {object} targetObj - 目标对象如 CryptoJS.AES * param {string} methodName - 方法名如 ‘encrypt’ * param {function} hookFunc - 自定义的Hook处理函数 * returns {boolean} - 是否Hook成功 */ function hookFunction(targetObj, methodName, hookFunc) { // 1. 安全检查目标和方法必须存在且为函数 if (!targetObj || typeof targetObj[methodName] ! ‘function’) { console.warn([Hook] 无法Hook: ${targetObj}.${methodName} 不存在或不是函数); return false; } // 2. 保存原函数的引用。这是Hook的基石必须用闭包保存起来。 const originalFunc targetObj[methodName]; // 3. 使用一个立即执行函数来创建新的作用域避免变量污染 targetObj[methodName] function(...args) { // 4. 构建调用上下文信息对象 const context { target: targetObj, method: methodName, arguments: args, // 原始参数列表 timestamp: Date.now(), // 可以在这里尝试捕获调用栈但注意性能 // stackTrace: new Error().stack }; // 5. 执行用户自定义的Hook逻辑前置 // hookFunc需要返回一个可能被修改过的参数列表 let modifiedArgs args; if (typeof hookFunc.before ‘function’) { const result hookFunc.before.call(this, context); if (Array.isArray(result)) { modifiedArgs result; // 如果Hook函数返回了新参数则使用新参数 } } let returnValue; let error null; try { // 6. 使用apply调用原函数确保正确的this上下文和参数。 // 这里的this是调用该方法的实例对象对于原型方法来说至关重要。 returnValue originalFunc.apply(this, modifiedArgs); } catch (e) { error e; // 7. 错误处理Hook if (typeof hookFunc.error ‘function’) { hookFunc.error.call(this, context, e); } // 继续抛出错误不干扰原有逻辑 throw e; } // 8. 执行用户自定义的Hook逻辑后置 if (typeof hookFunc.after ‘function’) { const afterContext { ...context, returnValue: returnValue, error: error }; hookFunc.after.call(this, afterContext); } // 9. 必须返回原函数的执行结果 return returnValue; }; // 10. 可选在新函数上保留原函数的属性如length, name等增强隐蔽性 Object.defineProperties(targetObj[methodName], { ‘toString’: { value: () originalFunc.toString(), writable: false } // 可以添加更多属性 }); console.log([Hook] 成功挂载: ${targetObj.constructor?.name || targetObj}.${methodName}); return true; }关键点解析与避坑指南第2行与第6行originalFunc与apply这是Hook的灵魂。一定要用apply或call来调用原函数并将动态的this传递过去。如果直接用originalFunc(...args)当原函数内部使用this时比如访问this._keythis会指向错误的上下文可能是window或undefined导致原函数执行错误页面功能崩溃。第4-5部分context对象与hookFunc.before我们将调用信息封装成一个上下文对象这比单纯传递参数更灵活。hookFunc.before允许我们在调用前修改参数这在某些动态调试场景下非常有用例如固定一个随机数IV以便于分析。但务必谨慎修改参数可能破坏页面逻辑。第10部分属性拷贝这是一个高级技巧。有些防御代码会检查函数的toString()结果是否包含特定字符或者检查函数的length属性形参个数。我们拷贝这些属性能让被Hook的函数看起来更“原生”提高隐蔽性。但这并非必需在大多数逆向场景下页面不会做如此深入的检测。3.2 针对CryptoJS的专项Hook实现有了通用工具我们就可以针对特定库进行部署。以CryptoJS为例/** * 自动Hook已加载的CryptoJS库 */ function hookCryptoJS() { if (typeof CryptoJS ‘undefined’) { console.log(‘[Hook] CryptoJS 未加载跳过。’); return false; } const modulesToHook [ { module: CryptoJS.AES, methods: [‘encrypt’, ‘decrypt’] }, { module: CryptoJS.DES, methods: [‘encrypt’, ‘decrypt’] }, { module: CryptoJS.TripleDES, methods: [‘encrypt’, ‘decrypt’] }, { module: CryptoJS.MD5, methods: [‘update’, ‘finalize’] }, // 哈希算法通常是update/finalize模式 { module: CryptoJS.SHA256, methods: [‘update’, ‘finalize’] }, { module: CryptoJS.HmacSHA256, methods: [‘update’, ‘finalize’] }, // 可以继续添加RC4, Rabbit等 ]; let hookCount 0; const hookHandler { before: function(context) { // 记录调用信息 console.group( [CryptoJS Hook] ${context.method} 被调用); console.log(‘调用对象:’, context.target); console.log(‘参数列表:’, context.arguments); // 特别关注密码、密钥、IV通常以WordArray对象形式存在我们可以尝试转换 context.arguments.forEach((arg, idx) { if (arg arg.sigBytes ! undefined) { // 判断是否为CryptoJS.WordArray console.log(参数${idx} (WordArray):, { ‘sigBytes’: arg.sigBytes, ‘words’: arg.words, ‘toString(Hex)’: arg.toString(CryptoJS.enc.Hex), // 转16进制字符串 ‘toString(Base64)’: arg.toString(CryptoJS.enc.Base64) }); } else if (typeof arg ‘string’) { console.log(参数${idx} (String):, arg); } // 可以添加其他类型的判断 }); }, after: function(context) { // 记录返回值 if (context.returnValue context.returnValue.sigBytes ! undefined) { console.log(‘返回值 (WordArray):’, { ‘sigBytes’: context.returnValue.sigBytes, ‘toString(Hex)’: context.returnValue.toString(CryptoJS.enc.Hex), ‘toString(Base64)’: context.returnValue.toString(CryptoJS.enc.Base64) }); } console.groupEnd(); // 实战中这里可以将信息发送到后台服务器或存储到全局变量供分析 // window._crypto_logs window._crypto_logs || []; // window._crypto_logs.push(context); } }; modulesToHook.forEach(item { if (item.module) { item.methods.forEach(methodName { if (hookFunction(item.module, methodName, hookHandler)) { hookCount; } }); } }); console.log([Hook] CryptoJS 挂钩完成共Hook了 ${hookCount} 个方法。); return hookCount 0; }关键点解析与实操心得modulesToHook数组这里定义了我们要Hook的模块和方法。注意对于分组加密算法AES/DES核心方法是encrypt和decrypt对于哈希算法MD5/SHA则是update和finalize。你需要根据目标网站实际使用的算法来调整这个列表。参数处理CryptoJS的核心数据单位是WordArray。在before钩子中我们判断参数是否为WordArray并将其转换为可读的16进制或Base64字符串。这是将内存中的二进制数据转化为可分析文本的关键一步。arg.sigBytes ! undefined是一个判断WordArray的实用技巧。信息输出与收集这里使用console.group让输出更清晰。在真实逆向环境中你更可能需要将捕获的数据context对象存入一个全局数组如window._crypto_logs或者通过fetch发送到你的调试服务器以便进行批量分析。执行时机这段代码必须在CryptoJS库加载之后、页面业务逻辑调用它之前执行。最稳妥的方式是将Hook脚本作为浏览器插件的内容脚本Content Script注入或者使用Fiddler/Charles等代理工具在响应中注入也可以直接在开发者工具的Console中执行用于临时调试。4. 高级技巧与边缘情况处理基础Hook能解决80%的问题但剩下的20%需要更精细的操作。4.1 处理异步加密与Web Crypto API现代网站越来越多地使用异步的Web Crypto API (window.crypto.subtle)。Hook它需要不同的策略因为它的方法返回的是Promise。function hookWebCrypto() { if (!window.crypto || !window.crypto.subtle) return; const subtle window.crypto.subtle; const methods [‘encrypt’, ‘decrypt’, ‘sign’, ‘verify’, ‘digest’, ‘generateKey’, ‘deriveKey’]; methods.forEach(methodName { if (typeof subtle[methodName] ‘function’) { const original subtle[methodName]; subtle[methodName] function(...args) { console.log([WebCrypto Hook] ${methodName} 被调用参数:, args); // 尝试记录密钥信息通常是CryptoKey对象无法直接读取内容 args.forEach(arg { if (arg instanceof CryptoKey) { console.log(‘ 发现CryptoKey:’, { type: arg.type, extractable: arg.extractable, algorithm: arg.algorithm, usages: arg.usages }); } }); // 调用原函数它返回一个Promise const promise original.apply(this, args); // 在Promise的then链中插入我们的逻辑 return promise.then(result { console.log([WebCrypto Hook] ${methodName} 成功结果:, result); // 结果通常是ArrayBuffer需要转换 if (result instanceof ArrayBuffer) { console.log(‘ ArrayBuffer转Hex:’, ab2hex(result)); } return result; // 必须返回结果保证链式调用 }).catch(err { console.error([WebCrypto Hook] ${methodName} 失败:, err); throw err; // 重新抛出错误 }); }; } }); } // 辅助函数ArrayBuffer 转 16进制字符串 function ab2hex(buffer) { return Array.from(new Uint8Array(buffer)) .map(b b.toString(16).padStart(2, ‘0’)) .join(‘‘); }要点Hook异步函数时核心是拦截其返回的Promise然后在.then()和.catch()中注入我们的日志逻辑并确保返回一个新的Promise链。注意CryptoKey对象的内容出于安全原因是不可直接读取的除非创建时extractable为true我们只能记录其元数据。4.2 对抗反Hook与混淆有些网站会采用一些防御措施来检测或阻止Hook。检测函数是否被重写它们可能会比较Function.prototype.toString.call(targetFunc)的结果是否包含预期外的代码。应对我们之前提到的拷贝原函数属性包括toString就是一种对抗。更彻底的做法是使用ES6的Proxy它能在不改变原函数引用的情况下拦截调用隐蔽性更高但兼容性稍差。// 使用Proxy进行Hook示例 const original targetObj[methodName]; const handler { apply: function(target, thisArg, argumentsList) { console.log(‘[Proxy Hook] 函数被调用’, argumentsList); return Reflect.apply(target, thisArg, argumentsList); } }; targetObj[methodName] new Proxy(original, handler);代码高度混淆与动态生成函数名和结构完全无法识别。应对通用Hook对此无能为力。此时需要回归传统逆向通过“特征码”来定位。例如搜索所有函数寻找那些参数中包含key,iv,mode加密模式或者函数体中含有典型加密常量如AES的S盒的函数。找到后再针对这个具体的函数地址进行Hook。将加密函数放在闭包或Web Worker中使其不在全局作用域无法通过window访问。应对如果加密逻辑在同一个主线程的闭包中我们可以在代码执行前的某个时机比如在HTML解析阶段通过script标签注入重写Function构造函数或eval来监控函数的定义。但这非常复杂且不稳定。对于Web Worker通信数据通常已经过加密我们可以在主线程与Worker的postMessage通道上进行Hook。4.3 性能考量与生产环境注意事项在控制台调试无所谓但如果要将Hook脚本长期注入页面必须考虑性能。选择性Hook不要Hook所有方法。先分析网络请求确定可能使用的算法只Hook相关的方法。例如如果请求参数里只有MD5那就只HookCryptoJS.MD5。减少日志量在生产日志中不要用console.log输出完整的WordArray.words数组可能很长。只记录其摘要如Hex字符串的前16位和时间戳。使用轻量级存储避免在before/after钩子中执行复杂的同步操作。如果要将日志发回服务器使用sendBeacon或微任务Promise.resolve().then()异步发送避免阻塞加密线程导致页面卡顿。错误隔离我们的Hook函数必须用try...catch包裹确保自身的任何错误都不会导致原加密流程崩溃从而暴露我们的存在。5. 实战演练与问题排查让我们模拟一个真实场景一个登录接口密码被一个未知的AES加密了。我们如何使用这套方案环境准备打开目标网站登录页进入开发者工具F12的Console面板。注入Hook脚本将完整的hookFunction和hookCryptoJS函数代码粘贴到Console中执行。触发加密在用户名和密码框输入测试内容点击登录。观察输出此时Console会滚动输出显示AES.encrypt被调用并打印出参数和结果。你重点关注第一个参数通常是明文密码可能已经是WordArray或字符串。第二个参数通常是密钥key也是一个WordArray。第三个参数可能是一个配置对象包含iv初始化向量、mode加密模式如CBC、padding填充方式如Pkcs7。数据提取从日志中直接获取key.toString(CryptoJS.enc.Hex)和iv.toString(CryptoJS.enc.Hex)以及加密后的密文。这些信息足以让你在Python或Node.js中用相同的参数重现加密过程。常见问题排查速查表问题现象可能原因解决方案Hook后页面功能异常加密失败1. Hook函数未正确传递this上下文。2. 修改了参数导致原函数逻辑错误。3. 原函数被多次Hook顺序错乱。1. 检查Hook工具中是否使用originalFunc.apply(this, args)。2. 暂时注释掉before钩子中修改参数的代码。3. 检查页面是否加载了多个版本的库或反调试脚本。Console没有输出任何Hook信息1. 目标加密库未加载或名称不对。2. Hook脚本执行时机太晚。3. 网站使用了非主流的自定义加密。1. 在Console输入CryptoJS等库名确认其存在。2. 尝试在head最早注入脚本或监听DOMContentLoaded事件后执行。3. 在Network面板查看登录请求根据参数名如encryptedData在源码中搜索手动定位函数再Hook。捕获到的key或iv是undefined或空密钥可能来自更上层的函数或是在加密对象内部维护的状态。1. 尝试Hook更早的环节如密钥生成函数generateKey或密钥派生函数。2. 查看加密对象的实例属性在Hook函数内打印this密钥可能存储在this._key这样的属性里。Hook成功了但输出的Hex字符串不对WordArray的words属性是32位整数数组直接console.log显示的是十进制需用toString(Hex)转换。确保在日志中使用arg.toString(CryptoJS.enc.Hex)进行转换。检查CryptoJS的编码器对象是否可用。我个人在实际操作中的体会是这套方案的威力在于“第一次成功部署”。一旦你成功Hook并捕获了一次完整的加密流程你就拥有了这个站点的“解密钥匙”。之后无论它的前端JS如何混淆、如何更新只要它继续使用同一个加密库的同一个方法你的Hook就能持续生效。你可以将配置好的Hook脚本保存为书签脚本Bookmarklet或浏览器插件下次访问时一键激活极大提升了逆向分析的效率。当然它并非银弹面对极度定制化和强防御的站点仍需结合静态分析与动态调试。但毫无疑问它应该成为你JS逆向工具箱中最锋利、最常用的一把工具。