Lazy loaded image
Frida学习笔记(十五):算法自吐 · Native 层 OpenSSL/BoringSSL Hook
Words 11097Read Time 28 min
2026-7-10
2026-7-10
type
Post
status
Published
date
Jul 10, 2026
slug
summary
本篇目标:解决「Java 层算法自吐没有输出,但 App 确实在做加密」的问题。第14篇的 crypto_monitor.js 覆盖了 Java 标准加密 API,但有些 App 的加密操作完全在 Native 层——直接调用 OpenSSL/BoringSSL 的 C 函数,或者自己用 C 实现加密算法。本篇教你在 Native 层建立同等的「算法自吐」能力:覆盖 EVP 高级 API 和 AES\_/RSA\_/MD5\_\* 低级 API 两套路径,识别自研算法的特征,最终交付一个 native_crypto_monitor.js 拿到密钥和明文。
tags
frida
加解密
实用教程
category
公众号
icon
password
网址
上次编辑时间
Jul 10, 2026 06:40 AM
comment
AI 总结
本篇目标:解决「Java 层算法自吐没有输出,但 App 确实在做加密」的问题。第14篇的 crypto_monitor.js 覆盖了 Java 标准加密 API,但有些 App 的加密操作完全在 Native 层——直接调用 OpenSSL/BoringSSL 的 C 函数,或者自己用 C 实现加密算法。本篇教你在 Native 层建立同等的「算法自吐」能力:覆盖 EVP 高级 API 和 AES\_/RSA\_/MD5\_\* 低级 API 两套路径,识别自研算法的特征,最终交付一个 native_crypto_monitor.js 拿到密钥和明文。
配套脚本:本篇产出的 native_crypto_monitor.js(单文件 1100+ 行、7 个模块开关、EVP/HMAC/AES/RSA/MD5/Base64 全覆盖、多副本 SO 自动装钩、TLS 噪声过滤、Spawn 模式 dlopen 兜底、ANSI 256 色对齐第 14 篇视觉)以及配套的 hook_registernatives.js / find_crypto_constants.js / check_app_crypto_imports.js 等已整理打包。关注本公众号后私信回复关键词「脚本」 即可获取,本系列与后续 Unidbg / SO 逆向 / ARM 汇编 等系列脚本会统一在此发放并持续更新。

一、什么时候需要 Native 层加密分析

1.1 判断加密是否在 Native 层

加载第14篇的 crypto_monitor.js,在 App 中执行目标操作(如登录),观察输出:
  • 有 Cipher/HMAC 输出 → 加密在 Java 层,第14篇已解决
  • 没有任何加密输出,但抓包确认有加密数据 → 加密在 Native 层,本篇的内容
进一步确认:用第06篇的模块枚举脚本看 App 加载了哪些 SO,重点关注:
| SO 文件 | 说明 | | :-- | :-- | | libcrypto.so / libssl.so | OpenSSL/BoringSSL(系统或 App 自带) | | libnative-lib.so / libapp.so | App 自己的 Native 库(可能含自研加密) | | libflutter.so | Flutter 框架(使用 BoringSSL) | | libsqlcipher.so | 加密数据库 |
关于本文的真机样例:为了让方法落地可见、避免"全是抽象代码",本文贯穿一个具体真机样例——酷狗音乐 v20.6.4(Pixel 6 Pro · Android 13 · frida-server 17.9.10)。这只是一个 BoringSSL App 的具体例子,本文讲的所有方法对任何走 BoringSSL/OpenSSL 的 App 同样适用,酷狗不是本文要分析/突破的对象,只是说明性载体。读者拿自己的目标 App 替换样例里的 SO 名即可。
在这个样例上跑第 1.1 节的扫描脚本,输出长这样——一眼能看出 libcrypto_kg.so 是 App 自带的定制 BoringSSL(1668 KB,596 个加密导出,包含完整 AES_* / RSA_* / BN_mod_exp_mont 低级 API 套):
notion image
libcrypto\_kg 导出扫描

1.2 Native 加密的三种形式

