Lazy loaded image
Frida学习笔记(十二):加密算法还原(上)· AES、DES、RSA
Words 10629Read Time 27 min
2026-7-10
2026-7-10
type
Post
status
Published
date
Jul 10, 2026
slug
summary
本篇目标:掌握 Android App 中最常见的加密操作的 Hook 方法,能够从运行中的 App 中提取完整的加密参数——算法名称、加密模式、填充方式、密钥、IV(初始化向量)、明文、密文。前面十几篇已经把环境搭建、Java/Native Hook、SSL 绕过这些基本盘打通了,本篇开始进入加密分析这一段。
tags
frida
实用教程
逆向工程
category
公众号
icon
password
网址
上次编辑时间
Jul 10, 2026 06:31 AM
comment
AI 总结
本篇目标:掌握 Android App 中最常见的加密操作的 Hook 方法,能够从运行中的 App 中提取完整的加密参数——算法名称、加密模式、填充方式、密钥、IV(初始化向量)、明文、密文。前面十几篇已经把环境搭建、Java/Native Hook、SSL 绕过这些基本盘打通了,本篇开始进入加密分析这一段。

一、Android 加密体系的全景图

理解 Android 加密体系的结构,才能明白为什么 Hook 几个特定的类就能覆盖绝大多数 App 的加密操作。

1.1 JCE:Java 加密的标准框架

Android 的加密能力建立在 Java 的 JCE(Java Cryptography Extension) 框架之上。JCE 定义了一套标准 API——不管底层用哪个 Provider(BouncyCastle、Conscrypt、自定义实现),上层 Java 代码都通过相同的入口调用。
JCE 框架中与加密直接相关的核心类有四个,各自对应一类加密操作:
| 类 | 职责 | 常见算法 | 在 App 中的典型用途 | | :-- | :-- | :-- | :-- | | javax.crypto.Cipher | 对称/非对称加解密 | AES、DES、3DES、RSA、SM4 | 业务数据加密(最重要的 Hook 目标) | | java.security.MessageDigest | 单向哈希(摘要) | MD5、SHA-1、SHA-256、SHA-512 | 密码校验、数据完整性、缓存键 | | javax.crypto.Mac | 带密钥的哈希(HMAC) | HmacSHA256、HmacSHA1、HmacMD5 | API 请求签名 | | java.security.Signature | 数字签名 | RSA 签名、ECDSA 签名 | 支付、身份认证 |
notion image
JCE 加密体系架构
核心洞察:不管 App 的业务代码怎么封装、怎么混淆,只要它使用 Java 标准库做加密,最终一定会调用这四个类的方法。混淆可以把 EncryptUtils 变成 a.b.caesEncrypt 变成 a,但它改不了 javax.crypto.Cipher——这是 Android 系统库的一部分,不受 App 混淆影响。Hook 这四个类就能覆盖绝大多数纯 Java 层的加密操作,剩下走 Native 层或自实现的部分,见第 1.2 节。

1.2 两类例外:什么时候标准 Hook 不够用

JCE Hook 覆盖不到的情况主要有两类。
第一类是 Native 层加密。App 直接在 C/C++ 代码里调用加密函数,完全绕过 Java 层。这里又分两个子情况:如果调用的是 OpenSSL/BoringSSL 的标准函数(AES_cbc_encryptRSA_public_encryptMD5_UpdateEVP_EncryptInit_ex 等),Hook 思路与 JCE 完全一致——只是把 Java 类方法换成 SO 导出函数,第15篇会专门讲;如果加密被藏在 stripped 的 SO 里、做了加固、或者算法被魔改/自实现,需要先做 Native 层逆向定位与脱壳,这部分留给第21篇(Native Hook 入门)详细讲。
第二类是 纯自定义加密实现。极少数 App 自己用 Java 实现了加密算法(不调用任何标准库),比如自己写了一个 AES 或者使用了查表法加密。这种情况需要通过 jadx 逆向分析搞清楚算法逻辑,无法用通用的 Hook 脚本覆盖。这种做法在实际中非常罕见——自己实现加密算法容易出安全漏洞,绝大多数正规 App 不会这么做。

1.3 一张图看懂「从密文反推算法」的判断路径

