文章目录
- 1 前言
- 2 非标准加密源码解析
- 2.1 代码作用分析
- 3 未混淆APK的Hook流程
- 3.1 定位目标方法
- 3.2 编写Hook脚本
- 3.3 脚本执行效果
- 4 混淆APK的Hook流程
- 4.1 混淆带来的问题
- 4.2 第一步:通过系统方法定位加密逻辑
- 4.2.1 定位脚本
- 4.2.2 分析定位结果
- 4.3 第二步:验证疑似方法
- 4.3.1 测试脚本
- 4.3.2 验证结果
- 4.4 第三步:补全加密方法Hook
- 4.4.1 完整Hook脚本
- 4.4.2 最终效果
- 5 本章总结
- 5.1 未混淆场景Hook要点
- 5.2 混淆场景Hook要点
⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。
1 前言
在现代企业应用开发中,加密技术是保障数据安全的核心手段。除了AES、RSA等广泛认可的标准加密算法外,许多企业会针对特殊业务场景设计并实现非标准加密算法——这些算法可能是基于异或、位移、自定义置换等基础操作的组合,也可能是对标准算法的魔改(如修改密钥生成规则、调整分组模式等)。
学习对非标准算法进行Hook的必要性在于:这类算法没有统一规范,逆向分析时无法像前几个章节通过"识别标准算法特征"快速破解,必须通过动态Hook捕获输入输出、密钥等关键信息。
掌握非标准算法的Hook技巧,能帮助开发者在安全测试、漏洞分析、兼容性验证等场景中高效定位加密逻辑,是逆向工程与应用调试的核心技能之一。
本章节使用的示例 APK、相关源码如下:
链接: https://pan.baidu.com/s/1bu9TqgfT5GCisXJ3lA2EWQ?pwd=ti1e
提取码: ti1e
2 非标准加密源码解析
以下是示例 APK 中的非标准加密工具类实现,包含加密(encrypt)与解密(decrypt)两个核心方法:
packagecom.example.fridaapkimportandroid.util.Base64objectCustomEncryptionUtils{privateconstvalDEFAULT_KEY="FridaCustomKey"funencrypt(plaintext:String,key:String=DEFAULT_KEY):String{// 将明文和密钥转换为UTF-8字节数组valplainBytes=plaintext.toByteArray(Charsets.UTF_8)valkeyBytes=key.toByteArray(Charsets.UTF_8)valencryptedBytes=ByteArray(plainBytes.size)for(iinplainBytes.indices){// 密钥循环复用(取模运算实现密钥字节重复)valkeyByte=keyBytes[i%keyBytes.size]// 异或操作:明文字节与密钥字节异或varencryptedByte=plainBytes[i].toInt()xorkeyByte.toInt()// 左移1位+右移7位(循环位移)增加复杂度encryptedByte=(encryptedByteshl1)or(encryptedByteshr7)// 确保结果在byte范围内(0-255)encryptedBytes[i]=(encryptedByteand0xFF).toByte()}// 加密结果通过Base64编码为字符串returnBase64.encodeToString(encryptedBytes,Base64.NO_WRAP)}fundecrypt(encryptedData:String,key:String=DEFAULT_KEY):String{// 将Base64编码的密文解密为字节数组valencryptedBytes=Base64.decode(encryptedData,Base64.NO_WRAP)valkeyBytes=key.toByteArray(Charsets.UTF_8)valdecryptedBytes=ByteArray(encryptedBytes.size)for(iinencryptedBytes.indices){// 密钥循环复用(同加密逻辑)valkeyByte=keyBytes[i%keyBytes.size]// 逆向位移:先还原循环位移操作vardecryptedByte=encryptedBytes[i].toInt()and0xFFdecryptedByte=(decryptedByteshr1)or(decryptedByteshl7)// 逆向异或:用密钥字节还原明文decryptedByte=decryptedBytexorkeyByte.toInt()decryptedBytes[i]=(decryptedByteand0xFF).toByte()}// 将字节数组转换为UTF-8字符串(明文)returnString(decryptedBytes,Charsets.UTF_8)}}2.1 代码作用分析
该工具类的核心逻辑基于"异或+循环位移"的组合:
- 加密过程:明文字节与密钥字节循环异或后,进行左移1位+右移7位的循环位移操作,最终通过Base64编码输出;
- 解密过程:先对Base64密文解码,再逆向执行位移操作(右移1位+左移7位),最后通过异或还原明文;
- 密钥机制:支持自定义密钥,默认密钥为"FridaCustomKey",通过取模运算实现密钥字节的循环复用(适合任意长度明文)。
3 未混淆APK的Hook流程
当APK未经过混淆处理时,类名、方法名、参数列表等信息会完整保留,此时可直接通过静态分析定位目标方法并编写Hook脚本。
3.1 定位目标方法
通过反编译工具(如jadx)打开APK,可直接搜索到com.example.fridaapk.CustomEncryptionUtils类,以及其中的encrypt和decrypt方法(方法名与源码一致)。
3.2 编写Hook脚本
核心目标是捕获加密/解密的输入(明文/密文、密钥)和输出(密文/明文),同时兼容默认密钥的调用场景(即调用方法时未传入密钥,使用DEFAULT_KEY的情况)。
importJavafrom"frida-java-bridge";Java.perform(()=>{// 获取目标类的引用(类名与源码一致)constCustomEncryptionUtils=Java.use("com.example.fridaapk.CustomEncryptionUtils");// Hook encrypt方法CustomEncryptionUtils.encrypt.overload('java.lang.String','java.lang.String').implementation=function(plaintext,key){console.log("\n=========== 加密方法(encrypt)调用 ===========");console.log("[输入] 明文 (plaintext):",plaintext);console.log("[输入] 密钥 (key):",key);// 调用原方法获取加密结果constciphertext=this.encrypt(plaintext,key);console.log("[输出] 密文 (ciphertext):",ciphertext);console.log("============================================\n");returnciphertext;// 返回原方法结果,不影响程序运行};// Hook decrypt方法CustomEncryptionUtils.decrypt.overload('java.lang.String','java.lang.String').implementation=function(encryptedData,key){console.log("\n=========== 解密方法(decrypt)调用 ===========");console.log("[输入] 密文 (encryptedData):",encryptedData);console.log("[输入] 密钥 (key):",key);// 调用原方法获取解密结果constplaintext=this.decrypt(encryptedData,key);console.log("[输出] 明文 (plaintext):",plaintext);console.log("============================================\n");returnplaintext;};});3.3 脚本执行效果
运行脚本后,当应用调用加密/解密方法时,会输出完整的输入输出日志,例如:
通过日志可直接获取明文、密文、密钥的对应关系,完成对非标准加密的动态分析。
4 混淆APK的Hook流程
当APK经过ProGuard/R8等工具混淆后,自定义类名、方法名会被替换为无意义的短字母(如a.b、a、b),直接定位目标方法变得困难。此时需要通过"间接定位"技巧逐步缩小范围,最终实现Hook。
4.1 混淆带来的问题
混淆工具会对自定义代码进行如下处理:
- 类名:
com.example.fridaapk.CustomEncryptionUtils→ 任意字母(例如a.b) - 方法名:
encrypt→ 任意字母(例如b)、decrypt→ 任意字母(例如a) - 参数/变量名:全部替换为无意义名称(如
p0、p1)
但系统类(如java.lang.String、android.util.Base64)不会被混淆,这是后续Hook的关键突破口。
4.2 第一步:通过系统方法定位加密逻辑
加密/解密过程必然涉及字符串与字节数组的转换(如String.getBytes()),因此HookString.getBytes()方法可捕获所有字符串的字节转换操作,结合调用栈分析定位加密相关代码。
4.2.1 定位脚本
该脚本用于探测可能存在加密的地方。
importJavafrom"frida-java-bridge";// 打印调用栈的工具函数(用于追踪方法调用链)functionshowStacks(){varException=Java.use("java.lang.Exception");varins=Exception.$new("Exception");// 通过异常对象获取调用栈varstraces=ins.getStackTrace();if(undefined==straces||null==straces){return;}console.log("============================= Stack start=======================");console.log("");for(vari=0;i<straces.length;i++){varstr=" "+straces[i].toString();console.log(str);}console.log("");console.log("============================= Stack end=======================\r\n");Exception.$dispose();// 释放对象,避免内存泄漏}Java.perform(()=>{// Hook String类的getBytes方法varstr=Java.use("java.lang.String");str.getBytes.overload().implementation=function(){varresult=this.getBytes();// 调用原方法varnewStr=str.$new(result);// 将字节数组转回字符串,便于查看内容console.log("str.getBytes result: ",newStr);// 打印转换后的字符串showStacks();// 打印调用栈returnresult;}// Hook String类的getBytes方法str.getBytes.overload('java.lang.String').implementation=function(a){varresult=this.getBytes(a);varnewStr=str.$new(result,a);console.log("str.getBytes result: ",newStr);showStacks();returnresult;}});4.2.2 分析定位结果
运行脚本后,操作应用中涉及加密/解密的功能(如点击"加密"或"解密"按钮),会输出如下日志:
# 启动时打印的日志,忽略str.getBytes result:AAAAAAAgAgwgLA===============================Stackstart=======================java.lang.String.getBytes(Native Method)android.util.Base64.decode(Base64.java:120)# 涉及Base64解码(可能是解密步骤)z2.c.a(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:7)# 自定义方法,紧跟Base64.decode,疑似解密方法z2.i.c(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:28)n.e1.l(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:139)h3.a.m(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:9)w3.w.n(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:83)w3.f.p(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:114)w3.f.D(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:33)w3.f.m(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:17)b1.w.b0(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:51)b1.w.i(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:31)m.k.i(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:105)b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:150)b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:129)b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:129)b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:129)b1.f.h(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:129)a1.d.e(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:36)b1.q.c(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:144)i1.v.E(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:81)i1.v.l(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:385)i1.v.dispatchTouchEvent(r8-map-id-4980774bcd6c4c893027d1435ad3433e3c66659947197c0df3deb2f481251009:76)android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2801)com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:498)com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1900)android.app.Activity.dispatchTouchEvent(Activity.java:4203)com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:456)android.view.View.dispatchPointerEvent(View.java:14858)android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:6468)android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:6269)android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5734)android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5791)android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5757)android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5931)android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5765)android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5988)android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5738)android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5791)android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5757)android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:5765)android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5738)android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:8742)android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:8693)android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:8662)android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:8865)android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:259)android.os.MessageQueue.nativePollOnce(Native Method)android.os.MessageQueue.next(MessageQueue.java:335)android.os.Looper.loopOnce(Looper.java:161)android.os.Looper.loop(Looper.java:288)android.app.ActivityThread.main(ActivityThread.java:8062)java.lang.reflect.Method.invoke(Native Method)com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1091)=============================Stackend=======================关键信息解读:
str.getBytes result: AAAAAAAgAgwgLA==:被转换的字符串是Base64格式,符合加密后的数据特征;- 调用栈中
android.util.Base64.decode后紧跟z2.c.a并且带了r8-map-id-xxx:说明z2.c.a方法可能在调用Base64解码后进行解密处理,并且经过了R8代码混淆,因此z2.c.a是疑似解密方法。
4.3 第二步:验证疑似方法
通过反编译工具找到z2.c类的a方法,观察其逻辑是否与解密流程一致(如包含逆向位移、异或等操作)。确认后编写测试脚本验证。
4.3.1 测试脚本
importJavafrom"frida-java-bridge";Java.perform(()=>{try{// 定位疑似解密方法所在的类constDecryptClass=Java.use("z2.c");// Hook z2.c类的a方法(参数为密文字符串)DecryptClass.a.overload("java.lang.String").implementation=function(encryptedData){console.log("Hook到解密方法 z2.c.a:");console.log("[输入] 密文:",encryptedData);// 调用原方法获取结果constresult=this.a(encryptedData);console.log("[输出] 明文:",result);returnresult;};}catch(error){console.error("Hook执行出错:",error.message);// 捕获类或方法不存在的错误}});4.3.2 验证结果
运行脚本后,操作应用的非标准算法区域按钮,输出如下日志,说明z2.c.a确实是解密方法:
4.4 第三步:补全加密方法Hook
观察上述反编译代码,继续可定位到加密方法(z2.c.b),最终编写完整脚本。
4.4.1 完整Hook脚本
importJavafrom"frida-java-bridge";Java.perform(()=>{try{// 定位加密/解密所在的类(z2.c)constCustClass=Java.use("z2.c");// Hook解密方法 z2.c.aCustClass.a.overload("java.lang.String").implementation=function(encryptedData){console.log("Hook到解密方法 z2.c.a:");console.log("[输入] 密文:",encryptedData);constresult=this.a(encryptedData);console.log("[输出] 明文:",result);returnresult;};// Hook加密方法 z2.c.bCustClass.b.implementation=function(){console.log("Hook到加密方法 z2.c.b:");// 获取明文(根据业务场景确定,此处为示例值)constplaintext="FridaStudy";console.log("[输入] 明文:",plaintext);constciphertext=this.b();console.log("[输出] 密文:",ciphertext);returnciphertext;};}catch(error){console.error("Hook执行出错:",error.message);}});4.4.2 最终效果
运行脚本后,加密与解密的输入输出均被成功捕获:
5 本章总结
5.1 未混淆场景Hook要点
- 核心思路:直接通过类名+方法名定位目标(依赖反编译后可见的代码结构);
- 关键操作:
- 反编译APK,找到加密工具类(如
CustomEncryptionUtils); - 确定加密/解密方法的参数列表(如
encrypt(String, String)); - 编写Hook脚本,打印方法的输入参数(明文/密文、密钥)和返回值;
- 反编译APK,找到加密工具类(如
- 优势:操作简单,定位精准,适合快速验证。
5.2 混淆场景Hook要点
- 核心思路:利用"系统类不混淆"的特性,通过Hook基础方法(如
String.getBytes()、Base64.encode())追踪调用栈,间接定位混淆后的目标方法; - 关键步骤:
- Hook与加密相关的系统方法(字符串转换、Base64编解码等);
- 操作应用触发加密/解密流程,通过日志捕获可疑数据(如Base64字符串);
- 分析调用栈,定位可疑的自定义方法(如
z2.c.a); - 反编译验证方法逻辑,编写测试脚本确认;
- 补全加密/解密方法的Hook,获取完整数据;
- 优势:不受混淆影响,通用性强,适合复杂场景。