形式一:调用系统 OpenSSL/BoringSSL。 Android 系统自带 BoringSSL(Google 的 OpenSSL 分支),提供了 libcrypto.solibssl.so。App 的 Native 代码可以直接 #include <openssl/evp.h> 调用系统库。Flutter 和 Chromium 内核的 WebView 也使用 BoringSSL。
形式二:App 自带 OpenSSL。 有些 App 把自编译的 OpenSSL/BoringSSL 打包在 APK 中(如 lib/arm64-v8a/libcrypto.so),而不是使用系统的。函数名可能相同,但版本和编译选项不同。
形式三:自研加密算法。 少数高安全需求的 App 自己用 C 实现 AES/SM4 等算法,不调用任何标准库。这种情况下导入表中没有 EVP/AES 相关函数,需要通过 IDA 分析识别算法。

二、OpenSSL/BoringSSL 的两套 API · 选择策略

OpenSSL/BoringSSL 对外暴露两套 API,定位不同:
| API 风格 | 例子 | 何时遇到 | | :-- | :-- | :-- | | EVP 高级 API | EVP_EncryptInit_ex / EVP_DigestUpdate / HMAC_Init_ex | 新代码、Conscrypt、Flutter、Chromium 默认走这条 | | 低级 API | AES_set_encrypt_key / AES_cbc_encrypt / RSA_public_encrypt / MD5_Update | 老代码、嵌入式、自带定制 BoringSSL 的大型 App、为追求性能直接调底层算法 |
Hook 策略:实战中两套都要 Hook——你不知道目标 App 走哪条。三步:第三章定位到 SO 里的 crypto 函数 → 第四章加载综合脚本 native_crypto_monitor.js 一次抓全 → 第五章学会读输出(key 形态识别、公钥提取、ctx 累积模式)。

2.1 EVP 是什么

EVP(Envelope)是 OpenSSL 的高级加密 API。它提供了一套统一的接口来使用各种加密算法——不管底层是 AES、DES、ChaCha20 还是 SM4,上层调用的 API 格式都相同。这和 Java 的 Cipher 类设计理念一致。
EVP 的对称加密工作流程:
解密使用对应的 EVP_DecryptInit_ex / EVP_DecryptUpdate / EVP_DecryptFinal_ex

2.2 JCE ↔ OpenSSL/BoringSSL 函数对照表

Hook 时按需查表即可——左列是 Java 层调用、右列是对应的 Native 函数:
| Java JCE | OpenSSL/BoringSSL 函数 | 抓什么 | | :-- | :-- | :-- | | Cipher.init (AES) | AES_set_encrypt_key(userKey, bits, AES_KEY*) | 原始 key、bits | | Cipher.doFinal (AES-CBC) | AES_cbc_encrypt(in, out, len, key, iv, enc) | 明文、密文、IV、方向 | | Cipher.doFinal (AES 单块) | AES_encrypt(in, out, key) | 明文 16B、密文 16B | | Cipher.init/doFinal (DES) | DES_set_key_unchecked + DES_cbc_encrypt | key(8B)、IV(8B)、明密文 | | Cipher.init/doFinal (3DES) | DES_ede3_cbc_encrypt | 三组 key、IV、明密文 | | Cipher.doFinal (EVP 高层) | EVP_EncryptInit_ex + EVP_EncryptUpdate + EVP_EncryptFinal_ex | key、iv、明密文 | | Cipher.doFinal (RSA 加密) | RSA_public_encrypt(flen, from, to, rsa, padding) | 明文、密文、padding | | Signature.sign (RSA) | RSA_sign(type, m, mlen, sig, siglen, rsa) | 摘要、签名 | | MessageDigest.update + digest | MD5_Update / MD5_FinalSHA256_Update / SHA256_Final 或 EVP\_Digest\* | 输入(可多次)、最终输出 | | Mac.doFinal (HMAC) | HMAC_Init_ex / Update / Final 或一次性 HMAC | key、消息、tag |
补充: - 新写的 Native 代码更倾向走 EVP 系列,而不是直接调 AES_* / DES_* / RSA_* 低级 API。EVP_EncryptInit_ex(ctx, type, impl, key, iv) 一次拿到 key 和 iv,EVP_EncryptUpdate 拿到明密文,一组 Hook 覆盖所有对称算法。优先尝试 EVP;只有当 SO 里没有 EVP 导出符号时,再退回低级 API。 - **RSA_public_encrypt 底下最终会调 BN_mod_exp_mont** 完成 c = m^e mod n,第 3、4 参数(指数 e、模数 n)直接暴露 RSA 公钥本体——IDA 里看不到明文公钥常量(key 是从 PEM 解析出来或堆上分配)时,这是最稳的公钥提取路径,第 4 章脚本段 6 内置 + 第 5.2 节讲怎么读出来的公钥。

