Lazy loaded image
Frida学习笔记(十四):算法自吐 · 一个脚本监控所有加密
Words 8036Read Time 21 min
2026-7-10
2026-7-10
type
Post
status
Published
date
Jul 10, 2026
slug
summary
本篇目标:构建一个「加密操作全自动监控系统」。第12-13篇分别讲了 Cipher、MessageDigest、Mac、Signature 的 Hook 方法——但在实际逆向中,你面对一个陌生 App 时不知道它用了哪些加密、在什么时机调用、密钥从哪来。一个个试效率太低。本篇的方案是:加载一个脚本,操作 App,所有加密操作自动"吐"出来——算法名称、密钥、IV、输入、输出、调用栈,一目了然。
tags
frida
实用教程
加解密
逆向工程
category
公众号
icon
password
网址
上次编辑时间
Jul 10, 2026 06:38 AM
comment
AI 总结
本篇目标:构建一个「加密操作全自动监控系统」。第12-13篇分别讲了 Cipher、MessageDigest、Mac、Signature 的 Hook 方法——但在实际逆向中,你面对一个陌生 App 时不知道它用了哪些加密、在什么时机调用、密钥从哪来。一个个试效率太低。本篇的方案是:加载一个脚本,操作 App,所有加密操作自动"吐"出来——算法名称、密钥、IV、输入、输出、调用栈,一目了然。这就是安卓逆向圈子里说的「算法自吐」。
配套脚本:本篇产出的 crypto_monitor.js(700+ 行,6 个模块,单文件复制即用)已整理好打包。 关注本公众号后私信回复关键词「脚本」 即可获取,本系列与后续 Unidbg / SO 逆向 / ARM 汇编 等系列脚本会统一在此发放并持续更新。

一、什么是「算法自吐」

1.1 概念

「算法自吐」不是一个 Frida API 的名字,而是一种逆向工作模式
  1. 加载一个预先编写好的、覆盖所有标准加密 API 的 Hook 脚本
  1. 正常操作 App(登录、下单、搜索……)
  1. 从控制台输出中直接读出 App 使用的加密方案
你不需要提前知道 App 用了什么算法、不需要在 jadx 中搜索加密代码、不需要处理代码混淆——因为不管 App 的业务代码怎么写、怎么混淆,它最终都要调用 javax.crypto.Cipherjava.security.MessageDigest 等标准 API。Hook 标准 API 就能把整个加密链路看清楚。

1.2 与第12-13篇的区别

| | 第 12-13 篇 | 本篇(算法自吐) | | :-- | :-- | :-- | | 定位 | 理解每个 API 的原理和 Hook 方法 | 一键使用 | | 脚本结构 | 每个 API 单独的脚本 | 全部整合为一个脚本 | | 输出格式 | 基础 console.log | 彩色彩条事件头、字段对齐、调用栈过滤 | | Cipher 实例关联 | 无(getInstance/init/doFinal 分散输出) | 自动关联同一 Cipher 实例的三步操作 | | 输出范围 | 单 API 字段 | 完整自吐参数(算法+密钥+IV+输入+输出+栈) | | 性能控制 | 无 | 模块开关、栈帧包名过滤、限速 | | 落地姿势 | 需要按目标 API 改 | 复制脚本、改 CONFIG 即用 |

1.3 工作流

notion image
算法自吐工作流

二、核心挑战:Cipher 实例关联

2.1 问题

Cipher 的三步操作(getInstanceinitdoFinal)是在同一个 Cipher 实例上依次调用的:
如果你分别 Hook 这三个方法,它们的回调是独立触发的——你在 getInstance 回调中看到了 AES/CBC,在 init 回调中看到了密钥,在 doFinal 回调中看到了明文和密文。但问题是:这三次回调之间没有天然的关联。如果 App 在多线程中同时进行多次加密(比如同时加密密码和签名),三步操作的回调会交错出现,你无法区分哪个密钥对应哪次加密。

2.2 解决方案:用 Cipher 实例的 hashCode 作为关联 ID

为什么选 hashCode?Frida JS 端没有标准 WeakRef 引 Java 对象的能力,自增 ID 又无法在 doFinal 时拿回 getInstance 时分配的值。hashCode 是 Java 对象自带、跨方法可复读的身份标识——最适合做"查 ctx"的 key。
在三步操作中,this(Java 对象引用)是同一个 Cipher 实例,所以 this.hashCode() 在三处取到的值相同,可以将信息聚合到一起:
notion image
hashCode 关联机制
这个关联机制是本篇脚本与第12/13篇分散 Hook 脚本的核心区别
风险注脚:Java Object.hashCode() 默认是 identity hash(System.identityHashCode),32-bit 整数——理论上两个不同对象可碰撞。本脚本的应对措施是在 doFinal 输出报告后立刻 delete cipherCtxMap[id]:既避免长会话内存累积,也将"同一时刻活跃实例数"压到最小,让碰撞概率降到可忽略。代价是同一 Cipher 重复 doFinal(连续多块加密)时第二次会丢上下文——绝大多数业务"一次性 doFinal" 不受影响。

