type
Post
status
Published
date
Jul 10, 2026
slug
summary
本篇目标:补全加密分析的另一半拼图。第12篇覆盖了对称/非对称加密(Cipher),本篇覆盖哈希运算(MessageDigest)、消息认证码(Mac/HMAC)、数字签名(Signature)和国密算法。在实际逆向中,HMAC 签名在 API 防篡改场景中出现的频率甚至比 AES 加密还高——很多 App 的每一个网络请求都携带 HMAC 签名。
tags
frida
加解密
实用教程
category
公众号
icon
password
网址
上次编辑时间
Jul 10, 2026 06:36 AM
comment
AI 总结
本篇目标:补全加密分析的另一半拼图。第12篇覆盖了对称/非对称加密(Cipher),本篇覆盖哈希运算(MessageDigest)、消息认证码(Mac/HMAC)、数字签名(Signature)和国密算法。在实际逆向中,HMAC 签名在 API 防篡改场景中出现的频率甚至比 AES 加密还高——很多 App 的每一个网络请求都携带 HMAC 签名。掌握这些 Hook 技术后,你就拥有了完整的加密分析能力。
Native 层加密怎么办? 本篇覆盖 Java 层 MessageDigest/Mac/Signature 的完整 Hook 链路。如果 App 把 Hash/HMAC/RSA 签名推到 Native 层(直接调MD5_Update/SHA256_Update/HMAC_Init_ex/HMAC/RSA_sign/EVP_DigestUpdate这些 C 函数),完整教学 + 一键自动监控脚本native_crypto_monitor.js见第15篇《Native 层加密还原》——本篇的 Java 层 Hook 思路与那里的 Native 层完全一致,只是工具换函数名。
一、哈希与加密的本质区别

哈希 vs 加密 · 本质区别:可逆/不可逆、Hook 关注 key/input、Python 复现路径完全不同
读完这一章,你能在 Frida 日志里一眼区分:这是个可被还原的密文,还是只能拼接复现的签名/哈希——两者的分析策略完全不同。
加密是可逆的。你用密钥把明文变成密文,可以用同一个密钥(对称加密)或配对的私钥(非对称加密)把密文还原成明文。加密的目标是保护数据的机密性——不让未授权的人看到数据内容。
哈希是不可逆的。你把任意长度的输入数据通过哈希函数变成一个固定长度的摘要值(比如 MD5 产生 128 位/16 字节的摘要,SHA-256 产生 256 位/32 字节的摘要),但你无法从摘要值反推出原始数据。哈希的目标是验证数据的完整性——只要输入数据改变一个字节,输出的摘要就会完全不同。
这个区别意味着:对于 Cipher 加密,你可以用捕获的密钥在 Python 中解密密文,看到明文内容。但对于 MessageDigest 哈希,你无法「解密」哈希值——你只能看到哈希的输入是什么、输出是什么。
判断模型:对于逆向分析来说,看到哈希的输入通常就够了——因为 App 使用哈希的典型场景是hash(password + salt)或hash(timestamp + nonce + params),你需要的是理解这个拼接规则,而不是「解密」哈希值。
HMAC 是哈希的「升级版」——它在哈希的基础上引入了一个密钥。
HMAC-SHA256(key, data) 的意思是「用密钥 key 对数据 data 计算带认证的 SHA-256 摘要」。只有持有相同密钥的人才能算出正确的 HMAC 值,这让它可以用于验证数据的来源(身份认证)和完整性(防篡改)。HMAC 在 API 签名场景中极其常见。二、MessageDigest Hook:捕获哈希运算
2.1 MessageDigest 的工作流程
和 Cipher 类似,MessageDigest 也有一个三步工作流程,但更简单:
第一步是
MessageDigest.getInstance("SHA-256") —— 创建一个指定算法的哈希实例。第二步是
digest.update(data) —— 送入待哈希的数据。可以多次调用 update 来送入多块数据(最终结果等同于把所有数据拼接后一次性哈希)。第三步是
digest.digest() —— 完成哈希计算,返回摘要字节数组。也有一个便捷版本 digest.digest(data) 同时送入数据并返回结果。关键的 Hook 点是
update(看输入了什么数据)和 digest(看输出了什么摘要)。getInstance 告诉你使用的是哪种哈希算法。
MessageDigest 三步工作流程 · getInstance/update/digest 三个 Hook 点分别拿到算法、输入、输出
2.2 完整的 MessageDigest Hook 脚本
下面按 3 段给出脚本——按顺序拼到同一个文件就是完整的
hook_messagedigest.js。第 1 段是辅助函数与入口,第 2 段对应 update(输入数据),第 3 段对应 digest(输出哈希)。**段 1 · 辅助函数 +
getInstance****段 2 ·
update 收集输入数据**两个常用重载:
update(byte[]) 是最常见的,update(byte[], int, int) 用于传入数组的一部分(流式分块场景)。**段 3 ·
digest 输出哈希结果**digest() 是基于之前 update 数据的最终输出;digest(byte[]) 是一步完成(送入数据并返回哈希)。用输出长度反推算法是个实用兜底——即使 getInstance 的日志被错过了,也能从摘要字节数猜出算法(MD5=16 / SHA-1=20 / SHA-256=32 / SHA-512=64)。2.3 读懂 MessageDigest 的输出
一个典型的 MD5 密码哈希输出长这样:
从这段输出你可以看到:App 对
password123extra_salt 计算了 MD5 哈希。password123 是用户输入的密码,extra_salt 是一个附加的盐值。哈希结果是 e99a18c428cb38d5f260853678922e03。调用链显示这个哈希是在登录流程中完成的。如果你需要在 Python 中验证:
2.4 多次 update:Python 复刻时如何拼接输入
有时候 App 不是一次性把所有数据送进去,而是分多次
update。比如:这意味着最终被哈希的数据是这三次
update 的内容拼接在一起:timestamp=1700000000&nonce=abc123¶ms=key1=val1&key2=val2。在 Python 中复现时,你需要按照相同的顺序拼接这些数据,或者使用 hashlib 的 update 方法分多次送入:三、Mac Hook:API 签名的核心
3.1 HMAC 在 API 签名中的作用
安全性要求较高的 App 通常会为每个 API 请求附加一个签名(sign)。签名的目的是让服务器能验证:这个请求确实是由合法的客户端发出的(而不是被攻击者伪造的),并且请求内容没有被篡改。