三、三步定位法:从 Java 到 SO 函数

定位 Native 加密入口的标准流程:
第一步,在 jadx 里找到声明为 native 的方法,记下完整签名(类名 + 方法名 + JNI 签名)。
第二步,Hook libart.so 的 RegisterNatives 拿到函数地址。Java native 方法通过 RegisterNatives 绑定到 SO 里的具体地址。
在样例 App(Android 13)上 spawn 跑出来是这样的。
notion image
hook\_registernatives 真机命中
接下来每条 [RegisterNatives] 命中包含 5 个维度:
| 字段 | 例子 | 用途 | | :-- | :-- | :-- | | java_class | android.NetworkUtils | Java 类全限定名 | | name | bindProcessToNetworkHandle | Java 方法名 | | sig | (J)Z(I)Z 等 | JNI 签名(参数类型 + 返回类型) | | fnPtr + fnOffset | 0x6e4a3e860libframework-connectivity-jni.so!_ZN7android..._utils_bindProcessToNetworkHandleEP7_JNIEnv+0x60 | 函数实现地址 + SO 偏移 | | callee | libframework-connectivity-jni.so!_ZN7android33register_android_net_NetworkUtilsEP7_JNIEnv+0x60 | 谁在 JNI_OnLoad 里调 RegisterNatives 注册的 |
第三步,用 IDA/Ghidra 打开对应 SO,跳到那个偏移,看调用了哪个 crypto API。看到 AES_cbc_encryptRSA_public_encryptMD5_Update 这类名字,直接 Hook 这些标准函数即可,业务函数本身可以不深入逆向。看不到名字(stripped)时需要用常量特征定位(S-box、0x10001、MD5 IV 67 45 23 01),那部分见 第七章 自研算法识别。

四、完整的 native\_crypto\_monitor.js

上一章把"从 Java 到 SO 函数"的入口定位讲清楚了;本章给一个综合脚本 native_crypto_monitor.js,把 EVP 高级 API、HMAC、AES + RSA + MD5 低级 API、Base64 编解码一次性覆盖,加载即用。这个脚本是本篇的核心交付物。

脚本结构 · 7 段拼装