三、完整的 crypto\_monitor.js

本篇的核心产出。所有代码包在一个 IIFE 中——把下方第 3.1 ~ 3.7 节七段代码顺序拼接进同一个 .js 文件即可使用。
各模块之间的关系:
notion image
crypto\_monitor.js 7 模块架构

3.1 骨架:配置与辅助函数

入口、CONFIG 开关、智能格式化、调用栈过滤、限速器——后续每个模块都会复用。
阅读地图(这段代码长,扫读时按顺序找这 4 处): 1. CONFIG(功能开关 + 输出控制 + 性能 + 外观)—— 改这里就能定制脚本行为 2. C 调色板 + tint() —— 256 色背景标签 + ANSI 着色封装 3. bytesToHex / smartFormat —— 字节到展示字符串的两个核心格式器 4. printHeader / printField / printMultiInput / printStack —— 4 个输出原语,全部带 buf 参数支持原子事件

3.2 Cipher 监控

第二章介绍的 hashCode 关联机制在这里实现。getInstance 创建上下文 → init 补充 mode/key/iv → doFinal 输出完整报告并清理上下文(避免长会话累积内存)。
阅读地图: 1. Cipher.getInstance 三个重载(String / String,String / String,Provider)—— 都建 cipherCtxMap[id] 2. handleInit(...) 抽取共享逻辑 —— 再被五个 Cipher.init 重载复用 3. reportCipher(...) —— 用 buf 累积事件,结尾一次 console.log(buf.join("\n")) 原子吐出 4. Cipher.doFinal 三个重载分别接 null / byte[] / byte[],offset,len,全部调 reportCipher

3.3 MessageDigest 监控

Hash 不需要复杂关联——每个 MessageDigest 实例独立计算。把 update 累积的输入用同一个 hashCode 串起来,在 digest() 时一并打印。

3.4 Mac / HMAC 监控

模式与 MessageDigest 类似,但 Mac 在 init 时即可拿到 key,所以上下文从 init 起。
阅读地图: 1. Mac.init 两个重载(Key / Key+ParamSpec)—— 建 macDataMap[id] 并存 key 2. Mac.update 三个重载(byte\[\] / byte\[\],int,int / ByteBuffer)—— 累积到 inputs 3. Mac.doFinal() 无参 —— 用 printMultiInput 把多段 update 用 ①②③ 区分后打印 4. 故意hook doFinal(byte[]) —— 它内部 = update + doFinal(),hook 便捷版会双打印

3.5 Signature 监控

