文章目录
- [1 前言](#1 前言)
- [2 非标准加密源码解析](#2 非标准加密源码解析)
-
- [2.1 代码作用分析](#2.1 代码作用分析)
- [3 未混淆APK的Hook流程](#3 未混淆APK的Hook流程)
-
- [3.1 定位目标方法](#3.1 定位目标方法)
- [3.2 编写Hook脚本](#3.2 编写Hook脚本)
- [3.3 脚本执行效果](#3.3 脚本执行效果)
- [4 混淆APK的Hook流程](#4 混淆APK的Hook流程)
-
- [4.1 混淆带来的问题](#4.1 混淆带来的问题)
- [4.2 第一步:通过系统方法定位加密逻辑](#4.2 第一步:通过系统方法定位加密逻辑)
-
- [4.2.1 定位脚本](#4.2.1 定位脚本)
- [4.2.2 分析定位结果](#4.2.2 分析定位结果)
- [4.3 第二步:验证疑似方法](#4.3 第二步:验证疑似方法)
-
- [4.3.1 测试脚本](#4.3.1 测试脚本)
- [4.3.2 验证结果](#4.3.2 验证结果)
- [4.4 第三步:补全加密方法Hook](#4.4 第三步:补全加密方法Hook)
-
- [4.4.1 完整Hook脚本](#4.4.1 完整Hook脚本)
- [4.4.2 最终效果](#4.4.2 最终效果)
- [5 本章总结](#5 本章总结)
-
- [5.1 未混淆场景Hook要点](#5.1 未混淆场景Hook要点)
- [5.2 混淆场景Hook要点](#5.2 混淆场景Hook要点)
⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。
1 前言
在现代企业应用开发中,加密技术是保障数据安全的核心手段。除了AES、RSA等广泛认可的标准加密算法外,许多企业会针对特殊业务场景设计并实现非标准加密算法------这些算法可能是基于异或、位移、自定义置换等基础操作的组合,也可能是对标准算法的魔改(如修改密钥生成规则、调整分组模式等)。
学习对非标准算法进行Hook的必要性在于:这类算法没有统一规范,逆向分析时无法像前几个章节通过"识别标准算法特征"快速破解,必须通过动态Hook捕获输入输出、密钥等关键信息。
掌握非标准算法的Hook技巧,能帮助开发者在安全测试、漏洞分析、兼容性验证等场景中高效定位加密逻辑,是逆向工程与应用调试的核心技能之一。
本章节使用的示例 APK、相关源码如下:
链接: https://pan.baidu.com/s/1bu9TqgfT5GCisXJ3lA2EWQ?pwd=ti1e
提取码: ti1e
2 非标准加密源码解析
以下是示例 APK 中的非标准加密工具类实现,包含加密(encrypt)与解密(decrypt)两个核心方法:
kotlin
package com.example.fridaapk
import android.util.Base64
object CustomEncryptionUtils {
private const val DEFAULT_KEY = "FridaCustomKey"
fun encrypt(plaintext: String, key: String = DEFAULT_KEY): String {
// 将明文和密钥转换为UTF-8字节数组
val plainBytes = plaintext.toByteArray(Charsets.UTF_8)
val keyBytes = key.toByteArray(Charsets.UTF_8)
val encryptedBytes = ByteArray(plainBytes.size)
for (i in plainBytes.indices) {
// 密钥循环复用(取模运算实现密钥字节重复)
val keyByte = keyBytes[i % keyBytes.size]
// 异或操作:明文字节与密钥字节异或
var encryptedByte = plainBytes[i].toInt() xor keyByte.toInt()
// 左移1位+右移7位(循环位移)增加复杂度
encryptedByte = (encryptedByte shl 1) or (encryptedByte shr 7)
// 确保结果在byte范围内(0-255)
encryptedBytes[i] = (encryptedByte and 0xFF).toByte()
}
// 加密结果通过Base64编码为字符串
return Base64.encodeToString(encryptedBytes, Base64.NO_WRAP)
}
fun decrypt(encryptedData: String, key: String = DEFAULT_KEY): String {
// 将Base64编码的密文解密为字节数组
val encryptedBytes = Base64.decode(encryptedData, Base64.NO_WRAP)
val keyBytes = key.toByteArray(Charsets.UTF_8)
val decryptedBytes = ByteArray(encryptedBytes.size)
for (i in encryptedBytes.indices) {
// 密钥循环复用(同加密逻辑)
val keyByte = keyBytes[i % keyBytes.size]
// 逆向位移:先还原循环位移操作
var decryptedByte = encryptedBytes[i].toInt() and 0xFF
decryptedByte = (decryptedByte shr 1) or (decryptedByte shl 7)
// 逆向异或:用密钥字节还原明文
decryptedByte = decryptedByte xor keyByte.toInt()
decryptedBytes[i] = (decryptedByte and 0xFF).toByte()
}
// 将字节数组转换为UTF-8字符串(明文)
return String(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的情况)。
javascript
import Java from "frida-java-bridge";
Java.perform(() => {
// 获取目标类的引用(类名与源码一致)
const CustomEncryptionUtils = 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);
// 调用原方法获取加密结果
const ciphertext = this.encrypt(plaintext, key);
console.log("[输出] 密文 (ciphertext):", ciphertext);
console.log("============================================\n");
return ciphertext; // 返回原方法结果,不影响程序运行
};
// 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);
// 调用原方法获取解密结果
const plaintext = this.decrypt(encryptedData, key);
console.log("[输出] 明文 (plaintext):", plaintext);
console.log("============================================\n");
return plaintext;
};
});
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 定位脚本
该脚本用于探测可能存在加密的地方。
javascript
import Java from "frida-java-bridge";
// 打印调用栈的工具函数(用于追踪方法调用链)
function showStacks() {
var Exception = Java.use("java.lang.Exception");
var ins = Exception.$new("Exception"); // 通过异常对象获取调用栈
var straces = ins.getStackTrace();
if (undefined == straces || null == straces) {
return;
}
console.log("============================= Stack start=======================");
console.log("");
for (var i = 0; i < straces.length; i++) {
var str = " " + straces[i].toString();
console.log(str);
}
console.log("");
console.log("============================= Stack end=======================\r\n");
Exception.$dispose(); // 释放对象,避免内存泄漏
}
Java.perform(() => {
// Hook String类的getBytes方法
var str = Java.use("java.lang.String");
str.getBytes.overload()
.implementation = function () {
var result = this.getBytes(); // 调用原方法
var newStr = str.$new(result); // 将字节数组转回字符串,便于查看内容
console.log("str.getBytes result: ", newStr); // 打印转换后的字符串
showStacks(); // 打印调用栈
return result;
}
// Hook String类的getBytes方法
str.getBytes.overload('java.lang.String')
.implementation = function (a) {
var result = this.getBytes(a);
var newStr = str.$new(result, a);
console.log("str.getBytes result: ", newStr);
showStacks();
return result;
}
});
4.2.2 分析定位结果
运行脚本后,操作应用中涉及加密/解密的功能(如点击"加密"或"解密"按钮),会输出如下日志:
shell
# 启动时打印的日志,忽略
str.getBytes result: AAAAAAAgAgwgLA==
============================= Stack start=======================
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)
============================= Stack end=======================
关键信息解读:
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 测试脚本
javascript
import Java from "frida-java-bridge";
Java.perform(() => {
try {
// 定位疑似解密方法所在的类
const DecryptClass = 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);
// 调用原方法获取结果
const result = this.a(encryptedData);
console.log("[输出] 明文:", result);
return result;
};
} catch (error) {
console.error("Hook执行出错:", error.message); // 捕获类或方法不存在的错误
}
});
4.3.2 验证结果
运行脚本后,操作应用的非标准算法区域按钮,输出如下日志,说明z2.c.a确实是解密方法:

4.4 第三步:补全加密方法Hook
观察上述反编译代码,继续可定位到加密方法(z2.c.b),最终编写完整脚本。
4.4.1 完整Hook脚本
javascript
import Java from "frida-java-bridge";
Java.perform(() => {
try {
// 定位加密/解密所在的类(z2.c)
const CustClass = Java.use("z2.c");
// Hook解密方法 z2.c.a
CustClass.a.overload("java.lang.String").implementation = function (encryptedData) {
console.log("Hook到解密方法 z2.c.a:");
console.log("[输入] 密文:", encryptedData);
const result = this.a(encryptedData);
console.log("[输出] 明文:", result);
return result;
};
// Hook加密方法 z2.c.b
CustClass.b.implementation = function () {
console.log("Hook到加密方法 z2.c.b:");
// 获取明文(根据业务场景确定,此处为示例值)
const plaintext = "FridaStudy";
console.log("[输入] 明文:", plaintext);
const ciphertext = this.b();
console.log("[输出] 密文:", ciphertext);
return ciphertext;
};
} 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,获取完整数据;
- 优势:不受混淆影响,通用性强,适合复杂场景。