脚本按 7 段 给出——这是结构骨架,按顺序拼起来就是一份能跑的 native_crypto_monitor.js。配置开关在段 1,可按需关闭模块以减少日志噪音。本章最后一节列出生产环境下还需要的几项工程化能力(多副本装钩、TLS 噪声过滤、ANSI 着色 ……);完整版(单文件 1100+ 行,文末关注公众号回复关键词获取)已合入这些能力,复制即用。
#### 段 1 · 入口 + 配置 + 辅助函数
CONFIG 集中六个模块开关 + dump 字节上限 + 调用栈控制。辅助函数中 getEvpCipherName 是个小亮点——通过 EVP_CIPHER_CTX_cipher + EVP_CIPHER_nid + OBJ_nid2sn 三步反查算法名(aes-128-cbc / sm4-cbc 等),后续 EVP Hook 输出的算法标签全靠它。
本段重点:CONFIG 开关 / getCryptoExport 跨 SO 多名查找 / safeDump 限长 + 编码兼容 hex / getNativeStack 可选 backtrace / getEvpCipherName 三步反查。后面 6 段只调用这些 helper,不再展开。
#### 段 2 · EVP 对称加密 · EVP_Encrypt* / EVP_Decrypt*
覆盖 Init / Update / Final 三步。Init 拿算法名 + 密钥 + IV;Update 拿明文 + 密文,顺便尝试把输入解为可读字符串(JSON、URL 等场景下 hex 看起来无意义但文本很直观);Final 拿最后一块填充。三步都用 EVP_CIPHER_CTX_cipher 反查算法名,日志里直接显示 aes-256-cbc 而不是 NID 数字。
本段重点:三个 forEach 循环分别 hook Init / Update / Final,每个 onLeave 都用 retval !== 1 过滤失败调用;密钥长度根据算法名后缀 128/192/256 推断;IV 固定 16 字节。
#### 段 3 · EVP 哈希 · EVP_Digest*
只 Hook EVP_DigestUpdate + EVP_DigestFinal_ex——不 Hook Init 是因为算法名要从 ctx 反查,需要的额外代码与 Init 本身价值不匹配;真正需要时根据输出长度(MD5=16 / SHA-1=20 / SHA-256=32 / SHA-512=64)反推即可。
#### 段 4 · HMAC · 一次性 + 三步
OpenSSL HMAC 有两种用法:一次性 HMAC() 把 key+消息+输出一次塞过去,或 HMAC_Init_ex / HMAC_Update / HMAC_Final 三步链。两种都 Hook。API 签名场景里的密钥常常是可读字符串(如 "app_secret_key_2024"),所以 key 输出同时给 hex 和 utf8 两种形态。
本段重点:四个独立 hook —— 一次性 HMAC()(key + 数据 + 摘要一次性拿到)、HMAC_Init_ex(key)、HMAC_Update(每段消息)、HMAC_Final(摘要)。Update 每段独立打印,看 ctx 聚合输出的形态见第 5.4 节截图。
#### 段 5 · AES 低级 API · AES_set_encrypt_key / AES_cbc_encrypt
针对不走 EVP 直接调用 AES\_\* 的代码——自带定制 BoringSSL 的大型 App、嵌入式 SDK、老代码常见。关键AES_cbc_encrypt 的第 4 参是 key schedule 展开后的扩展密钥(176/240 字节,反推不出来),必须同时 hook AES_set_encrypt_key 抓原始 key——输出的具体含义见第 5.1 节解读。
本段重点:三个 hook —— AES_set_encrypt_key(原始 key + bits)、AES_set_decrypt_key(同上但解密方向)、AES_cbc_encrypt(IV + 明密文 + enc/dec 方向)。前两个对应第 5.1 节截图中的"双行密钥"事件。
#### 段 6 · RSA + MD5 低级 API · RSA_public_encrypt / BN_mod_exp_mont / MD5_Update / MD5_Final
针对不走 EVP 直接调用 RSA\_\ / MD5\_\ 的代码。RSA 段双 hook:RSA_public_encrypt 抓明密文,BN_mod_exp_mont 解 BIGNUM 拿公钥本体(e + n)——具体读法见第 5.2 节。MD5 段按 MD5_CTX* 索引累积 Update 输入,Final 时一次性吐出所有分段 + 摘要——即便业务方复用同一 ctx 也不串(见第 5.5 节的 ctx 累积模式)。BN_mod_exp_mont 也被 DH/DSA 调用,BIGNUM 读不出时静默跳过避免噪音。
本段重点:四个 hook —— readBigNumHex helper(按 BoringSSL {d, top} 布局读 BIGNUM 转大端 hex)、RSA_public_encrypt(flen / padding / 明密文)、BN_mod_exp_mont(args\[2\] 指数 e、args\[3\] 模数 n,公钥本体)、MD5_Update + MD5_Final 配对(按 args[0] ctx 指针索引累积 chunks,Final 时一次性吐出)。MD5 这套 ctx-key 累积模式可直接套到 EVP\_Digest / HMAC\_Update 上做相同的"分段聚合"输出。
#### 段 7 · 启动信息
加载完成横幅。和第 14 篇 Java 层算法自吐保持同一视觉风格——单边横线 + emoji,避免中英文混排导致右侧框线对不齐。
**零代码替代方案 frida-trace**:如果只想快速验证目标 SO 是否走 EVP / AES / RSA 路径、不需要明密文,frida-trace -U -f <包名> -i 'AES_*' -i 'RSA_*' -i 'EVP_*' 一条命令即可——自动生成模板 handler,函数命中就打印。本节脚本的价值是抓全参数 + 智能格式 + 调用栈 + ctx 累积,frida-trace 只告诉你"有没有调"。两者互补:先用 frida-trace 看哪些函数被命中,再用本脚本拿数据。

完整版补强清单

