1. 项目概述今天我们来聊聊一个在移动安全逆向分析中非常经典且实用的场景如何利用Frida去Hook企业级Android应用中常见的哈希加密算法。如果你正在从事安全研究、应用审计或者对App的加密机制感到好奇这篇文章就是为你准备的。在企业应用里哈希算法无处不在从用户密码的存储、API请求签名的生成到本地敏感数据的脱敏它都是保障数据“不可逆”性的基石。但很多时候我们作为安全研究者或开发者需要验证这些加密实现是否真的安全是否存在使用弱哈希如MD5、未加盐或迭代次数过低等隐患。这时候静态分析代码往往因为混淆而困难重重动态Hook就成了最直接有效的“透视镜”。Frida作为一款强大的动态插桩工具能让我们在应用运行时像做外科手术一样精准地在加密函数执行前后插入我们的逻辑从而捕获到原始的明文、使用的算法、盐值、迭代次数等关键信息。这不仅仅是“看到”加密结果更是理解整个加密链路和安全设计思路的过程。本次实战我们将聚焦于两类最核心的企业级哈希场景纯哈希计算如SHA-256, SHA-512和基于口令的密钥派生函数以PBKDF2为代表。通过手把手拆解Hook脚本你会掌握从定位关键类、编写Hook代码到解析输出结果的全套方法并能将这套方法论应用到实际的应用安全评估中去。2. 核心思路与目标拆解在动手写代码之前我们必须先理清思路明确要“钩”住什么以及为什么要这么“钩”。企业应用的哈希逻辑通常封装在标准的Java密码学架构中这反而为我们提供了清晰的Hook切入点。2.1 目标场景深度解析首先我们得明白要对付的两种“敌人”分别是什么路数。纯哈希场景比如计算一个字符串的SHA-256值。它的核心非常简单输入一段数据明文经过一个确定的哈希函数处理输出一段固定长度的、看似随机的字符串哈希值。在Java中这个任务几乎都由java.security.MessageDigest这个类来承担。应用会调用MessageDigest.getInstance(“SHA-256”)获取一个实例然后通过update和digest方法完成计算。我们要Hook的关键点就是digest方法因为在这里输入明文和输出密文会同时出现。通过Hook这里我们可以立刻判断出应用是否错误地使用了MD5这样的不安全算法或者直接捕获到一些关键数据的哈希值用于后续分析。口令哈希场景典型代表就是PBKDF2。这比纯哈希复杂得多也是目前存储用户密码的推荐方式。它之所以安全是因为引入了“盐”和“高迭代次数”这两个关键因素。盐是一个随机值确保即使两个用户密码相同最终哈希值也不同彻底防御了彩虹表攻击。高迭代次数通常上万次则极大地增加了暴力破解的计算成本。在Java中PBKDF2的实现涉及几个类的协作javax.crypto.spec.PBEKeySpec这个类的构造函数承载了核心参数——口令明文、盐、迭代次数和期望的密钥长度。javax.crypto.SecretKeyFactory通过getInstance(“PBKDF2WithHmacSHA256”)来指定具体的算法引擎。最后生成的密钥字节数组通常会被转换为十六进制或Base64字符串进行存储或传输。因此要完整还原一个PBKDF2加密链路我们需要对这三个环节进行“多点布控”。只Hook其中一个得到的信息都是片面的。2.2 Hook策略制定基于以上分析我们的Hook策略可以确定为“擒贼先擒王链路全覆盖”。对于纯哈希我们集中火力攻击MessageDigest.digest()这个单一节点。只要成功核心信息一览无余。对于PBKDF2我们则需要打一场“组合拳”第一拳HookPBEKeySpec的构造函数夺取最宝贵的“明文”、“盐”和“迭代次数”。第二拳HookSecretKeyFactory.getInstance()确认敌人使用的具体“武器”型号算法。第三拳HookBase64.encodeToString()或类似编码方法截获最终准备“送出去”的加密结果。这个策略的优势在于它完全遵循了Java密码学扩展的标准API调用流程通用性极强。无论应用内部的业务逻辑多么复杂只要它使用了这些标准类就逃不过我们的监控。注意在实际Hook中你可能会遇到一些“噪音”。比如在应用启动时系统或某些库可能会对文件进行完整性校验如对.so文件的ELF头进行MD5计算并在控制台打印出无关的哈希信息。我们需要具备区分“业务逻辑”和“系统噪音”的能力通常通过观察调用的上下文如堆栈信息或哈希的输入数据是否是文件路径、特定二进制头来判断。3. 环境准备与脚本框架搭建工欲善其事必先利其器。在开始编写核心Hook逻辑前我们需要一个稳定、可复现的实验环境。3.1 基础环境配置Frida环境确保你的开发机上安装了Frida客户端 (pip install frida-tools)并准备了与目标Android设备或模拟器架构匹配的frida-server。将frida-server推送到设备并在adb shell中以root权限运行。目标应用你需要一个包含目标加密逻辑的应用。可以是自己编写的Demo APK用于学习和验证也可以是待分析的应用。对于Demo建议明确实现我们提到的两种哈希场景方便对照。开发工具一个顺手的代码编辑器如VSCode用于编写JavaScript脚本以及Chrome浏览器或frida-cli用于执行和调试。3.2 通用Hook脚本框架一个健壮的Hook脚本不仅仅包含核心的Hook点还需要一些辅助函数来处理数据以及良好的错误处理和日志输出。下面是一个高度可复用的脚本框架import Java from frida-java-bridge; // 目标应用的包名用于快速定位和过滤 const TARGET_PACKAGE com.example.targetapp; // 核心辅助函数 /** * 将char数组转换为可读的字符串。 * 常用于处理PBEKeySpec中的口令因为安全考虑口令常以char数组形式传递。 * param {Array} charArr - 字符数组 * return {string} 转换后的字符串或“空” */ function charsToString(charArr) { if (!charArr || charArr.length 0) return [空]; let str ; for (let i 0; i charArr.length; i) { str charArr[i]; } return str; } /** * 智能字节数组转字符串。 * 优先尝试解码为UTF-8明文失败则转为十六进制表示。 * param {Array} byteArr - 字节数组 * return {string} 字符串或十六进制串 */ function bytesToReadableString(byteArr) { if (!byteArr || byteArr.length 0) return [空]; try { // 尝试作为文本解码 const StringClass Java.use(java.lang.String); const str StringClass.$new(byteArr, UTF-8); // 简单判断是否为可打印字符避免将二进制误显示为乱码 const result str.toString(); if (/^[\x20-\x7E]*$/.test(result)) { // 基本可打印ASCII范围 return result; } throw new Error(非纯文本数据); } catch (e) { // 作为二进制数据转为十六进制 return bytesToHex(byteArr); } } /** * 将字节数组转换为十六进制字符串。 * 这是显示哈希值、盐等二进制数据的标准方式。 * param {Array} byteArr - 字节数组 * return {string} 十六进制字符串 */ function bytesToHex(byteArr) { if (!byteArr || byteArr.length 0) return [空]; let hex ; for (let i 0; i byteArr.length; i) { const v byteArr[i] 0xff; // 确保是无符号字节 hex (v 0x10 ? 0 : ) v.toString(16); } return hex; } // 主Hook逻辑入口 Java.perform(() { console.log([] Hook脚本已注入目标进程: ${TARGET_PACKAGE}); try { // 在这里放置具体的Hook代码后续章节会填充 hookPureHash(); hookPBKDF2(); } catch (error) { console.error([-] Hook过程发生致命错误: ${error}); console.error(error.stack); } }); // 具体的Hook函数定义将在下面章节实现 function hookPureHash() { // TODO: 实现纯哈希Hook } function hookPBKDF2() { // TODO: 实现PBKDF2 Hook }这个框架提供了数据转换的“瑞士军刀”并搭建了清晰的代码结构。Java.perform是Frida在目标Java虚拟机中执行代码的入口所有Hook操作都必须放在这个回调里。4. 纯哈希场景Hook实战现在我们来填充第一个核心函数hookPureHash。目标是捕获所有通过MessageDigest进行的哈希计算。4.1 定位与Hook MessageDigestjava.security.MessageDigest类是整个纯哈希计算的枢纽。我们关心的是digest方法它有多个重载最常见的是digest(byte[] input)。function hookPureHash() { console.log([*] 开始Hook纯哈希场景 (MessageDigest)...); const MessageDigest Java.use(java.security.MessageDigest); // Hook digest(byte[]) 方法这是最常用的方法 MessageDigest.digest.overload([B).implementation function (inputBytes) { // 1. 调用原方法获取真实的哈希结果 const hashResult this.digest(inputBytes); // 注意这里用this调用原方法 // 2. 获取算法名称 (如 SHA-256) const algorithm this.getAlgorithm(); // 3. 转换输入和输出为可读格式 const inputStr bytesToReadableString(inputBytes); const outputHex bytesToHex(hashResult); // 4. 打印关键信息 console.log(\n[纯哈希] 算法: ${algorithm}); console.log( 输入(明文): ${inputStr}); console.log( 输出(哈希): ${outputHex}); console.log( 调用栈:); // 打印调用栈有助于区分业务调用和系统调用 console.log(Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Exception).$new())); // 5. 返回原结果不影响程序正常执行 return hashResult; }; // 也可以Hook update方法用于处理流式哈希但digest通常已包含完整数据 // MessageDigest.update.overload([B).implementation function (data) { ... }; console.log([] MessageDigest.digest() Hook 已安装); }关键点解析this.digest(inputBytes)在implementation函数内部我们通过this调用原方法这是Frida Hook的标准做法确保了原始功能不被破坏。this.getAlgorithm()MessageDigest实例方法直接告诉我们当前使用的是MD5、SHA-1还是SHA-256等。调用栈打印Log.getStackTraceString是Android环境下获取当前调用栈的便捷方法。这对于过滤噪音至关重要。当你看到一条哈希调用来自java.io.File或java.lang.ClassLoader时很可能就是系统在验证文件完整性可以忽略。而来自你目标应用包名下的调用才是真正的业务逻辑。4.2 实战测试与结果分析编写一个简单的测试应用包含一个按钮点击后计算字符串“HelloFrida”的SHA-256值。运行Hook脚本并点击按钮控制台预期输出如下[纯哈希] 算法: SHA-256 输入(明文): HelloFrida 输出(哈希): a1b2c3d4e5f67890... (64位十六进制字符串) 调用栈: at com.example.targetapp.MainActivity.onClick(MainActivity.java:50) at android.view.View.performClick(View.java:7500) ...通过这个输出我们不仅验证了Hook成功还精准地定位到了触发哈希计算的代码位置MainActivity.java:50。如果这里显示的算法是MD5那么这就是一个明显的安全风险点需要在安全报告中指出。实操心得在实际逆向中目标应用可能使用第三方库如Bouncy Castle或自定义的JNI本地代码来实现哈希。如果HookMessageDigest没有收获就需要扩大搜索范围通过Java.choose枚举类或者使用Frida的Interceptor.attach去Hook本地函数。但绝大多数遵循Android开发规范的应用都会使用标准的MessageDigest。5. PBKDF2场景Hook实战PBKDF2的Hook更像一个侦探游戏我们需要串联起多个线索点。让我们一步步实现hookPBKDF2函数。5.1 截获密钥规范PBEKeySpec这是整个链条的起点包含了最丰富的原始信息。function hookPBKDF2() { console.log([*] 开始Hook PBKDF2场景...); const PBEKeySpec Java.use(javax.crypto.spec.PBEKeySpec); // Hook最常用的构造函数 PBEKeySpec(char[] password, byte[] salt, int iterationCount, int keyLength) PBEKeySpec.$init.overload([C, [B, int, int).implementation function (passwordChars, saltBytes, iterationCount, keyLength) { // 先调用原始构造函数 const result this.$init(passwordChars, saltBytes, iterationCount, keyLength); // 转换并打印参数 const passwordPlaintext charsToString(passwordChars); // 口令明文 const saltHex bytesToHex(saltBytes); // 盐值 console.log(\n[PBKDF2-参数] 口令明文: ${passwordPlaintext}); console.log( 盐(Salt): ${saltHex}); console.log( 迭代次数: ${iterationCount}); console.log( 密钥长度: ${keyLength} 位); // 记录到全局变量供后续环节使用如果需要关联 // send({type: pbkdf2_spec, password: passwordPlaintext, salt: saltHex, iter: iterationCount}); return result; // 返回构造的对象 }; console.log([] PBEKeySpec 构造函数 Hook 已安装);这里我们Hook了$init这是Frida中对Java构造函数的表示。我们成功捕获了四个核心安全参数。盐值的随机性和长度通常至少16字节、迭代次数推荐10000次以上是评估PBKDF2安全性的关键。5.2 确认算法引擎SecretKeyFactory接下来我们需要知道应用使用的是PBKDF2WithHmacSHA1还是更安全的PBKDF2WithHmacSHA256。const SecretKeyFactory Java.use(javax.crypto.SecretKeyFactory); SecretKeyFactory.getInstance.overload(java.lang.String).implementation function (algorithm) { const factoryInstance this.getInstance(algorithm); // 调用原方法 console.log([PBKDF2-算法] 使用的算法: ${algorithm}); // 可以进一步Hook factoryInstance.generateSecret(KeySpec) 来获取中间密钥 return factoryInstance; }; console.log([] SecretKeyFactory.getInstance() Hook 已安装);这个Hook点相对简单但它确认了加密算法的具体类型是安全评估的另一个维度。5.3 捕获最终输出Base64编码PBKDF2最终生成的密钥是一个字节数组在存储或传输前几乎都会被编码。Base64是最常见的选择。// 方式一Hook Java标准库的Base64 (API 8) try { const Base64 Java.use(java.util.Base64); const Encoder Base64.getEncoder(); // 注意需要Hook返回的Encoder对象的方法 Java.use(java.util.Base64$Encoder).encodeToString.overload([B).implementation function (byteArray) { const encoded this.encodeToString(byteArray); console.log([PBKDF2-输出] Base64密文: ${encoded}); console.log(----------------------------------------); return encoded; }; console.log([] java.util.Base64.Encoder Hook 已安装); } catch (e) { console.log([-] java.util.Base64 不可用尝试Android Base64: ${e.message}); } // 方式二Hook Android SDK的Base64 (更通用) try { const AndroidBase64 Java.use(android.util.Base64); AndroidBase64.encodeToString.overload([B, int).implementation function (data, flags) { const encoded this.encodeToString(data, flags); // 通过flags或数据来源判断是否为我们的目标输出避免误报 // 一个简单的判断如果数据长度和密钥长度匹配且上下文是PBKDF2 console.log([PBKDF2-输出] Android Base64密文: ${encoded} (flags: ${flags})); console.log(----------------------------------------); return encoded; }; console.log([] android.util.Base64 Hook 已安装); } catch (e) { console.log([-] android.util.Base64 Hook 失败: ${e.message}); } console.log([] PBKDF2全链路Hook已部署完成); } // 结束 hookPBKDF2 函数重要提示应用可能使用java.util.Base64(Android API 8及以上) 或android.util.Base64甚至可能是第三方库。我们通过try-catch块尝试Hook两者确保覆盖。在实际环境中你可能需要根据反编译的代码或日志输出来确定具体使用哪一种。5.4 实战测试与链路还原现在在一个使用PBKDF2存储密码的Demo应用上测试。假设用户输入密码“MySecretPass123”系统使用随机盐“s0m3Rnd0mSlt”和迭代次数10000进行加密。运行Hook脚本并触发加密后控制台会输出一个完整的故事[PBKDF2-参数] 口令明文: MySecretPass123 盐(Salt): 73306d3352406e646f6d53406c74 迭代次数: 10000 密钥长度: 256 位 [PBKDF2-算法] 使用的算法: PBKDF2WithHmacSHA256 [PBKDF2-输出] Base64密文: jGvV6...XyZQ ----------------------------------------至此我们成功还原了整个PBKDF2加密链路的所有核心参数。这份信息对于安全评估价值巨大我们可以检查盐是否足够随机且唯一、迭代次数是否符合当前安全标准NIST推荐至少10000次并根据硬件性能增加、是否使用了强哈希算法如SHA-256而非SHA-1。6. 高级技巧与问题排查掌握了基础Hook后我们还需要一些进阶技巧来应对更复杂的情况并学会解决常见问题。6.1 处理混淆与多线程场景目标应用经过了严重的代码混淆类名和方法名都变成了a.a,b.c这种。对策特征搜索即使被混淆它们调用的系统API如MessageDigest.getInstance是不会变的。我们可以先Hook这些系统API然后打印调用栈。调用栈中混淆的类名和方法名就是我们要找的目标。然后可以再用Frida去枚举和Hook这些具体的混淆类。// 示例通过系统API反推业务类 SecretKeyFactory.getInstance.overload(java.lang.String).implementation function(algo){ console.log(Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Exception).$new())); return this.getInstance(algo); };枚举与模式匹配使用Java.enumerateLoadedClasses()或Java.choose()来寻找内存中已加载的、可能包含“Digest”、“Crypto”、“PBE”等关键词的类再进行试探性Hook。场景加密操作在非主线程进行日志打印混乱。对策在日志输出中包含线程ID。javascript const Thread Java.use(java.lang.Thread); const currentThread Thread.currentThread(); console.log([TID:${currentThread.getId()}] 哈希算法: ${algorithm});6.2 性能优化与稳定性避免阻塞操作implementation函数内的代码执行会阻塞原线程。避免在这里进行复杂的网络请求或文件IO操作。所有数据处理应尽可能简单高效。异常处理务必用try-catch包裹implementation函数内的逻辑防止你的Hook代码抛出异常导致目标应用崩溃。MessageDigest.digest.implementation function(input) { try { // ...你的Hook逻辑 } catch (e) { console.error(Hook处理出错: ${e}); // 无论如何调用原方法保证程序不崩 return this.digest(input); } };选择性Hook如果目标应用哈希调用极其频繁全量Hook可能会影响性能或产生海量日志。可以通过包名、调用栈、算法类型等条件进行过滤。if (!algorithm.includes(SHA-256)) { return this.digest(inputBytes); // 只Hook SHA-256其他直接放过 }6.3 常见问题排查表问题现象可能原因解决方案TypeError: cannot read property ‘$init’ of undefined目标类未加载。混淆后类名不对或应用使用了自定义类加载器。1. 确认应用已运行到相关逻辑类已加载。2. 使用Java.enumerateLoadedClasses()搜索相关类。3. 在Java.perform外使用setImmediate延迟Hook。Hook后无任何输出1. Hook的类或方法签名错误。2. 目标逻辑未被触发。3. Frida脚本未成功注入或附加。1. 检查方法重载overload的签名是否完全匹配使用jdb或frida-trace辅助。2. 确保执行了触发加密的UI操作或网络请求。3. 使用frida-ps -U确认设备连接用-f参数以spawn方式启动应用确保早期注入。输出大量无关的系统哈希日志系统或其他库调用了哈希函数。在Hook逻辑中添加过滤条件如检查调用栈是否包含目标包名或忽略特定算法如MD5的特定输入模式如.so、.dex文件头。应用崩溃或行为异常Hook代码逻辑错误如死循环、未正确调用原方法、或修改了关键数据。1. 精简Hook代码确保return this.originalMethod(...)被正确执行。2. 检查对输入/输出参数的修改是否安全。3. 使用try-catch包裹。无法捕获PBKDF2最终输出应用使用了非标准的编码方式如自定义十六进制编码或直接使用字节数组。1. 尝试HookSecretKeyFactory.generateSecret()的返回值。2. 搜索并Hook应用内自定义的encode、toHex等方法。3. 直接Hook业务层最终存储或发送数据的方法。7. 实战案例构建一个自动化哈希审计脚本将前面的知识点整合我们可以编写一个更自动化、信息更全面的审计脚本。这个脚本不仅能捕获信息还能进行简单的安全评估。Java.perform(() { let securityWarnings []; // Hook MessageDigest并评估算法强度 const MessageDigest Java.use(java.security.MessageDigest); MessageDigest.digest.overload([B).implementation function(input) { const result this.digest(input); const algo this.getAlgorithm(); // 安全评估 if (algo.includes(MD5) || algo.includes(SHA-1)) { const warning 发现弱哈希算法: ${algo}; console.warn([!] 安全警告: ${warning}); securityWarnings.push(warning); } console.log([审计-哈希] 算法${algo}, 输入${bytesToReadableString(input)}, 输出${bytesToHex(result).substring(0, 32)}...); return result; }; // Hook PBEKeySpec评估参数安全性 const PBEKeySpec Java.use(javax.crypto.spec.PBEKeySpec); PBEKeySpec.$init.overload([C, [B, int, int).implementation function(pwd, salt, iter, len) { const ret this.$init(pwd, salt, iter, len); // 安全评估 if (iter 10000) { const warning PBKDF2迭代次数过低: ${iter} (推荐 10000); console.warn([!] 安全警告: ${warning}); securityWarnings.push(warning); } if (!salt || salt.length 16) { // 盐长度至少16字节 const warning PBKDF2盐长度不足: ${salt ? salt.length : 0} 字节 (推荐 16); console.warn([!] 安全警告: ${warning}); securityWarnings.push(warning); } console.log([审计-PBKDF2] 明文${charsToString(pwd)}, 盐长${salt.length}, 迭代${iter}); return ret; }; // 在脚本结束时或特定时机输出总结报告 setTimeout(() { if (securityWarnings.length 0) { console.log(\n 安全审计报告 ); console.log(共发现 ${securityWarnings.length} 个潜在风险点:); securityWarnings.forEach((w, i) console.log( ${i1}. ${w})); console.log(); } else { console.log(\n[] 未发现明显的哈希加密安全风险。); } }, 10000); // 10秒后输出报告 });这个脚本在基础Hook之上增加了简单的安全策略检查能够实时警告使用弱算法、迭代次数不足或盐值过短的风险并将所有警告汇总成一份简易的审计报告。在实际渗透测试或代码审计中这样的脚本能极大提升效率。最后我想分享一点个人体会。Frida Hook加密算法的过程就像是在程序的运行时间线上安装监控探头。成功的核心不在于编写多复杂的脚本而在于对目标系统加密体系如JCE的深刻理解。你需要清楚地知道数据从明文到密文的流转路径上经过了哪些“收费站”关键类和方法。当你能够熟练地钩住MessageDigest、PBEKeySpec这些点时你会发现很多应用的“加密黑盒”对你而言已经变得透明。这种能力对于安全研究、漏洞挖掘甚至理解优秀应用的安全设计都是不可或缺的。记住多动手测试从简单的Demo开始逐步挑战真实世界中被混淆的应用你会积累下最宝贵的经验。