1. 项目概述为什么选择Frida来Hook安卓APP的MD5加密如果你正在尝试分析一个安卓应用发现它在网络请求或者本地数据存储时使用了MD5进行加密或签名而你手头只有这个APK文件你会怎么做直接反编译看代码如果代码被混淆得一塌糊涂关键逻辑还藏在so库里那阅读成本就太高了。或者你想动态地修改某个加密函数的返回值看看应用会有什么反应静态分析更是无能为力。这就是Frida这类动态插桩工具大显身手的地方。简单来说Frida允许你在应用运行时像“外科手术”一样精确地拦截、修改、甚至替换应用内部的函数调用和内存数据。对于分析MD5加密我们不需要去硬啃混淆后的Java代码或者复杂的Native代码只需要找到那个执行加密的函数然后“钩住”它就能看到输入是什么、输出是什么甚至能按我们的意愿修改输出结果。我选择这个实战项目是因为MD5虽然是一种相对简单的哈希算法但在很多安卓应用中它被广泛用于生成签名、校验数据完整性甚至是不安全的密码存储。通过一个完整的脚本示例你不仅能学会如何用Frida对付MD5更能掌握一套通用的、用于逆向分析任何加密/解密、网络请求、关键逻辑函数的方法论。这个脚本就像一把万能钥匙稍加修改就能打开许多APP内部的黑盒。2. 核心思路与工具准备构建你的动态分析环境在动手写脚本之前我们必须把“手术台”——也就是分析环境搭建好。思路很清晰我们需要一个运行着目标APP的设备或模拟器以及一台能控制Frida的电脑。整个数据流是我们的Python脚本运行在电脑上通过Frida的客户端与安装在手机上的Frida服务端通信服务端再将我们的JavaScript注入到目标APP的进程中从而实现对APP的实时操控。2.1 环境搭建与组件解析首先你需要准备以下三样东西一部已Root的安卓手机或一个可Root的模拟器这是硬性要求因为Frida需要较高的权限来注入代码。对于模拟器我推荐使用官方Android Studio自带的AVD并选择x86或x86_64架构的镜像兼容性最好。像夜神、雷电这类模拟器也可以但可能需要单独处理Frida-server的架构兼容问题。Frida-server这是一个需要推送到手机或模拟器上运行的后台服务程序。它的版本必须与你电脑上安装的Frida客户端版本严格一致否则无法连接。你可以在Frida的GitHub Release页面找到对应版本根据你设备的CPU架构通常是arm64下载对应的frida-server-xx.x.x-android-arm64.xz文件。电脑端的Frida-tools通过Python的pip包管理器安装它包含了与我们交互的Python库和命令行工具。在电脑终端执行pip install frida-tools即可。这里有一个关键的实操心得版本同步是成功的第一步。很多新手卡在连接不上八成是版本不对。安装完frida-tools后在电脑终端执行frida --version查看客户端版本然后务必下载相同版本的server。2.2 部署Frida-server到设备假设你已经解压得到了frida-server可执行文件。使用ADB连接你的设备adb devices确认设备已连接。将frida-server推送到设备的一个可执行目录比如/data/local/tmpadb push frida-server /data/local/tmp/登录设备shell赋予执行权限并以root身份在后台运行它adb shell su # 获取root权限 cd /data/local/tmp chmod 755 frida-server ./frida-server 注意运行./frida-server 后进程会在后台运行。如果你想关闭它需要先用ps | grep frida找到进程ID再用kill命令结束。保持这个shell窗口不要关闭或者让服务在后台运行另开一个电脑终端窗口测试连接frida-ps -U如果能看到设备上运行的进程列表恭喜你环境搭建成功。参数-U表示连接到USB设备。2.3 目标APP的选择与关键函数定位为了这次实战我选择了一个简单的、自己编写的Demo APP作为目标。它包含一个按钮点击后会将输入的字符串用MD5加密并显示出来。在真实场景中你需要先对目标APP进行初步的静态分析以确定需要Hook的函数。定位MD5相关函数的方法通常有几种字符串搜索在反编译后的Java代码使用Jadx-GUI等工具中搜索“MD5”、“MessageDigest”等关键词找到调用处。堆栈跟踪如果APP有日志输出可以先触发一次加密操作通过logcat查看调用堆栈找到关键的类和方法名。枚举类和方法对于混淆严重的APP你可以写一个Frida脚本先枚举出所有类再根据方法签名特征如参数、返回值类型进行过滤。例如MD5加密函数通常接收一个String或byte[]参数返回一个String或byte[]。在我们的Demo中假设我们已经通过静态分析知道加密逻辑位于类com.example.demo.MD5Utils的静态方法getMD5(String input)中。这是我们Hook的明确目标。3. Frida Hook脚本的核心原理与结构拆解Frida脚本的核心是JavaScript代码这段代码会被注入到目标进程并赋予我们访问和操作该进程内存和运行时对象的能力。脚本主要依赖Frida提供的Interceptor拦截器和JavaJava运行时接口两大模块。3.1 脚本骨架与连接逻辑一个典型的Frida脚本结构如下我们使用Python作为外部控制器import frida import sys # 定义需要注入的JavaScript代码 jscode Java.perform(function () { // 所有的Hook逻辑都将写在这个函数内部 console.log([*] Script loaded successfully.); // 1. 定位目标类 var MD5Utils Java.use(com.example.demo.MD5Utils); // 2. Hook目标方法 MD5Utils.getMD5.implementation function (input) { // 这里是我们的Hook逻辑 }; }); def on_message(message, data): # 处理从JavaScript端发送过来的消息 if message[type] send: print(f[*] Message from script: {message[payload]}) else: print(message) # 连接到设备上的目标进程 device frida.get_usb_device() # 附加到正在运行的进程。也可以使用spawn启动应用。 pid device.attach(com.example.demo) # 替换为你的包名 # 创建脚本并加载 script pid.create_script(jscode) script.on(message, on_message) # 注册消息回调 script.load() # 保持Python脚本运行不让它退出 sys.stdin.read()关键点解析Java.perform()这是一个Frida提供的核心函数它确保其内部的代码在Java虚拟机JVM的上下文中执行这样我们才能安全地调用Java.use等API。Java.use(className)这个函数用于获取一个Java类的包装对象通过这个对象我们可以访问类的静态方法、Hook实例方法等。.implementation这是Frida最强大的特性之一。当你将一个函数赋值给某个方法的.implementation属性时你就替换了该方法的原始实现。当APP调用这个方法时实际执行的是你提供的函数。3.2 Hook函数内部的四步操作在我们替换的implementation函数里通常遵循“获取参数 - 执行原方法 - 处理结果 - 返回结果”的流程。这保证了APP原有逻辑在可控范围内依然能运行我们只是做了“监视”和“可能的小修改”。MD5Utils.getMD5.implementation function (input) { // 步骤1: 打印传入的参数 console.log([*] MD5Utils.getMD5 called!); console.log( [-] Original input: ${input}); // 步骤2: 调用原方法获取原始结果 // 使用this.getMD5(input)来调用原始方法 var originalResult this.getMD5(input); // 步骤3: 分析和处理结果 console.log( [-] Original MD5 result: ${originalResult}); // 步骤4: 返回结果可以是原结果也可以是修改后的结果 return originalResult; };为什么这样设计这种“先记录再调用原函数”的模式是最稳妥的。它确保了稳定性APP原有的加密逻辑得以完整执行不会因为我们的Hook而崩溃。可观测性我们清晰地看到了函数的输入和输出。可干预性我们可以在return之前轻松地修改originalResult。比如我们可以让特定的输入永远返回一个固定的MD5值这对于绕过某些校验非常有用。注意在implementation函数内部this关键字指向的是被Hook方法的原始对象对于实例方法或类本身对于静态方法。通过this.getMD5(input)来调用原方法避免了递归调用自身导致的死循环。4. 完整脚本示例与逐行深度解析下面我将展示一个功能更丰富的完整脚本并加入错误处理、多场景适配等实战技巧。import frida import sys import time jscode Java.perform(function () { console.log([*] Starting MD5 Hook Script...); // 尝试Hook Java层的MD5工具类 try { var MD5Utils Java.use(com.example.demo.MD5Utils); console.log([] Successfully found class: com.example.demo.MD5Utils); MD5Utils.getMD5.implementation function (input) { console.log(\\n[ Java MD5 Hook Triggered ]); console.log([*] Timestamp: ${new Date().toLocaleString()}); console.log([*] Caller Stack:\\n${Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Exception).$new())}); // 记录原始输入 console.log([-] Input String: ${input}); var inputBytes []; for (var i 0; i input.length; i) { inputBytes.push(input.charCodeAt(i)); } console.log([-] Input Bytes (Hex): ${Array.from(inputBytes, b (0 (b 0xFF).toString(16)).slice(-2)).join(:)}); // 调用原方法 try { var result this.getMD5(input); console.log([-] Original MD5 Result: ${result}); // 模拟一个“后门”当输入特定字符串时返回一个固定的MD5值 if (input secret2024) { var fakeResult e10adc3949ba59abbe56e057f20f883e; // 这是123456的MD5 console.log([!] Input matched keyword secret2024, overriding result to: ${fakeResult}); result fakeResult; } // 计算并打印结果的字节形式 if (result) { var resultBytes []; for (var i 0; i result.length; i) { resultBytes.push(result.charCodeAt(i)); } // MD5结果是32位十六进制字符串我们可以尝试按十六进制解析每两个字符 if (result.length 32) { var hexBytes []; for (var i 0; i 32; i 2) { hexBytes.push(parseInt(result.substr(i, 2), 16)); } console.log([-] MD5 Bytes (Hex): ${hexBytes.map(b (0 (b 0xFF).toString(16)).slice(-2)).join(:)}); } } console.log([ Hook Execution Finished ]\\n); return result; } catch (e) { console.log([!] Error calling original method: ${e}); return null; } }; console.log([] Hook on MD5Utils.getMD5 installed successfully.); } catch (e) { console.log([-] Failed to hook Java class. Reason: ${e}); console.log([*] Maybe the class name is obfuscated or the method is in native code.); } // 尝试Hook更底层的MessageDigest类通用性更强 try { var MessageDigest Java.use(java.security.MessageDigest); console.log([] Found system class: java.security.MessageDigest); MessageDigest.getInstance.overload(java.lang.String).implementation function (algorithm) { var result this.getInstance(algorithm); console.log([*] MessageDigest.getInstance(${algorithm}) called.); if (algorithm.toUpperCase().indexOf(MD5) ! -1) { console.log([!] An MD5 MessageDigest instance is being created!); // 这里可以进一步Hook这个实例的update/digest方法 } return result; }; console.log([] Hook on MessageDigest.getInstance installed.); } catch (e) { console.log([-] Failed to hook MessageDigest.); } }); def on_message(message, data): if message[type] send: print(f{message[payload]}) elif message[type] error: print(f[!] Script Error: {message}) else: print(f[?] Unknown message: {message}) if __name__ __main__: try: # 连接设备 device frida.get_usb_device() print(f[*] Connected to device: {device}) # 方式一附加到已运行进程 # session device.attach(com.example.demo) # 方式二重启应用并注入更适合从启动开始监控 pid device.spawn([com.example.demo]) session device.attach(pid) device.resume(pid) # 恢复进程执行 print(f[*] App spawned with PID: {pid}) # 创建并加载脚本 script session.create_script(jscode) script.on(message, on_message) print([*] Loading script...) script.load() print([*] Script loaded. Waiting for events...) # 保持主线程运行 sys.stdin.read() except KeyboardInterrupt: print(\\n[*] User interrupted.) except Exception as e: print(f[!] Main process error: {e}) sys.exit(1)4.1 脚本增强功能解析异常处理Try-Catch在Hook过程中类或方法可能不存在尤其是混淆后的APP。使用try-catch包裹关键操作可以防止脚本因单个错误而整体崩溃并给出友好提示。调用堆栈打印Java.use(“android.util.Log”).getStackTraceString(...)这行代码能打印出当前函数被调用时的完整堆栈信息。这对于逆向分析至关重要它能告诉你这个MD5函数是在什么业务逻辑下被触发的比如是在用户登录时还是在提交订单时。数据格式转换脚本不仅打印字符串还展示了字符串的字节数组和十六进制表示。这在分析二进制数据或调试编码问题时非常有用。条件逻辑干预脚本演示了如何根据输入参数input “secret2024”动态修改返回值。这是实现“破解”或“绕过”功能的核心。多层级Hook除了Hook业务层的MD5Utils脚本还尝试Hook了系统级的MessageDigest.getInstance方法。这是一种更通用的方法即使业务类名被混淆只要APP最终调用了标准的Java API来获取MD5实例我们就能捕获到。在这个Hook点我们可以继续深入去Hook返回的MessageDigest对象的update和digest方法从而捕获所有通过该API的MD5计算。4.2 Python控制端技巧device.spawn()与device.attach()spawn用于启动一个应用进程并暂停它然后我们attach上去加载脚本最后resume恢复执行。这确保了我们的Hook从应用启动的第一时间就生效不会错过早期的初始化调用。而attach仅附加到已运行的进程。sys.stdin.read()这是一个简单的技巧让Python脚本保持阻塞状态持续监听来自Frida脚本的消息直到用户按下CtrlC中断。5. 实战运行、结果分析与高级技巧将上述脚本保存为hook_md5.py并确保你的Demo APP已经安装在设备上。运行脚本python hook_md5.py在手机上操作Demo APP点击加密按钮。观察电脑终端的输出。你可能会看到类似这样的日志[*] Connected to device: USB Device (idxxxxxx) [*] App spawned with PID: 12345 [*] Loading script... [*] Script loaded. Waiting for events... [*] Starting MD5 Hook Script... [] Successfully found class: com.example.demo.MD5Utils [] Hook on MD5Utils.getMD5 installed successfully. [] Found system class: java.security.MessageDigest [] Hook on MessageDigest.getInstance installed. [ Java MD5 Hook Triggered ] [*] Timestamp: 2023/10/27 14:30:00 [*] Caller Stack: at com.example.demo.MainActivity.onClick(MainActivity.java:50) at android.view.View.performClick(View.java:7500) ... [-] Input String: HelloFrida [-] Input Bytes (Hex): 48:65:6c:6c:6f:46:72:69:64:61 [-] Original MD5 Result: a3b2c1d4e5f678901234567890abcdef0 [-] MD5 Bytes (Hex): a3:b2:c1:d4:e5:f6:78:90:12:34:56:78:90:ab:cd:ef:00 [ Hook Execution Finished ]结果分析从日志中我们清晰地看到了加密发生的时刻、触发加密的界面MainActivity.onClick、原始字符串、其字节表示、计算出的MD5值以及MD5的16字节二进制形式。这一切都是在APP运行时动态获取的无需阅读一行反编译代码。5.1 处理Native层C/C的MD5加密很多安全级别较高的APP会将核心加密算法放在Native层.so库文件用C/C实现。Frida同样可以Hook Native函数。假设通过分析我们知道在libnative-lib.so中有一个导出函数Java_com_example_demo_MD5Utils_nativeGetMD5这是JNI函数的常见命名规则。我们可以使用Frida的Interceptor模块来Hook它Java.perform(function () { console.log([*] Attempting to hook native MD5 function...); // 首先确定so库是否已加载 var moduleName libnative-lib.so; var isModuleLoaded false; Process.enumerateModules().forEach(function (module) { if (module.name.indexOf(moduleName) ! -1) { console.log([] Found module: ${module.name} at base ${module.base}); isModuleLoaded true; // 找到函数地址这里假设我们知道函数符号 // 方法1如果知道导出符号名 var funcAddr Module.findExportByName(module.name, Java_com_example_demo_MD5Utils_nativeGetMD5); // 方法2如果不知道可以通过偏移量计算需要IDA等静态分析 // var funcAddr module.base.add(0x1234); if (funcAddr) { console.log([] Found native function at: ${funcAddr}); Interceptor.attach(funcAddr, { onEnter: function (args) { // args[0]是JNIEnv*, args[1]是jclass/jobject, args[2]是jstring input console.log([ Native MD5 Hook (onEnter) ]); var inputJString args[2]; var inputStr Java.vm.getEnv().getStringUtfChars(inputJString, null).readCString(); console.log([-] Native Input: ${inputStr}); // 保存输入以便在onLeave中使用 this.inputStr inputStr; }, onLeave: function (retval) { // retval是jstring即MD5结果 console.log([ Native MD5 Hook (onLeave) ]); var resultStr Java.vm.getEnv().getStringUtfChars(retval, null).readCString(); console.log([-] Native Output: ${resultStr}); console.log([-] For Input: ${this.inputStr}); // 同样这里可以修改retval // 例如Memory.writeUtf8String(retval, fake_md5_result); } }); } else { console.log([-] Could not find the target function in module.); } } }); if (!isModuleLoaded) { console.log([-] Module ${moduleName} not loaded yet. You may need to trigger its loading first.); } });注意Hook Native函数需要对JNI和C语言指针有基本了解难度比Hook Java层要高。关键点在于正确解析JNIEnv*指针和jstring等JNI类型。5.2 通用化与自动化思路面对成千上万个不同的APP我们不可能为每一个都写定制脚本。可以尝试以下通用化策略特征Hook不Hook具体的类名而是Hook所有满足特征的方法。例如Hook所有返回值为String、参数为一个String的静态方法并在函数体内检查其返回值是否符合MD5的32位十六进制特征。Java.enumerateLoadedClasses({ onMatch: function(className) { var clazz Java.use(className); var methods clazz.class.getDeclaredMethods(); for (var i in methods) { var method methods[i]; // 检查方法特征需根据实际情况调整 if (method.getReturnType().getName().equals(java.lang.String) ...) { // 尝试Hook并记录 } } }, onComplete: function() {} });这种方法计算量大可能产生大量日志需要仔细过滤。RPC远程过程调用Frida支持将JavaScript函数暴露给Python端调用。你可以写一个通用的“加密/解密”函数当Python端需要计算某个字符串的MD5时通过RPC调用APP中的函数让APP自己算出来。这在需要批量处理或与外部工具联动时非常有用。// JS端 rpc.exports { computeMD5: function (input) { var result null; Java.perform(function () { var MD5Utils Java.use(com.example.demo.MD5Utils); result MD5Utils.getMD5(input); }); return result; } };# Python端 md5_hash script.exports_sync.compute_md5(test) print(fMD5 via RPC: {md5_hash})6. 常见问题、排查技巧与安全考量在实际操作中你肯定会遇到各种问题。下面是我踩过坑后总结的一些排查清单问题现象可能原因排查步骤与解决方案frida-ps -U无输出或报错1. Frida-server未运行或版本不匹配。2. 设备未Root或ADB未以root权限运行。3. 端口冲突或被防火墙拦截。1. 检查server进程adb shell ps | grep frida。2. 确认设备已Rootadb root后重试。3. 重启adbadb kill-server adb start-server。脚本注入失败提示TypeError: cannot read property implementation of undefined1. 目标类不存在类名错误或未加载。2. 目标方法不存在方法名或签名错误。1. 使用Java.enumerateLoadedClasses()确认类是否已加载。2. 使用Java.use(className).class.getDeclaredMethods()枚举类的方法核对签名。Hook后APP闪退1. Hook的函数内部逻辑有误如递归调用。2. 修改了返回值或参数类型不匹配。3. Native Hook时参数解析错误。1. 确保在implementation内用this.原方法名调用原函数。2. 检查返回类型确保与原函数一致。3. 对于Native Hook仔细核对JNI函数签名和参数索引。看不到任何Hook日志1. Hook的类/方法从未被调用。2. 脚本加载时机太晚错过了调用。3.console.log输出被缓冲或重定向。1. 确认触发路径。在Java.perform开头加日志确认脚本已加载。2. 使用spawn方式确保尽早注入。3. 在Python的on_message回调中确保打印了消息。性能急剧下降或卡死1. Hook了非常频繁调用的函数如日志函数。2. 在Hook函数中执行了复杂耗时的操作。1. 避免Hook高频函数或在其内部实现高效的过滤逻辑。2. 将复杂操作如网络请求移到Hook函数外部或使用异步方式。安全与合规考量 最后必须强调Frida是一个强大的安全研究工具务必在法律允许和道德规范的范围内使用。仅将其用于分析自己开发的应用进行安全审计。在拥有明确授权的范围内对第三方应用进行安全评估。学习移动安全技术和逆向工程知识。切勿将其用于破解商业软件、侵犯他人隐私、制作外挂等非法用途。技术本身无善恶但使用技术的人需要为自己的行为负责。在实际工作中许多企业也会使用类似技术进行自家的APP加固测试这属于正当的攻防演练范畴。