上面 7 段把 Hook 时机和参数布局讲清楚,但直接放到生产 App 上会撞到几堵墙。完整版(文末关注公众号回复关键词获取)在 7 段之上额外合入了下面这些能力,每一项都对应一个真机踩过的坑:
| 能力 | 解决的问题 | 关键接口 | | :-- | :-- | :-- | | **safeAttach 多副本装钩** | 腾讯系 App 一个进程内 3+ 份 libcrypto.sofindModuleByName 只返第一个,业务真实加密的副本可能整体 0 命中 | enumerateModules + addr 去重 | | Base64 段 | JWT / cookie / 自定义协议高频走 BoringSSL 单步 base64,与第 14 篇 Java 层 android.util.Base64 形成同一事件的双观察点 | EVP_EncodeBlock / EVP_DecodeBlock | | **isTlsNoise 握手噪声过滤** | TLS 1.2 的 tls1_P_hash PRF、TLS 1.3 的 HKDF_extract/expand + SSLTranscript::GetFinishedMAC 会触发海量派生,淹没业务事件 | caller LR + 符号名双层匹配 | | **BN_mod_exp_mont 完整四元组** | 只 dump modulus N + e 没法验证 hook 抓到的真是这次 RSA 运算 | r = a^p mod m 四个 BIGNUM + caller LR | | ANSI 256 色 + buf 累积 | 多线程并发时字段级输出会被其它线程割开,与第 14 篇 Java 版的视觉风格保持一致 | 事件累积成单次 console.log | | **android_dlopen_ext spawn 兜底** | spawn 模式下 setImmediate 早于 SO 加载,getCryptoExport 全返 null,hook 全装不上 | dlopen 命中后 installAllHooks() | | 一次性 export 单独 hook | BoringSSL 把 MD5(d,l,m) / HMAC(...) / EVP_Digest(...) 内部 Init/Update/Final 编译期内联,只 hook Update/Final 会出现"0 段 Update"怪象 | 直接 hook 一次性 export 本身 | | 性能控制三件套 | TLS 解密路径上 Update 类 hook 一帧上千次,不限速直接 ANR | skipHugeUpdates + rateLimitPerSecond + showBacktrace 默认关 | | **hideTestVectors 自检过滤** | AES / DES / RSA 库启动自检会用全零 key 与 NIST 测试向量打几次,污染输出 | key 内容匹配后隐藏 |
每一项的实现都不复杂,但踩过一遍才知道哪几项是必须的——上面的清单就是踩过的坑的索引。

五、输出格式解读

脚本跑起来后会吐出大量日志。真正的功夫是「怎么从日志读出业务模式」——不是去看每条 hook 命中,而是识别几种典型形态。下面五节按 AES / RSA / SHA-256 / HMAC / MD5 顺序展开五种输出形态,每节给一张真机截图 + 关键字段读法。

5.1 AES 输出

样例 App 跑出来一条典型的 AES 输出长这样:
notion image
native\_crypto\_monitor AES 命中
两条相关事件配对 —— 第一条 AES set_encrypt_key · 256 bits 抓密钥装载,第二条 AES_cbc_encrypt · ENCRYPT · 160 bytes 抓实际加密:
密钥字段的双行设计 —— 第一行用引号包裹显示 "fd387891254e6cedc4019ca0061ea6d9",因为 32 个字节全部落在 ASCII 可打印范围(0x30-0x66),脚本的 smartFormat 自动识别成 text 视图。第二行是同样字节的 hex 展开 6664333837...(首字节 0x66 = 'f'、0x64 = 'd'、0x33 = '3' ...)。

5.2 RSA 输出

RSA 比 AES 多一步——除了明密文,还要拿到公钥才能在 Python 里独立加密任意 payload。
notion image
native\_crypto\_monitor RSA 命中
**这是 BN_mod_exp_mont · r = a^p mod m 的命中** —— BN_mod_exp_mont 是 BoringSSL 内所有 RSA 运算(公钥加密 / 公钥验签 / 私钥解密 / 私钥签名)的底层"模幂运算" helper。覆盖率比 RSA_public_encrypt 高一个数量级——后者只在业务直接调高级 API 时才命中,前者所有路径都走。
四个字段一一对应到 RSA 数学:
| 字段 | 含义 | 这条记录的值 | | :-- | :-- | :-- | | 模数 m | RSA modulus N | 2048 bit,开头 0xd33a629f... | | 指数 p | 公钥 e 或私钥 d | 0x10001 = 65537 = 标准公钥指数 | | 输入 a | 被加密 / 被验签的数据(已 padding) | 0x387c3db8... | | 输出 r | 计算结果 = a^p mod m | 0x47c7ce3d... |
指数判读约定 —— 脚本对 指数 p 短于 16 个 hex 字符的视为公钥(e 通常是 65537),更长的认为是私钥 d(2048 bit)。这里 0x10001 是教科书级的公钥指数。
抓到 m + e 就可以直接构造 Python 公钥(任意 PKCS1 / NoPadding 明文都可加密),不需要 X.509 / PEM 序列化:

5.3 SHA-256 输出