API 请求签名流程
最常见的签名算法是 HMAC-SHA256 或 HMAC-SHA1。签名的计算过程通常是这样的:
核心公式:sign = HMAC-SHA256(secretKey, sortedParams + timestamp + nonce)看 HMAC 日志时就在找这三段输入:密钥 / 排序后的参数 / 时间戳+nonce。其中secretKey是 App 和服务器共享的密钥(硬编码在 App 中或通过登录获取),sortedParams是按字母顺序排列的请求参数,timestamp是当前时间戳,nonce是一个随机数(防止重放攻击)。
如果你能通过 Frida 拿到
secretKey 和签名的拼接规则,你就可以在 Python 中为任意请求生成合法的签名——这是爬虫和自动化测试场景中最核心的技术突破点。3.2 完整的 Mac Hook 脚本
下面按 3 段给出脚本——按顺序拼到同一个文件就是完整的
hook_mac.js。第 1 段是辅助函数与 getInstance + init(拿算法和密钥),第 2 段对应 update(收集签名数据),第 3 段对应 doFinal(拿最终签名 + 调用栈)。**段 1 · 辅助函数 +
getInstance + init(算法 + 密钥)**Mac.init 接收一个 Key 对象,Key.getEncoded() 返回密钥的字节数组。HMAC 密钥通常是一个共享的 secret string——所以同时打印 HEX 和 UTF8 两种形式。**段 2 ·
update 收集签名数据**HMAC 的
update 和 MessageDigest 的 update 类似——送入待签名的数据。在 API 签名场景中,这里送入的就是排序后的参数字符串。**段 3 ·
doFinal 拿最终签名与调用栈**doFinal() 基于之前 update 的数据计算最终 HMAC;doFinal(byte[]) 是一步完成。输出阶段务必同时打印调用栈——它告诉你"哪个业务方法触发了这次签名"。3.3 读懂 HMAC 输出并还原签名逻辑
一个典型的 API 签名场景的输出:
从这段输出中可以提取出完整的签名逻辑。密钥是字符串
app_secret_key_2024。签名的输入数据是按字母顺序排列的参数:nonce=f8a3b2¶ms=...×tamp=1700000000。算法是 HmacSHA256。在 Python 中复现签名:
一旦验证成功,你就掌握了这个 App 的 API 签名算法,可以为任意请求生成合法签名了。
四、Signature Hook:数字签名
4.1 数字签名与 HMAC 的区别
数字签名(
java.security.Signature)和 HMAC 都可以用于验证数据的完整性和来源,但它们的机制不同。HMAC 是对称的——签名方和验证方使用同一个密钥,所以任何持有密钥的人都能伪造签名。数字签名是非对称的——用私钥签名,用公钥验证。只有持有私钥的人能创建签名,但任何持有公钥的人都能验证签名。在 App 逆向中,数字签名通常出现在支付场景和身份认证场景中。App 内嵌了服务器的公钥,用它来验证服务器发来的数据是否被篡改。或者 App 持有自己的私钥,用它为重要请求(如支付请求)生成签名。
4.2 Signature Hook 脚本
4.3 读懂 Signature 输出
一次典型的 SHA256withRSA 支付签名输出长这样:
三个关键判读点:
- **算法名
SHA256withRSA** 告诉你两件事:用 SHA-256 做摘要、用 RSA 签名。常见组合还有SHA1withRSA(老 App)、SHA256withECDSA(用 ECC 私钥)。
- 签名长度 = RSA 密钥位数 / 8:RSA-2048 固定 256 字节、RSA-4096 固定 512 字节;ECDSA 签名长度可变(P-256 通常 70-72 字节)。
- update 看到的就是被签名的原始数据——这是 Python 复刻时要喂给 verify 的字节,不要做任何额外处理(不像 HMAC 还要拼参数,这里 update 已经是最终签名输入)。
4.4 Python 复刻:验证 Signature
数字签名场景下,逆向最常需要的是用公钥验证签名(确认 App 抓到的签名确实由私钥持有方生成)。如果 App 内嵌的是服务端公钥,你可以用它验证服务端下发数据;如果 App 自己持有私钥,验签时需要先把对应的公钥也抓到(
KeyFactory.generatePublic 见第12篇 第3.3节)。算法名映射速查:JavaSHA256withRSA↔ Pythonpadding.PKCS1v15() + hashes.SHA256();SHA256withRSAandMGF1↔ Pythonpadding.PSS(mgf=MGF1(SHA256()), salt_length=PSS.MAX_LENGTH) + hashes.SHA256();SHA256withECDSA↔ Pythonpub_key.verify(sig, data, ec.ECDSA(hashes.SHA256()))。
如果验签通过——你就完整还原了 App 的签名验证逻辑,可以在 Python 中独立判断"服务端下发的数据是否被中间人篡改过"。
五、国密算法:SM2/SM3/SM4
5.1 为什么要关注国密
国密算法是中国国家密码管理局发布的密码算法标准。在金融、政务、电信等行业,越来越多的 App 被要求使用国密算法替代国际算法。如果你分析的是国内银行、政务或运营商的 App,遇到国密算法的概率很高。
国密算法和国际算法的对应关系如下图所示,理解这个映射关系有助于你快速理解国密算法的功能和用途:

国密 vs 国际算法对照
SM4 对应 AES。SM4 是 128 位分组密码(block cipher),密钥长度固定为 128 位。和 AES 一样,它支持 ECB、CBC、CTR 等加密模式。在 JCE 框架中,SM4 也通过
Cipher 类调用(Cipher.getInstance("SM4/CBC/PKCS5Padding")),所以第12篇的 Cipher Hook 已经能自动捕获走标准 JCE 路径的 SM4 加密操作。本节额外 Hook 的是直接使用 BouncyCastle 底层 API 的情况。SM3 对应 SHA-256。SM3 是 256 位哈希算法,输出长度固定为 32 字节。在 JCE 中可以通过
MessageDigest.getInstance("SM3") 调用。SM2 对应 ECDSA / RSA。SM2 是基于椭圆曲线的非对称加密和签名算法。它可以用于加密(类似 RSA)和数字签名(类似 ECDSA)。
前提:JCE 路径需要 App 已注册支持 SM 的 Provider。Android 系统自带的 BouncyCastle 是精简版,不含 SM 算法(Android 9+ Conscrypt 接管 JCE 后更不会带 SM)。App 要走Cipher.getInstance("SM4/...")/MessageDigest.getInstance("SM3"),必须在启动时调用Security.addProvider(new BouncyCastleProvider())注册完整 BC(≥1.58 默认包含 SM 系列)。如果 App 没注册或用了不含 SM 的老版 BC,getInstance会抛NoSuchAlgorithmException——这种情况它就只能走 第 5.2 节 的 BouncyCastle 底层 API。