Signature 多用于 RSA / ECDSA 签名,逻辑比 Mac 更直接:update 显示数据,sign / verify 输出最终结果。
阅读地图: 1. Signature.update 两个重载(byte\[\] / byte\[\],int,int)—— 每次调用直接打一个独立事件,不做累积 2. Signature.sign() —— 单独打 sign 事件(含签名结果 + 栈) 3. Signature.verify(byte[]) —— 单独打 verify 事件,标题里写明 → true/false 4. 共 4 个独立 hook,无共享上下文(不像 Cipher 需要 ctxMap

3.6 密钥生成 / PBKDF2 监控

SecretKeySpec 构造拦截硬编码密钥;SecretKeyFactory.generateSecret 抓 PBKDF2 派生过程(密码 + 盐 + 迭代次数 + 派生密钥)。
阅读地图: 1. SecretKeySpec.$init(byte[], String) —— 拦截密钥装配,看到字节 + 算法名 2. IvParameterSpec.$init(byte[]) —— 拦截 IV 装配,常与上一条紧邻出现(参考第 4.3 节双事件案例) 3. SecretKeyFactory.generateSecret(KeySpec) —— 拦截 PBKDF2/PBE 派生,能拿到原始密码、盐、迭代次数 + 派生密钥 4. 这一节是业务侧逆向最重要的入口:硬编码密钥几乎都在这里现形

3.7 启动 Banner 与 IIFE 收尾

notion image
crypto\_monitor Banner截图

四、输出格式解读

4.1 Cipher 输出

真机运行时,事件头是带背景色的彩条标签:Cipher 青、Hash 深蓝、HMAC 品红、Signature 金、KeyGen / KeyDerive 绿。下面三张截图都是 QQ 音乐启动后真实抓到的事件,与脚本结构一一对照。
notion image
Cipher 事件真机截图
如何解读
  • 算法AES/CBC/PKCS5Padding · ENCRYPT —— AES、CBC 模式、PKCS5 填充、加密方向
  • 密钥34633364643966623062346163335631(32 hex = 16 字节 = AES-128
  • 把这 16 字节当 ASCII 解码 = "4c3dd9fb0b4a3561" —— 本身就是个 16 字符的 hex 字符串(类似 MD5 截断结果直接 .getBytes() 当 AES key 用)。表面 AES-128,实际熵只有 8 字节
  • IV:与密钥完全相同的字节。CBC 模式的 IV 应该每次随机生成,IV ≡ Key 是经典反模式:相同明文永远加密成相同密文,且暴露了 key recovery 攻击面
  • 输入:起始字节 1f 8bGZIP 魔数 —— 业务先 gzip 压缩 JSON,再走 AES。末尾 ...(997 bytes)smartFormat 截断标记,告诉你完整长度,屏上只展示前 maxDataLength=256 字节
  • 输出...(496 bytes) 表明密文总长 496 字节(16 字节倍数,CBC 强制对齐)
  • 调用栈com.tencent.beacon.base.net.b.c.butil.b.b —— 这是腾讯灯塔 Beacon SDK 的网络上报路径,QQ 音乐用它进行用户行为埋点。

4.2 Hash 多段输入(SHA-256)

notion image
Hash 多段输入真机截图
如何解读
  • 算法:SHA-256
  • 输入分 ①~④ 四段 —— 每个环号是脚本帮你区分的"第几次 update()":
  • "false" —— 某个布尔标志
  • "https://y.qq.com/music/common/upload/t_cm3_photo_publish/7184371.png" —— 图片资源 URL
  • 8000000080000000 —— 8 字节占位(两个 0x80000000,常见的"未指定的宽高")
  • "android.support.rastermill.FrameSequenceDrawable" —— 解码器类全限定名
  • 输出:32 字节(256 位)摘要,用作磁盘缓存项的 key
  • 调用栈com.bumptech.glide.load.engine.cache.SafeKeyGenerator.calculateHexStringDigestgetSafeKeyDiskLruCacheWrapper.get —— Glide 图片库生成磁盘缓存键的标准路径。

4.3 KeyGen 双事件(SecretKeySpec + IvParameterSpec)

notion image
KeyGen 双事件真机截图
这是两条独立事件紧挨着出现,因为业务代码连续构造了 SecretKeySpec 和 IvParameterSpec。
上半:KEYGEN · SecretKeySpec · AES
  • 密钥"4c5d9fb0b4af2561"(smartFormat 双行:文本 + hex)
  • 长度:16 bytes(128 bits)→ AES-128
  • com.tencent.beacon.base.net.b.c.autil.b.b/a —— 同样是腾讯灯塔 Beacon SDK
下半:KEYGEN · IvParameterSpec
  • IV34633564396662306234616632353631,ASCII 解码 = 4c5d9fb0b4af2561
  • 与上面的密钥字节完全相同

五、性能控制:限速、过滤、按需启用

5.1 限速

某些 App 在启动时会进行大量的哈希操作(类校验、签名验证等),可能在几秒内触发几百次 MessageDigest 调用。CONFIG.rateLimitPerSecond 控制每秒最多输出多少条日志。
触发后行为:令牌耗尽时 canLog() 返回 false整个事件(含 header / 字段 / 栈)全部丢弃,不会出现半条事件的情况。设为 0 表示完全不限速。

5.2 包名过滤

设置 CONFIG.filterPackage = "com.example.app" 后,调用栈只显示包含该包名的帧。这样可以过滤掉 Android Framework 和第三方库的内部调用,只看 App 业务代码触发的加密操作。
触发后行为:只过调用栈这一字段——事件本身仍然完整打印(header、密钥、IV、输入、输出都在),仅栈帧被剪短。即使过滤后栈为空,事件其它字段也不受影响。

5.3 按需启用模块

如果你只关心对称加密(AES),可以关闭其他模块:
触发后行为:开关为 false 时,对应模块的 Java.perform 整个块根本不执行——hook 完全不安装到目标进程。和限速(事件丢弃)不一样,关闭模块连开销都没有,可放心用于性能敏感场景。

5.4 与 deoptimizeEverything 配合

如果加载脚本后操作 App 没有任何输出,可能是 JIT 导致 Hook 失效(第 07 篇讲过)。在脚本开头加上:
触发后行为:把 ART 已 JIT 编译的方法全部"反优化"回解释执行,Hook 命中率上升、但启动 1-3 秒会出现明显卡顿。仅在确实抓不到事件时再启用。

5.5 已知失效场景

「算法自吐」不是万能的。以下四种场景下脚本会看不到加密(或看到不完整)——提前知道边界,可以避免怀疑自己抄错代码。
notion image
无输出排查决策树
| 场景 | 现象 | 处理 | | :-- | :-- | :-- | | AndroidKeyStore 硬件密钥 | 能看到 Cipher 调用,但 密钥 字段显示对象引用而非字节 | 硬件密钥不可导出(getEncoded() 返回 null),Frida Java 层读不出。需在 Native 层 Hook 加密引擎(见第15篇 Native 层加密还原) | | Native 层直接加密 | App 不走 Java JCE,直接在 SO 里调 OpenSSL / Mbedtls / BoringSSL | 本篇脚本全无输出。切到 Native 层 Hook——AES_encryptEVP_CipherUpdateSHA256_Update(见第15篇 Native 层加密还原) | | Conscrypt EngineSpi 旁路 | 某些 OkHttp/TLS 实现直连 ConscryptEngineSocket,不走 javax.crypto.Cipher | 见第13篇 第五章 Provider 路径;补 hook ConscryptEngine.wrap/unwrap | | 自定义 JCE Provider | 加固方案自实现 Provider 替换标准实现 | 用 Security.getProviders() 观察是否有非标准 Provider;hook 其 engineXxx 系列方法 |
自检套路——加载脚本后正常操作 App 但完全无 Cipher / Hash 输出,先排除两件事:
  1. Hook 是否成功:看是否打印 [OK] Cipher 监控 等启动信息
  1. JIT 是否吃掉了 Hook:试加 Java.deoptimizeEverything()(见 第5.4节)
两件都做了仍无输出,大概率是上表中的某种边界——此时该转 Native 层 Hook(关键词 AES_encrypt / EVP_CipherUpdate / SHA256_Update / MD5_Update),而不是反复改本篇脚本。后续 Native 加密还原专题会展开。

六、与 Objection 的对比

Objection 是 Frida 的一个自动化封装工具,它也有加密监控功能:
| | crypto\_monitor.js(本篇) | Objection | | :-- | :-- | :-- | | Cipher 实例关联 | 有(getInstance/init/doFinal 关联输出) | 无(每个方法独立输出) | | 智能数据显示 | smartFormat(自动判断文本/二进制) | 原始 toString | | 密钥提取 | 自动 cast 为 SecretKeySpec 提取字节 | 只显示对象引用 | | IV 提取 | 自动 cast 为 IvParameterSpec 提取 | 不提取 | | PBKDF2 监控 | 有 | 无 | | 调用栈过滤 | 可按包名过滤 | 无过滤 | | 性能控制 | 限速 + 模块开关 | 无 | | 定制性 | 完全可定制 | 有限 |
上表基于 Objection v1.11 之前的实测印象;新版本的 watch 语法及输出行为可能变化,使用前请对照 objection GitHub 文档。
选型口诀:Objection 验证调用,crypto_monitor 还原方案。

总结

「算法自吐」是安卓逆向中效率最高的加密分析手段。核心思路是 Hook 标准加密 API 的「咽喉要道」——不管 App 怎么混淆,最终都要调用 CipherMessageDigestMac 等系统类。
本篇的 crypto_monitor.js 复制即用,关键特性包括:
  • Cipher 实例关联:通过 hashCode 将 getInstance / init / doFinal 三步操作关联到同一个上下文,输出完整的「算法 + 密钥 + IV + 输入 + 输出」
  • 智能数据显示smartFormat 自动判断文本 / 二进制,同时展示可读文本和十六进制
  • 密钥自动提取:通过 Java.cast 将 Key 接口转为 SecretKeySpec,提取密钥字节
  • 多段输入区分:MessageDigest / Mac 的多次 update() 用 ①②③ 环号标记,避免误读
  • 原子事件输出:每个事件用 buf 累积、一次 console.log 吐出,多线程并发也不会字段错位
  • 彩色彩条事件头:256 色背景标签,主题再黑也能区分事件类型
  • 调用栈追踪:指向 App 业务代码中调用加密的位置
  • 性能控制:限速、模块开关、包名过滤
实战工作流:加载脚本 → 操作 App → 读取自吐参数(算法 / 密钥 / IV / 输入 / 输出 / 栈)。从这些参数到 Python 离线复现只差一步——把抓到的字节抄进 pycryptodome,对照真机结果验证即可。

📦 获取本篇脚本

crypto_monitor.js 完整版(700+ 行,6 模块单文件,复制即用)+ 配套 README 使用说明已打包:
  1. 关注原公众号【泡泡以安】
  1. 私信回复关键词「脚本」
回复内含本系列与其它系列(Unidbg / SO 逆向 / ARM 汇编 ……)的脚本汇总,长期维护更新。
上一篇
Frida学习笔记(十五):算法自吐 · Native 层 OpenSSL/BoringSSL Hook
下一篇
Frida学习笔记(十三):加密算法还原(下)· 哈希、HMAC、国密

Comments
Loading...