notion image
native\_crypto\_monitor SHA-256 命中
**标签 SHA-256 (EVP) · 5 段 Update** —— (EVP) 后缀表示业务走的是 EVP 高级 API(EVP_DigestInit_ex + EVP_DigestUpdate × N + EVP_DigestFinal_ex),脚本通过 EVP_DigestFinal_ex 拿到最终摘要长度(32 字节 → SHA-256),通过 EVP_DigestUpdate 按 ctx 累积所有 Update 输入,Final 时一次性 dump。
5 个 ①②③④⑤ 圈数字是 printMultiInput 的多段标记。这里的输入序列展示了 图片缓存 key 派生的常见模式
  1. "false" ——某个布尔属性序列化
  1. ② 完整 URL(图片真实地址)
  1. ③ 32 字节 hex —— 大概率是 thumbnail 之类的 hash prefix
  1. -com.tencent.image.rcbitmap.a#200767... —— 业务组件 ID
  1. android.support.rastermill.FrameSequenceDrawable —— Drawable 类名
复合 hash → 缓存 key。**摘要 fc3ca943... 就是这个图片在本地缓存里的文件名前缀**。

5.4 HMAC 输出

notion image
native\_crypto\_monitor HMAC 命中
同一帧抓到 HMAC 的三种形态 —— 这是脚本对 BoringSSL HMAC 系列 API 全覆盖的产物:
| 副标题 | 来源 hook | 含义 | | :-- | :-- | :-- | | Native · oneshot | 业务直接调一次性 API HMAC(evp_md, key, klen, data, dlen, md, &mlen) | 一行代码出签名,典型业务调用 | | Native · N 段 Update | 业务三步:HMAC_Init_ex + HMAC_Update × N + HMAC_Final | 大块数据 / 流式签名 | | Native · 0 段 Update | HMAC_Final 命中但 HMAC_Update 没抓到 | BoringSSL 内部 Update 被 inline,我们的 export hook 只拦到 Final |

5.5 MD5 输出

notion image
native\_crypto\_monitor MD5 命中
输入与摘要字段含义同第 5.3 节 SHA-256,按 ctx 累积 N 段 Update + Final 16 字节摘要,不再赘述。

六、BoringSSL 与标准 OpenSSL 的差异

6.1 为什么需要单独讨论 BoringSSL

BoringSSL 是 Google 的 OpenSSL 分支,Android 系统自带的 libcrypto.so / libssl.so 实际上就是 BoringSSL。Flutter、Chromium WebView、Cronet 也使用各自编译的 BoringSSL。
BoringSSL 与标准 OpenSSL 的关键差异影响 Frida Hook:
| 差异点 | OpenSSL | BoringSSL | | :-- | :-- | :-- | | HMAC API | OpenSSL 1.1.0+ 推荐 HMAC_CTX_new / HMAC_CTX_free | 保留 OpenSSL 1.0.x 老风格的 HMAC_CTX_init / HMAC_CTX_cleanup | | 符号可见性 | 大部分函数有导出符号 | 很多内部函数被 static 隐藏,不在导出表中 | | SO 名称 | libcrypto.so.1.1 / libssl.so.1.1 | libcrypto.so / libssl.so(无版本后缀) | | EVP\_CIPHER\_nid | 通过 EVP_CIPHER_nid 获取 | 同上,但部分版本改名为 EVP_CIPHER_get_nid | | 内部函数名 | ssl3_read_bytes 等 | 名称可能不同,如 ssl_read_buffer_extend |

6.2 Hook BoringSSL 的注意事项

兼容查找的逻辑已经在第 4.1 节段 1 主脚本的 getEvpCipherName 里实现——它内部就是按以下顺序依次尝试:
如果想看当前 SO 命中的是哪个名字,在 getEvpCipherName 里加一行 console.log('[diag] cipher getter =', getCryptoExport("EVP_CIPHER_CTX_cipher") ? "cipher" : "get0_cipher") 即可;自己另写脚本时把 getEvpCipherName 整段抄过去复用 fallback 即可,不必重新搭这套。想直接在 REPL 里枚举命名:

6.3 Flutter App 的特殊处理

Flutter 内嵌的 BoringSSL 在 **libflutter.so** 中而不是系统的 libcrypto.so,本篇 native_crypto_monitor.js 默认只搜索 libcrypto.so,对 Flutter App 必须改搜索目标:
判断 App 是否为 Flutter 的最快方法:
Dart 端的 package:cryptography / package:pointycastle 最终都落到内嵌 BoringSSL,所以 Flutter App 的所有 Native 加密都集中在 libflutter.so。Cronet(Chromium 网络栈)同样内嵌一份 BoringSSL,部分大厂 App 用 Cronet 做网络层时也要看 libcronet.so