国密 Hook 路径决策树 · 从「是否用 SM」到「BC Provider 是否注册」最终确定 Hook 入口
5.2 国密 Hook 脚本
以下脚本针对 直接使用 BouncyCastle 底层 API 的场景(
org.bouncycastle.crypto.engines.SM4Engine 等)——走 JCE 标准路径的 SM 已被第12篇 Cipher Hook 自动覆盖。按 3 段给出,按顺序拼到同一个文件就是完整的
hook_sm_crypto.js。每段独立 try/catch 包裹,对应算法未被加载时友好提示而不是脚本崩溃。段 1 · SM4 对称加密(对应 AES)
SM4Engine 是 BouncyCastle 中 SM4 的核心实现类,processBlock 处理一个 16 字节的分组。如果 App 通过 Cipher API 调用 SM4,这里不会被触发(Cipher Hook 已经覆盖);这里捕获的是直接使用 BouncyCastle API 的情况。段 2 · SM3 哈希(对应 SHA-256)
SM3Digest.doFinal(out, outOff) 把 32 字节哈希值写到 out 数组指定偏移处。必须先调用原始方法再读取,否则 out 数组还未被填充。同时 Hook update 看输入数据。段 3 · SM2 非对称加密 + 签名(对应 ECDSA/RSA)
SM2 有两个入口:
SM2Engine 处理加密/解密,SM2Signer 处理数字签名。SM2 加密输出有两种字段布局——遇到 SM2 密文必须先确认 App 用的是哪种,顺序错会让 Python 复刻"长度对但解不开":
| 布局 | 顺序 | 出处 |
| :-- | :-- | :-- |
| BouncyCastle 默认 |
C1(65 字节椭圆曲线点) ‖ C2(明文等长) ‖ C3(32 字节 SM3 哈希) | bc-java SM2Engine.Mode.C1C2C3(默认,对应老国标) |
| GB/T 32918-2016 新国标 | C1(65 字节) ‖ C3(32 字节) ‖ C2(明文等长) | 金融/政务类 App 按新国标的越来越多,需在构造时显式传 Mode.C1C3C2 |两种共同特征:输出比输入长 97 字节——长度差能确认是 SM2,但 Python 还原前必须 jadx 看代码用的是哪种布局。

SM2 加密输出字段布局 · C1‖C2‖C3 三段构成与 97 字节判断信号
5.3 识别 App 是否使用了国密
第 5.1 节 已经提过行业线索(金融/政务/电信高概率使用国密),这里只补静态检测——拿到 APK 后不跑 Frida 就能快速判断的几个信号:
- jadx 中搜索关键字:
SM2/SM3/SM4/bouncycastle/GMssl/GMSSL
- lib/ 目录扫描:看到
libgmssl.so或类似 SO 文件,说明使用了国密的 Native 实现
- assets/ 或 res/raw/ 目录:
.sm2后缀文件通常是 SM2 的密钥文件
六、Base64 编码层:加密数据的「外衣」
6.1 为什么需要关注 Base64
加密操作产生的密文是二进制数据(任意字节值 0x00-0xFF),不能直接放在 JSON、URL、HTTP Header 等只支持可打印字符的地方。所以 App 在加密之后、传输之前,通常会用 Base64 将二进制密文编码成纯 ASCII 字符串。
⚠️ 抓包看到的不是原始密文:加密数据往往已经过 Base64 编码,是一段可打印 ASCII 字符串。如果你想在 Python 中复现加密过程,需要注意这个编码层——Python 脚本加密产生的二进制结果必须再经过 Base64 编码才能和抓包看到的数据对比。Python 复刻"长度对不上"约 1/3 的原因是漏算了这一步。 URL-safe Base64 变种:看到Base64.encodeToString(data, 8)(flags=8 即URL_SAFE),输出会把+/换成-_、去掉=padding——常见于 JWT、OAuth token、URL query 参数。Python 端用base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))(补回 padding)还原。
6.2 Base64 Hook
6.3 多层编码的分析思路
实际 App 中,数据从明文到最终传输形式可能经过多层处理:

数据处理链路:从明文到网络传输
时间顺序就是处理链路:同时加载 Cipher Hook 和 Base64 Hook 时,控制台日志按时间顺序读下来就是 App 真实的数据流——先看到 Cipher 的doFinal输出(二进制密文),紧接着 Base64 的encode输入(与 Cipher 输出字节完全相同),最后 Base64 输出一段可打印字符串。看到这个固定三连,就确认了 App 的加密+编码组合。
反向分析时,你需要逆序还原:先 Base64 解码,再 AES 解密,得到原始明文。
七、承接:从「单 API 教学」到「全量自动监控」
到这里,单个加密 API 的 Hook 教学就完整了:Cipher / MessageDigest / Mac / Signature / SM 系列 / Base64 编码层——每一类都能独立采集自己关心的数据。
真实分析中你需要的是「加载一个脚本,操作 App,所有加密自动吐出来」,而不是手动挑加载哪几个子脚本。这件事需要专门解决几个工程问题:同一次
Cipher.getInstance → init → doFinal 怎么关联成一行输出(不靠 hashCode 三条日志会散开)、性能怎么扛住高频调用(采样、限速、按需启用模块)、与 Objection 内建的 android hooking watch 怎么取舍。这些是\\第14篇《算法自吐 · 一个脚本监控所有加密》\\的全部内容——完整脚本、关联机制、性能策略、实战还原一次讲透。本篇到 第六章 为止已经覆盖单 API 教学的全部内容。
总结
本篇补全了加密分析的后半部分,覆盖了哈希、HMAC、数字签名和国密算法。
MessageDigest(MD5/SHA 系列)通过三步流程工作:
getInstance 确定算法 → update 送入数据 → digest 输出哈希值。哈希是不可逆的,分析的重点是理解哈希的输入拼接规则。当 App 分多次调用 update 时,需要把所有 update 的数据按顺序拼接起来才能在 Python 中正确复现。Mac(HMAC 系列)是 API 签名的核心。它的工作流程与 MessageDigest 类似但多了一个密钥。在 API 签名场景中,你需要从 Hook 输出中提取三个关键信息:HMAC 密钥、签名数据的拼接格式(参数排序规则、分隔符、时间戳/随机数的位置)、以及最终的签名值。掌握了这三个信息,你就能为任意请求生成合法签名。
国密算法(SM2/SM3/SM4)在金融、政务类 App 中越来越常见。SM4 走 JCE 标准路径时已被 Cipher Hook 覆盖;直接使用 BouncyCastle 底层 API 时需要额外 Hook SM4Engine、SM3Digest、SM2Engine 等类。
Base64 是加密数据传输前的常见编码层。同时监控 Base64 和加密 Hook 可以看到完整的数据处理链路:明文 → 加密 → Base64 → 传输。
实际工作中通常先用综合监控脚本(第14篇)采集加密方案全貌,再回头精读单 API 输出。
第12篇和第13篇合在一起,给你提供了完整的 Java 层加密分析能力。下一篇(第14篇)讲解算法自吐——把分散的 Cipher/MessageDigest/Mac/Signature Hook 整合为通用加密监控工具。
- Author:24th
- URL:https://24th.top/article/399e5b08-46db-801f-a675-e1500a72d868
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!









