文章总结: 本文介绍了使用frida工具hookAndroid应用中Java底层加密类的方法,重点演示了如何hookCipher类捕获AES加密的密钥和IV参数,以及如何hookMessageDigest类获取MD5签名生成过程。作者通过这种方法成功破解了某应用的加密通信和签名验证机制,发现签名是由Noncestr、Timestamp和固定密钥拼接后进行MD5计算得到的。文章提供了详细的操作步骤和frida脚本代码,展示了Android逆向工程中破解加密签名的实用技术。
综合评分: 89
文章分类: 逆向分析,移动安全,二进制安全,漏洞分析
【Android逆向】逆向加密签名之frida hook java底层类Cipher和MessageDigest
原创
挖个洞先
挖个洞先
2025年9月26日 09:59
北京
“ 世界上所有的不利因素,都是因为当事人的能力不足而导致的。——《东京喰种》S1E12 ”
01
—
环境版本
环境:
电脑,Windows 11 专业版 23H2
https://github.com/JiaoSuInfoSec/JiaoSuInfoSec_T00ls_Win11
软件:
mumu模拟器,5.3.4
https://mumu.163.com/
burpsuite,v2025.1
https://portswigger.net/burp/releases
charles,5.0.2
https://www.charlesproxy.com/download/
brook,v20230606.5
https://github.com/txthinking/brook
frida,16.1.8
https://github.com/frida/frida/releases/download/16.1.8/frida-server-16.1.8-android-x86_64.xz
02
—
操作步骤
1、使用adb推送frida-server到mumu模拟器中的tmp目录
adb push .\frida-server-16.1.8-android-x86_64 /data/local/tmp
2、进入shell,fridaserver给权限,启动
adb shellsucd /data/local/tmplschmod 777 frida-server-16.1.8-android-x86_64./frida-server-16.1.8-android-x86_64
3、查看运行的软件,最新版本1.1.6
frida-ps -Ua
4、电脑端安装burpsuite+Charles,移动端安装brook,转发socket到burpsuite实现抓包
安卓APP抓包解决方案(教程)
https://xz.aliyun.com/news/11263
5、转换burpsuite证书格式,推送到系统证书目录
adb push .\9bc4b260.0 /data/local/tmpadb shellmount -o remount,rw /systemmv /data/local/tmp/9bc4b260.0 /system/etc/security/cacerts/
6、浏览器访问百度测试
7、测试目标软件发现存在请求响应加密,请求header存在sign签名
8、响应加密
9、使用frida hook对称/非对称Java底层加密类Cipher,保存输出到1.txt
frida -U -f com.xxx -l password.js -o 1.txt
Java.perform(function() { console.log("[*] 启动加密监控脚本"); // --- 1. 获取所需Java类的句柄 --- var Cipher = Java.use('javax.crypto.Cipher'); var IvParameterSpec = Java.use('javax.crypto.spec.IvParameterSpec'); var Base64 = Java.use('android.util.Base64'); var StandardCharsets = Java.use('java.nio.charset.StandardCharsets'); var StringClass = Java.use('java.lang.String'); var Arrays = Java.use('java.util.Arrays'); // --- 全局变量,用于存储上下文信息 --- var cipherContexts = {}; // ============================================================================================== // 第一部分: 辅助函数 // ============================================================================================== /** * 将Java的 byte[] 字节数组转换为人眼可读的十六进制字符串。 */ function bytesToHex(arr) { if (!arr) return "[null]"; var hex = ''; for (var i = 0; i < arr.length; i++) { var n = arr[i]; if (n < 0) n += 256; var h = n.toString(16); if (h.length === 1) h = '0' + h; hex += h; } return hex; } /** * 打印当前的Java调用栈。 */ function printStackTrace() { try { var stack = Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Throwable').$new()); console.log(" |> 调用栈:\n" + stack.split('\n').slice(2).join('\n')); } catch (e) { console.log(" |> 调用栈: <获取失败>"); } } // ============================================================================================== // 第二部分: Hook Cipher.init() // ============================================================================================== var initOverloads = Cipher.init.overloads; initOverloads.forEach(function(overload) { overload.implementation = function() { try { var opmode = arguments[0], key = arguments[1], params = (arguments.length > 2) ? arguments[2] : null; var cipherId = this.hashCode(); var opmodeStr = (opmode == 1) ? "ENCRYPT_MODE (加密)" : (opmode == 2) ? "DECRYPT_MODE (解密)" : "UNKNOWN"; console.log("\n[+] Cipher.init() 被调用! [实例ID: " + cipherId + "]"); console.log(" |> 操作模式: " + opmodeStr); var keyBytes = key.getEncoded(); var keyHex = bytesToHex(keyBytes); console.log(" |> 密钥 (Hex): " + keyHex); var ivHex = null; if (params) { try { var ivBytes = Java.cast(params, IvParameterSpec).getIV(); ivHex = bytesToHex(ivBytes); console.log(" |> IV (Hex): " + ivHex); } catch (e) { console.log(" |> IV: 存在但无法解析为IvParameterSpec."); } } else { console.log(" |> IV: null"); } cipherContexts[cipherId] = { opmode: opmodeStr, algorithm: this.getAlgorithm(), key: keyHex, iv: ivHex }; printStackTrace(); } catch (e) { console.log("[!] Cipher.init hook 发生错误: " + e); } return overload.apply(this, arguments); }; }); // ============================================================================================== // 第三部分: Hook Cipher.doFinal() // ============================================================================================== /** * 这是一个统一的日志记录函数,用于打印 doFinal 的输入和输出。 * @param {object} context - 从cipherContexts中获取的上下文信息。 * @param {byte[]} inputData - doFinal的输入数据。 * @param {byte[]} outputData - doFinal的输出数据。 */ function logDoFinalData(context, inputData, outputData) { if (context.opmode.includes("ENCRYPT")) { console.log("\n[+] Cipher.doFinal() 操作完成! (加密) [实例ID: " + context.cipherId + "]"); console.log(" |> 算法: " + context.algorithm); console.log(" |> 输入明文 (Hex): " + bytesToHex(inputData)); if (inputData) { try { console.log(" |> 输入明文 (String): " + StringClass.$new(inputData, StandardCharsets.UTF_8)); } catch(e){} } if (outputData) { var b64Result = Base64.encodeToString(outputData, 2); console.log(" |> 输出密文 (Hex): " + bytesToHex(outputData)); console.log(" |> 输出密文 (Base64): " + b64Result); } else { console.log(" |> 输出密文: null"); } } else { // 解密模式 console.log("\n[+] Cipher.doFinal() 操作完成! (解密) [实例ID: " + context.cipherId + "]"); console.log(" |> 算法: " + context.algorithm); console.log(" |> 输入密文 (Hex): " + bytesToHex(inputData)); if (outputData) { console.log(" |> 输出明文 (Hex): " + bytesToHex(outputData)); try { console.log(" |> 输出明文 (String): " + StringClass.$new(outputData, StandardCharsets.UTF_8)); } catch(e){} } else { console.log(" |> 输出明文: null"); } } } // --- 专门Hook那些返回 byte[] 的 doFinal 版本 --- ['doFinal', 'doFinal', 'doFinal'].forEach(function(methodName, index) { var overload; if (index === 0) overload = Cipher[methodName].overload(); // doFinal() else if (index === 1) overload = Cipher[methodName].overload('[B'); // doFinal(byte[]) else if (index === 2) overload = Cipher[methodName].overload('[B', 'int', 'int'); // doFinal(byte[], int, int) if (overload) { overload.implementation = function() { var cipherId = this.hashCode(); var context = cipherContexts[cipherId]; context.cipherId = cipherId; var inputData = null; if (arguments.length > 0) inputData = arguments[0]; var result = overload.apply(this, arguments); // result 是 byte[] logDoFinalData(context, inputData, result); return result; }; } }); // --- 专门Hook那些返回 int 的 doFinal 版本 --- ['doFinal', 'doFinal'].forEach(function(methodName, index) { var overload; if (index === 0) overload = Cipher[methodName].overload('[B', 'int', 'int', '[B'); // doFinal(byte[], int, int, byte[]) else if (index === 1) overload = Cipher[methodName].overload('[B', 'int', 'int', '[B', 'int'); // doFinal(byte[], int, int, byte[], int) if (overload) { overload.implementation = function() { var cipherId = this.hashCode(); var context = cipherContexts[cipherId]; context.cipherId = cipherId; var inputData = arguments[0]; var outputBuffer; // 在返回int的重载中,输出缓冲区的位置是固定的 if(arguments.length === 4) outputBuffer = arguments[3]; if(arguments.length === 5) outputBuffer = arguments[3]; var bytesWritten = overload.apply(this, arguments); // result 是 int // 从输出缓冲区中截取实际写入的数据 var outputData = Arrays.copyOfRange(outputBuffer, 0, bytesWritten); logDoFinalData(context, inputData, outputData); return bytesWritten; }; } }); console.log("[*] 加密监控脚本已激活。");});
10、再次登录,将抓包到的password值到1.txt中搜索得到AES的key和iv
11、解密请求成功
12、解密响应成功
13、使用frida hook哈希Java底层加密类MessageDigest,保存输出到2.txt
frida -U -f com.xxx -l md5.js -o 2.txt
Java.perform(function() { // 辅助函数:将 byte 数组转换为 Hex 字符串 function bytesToHex(bytes) { var hex = []; for (var i = 0; i < bytes.length; i++) { var val = bytes[i] & 0xFF; if (val < 16) { hex.push("0"); } hex.push(val.toString(16)); } return hex.join(""); } // 辅助函数:将 byte 数组转换为可读字符串 (UTF-8) function bytesToString(bytes) { var str = ""; try { str = new java.lang.String(bytes, "UTF-8"); } catch (e) { // 如果转换失败,则返回原始字节信息 return "[非UTF-8编码] " + bytes.toString(); } return str; } var MessageDigest = Java.use('java.security.MessageDigest'); // Hook MessageDigest.getInstance 方法,确认算法类型 MessageDigest.getInstance.overload('java.lang.String').implementation = function(algorithm) { console.log("\n[+] MessageDigest.getInstance('" + algorithm + "') 被调用"); // 打印调用堆栈,帮助定位代码位置 console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); return this.getInstance(algorithm); }; // Hook update 方法,捕获输入数据 MessageDigest.update.overload('[B').implementation = function(input) { // 筛选出 MD5 实例的 update 调用 if (this.getAlgorithm() === "MD5") { console.log("\n[+] MD5.update() 被调用"); console.log(" [+] 输入数据 (String): " + bytesToString(input)); console.log(" [+] 输入数据 (Hex) : " + bytesToHex(input)); } return this.update(input); }; // Hook digest 方法,捕获最终的哈希结果 MessageDigest.digest.overload().implementation = function() { // 筛选出 MD5 实例的 digest 调用 if (this.getAlgorithm() === "MD5") { console.log("\n[!] MD5.digest() 被调用,计算完成"); var result = this.digest(); // 调用原始方法获取结果 var hexResult = bytesToHex(result); console.log(" [!] MD5 哈希结果 (Hex): " + hexResult); // 如果能找到这个 sign,说明我们成功捕获了正确的加密过程 if (hexResult === 'd41d8cd98f00b204e9800998ecf8427e') { console.warn(" [!] 警告:捕获到的 sign 是空字符串的MD5,请检查应用逻辑。"); } else { console.log(" [*] 这个值可能就是你要找的 sign!"); } console.log("----------------------------------------------------"); return result; } // 对于非 MD5 算法,正常返回 return this.digest(); };});
14、再次登录,将抓包到的sign值到1.txt中搜索得到十六进制的输入数据
15、解码十六进制得到字符串
16、分析字符串发现签名生成逻辑为拼接Noncestr+Timestamp+固定密钥然后MD5
Noncestr: huxxxTimestamp: 1758785723固定密钥:137xxx
17、再次登录,根据请求手动计算,还原sign成功
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:挖个洞先 挖个洞先《【Android逆向】逆向加密签名之frida hook java底层类Cipher和MessageDigest》