抓到一段密文,凭长度特征几乎能猜对算法组合。把这张图刻进脑子里,日后看抓包能省下大半定位时间:
notion image
密文长度决策树:5 种长度特征定位算法 + 3 类组合特征识别混合方案与签名
定位到算法只是第一步。把 Frida 抓到的参数喂进 Python 跑一遍、密文与 Frida 日志逐字节对得上,才算真正还原。如果 Python 输出与抓包对不上,直接跳第 5 章查 5 类成因。

二、Cipher Hook:对称加密和非对称加密的全量捕获

2.1 Cipher 的三步工作流程

一次完整的加密(或解密)操作,Cipher 类要经历三个步骤:
第一步是 获取实例Cipher.getInstance("AES/CBC/PKCS5Padding"))。这一步确定了加密算法、加密模式和填充方式。传入的字符串叫做 transformation,格式是 算法/模式/填充。常见的组合包括 AES/CBC/PKCS5Padding(最常见的 AES 配置)、AES/ECB/PKCS5Padding(不安全但仍有人用)、AES/GCM/NoPadding(安全的认证加密模式)、RSA/ECB/PKCS1Padding(RSA 加密)等。
第二步是 初始化cipher.init(mode, key) 或 cipher.init(mode, key, ivSpec))。这一步传入了操作模式(加密还是解密)、密钥和 IV。mode 参数是一个整数常量:Cipher.ENCRYPT_MODE = 1 表示加密,Cipher.DECRYPT_MODE = 2 表示解密。密钥通过 Key 接口传入(通常是 SecretKeySpec 的实例)。如果加密模式需要 IV(如 CBC、GCM),IV 通过 AlgorithmParameterSpec 参数传入(通常是 IvParameterSpec 或 GCMParameterSpec 的实例)。
第三步是 执行加解密cipher.doFinal(data) 或 cipher.update(data) + cipher.doFinal())。对于小数据量(绝大多数场景),直接调用 doFinal(data) 一步完成。对于大数据量的流式加密,会多次调用 update(data) 送入数据块,最后调用 doFinal() 完成。
我们的 Hook 策略是在每个步骤上都设置拦截点:在 getInstance 获取算法信息,在 init 获取密钥和 IV,在 doFinal 获取明文和密文。三步的信息组合起来,就是一次加密操作的完整画像。
notion image
Cipher 三步工作流程

2.2 完整的 Cipher Hook 脚本

下面按 5 段 给出完整脚本——按顺序拼到同一个文件就是可直接跑的 hook_cipher.js。第 1 段是公用辅助函数与入口变量,第 2-4 段对应三步工作流程,第 5 段是次要重载兜底。
段 1 · 辅助函数与入口变量
**段 2 · Step 1 getInstance 拿算法画像**
getInstance 的参数是 transformation 字符串(格式 算法/模式/填充),Hook 它就拿到 App 使用的加密算法画像。
**段 3 · Step 2 init 拿密钥和 IV**
init 重载较多,覆盖最常用的两个就能拿到 95% 场景的密钥和 IV——无 IV 版本(ECB / RSA)和带 AlgorithmParameterSpec 版本(CBC / GCM)。getEncoded() 返回 null 即说明密钥在硬件 KeyStore 里,Frida 也导不出。
带 IV 的重载需要分支处理 params 的实际类型:CBC 拿到 IvParameterSpec、GCM 拿到 GCMParameterSpec,二者的取 IV 方法不同。
**段 4 · Step 3 doFinal + update 拿明文/密文/调用栈**
doFinal(byte[]) 是「一次完成」模式,update(...) + doFinal() 是「流式完成」模式,两套都要覆盖。输出阶段务必同时打印调用栈——这是定位「哪个业务方法触发了这次加密」的唯一线索。
段 5 · 次要重载兜底
上四段覆盖 95% 的用法。剩下 5% 是带 SecureRandom、带 offset/length、用 AlgorithmParameters 而非 Spec、用 Certificate 等次要重载。遇到「明明加密了但日志没打印」时,查 javax.crypto.Cipher 的 init/doFinal 重载表,按下面这个模板照搬一份即可。

2.3 读懂 Cipher Hook 的输出