七、自研算法的识别与分析

7.1 如何判断是自研算法

当以上所有 Hook 都没有输出,但 App 确实在做加密(抓包可见密文)时,很可能是自研加密。
进一步确认:

7.2 IDA 中识别常见算法

当确认是自研加密后,需要用 IDA/Ghidra 打开 SO 文件进行静态分析:
AES 的特征
  • 包含 AES S-Box 常量表(256 字节的查找表,以 0x63, 0x7c, 0x77, 0x7b 开头)
  • 有 10/12/14 轮循环(分别对应 AES-128/192/256)
  • IDA 插件 FindCrypt 可以自动识别
DES/3DES 的特征
  • 包含 DES 初始置换表(IP 表)和最终置换表(FP 表)
  • S-Box 是 8 个 4×16 的二维数组
SM4 的特征
  • 包含 SM4 S-Box(以 0xD6, 0x90, 0xE9, 0xFE 开头,与 AES 不同)
  • 32 轮循环
  • 系统密钥 FK = {0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc}
MD5 的特征
  • 初始值 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476
  • 64 个 T 表常量
SHA-256 的特征
  • 初始值 0x6a09e667, 0xbb67ae85, 0x3c6ef372, ...
  • 64 个 K 表常量

7.3 Memory.scan 搜索算法常量

不用 IDA 也能做初步判断——在 Frida 中搜索已知的算法常量:
如果搜索到 AES S-Box 但没有 EVP 导入——说明 SO 自己实现了 AES。接下来你需要在 IDA 中找到引用该 S-Box 的函数,那个函数就是 AES 加密/解密的入口。然后用 Frida 的 Interceptor.attach 通过偏移地址 Hook 它,捕获参数。
在样例 App 上跑这个脚本,能看到一个典型的「商业加固 SO + 现代 BoringSSL」扫描结果:
notion image
find\_crypto\_constants 在 QQ 音乐上的真机扫描结果
输出解读 —— 头部块 [*] 扫描 libxxx.so (NNN KB) 后跟两类结果:
  • [SKIP] AES S-Box / SM4 S-Box / MD5 Init / DES IP — access violation accessing 0xXXXX —— 商业加固 SO(libturingbase / libtmesec / libDownloadProxy 等)把 .rodata 段标成 PROT_NONEMemory.scan 触发访问异常,脚本 try/catch 静默跳过。这是预期行为,不是脚本 bug,扫不动的 SO 不阻塞后续扫描。
  • [FOUND] AES S-Box @ libstar.so+0x961f8 / [FOUND] AES S-Box @ libostar.so+0x98a1b / [FOUND] SM4 S-Box @ libTPCore-master.so+0x94c328 —— 真正命中:libstar.so 内含 AES S-Box(自实现 AES)、libTPCore-master.so 内含 SM4 S-Box(国密算法)。

7.4 Hook 自研加密函数

假设通过 IDA 分析确定了自研 AES 函数的偏移和参数:
如何从 IDA 拿到这个 0x4A8C 偏移? 第 7.3 节扫到 AES S-Box 在 SO 内某个地址后,在 IDA 中右键 Jump to xref to operand,跳到引用该 S-Box 的函数,函数入口的 RVA(相对于 SO 基址的偏移)就是这里的 0x4A8C。把参数签名按 IDA 的类型推断(Y 键)猜出来,就能写 Hook。

八、Java + Native 双层监控

实战中最好的做法是同时加载 Java 和 Native 两个监控脚本,确保不遗漏任何加密操作:
两个脚本不冲突——一个 Hook Java 类,一个 Hook Native 函数,各管各的层。

九、适用边界:三类例外场景

