文章目录
- [1. 前言](#1. 前言)
- [2. Hook](#2. Hook)
-
- [2.1 核心加密源码分析](#2.1 核心加密源码分析)
- [2.2 Hook 完整脚本](#2.2 Hook 完整脚本)
- [2.3 Hook脚本详细分析](#2.3 Hook脚本详细分析)
-
- [2.3.1 `bytesToString(byteArr)`](#2.3.1
bytesToString(byteArr)) - [2.3.2 `getHexBytes(byteArr)`](#2.3.2
getHexBytes(byteArr)) - [2.3.3 `encodeBase64(data)`](#2.3.3
encodeBase64(data)) - [2.3.4 核心Hook逻辑(`Cipher.doFinal`)](#2.3.4 核心Hook逻辑(
Cipher.doFinal))
- [2.3.1 `bytesToString(byteArr)`](#2.3.1
- [3. 本章总结](#3. 本章总结)
-
- [3.1 重要知识点](#3.1 重要知识点)
- [3.2 Hook思路](#3.2 Hook思路)
⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。
1. 前言
非对称加密通过公钥(公开可访问)与私钥(严格保密)的密钥对实现数据交互,其中RSA作为应用最广泛的非对称算法,在企业场景中承担着核心安全职责,其主要用途包括:
- 密钥交换:安全传输对称加密密钥(如AES密钥通过RSA公钥加密后传输,避免明文泄露);
- 数字签名:通过私钥对数据哈希值签名,接收方用公钥验签,确保数据未被篡改且来源可信(如API接口防伪造、软件安装包合法性验证)。
大厂对RSA的安全规范要求极为严格,具体如下:
- 密钥长度:最低强制2048位(随着算力提升,该标准可能进一步提高);金融、政务等核心敏感场景必须使用4096位(密钥越长破解难度指数级增加,但运算效率略有降低,需在安全与性能间平衡)。
- 数字签名配套算法:单独使用RSA仅能保证机密性,无法防止数据篡改,因此必须结合SHA-256(最低要求)或SHA-512等强哈希算法------流程为"数据哈希→私钥签名→公钥验签+哈希比对",双重保障完整性。
- 密钥管理:私钥必须离线存储(如硬件安全模块HSM),禁止硬编码在代码中;密钥需定期轮换(通常90-180天),避免长期使用导致泄露风险。
- 合规性:金融领域需符合PCI DSS标准,政务领域需满足等保2.0三级及以上要求,确保算法实现符合国家或行业加密标准。
本章节使用的示例 APK、相关源码如下:
链接: https://pan.baidu.com/s/1M2sVSNuvFFz4VmPIRNc8yw?pwd=qnxa
提取码: qnxa
2. Hook
2.1 核心加密源码分析
以下是示例 APK 中 RSA 加密工具类RSAUtils的核心逻辑解析,该类采用企业级RSA-2048方案,适配大文本分段加解密。
- 算法选型 :采用
RSA/ECB/OAEPWithSHA-256AndMGF1Padding,是企业级RSA的"安全标配"------相比基础的PKCS1Padding,OAEP填充抗破解能力更强,结合SHA-256哈希算法,完全符合大厂"强填充+强哈希"的安全基线。 - 密钥规格:强制使用2048位密钥(企业非对称加密最低要求,金融/政务场景可升级至4096位),密钥对动态生成(无硬编码),符合"密钥动态管理"的企业规范。
- 分段加解密:RSA存在明文长度限制(2048位密钥的加密块大小=2048/8 - 42=214字节),因此代码实现了分段加密 / 解密逻辑。
kotlin
package com.example.fridaapk
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import android.util.Base64
import javax.crypto.Cipher
object RSAUtils {
private const val KEY_SIZE_2048 = 2048
private const val TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
fun generateRSA2048KeyPair(): KeyPair {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(KEY_SIZE_2048)
return keyPairGenerator.generateKeyPair()
}
fun encryptRSA2048(plaintext: String, publicKey: PublicKey): String {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
val plainBytes = plaintext.toByteArray(Charsets.UTF_8)
val maxBlockSize = getMaxEncryptBlockSize()
val encryptedData = mutableListOf<ByteArray>()
var offset = 0
while (offset < plainBytes.size) {
val blockSize = minOf(maxBlockSize, plainBytes.size - offset)
val block = plainBytes.copyOfRange(offset, offset + blockSize)
val encryptedBlock = cipher.doFinal(block)
encryptedData.add(encryptedBlock)
offset += blockSize
}
val result = encryptedData.flatten()
return Base64.encodeToString(result, Base64.NO_WRAP)
}
fun decryptRSA2048(encryptedData: String, privateKey: PrivateKey): String {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, privateKey)
val encryptedBytes = Base64.decode(encryptedData, Base64.NO_WRAP)
val maxDecryptBlockSize = getMaxDecryptBlockSize()
val decryptedData = mutableListOf<ByteArray>()
var offset = 0
while (offset < encryptedBytes.size) {
val blockSize = minOf(maxDecryptBlockSize, encryptedBytes.size - offset)
val block = encryptedBytes.copyOfRange(offset, offset + blockSize)
val decryptedBlock = cipher.doFinal(block)
decryptedData.add(decryptedBlock)
offset += blockSize
}
val result = decryptedData.flatten()
return String(result, Charsets.UTF_8)
}
private fun getMaxEncryptBlockSize(): Int {
return (KEY_SIZE_2048 / 8) - 42
}
private fun getMaxDecryptBlockSize(): Int {
return KEY_SIZE_2048 / 8
}
private fun List<ByteArray>.flatten(): ByteArray {
val totalSize = this.sumOf { it.size }
val result = ByteArray(totalSize)
var offset = 0
for (array in this) {
System.arraycopy(array, 0, result, offset, array.size)
offset += array.size
}
return result
}
}
2.2 Hook 完整脚本
javascript
import Java from "frida-java-bridge";
// 字节数组转明文
function bytesToString(byteArr) {
if (!byteArr || byteArr.length === 0) return "空";
try {
const str = Java.use("java.lang.String").$new(byteArr, "UTF-8");
return str.toString();
} catch (e) {
return getHexBytes(byteArr);
}
}
// 字节数组转16进制
function getHexBytes(byteArr) {
if (!byteArr || byteArr.length === 0) return "空";
let hex = "";
for (let i = 0; i < byteArr.length; i++) {
const b = byteArr[i] & 0xFF;
hex += (b < 16 ? "0" : "") + b.toString(16);
}
return hex;
}
// Base64编码(兼容android.util.Base64和java.util.Base64)
function encodeBase64(data) {
try {
return Java.use("android.util.Base64").encodeToString(data, 2);
} catch (e) {
try {
const base64Encoder = Java.use("java.util.Base64").getEncoder();
return base64Encoder.encodeToString(data);
} catch (e2) {
return "<Base64编码失败>";
}
}
}
try {
Java.perform(() => {
const Cipher = Java.use("javax.crypto.Cipher");
// Hook doFinal 方法
Cipher.doFinal.overload('[B').implementation = function (input) {
const result = this.doFinal(input);
try {
const algorithm = this.getAlgorithm();
const mode = this.opmode.value;
console.log(`\n=== 操作详情 ===`);
console.log(`[加密/解密] 算法名称: ${algorithm}`);
if (mode == 1) { // 加密模式
console.log(`[加密] 明文: ${bytesToString(input)}`);
console.log(`[加密] 密文(Base64): ${encodeBase64(result)}`);
} else if (mode == 2) { // 解密模式
console.log(`[解密] 密文(Base64): ${encodeBase64(input)}`);
console.log(`[解密] 明文: ${bytesToString(result)}`);
}
} catch (e) {
console.log(`[Cipher] 解析数据出错: ${e.message}`);
}
return result;
};
});
} catch (e) {
console.log(`[Frida] 脚本执行异常: ${e.message}`);
}
2.3 Hook脚本详细分析
脚本基于Frida框架,通过Hookjavax.crypto.Cipher类的doFinal方法捕获加密解密过程,核心方法及功能如下:
2.3.1 bytesToString(byteArr)
- 功能:将字节数组转换为可读字符串,优先尝试UTF-8编码(适用于文本类明文/解密结果);若转换失败(如密文字节非文本格式),则调用
getHexBytes转为16进制字符串。 - 作用:解决字节数组直接打印不可读的问题,方便直观查看明文或解析异常数据。
javascript
function bytesToString(byteArr) {
if (!byteArr || byteArr.length === 0) return "空";
try {
const str = Java.use("java.lang.String").$new(byteArr, "UTF-8");
return str.toString();
} catch (e) {
return getHexBytes(byteArr);
}
}
2.3.2 getHexBytes(byteArr)
- 功能:将字节数组转换为16进制字符串(如
[0x1F, 0x2A]转为"1f2a")。 - 作用:处理非文本类字节数据(如密文、二进制协议数据),确保字节信息可完整展示(16进制是字节的通用可读格式)。
javascript
function getHexBytes(byteArr) {
if (!byteArr || byteArr.length === 0) return "空";
let hex = "";
for (let i = 0; i < byteArr.length; i++) {
const b = byteArr[i] & 0xFF;
hex += (b < 16 ? "0" : "") + b.toString(16);
}
return hex;
}
2.3.3 encodeBase64(data)
- 功能:将字节数组转为Base64字符串,兼容Android(
android.util.Base64)和Java(java.util.Base64)两种实现。 - 作用:密文通常以Base64格式传输或存储,该方法确保hook中捕获的密文字节能正确转换为业务中实际使用的Base64格式。
javascript
function encodeBase64(data) {
try {
return Java.use("android.util.Base64").encodeToString(data, 2); // 2对应NO_WRAP模式
} catch (e) {
try {
const base64Encoder = Java.use("java.util.Base64").getEncoder();
return base64Encoder.encodeToString(data);
} catch (e2) {
return "<Base64编码失败>";
}
}
}
2.3.4 核心Hook逻辑(Cipher.doFinal)
- 目标:Hook
Cipher类的doFinal(byte[])方法(加密/解密的最终执行步骤)。 - 流程:
- 调用原方法获取结果(
this.doFinal(input)); - 通过
getAlgorithm()获取算法(如AES/GCM/NoPadding),通过opmode判断模式(1为加密,2为解密); - 针对加密模式:打印明文(
input)和Base64密文(result); - 针对解密模式:打印Base64密文(
input)和明文(result)。
- 调用原方法获取结果(
javascript
Java.perform(() => {
const Cipher = Java.use("javax.crypto.Cipher");
Cipher.doFinal.overload('[B').implementation = function (input) {
const result = this.doFinal(input);
try {
const algorithm = this.getAlgorithm();
const mode = this.opmode.value;
console.log(`\n=== 操作详情 ===`);
console.log(`[加密/解密] 算法名称: ${algorithm}`);
if (mode == 1) {
console.log(`[加密] 明文: ${bytesToString(input)}`);
console.log(`[加密] 密文(Base64): ${encodeBase64(result)}`);
} else if (mode == 2) {
console.log(`[解密] 密文(Base64): ${encodeBase64(input)}`);
console.log(`[解密] 明文: ${bytesToString(result)}`);
}
} catch (e) {
console.log(`[Cipher] 解析数据出错: ${e.message}`);
}
return result;
};
});
RSA脚本无需Hook Base64兜底的原因(对比AES)
与上一篇教程的AES的Hook需额外Hook Base64不同,本篇RSA无需兜底,核心差异在于数据结构和处理流程:
-
数据结构复杂度不同
- AES加密后的数据是复合结构 (如GCM模式中需拼接IV和密文,IV是解密必需的元数据),通常需要Base64编码后传输,此时Base64处理的是"IV+密文"的组合数据,若仅Hook
doFinal可能遗漏完整信息。 - RSA加密输出是单一密文块(无额外元数据),加密结果本身就是可直接使用的完整数据,无需拼接其他信息。
- AES加密后的数据是复合结构 (如GCM模式中需拼接IV和密文,IV是解密必需的元数据),通常需要Base64编码后传输,此时Base64处理的是"IV+密文"的组合数据,若仅Hook
-
Base64编码的作用不同
- AES中Base64编码是为了处理"二进制+元数据"的复合结构(确保传输兼容性),编码对象是组合数据,可能在
doFinal之后单独调用Base64方法,因此需要Hook Base64兜底。 - RSA中Base64仅用于单一密文的"二进制→文本"转换(方便传输),编码对象就是
doFinal的输出结果,通过HookdoFinal已能捕获完整密文,无需额外拦截Base64。
- AES中Base64编码是为了处理"二进制+元数据"的复合结构(确保传输兼容性),编码对象是组合数据,可能在
-
操作流程的直接性
- AES的加密流程通常是"加密(
doFinal)→拼接IV和密文→Base64编码",多步骤处理可能导致doFinal的输出不是最终传输的密文。 - RSA的加密流程通常是"加密(
doFinal)→Base64编码",doFinal的输出是核心密文,Base64仅为格式转换,不影响数据完整性,因此doFinal足以捕获关键信息。
- AES的加密流程通常是"加密(
启动 Hook 脚本后操作应用的非对称加密区域,执行效果如下图:

3. 本章总结
3.1 重要知识点
- RSA安全规范:密钥长度最低2048位(核心场景4096位),必须结合SHA-256及以上哈希算法实现数字签名,私钥需安全存储并定期轮换。
- AES-GCM实现细节:采用256位密钥,依赖12字节IV(随机生成)和128位Tag(完整性校验),加密后需拼接IV和密文再Base64编码。
- Frida Hook核心 :通过拦截
javax.crypto.Cipher的doFinal方法,可捕获加密/解密的输入(明文/密文)和输出(密文/明文),结合数据转换工具(字节→字符串/16进制/Base64)实现过程可视化。
3.2 Hook思路
- 目标选择 :加密算法的核心处理方法(如
Cipher.doFinal)是Hook的关键,因其是加密/解密的最终执行步骤,能直接获取原始输入输出。 - 数据解析 :通过工具方法(
bytesToString、getHexBytes、encodeBase64)处理字节数组,将不可读的二进制数据转换为可读格式(文本、16进制、Base64)。 - 模式区分 :利用
Cipher的opmode属性区分加密(1)和解密(2)模式,分别打印对应的数据流向(明文→密文或密文→明文)。 - 场景适配 :针对不同加密算法(AES/RSA)的特点调整Hook策略------AES需考虑复合数据结构可能需Hook Base64,RSA因数据结构简单可直接通过
doFinal捕获完整信息。