当 App 执行一次 AES-CBC 加密时,你会看到类似这样的输出:
日志里 transformation、key、IV、明文、密文、调用栈一次到位。两个细节顺便留意:输出 48 字节比输入 39 字节长,是 PKCS5 把 39 补齐到 16 的倍数;调用栈直接告诉你加密发生在 LoginActivity.onLoginClick 触发的请求构造里。有了这些信息,你已经可以用任何编程语言独立复现这次加密操作。

2.4 特殊场景与变种处理

下面四种场景在第 2.2 节主脚本里都能抓到,但行为或参数偏离标准 AES-CBC 范式,各自有特定的识别要点。前两种(ECB、硬件密钥)是 2-3 行就能讲清的边角形态;后两种(DES/3DES、AES-GCM)是需要单独展开的算法变种。
#### ECB 模式 · init 看不到 IV
如果输出里 transformation 是 AES/ECB/PKCS5Padding,init 调用只会带 mode 和 key 两个参数,没有 IV——这不是 Hook 漏抓,是 ECB 本身就不需要 IV。ECB 不安全(相同明文块永远加密成相同密文块,会暴露数据模式),但仍有不少老 App 在用,Python 复刻时把 AES.MODE_CBC 换成 AES.MODE_ECB、去掉 IV 参数即可。
#### 硬件密钥 · getEncoded() 返回 null
如果看到 密钥: (不可导出 - AndroidKeyStore),说明 App 用了 Android KeyStore 把密钥放进了 TEE(Trusted Execution Environment)或专用安全芯片,硬件级别拒绝导出,Frida 也绕不过去。但 doFinal 的明文和密文仍然能照常抓到——只是你拿不到密钥本身。如果需要在 App 外部独立加解密(比如在 Python 里),唯一办法是用第27篇介绍的 RPC 调用:让 Python 通过 Frida 远程触发 App 内部的加密函数,借 App 自己持有的硬件密钥完成加解密。
#### DES / 3DES
在国内银行 App、政企早期 SDK、部分 IM 与支付历史代码里,DES 与 3DES(Java 里叫 DESede)仍然时有出现。看到它通常意味着这段加密逻辑写于多年前,且当年因为兼容、监管或硬件加速等原因选了 DES——新项目几乎不会再选。但作为分析者你仍然要会还原。
好消息是:DES 系列与 AES 在 JCE 层面共用同一套 Cipher API,第 2.2 节的脚本完全不需要改动就能抓到它们的算法、密钥与明密文。两者的实际差别集中在以下几点:
| 项 | AES | DES | DESede(3DES) | | :-- | :-- | :-- | :-- | | transformation | AES/CBC/PKCS5Padding | DES/CBC/PKCS5Padding | DESede/CBC/PKCS5Padding | | 块大小 | 16 bytes | 8 bytes | 8 bytes | | PKCS5 填充 | 补到 16 字节倍数 | 补到 8 字节倍数 | 补到 8 字节倍数 | | 密钥长度 | 16 / 24 / 32 bytes | 8 bytes | 16 或 24 bytes | | 有效强度 | 128 / 192 / 256 bit | 56 bit(每字节 1 bit 奇偶校验) | 168 bit(24 字节)/ 112 bit(16 字节) | | IV 长度 | 16 bytes | 8 bytes | 8 bytes | | KeySpec | SecretKeySpec | DESKeySpecSecretKeySpec | DESedeKeySpecSecretKeySpec |
最容易踩的两个坑:
坑 1 · 块大小是 8 不是 16。pycryptodome 里 PKCS5 填充的 block_size 要传 DES3.block_size = 8,如果按 AES 习惯写 AES.block_size = 16 会得到错误结果。DES 系列的填充倍数是 8。 坑 2 · 3DES 的“24 字节”和"168 bit"是两个不同的数字encoded.length * 8 会显示 192 bit(24 × 8),但真正的密码学强度是 168 bit——每个 8 字节子密钥要扣掉 8 bit 奇偶校验。另一种常见形式是 16 字节双密钥 3DES(Two-Key 3DES,K1-K2-K1),Java 内部会自动把它扩展成 24 字节再做三次 DES,强度只有 112 bit(NIST SP 800-67r2 在 meet-in-the-middle 攻击下的评估口径,不是 Java 实现 bug)。
Python 还原(DESede/CBC/PKCS5):
如果 Frida 抓到的密钥是 16 字节,传给 DES3.new 时 pycryptodome 会自动按 Two-Key 3DES 处理;如果是 8 字节(普通 DES),改用 from Crypto.Cipher import DES,其他逻辑相同。
#### AES-GCM 的完整还原
AES-GCM 是新版 TLS、Conscrypt、近年新立项 App 的主流模式——加密+认证一体,密文被改一字节就解密失败,服务端不会再吃错乱明文。Python 复刻时若 tag 校验不过,八成是 IV 或 AAD 抄错(本节末尾会专门讲 AAD)。
GCM 在 Frida 输出里有两处一眼能认出的特征:
第一,init 阶段拿到的不是 IvParameterSpec 而是 GCMParameterSpec,第 2.2 节脚本已经分支处理过,日志会打印 GCM IV(HEX) 和 GCM Tag长度。IV(在 GCM 规范里叫 nonce)通常是 12 字节(96 bit)——这是规范推荐长度,几乎所有实现都遵循。
第二,doFinal 输出长度 = 输入长度 + tag 长度。GCMParameterSpec 里的 tag长度 默认是 128 bit(16 字节),输出的密文长度等于明文长度。也就是说:明文 39 字节,输出就是 39 + 16 = 55 字节。这跟 CBC 的“补齐到块大小整数倍”完全不一样——看到“输出 = 输入 + 16”就该往 GCM 上想。
Java 与 pycryptodome 在同一份输入下产物字节完全相同,只是 API 是「拼成一段」还是「分开返回」的差异:
notion image
AES-GCM 输出结构 · Java vs pycryptodome 对照
还有一个容易遗漏的输入:AAD(Additional Authenticated Data)。GCM 允许把一段“参与认证但不加密”的数据混进来(典型场景:HTTP header、协议头、版本号)。Java 通过 cipher.updateAAD(aad) 提供,第 2.2 节主脚本没 Hook 这个方法。在 第 2.2 节主脚本中 Cipher.update Hook 那一段附近补上:
**如果 App 用了 AAD 而你没记录,Python 复现时 tag 永远对不上、decrypt_and_verify 一定抛异常。** AAD 漏一个字节也会让 tag 完全不同——这是 GCM 还原最容易踩坑的地方。
Python 还原(pycryptodome 把 ct 和 tag 分开返回,与 Java 拼成一段不同):
反过来,从 Java 输出里拆 ct 和 tag 并验证
decrypt_and_verify 在 tag 不匹配时会抛 ValueError——这是验证你的参数(key / nonce / aad)是否完全对齐的最直接信号,比 CBC 模式里“解出来是乱码”靠谱得多。