本篇覆盖的是「标准库 + 符号未 strip + SO 未加固」的理想场景。实战中你会遇到三类例外:
第一类,符号被 strip。SO 里 AES_cbc_encryptMD5_UpdateEVP_* 等函数不在导出表里,findExportByName 返回 null。识别信号:本篇主脚本启动横幅中 [OK] 行寥寥无几,enumerateExports() 输出里看不到任何 AES_* / EVP_* / RSA_* 命名,但 enumerateImports() 还能见到——说明业务 SO 引用了加密符号、但目标 SO 把它们藏起来了。需要用算法常量特征定位:AES S-box 首字节 63 7C 77 7B、Rcon 表 01 02 04 08 10 20 40 80 1B 36、MD5 IV 67 45 23 01、SHA-256 IV 6A 09 E6 67。IDA 配 FindCrypt 插件能自动标注这些常量,再通过交叉引用回溯到使用它们的函数。第七章已给出 Frida 端 Memory.scanSync 在运行时直接搜索常量的方案。
第二类,SO 被加固(360、爱加密、梆梆等)。SO 文件磁盘内容是壳代码,真正的加密逻辑解密后才出现在内存里,findExportByName 拿到的可能是壳的桩函数甚至 null。识别信号Process.enumerateModules() 里看到 libjiagu(360)/ libshell(腾讯乐固)/ libDexHelper(梆梆)/ libnesec(网易易盾)/ libsec2023 / libsec2024(爱加密年份命名)/ libtmesec 之类的"壳" SO 与业务 SO 并存;或者 Memory.scan 在业务 SO 的 .rodata 上触发 access violation(参考第 7.3 节 [SKIP] 输出形态)。需要先做内存 dump、修复 ELF 头(常用 010 Editor 把原始加固 SO 的头复制过来)、再用 SoFixer 还原成可静态分析的形态。
第三类,算法被魔改或自实现。S-box 被换了、轮数被改了、用了查表法白盒,常量识别也失效。识别信号:第 7.3 节常量扫描全无命中、抓包却看到明显的加密数据;或扫到 S-box 但偏移落在非标准位置(如紧贴一段循环展开的 NEON 汇编、或字节序与官方表恰好不一致)。这种场景下 Hook 思路不变(还是定位函数、抓输入输出),但需要先把魔改逻辑搞清楚才能在 Python 里复现。
这三种进阶场景的完整脱壳和反魔改流程,留给第21篇《Native Hook 入门》专门讲。

总结

Java 层算法自吐没输出但抓包确认有加密,加密就在 Native 层。一条标准路径:
  1. 两套 API 都要覆盖:EVP 高级(现代代码、Conscrypt、Flutter)+ AES_* / RSA_* / MD5_* 低级(老代码、定制 BoringSSL)——目标 App 走哪条事先不知道,主脚本同时挂上。
  1. 入口定位用 RegisterNatives:jadx 找 native 方法 → Hook libart.so!RegisterNatives 拿到 SO 偏移 → IDA 跳过去看调用的是哪个 crypto API。看到标准函数名直接 Hook,不必深入业务代码。
  1. **native_crypto_monitor.js 一次覆盖六类**:EVP + HMAC + AES + RSA + MD5 + Base64,配 safeAttach 多副本装钩、isTlsNoise 握手噪声过滤、BN_mod_exp_mont 四元组 RSA 公钥提取、ANSI 256 色对齐第 14 篇视觉——加载即用,是日常分析的第一枪。
  1. 加载脚本没输出 = 自研或加固:IDA FindCrypt + Frida Memory.scanSync 走 AES S-Box / SM4 S-Box / MD5 IV / SHA-256 IV 常量定位;strip / 加固 / 魔改三类进阶场景见第 21 篇《Native Hook 入门》。
  1. 双层加载crypto_monitor.js(Java 层,第 14 篇)+ native_crypto_monitor.js(Native 层,本篇)同时挂上,是确认"加密在哪一层"最快的姿势。

📦 获取本篇脚本

native_crypto_monitor.js 完整版(单文件 1100+ 行,EVP / HMAC / AES / RSA / MD5 / Base64 六类全覆盖,含 safeAttach 多副本装钩、isTlsNoise TLS 噪声过滤、android_dlopen_ext spawn 兜底、BN_mod_exp_mont 完整四元组、ANSI 256 色对齐第 14 篇视觉、性能控制三件套)以及配套的 hook_registernatives.js / find_crypto_constants.js / check_app_crypto_imports.js / enum_evp_cipher_exports.js / hook_self_research_aes.js + README 使用说明,已统一打包:
  1. 关注原博主公众号【泡泡以安】
  1. 私信回复关键词「脚本」
回复内含本系列与其它系列(Unidbg / SO 逆向 / ARM 汇编 ……)的脚本汇总,长期维护更新。
上一篇
Frida学习笔记(十六):Root 检测绕过
下一篇
Frida学习笔记(十四):算法自吐 · 一个脚本监控所有加密

Comments
Loading...