三、密钥追踪:从「拿到密钥」到「理解密钥」

Hook Cipher.init 拿到密钥后,下一步问题是:这个密钥从哪来?
密钥来源决定了它能否在脱离 App 的情况下独立复用——硬编码常量可永久复用;服务端动态下发需要同时抓网络请求;基于设备信息派生则必须还原派生算法才能在其他设备上复现。

3.1 追踪 SecretKeySpec 的创建

对于对称加密(AES/DES),密钥通常通过 SecretKeySpec 类创建。Hook 它的构造函数可以追踪密钥的创建时机和来源:

3.2 追踪 PBKDF2 密钥派生

PBKDF2(Password-Based Key Derivation Function 2)是一种从密码派生加密密钥的算法。它接收密码、盐值(salt)、迭代次数和期望的密钥长度作为输入,输出一个密钥。很多 App 用这种方式将用户密码转换为 AES 密钥。

3.2.1 HKDF 与自制土法 KDF 的识别

PBKDF2 是 JCE 标准化的密钥派生,但实战里还有两类「非标准 KDF」需要警惕——遇到「Hook 了 PBKDF2 没动静,但密钥确实是派生出来的」就要往这两个方向查(两类在第 3.4 节的「设备特征生成」分类里出现率最高)。
第一类是 HKDF(HMAC-based Key Derivation Function)。Java 标准库到 Android API 33 之前都没有原生 HKDF 实现,App 通常通过 BouncyCastle(org.bouncycastle.crypto.generators.HKDFBytesGenerator)或自己用 Mac 拼装。Hook 思路是直接监听 javax.crypto.Mac.doFinal —— HKDF 的本质就是「HMAC-extract + HMAC-expand」,每次派生会产生多次连续的 HMAC 调用,密钥相同、输入不同,从日志里能直接看出来。
第二类是 自制土法 KDF,常见形态是「MD5(seed) → MD5(上一轮输出 + seed) → ... 迭代 N 轮」或者「SHA-256(deviceId + salt + counter) 切片拼接」。这类做法不调用任何 KDF API,而是直接调 MessageDigest。识别特征:在很短时间内、同一调用栈下,MessageDigest.digest() 被连续触发多次,且输入数据有规律(前一次的 hash 或 counter 递增)。Hook MessageDigest.update / digest 配合调用栈分组就能定位。

3.3 追踪 RSA 公私钥的来源

RSA 密钥不是 SecretKeySpec,而是通过 KeyFactory 把 DER 编码的字节数组(公钥用 X.509、私钥用 PKCS#8)转成 PublicKey / PrivateKey 对象。App 里使用 RSA 的典型路径是:把一段 PEM 字符串去掉头尾、Base64 解码,得到 DER 字节,再交给 KeyFactory.generatePublic 构造 PublicKey 对象,最后传给 Cipher.init
notion image
RSA 密钥还原链路 · 三个 Hook 点的分工
Hook 这条路径上的几个关键点,就能锁定 RSA 公钥的来源:
跑出来的典型日志长这样:
从来源堆栈可以快速判断公钥的分发模式,与第 3.4 节对称密钥四分类形成姊妹结构:
| 堆栈/特征 | 来源类型 | Python 复现所需输入 | | :-- | :-- | :-- | | 栈顶在 <clinit> / 工具类静态初始化 | PEM 硬编码 | 直接从源码读 PEM,跨设备/会话不变 | | 栈顶在网络响应处理(xxxResponse.parseXxx / xxxApi.onSuccess) | 服务端下发 | 配合抓包记录传输过程;注意:下发的"公钥"本身常被外层 AES 包一层——不是防窃听(公钥本就公开),而是防客户端被劫持后替换公钥 + 增加静态分析门槛 | | 栈顶涉及 AssetManager.open / Resources.openRawResource | Assets / res/raw 文件 | 从 APK 里抽 .pem / .der / .key 文件 | | 栈顶在 KeyPairGenerator 相关代码 | 本地动态生成 | 客户端身份认证场景;公钥上送服务端、私钥留本地——配合 Hook KeyPairGenerator.generateKeyPair() 把私钥也存下来 |
拿到 X.509 DER 字节后,可以直接喂给 Python 的 cryptography 库还原出公钥对象用于独立加密:

3.4 对称密钥来源的分类判断

RSA 公钥的来源判断已在 第 3.3 节给出(硬编码 PEM / 服务端下发 / Assets 文件 / 本地 KeyPairGenerator)。本节专门讨论对称密钥。
notion image
密钥来源分类与判断
通过 SecretKeySpec 构造时的堆栈,可以把对称密钥的来源归到下面四类,每类对应不同的还原策略:
| 堆栈/特征 | 来源类型 | Python 复现需要 | | :-- | :-- | :-- | | 调用者是工具类 + key 是可打印 ASCII(如 "abcdef1234567890") | 硬编码 | 直接用,跨设备/会话不变 | | 调用者是网络响应处理(xxxResponse.parseKey) | 服务端下发 | 配合抓包记录传输过程 | | 调用者是 SecretKeyFactory.generateSecret | PBKDF2 密码派生 | 同时记录密码、盐、迭代次数 | | 堆栈含 deviceId/androidId/IMEI 等 | 设备特征派生 | 还原派生算法 + 输入参数 |
Native 层加密怎么办? 当 App 把 AES/RSA/MD5 推到 Native 层(直接调 OpenSSL/BoringSSL 的 C 函数),思路与本篇完全一致,工具换函数名。完整教学(JCE↔OpenSSL 函数对照、三步定位法、实战示例、native_crypto_monitor.js 一键监控脚本)见第15篇《Native 层加密还原》;符号被 strip / SO 被加固 / 算法被魔改三类进阶场景见第21篇。

四、RSA + AES 混合加密的拆解

看到同一调用栈下先 RSA/ECB/PKCS1Padding 加密 16/32/48 字节明文、紧接着 AES/CBC/PKCS5Padding 用前者的明文当 key——就是混合加密。
RSA + AES 混合加密是现代 App 中 RSA 用于加密业务 payload 的主流方式。纯 RSA 加密大块数据已经很少见,但用 RSA 加密短字段(密码、token、AES 密钥本身)仍然常见。
为什么要混合?RSA 慢,且明文长度受密钥位数限制(2048-bit RSA 只能加密 ≤256 字节、扣掉 padding 后 ≤245 字节),不适合加密大块数据。AES 快、长度不限,但对称密钥怎么安全地交给服务端是难题。把两者拼起来正好互补:
notion image
RSA + AES 混合加密时序图
服务端能解开是因为它持有 RSA 私钥;中间人即使截到完整密文,没有私钥也拿不到 AES key,自然解不开 payload。
在 Frida 日志里怎么一眼看出这是混合加密?
完整的 第 2.2 节主脚本会按时间顺序打印 getInstance → init → doFinal。当 App 触发一次混合加密时,你会在很短时间内(通常 <50ms)、同一线程下看到一对 RSA + AES 接连出现的模式——三个判别信号 ①②③ 在日志中的位置如下图所标:
notion image
混合加密 · 三个判别信号在 Frida 日志中的位置
三个判别信号按重要度排:
  1. 同业务调用栈中 RSA 与 AES 接连出现
。getInstance 日志中先看到 RSA/ECB/PKCS1Padding、紧接着 AES/...,且两者通过 第 2.2 节的 getAppStack(4) 输出回溯到同一个业务方法(如 LoginApi.encryptRequest),就基本确诊是混合加密。RSA 输出长度 = 密钥位数 / 8(2048-bit 固定 256 字节),可作为附加验证。
  1. RSA 加密的明文长度恰为 16 / 32 / 48 字节
——分别对应 AES-128 key、AES-256 key、AES-128 key + 16 字节 IV。如果是 48 字节,常见拆法是「前 16 是 key、后 16 是 IV」或反之;用紧随其后的 AES init 日志一对照就知道顺序。
  1. RSA 加密的明文 = 下一次 AES init 的密钥
。这是最硬的证据,几乎不会撞车。
拆解还原的实操步骤
第一步,确认是混合加密结构——三个信号至少中两个。
第二步,不需要服务端私钥。你的目标不是“扮演服务端解密”,而是“扮演客户端重新加密任意 payload”。所以你只需要:① 服务端 RSA 公钥(已在 第 3.3 节拿到 X.509 DER);② AES 算法、模式、填充(在 RSA 之后的 AES init 日志里看到);③ AES key 生成方式——绝大多数情况是 SecureRandom.nextBytes(),你直接用任意 16 字节随机数即可,因为服务端会从 C1 解出来用,不在乎是不是上次那把。
第三步,Python 复刻:
第四步,最容易出错的环节是拼接格式。同样是混合加密,不同 App 会拼成 [C1][C2][C1][IV][C2]{"key":base64(C1),"data":base64(C2)}、或在 C1/C2 之间塞个长度前缀。这一步只能靠抓包对照,光看 Frida 日志看不出来——拼装发生在 Cipher.doFinal 返回之后的业务代码里,需要把 Frida 输出与抓包密文做字节级比对,找出“哪几段密文对应哪一次 doFinal 输出”。
notion image
混合密文拼接格式 · 4 种常见排布
最后一个常见变种:少数 App 反过来用——服务端用客户端的 RSA 公钥加密响应,客户端用本地保存的 RSA 私钥解密。这种场景下客户端持有的是私钥(通常是首次激活时由服务端下发的设备身份密钥),分析时 第 3.3 节的 PKCS8EncodedKeySpec 钩子会被触发,而 X509EncodedKeySpec 反而不一定出现。看到客户端持有 RSA 私钥就要警觉这种“反向混合”,常见于设备身份认证、推送 token 下发等场景。

五、Python 复刻验证与排错

前面各节给出了 DES/3DES(第 2.4 节)、AES-GCM(第 2.4 节)、RSA + AES 混合(第 4 章)的 Python 复刻片段。逐字节对得上是还原成功的唯一硬证据。如果 Python 输出和 Frida 抓到的密文不一致,按下面的决策树逐项排查——通常前 3 类(transformation 错配 / Key·IV 复制错 / 外层 Base64 编码)就能覆盖 80% 的情况:
notion image
Python 复刻输出对不上 · 5 类成因排查
文字版排查清单(按命中率从高到低):
| 序 | 成因 | 典型症状 | 排查动作 | | :-- | :-- | :-- | :-- | | ① | transformation 错配 | 长度对、字节完全不同 | 回查 Frida getInstance 日志,把 mode / padding 三段照抄;CBC ↔ ECB、PKCS5 ↔ NoPadding 不要凭印象选 | | ② | Key / IV 复制错 | 长度对、首字节就不同 | hex 字符串里有没有混进空格、换行、0x 前缀;从日志复制时有没有少 1 个字符;大小端有没有翻反(极少数 Native 场景) | | ③ | 外层 Base64 / Hex 编码 | Python 输出和抓包密文"长度都对不上",差值是 4/3 倍或 2 倍 | 抓包看到的可能不是原始 doFinal 输出,而是被外层 Base64.encode / Hex.toHexString 再包了一层;先 base64decode / unhexlify 再对比 | | ④ | 明文编码不一致 | 明文里 ASCII 部分密文一致、从首个非 ASCII 字符(通常是中文)所在块开始错 | 中文场景常见:Java 默认 UTF-8,但 App 显式用 GBK;Python 端把 .encode("utf-8") 改成 .encode("gbk") 再试 | | ⑤ | GCM AAD 漏抓 | decrypt_and_verifyValueError,但 key/nonce 都对得上 | 主脚本里 updateAAD Hook 是不是没加(第 2.4 节 AES-GCM 子节);AAD 漏一个字节也会让 tag 完全不同 |
排查顺序:先确认 ①②(占 60%+),再看 ③(外层编码是抓包对照里最常踩的坑),最后才查 ④⑤。
**Python 输出与 Frida doFinal 输出逐字节一致——这是还原成功的唯一硬证据**,比"解出来像是正常 JSON"靠谱得多。

总结

四条线索串起本篇所有 Hook 点:
  1. JCE 是入口
(第二章)— Cipher 三步 getInstance → init → doFinal 各设一个 Hook,一次加密的算法、密钥、IV、明文、密文、调用栈一次到位。Java 层的对称/非对称加密,都从这套三步流程切入。
  1. 密钥溯源
(第三章)— 结合 SecretKeySpec 构造 / SecretKeyFactory.generateSecret 的调用栈,把对称密钥归到「硬编码 / 服务端下发 / PBKDF2 派生 / 设备特征派生」四类,决定了能否在 App 外独立复用。RSA 公钥则按 X509EncodedKeySpec 调用栈分「PEM 硬编码 / 服务端下发 / Assets 文件 / KeyPairGenerator 本地生成」四类。
  1. 混合加密的三个判别信号
(第四章)— RSA + AES 混合是现代 App 主流:RSA 与 AES 同栈连续出现、RSA 明文恰好 16/32/48 字节、RSA 明文 = 下一次 AES init 的密钥。三中其二即可确诊
  1. 逐字节验证 + 5 类排错
(第五章)— Python 输出与 Frida doFinal 输出逐字节一致是还原成功的唯一硬证据。对不上时按 transformation 错配 → Key/IV 复制错 → 外层 Base64 → 明文编码 → GCM AAD 漏抓五类成因从前往后排查,前三类覆盖 80% 的情况。
Native 层 Hook:当 App 把 AES/RSA/MD5 推到 Native 层(直接调 AES_cbc_encrypt / RSA_public_encrypt / MD5_Update / EVP_EncryptInit_ex 等 C 函数),整套思路完全一致,工具换函数名——完整教程 + 实战示例 + 一键自动监控脚本 native_crypto_monitor.js 见第15篇《Native 层加密还原》。符号被 strip、SO 被加固、算法被魔改这三种进阶场景留给第21篇。
上一篇
Frida学习笔记(十三):加密算法还原(下)· 哈希、HMAC、国密
下一篇
Frida学习笔记(十一):Android 系统级 Hook

Comments
Loading...