某音 HTTPS 抓包与 libsscronet.so 逆向分析

三月 18, 2025 / Linso

本文仅用于安全研究、逆向学习和自有测试环境分析请勿用于未授权的数据抓取、攻击目标应用、绕过他人系统保护或窃取用户隐私

1. 背景

在分析某音 App 的 HTTPS 流量时,常规抓包方式通常会遇到两个问题:

  1. 系统代理已经配置,抓包工具也安装了 CA 证书,但关键请求仍然无法解密。
  2. 使用常见的 Java 层 SSL Hook 或 JustTrustMe 一类模块后,仍然只能抓到少量请求,后续连接可能出现 Connection reset by peer

原因在于这类 App 并不完全依赖 Android 系统原生网络栈,也不只使用 Java 层的 javax.net.ssl。它集成了基于 Chromium 网络栈改造的 Cronet,并将大量 TLS、QUIC、证书链验证和 pinning 逻辑放在 Native 库 libsscronet.so 中。

也就是说,系统证书信任、Java 层 TrustManager Hook、OkHttp Hook 等方式并不能覆盖全部请求。真正的证书校验发生在 libsscronet.so 内部,底层依赖 BoringSSL,并通过自定义 verify callback 接管握手时的证书验证流程。

本文记录一次 libsscronet.so 的逆向定位和 Patch 思路。重点不是简单地“让函数返回成功”,而是说明为什么直接入口早退容易闪退,以及如何选择更稳定的最终失败返回点进行 Patch。

2. Cronet 与 BoringSSL 的校验结构

Cronet 是 Chromium 的网络库。App 集成 Cronet 后,HTTPS 请求大致会走以下流程:

App 请求
  -> Cronet 网络栈
  -> BoringSSL 创建 SSL_CTX / SSL
  -> SSL_CTX_set_custom_verify 注册自定义证书校验回调
  -> TLS / QUIC 握手
  -> 提取服务端证书链、OCSP、SCT 等信息
  -> Cronet CertVerifier 执行证书链校验和 pinning 判断
  -> 返回 ssl_verify_ok / ssl_verify_invalid / ssl_verify_retry

关键点是 SSL_CTX_set_custom_verify

在 BoringSSL 中,SSL_CTX_set_custom_verify(ctx, mode, callback) 可以为 SSL 上下文设置自定义证书校验逻辑。Cronet 使用它来接管默认校验流程,因此我们可以围绕这个 API 反向定位证书校验入口。

常见返回语义如下:

0 = ssl_verify_ok
1 = ssl_verify_invalid
2 = ssl_verify_retry

实际版本中函数名可能被 strip 掉,但导入符号、字符串和调用模式仍然可以帮助定位。

3. 为什么 Java 层 Hook 经常无效

传统 SSL Pinning 绕过常见目标包括:

javax.net.ssl.X509TrustManager
javax.net.ssl.SSLContext
okhttp3.CertificatePinner
TrustManagerImpl.checkTrustedRecursive

这些方法对 Java/Kotlin 层网络库有效,但 Cronet 的证书链处理往往发生在 Native 层。请求进入 libsscronet.so 后,Java 层 Hook 不一定能影响 BoringSSL 的握手校验结果。

因此,当出现以下现象时,就应该优先怀疑 Native 层 Cronet 校验:

1. Java 层 SSL Hook 已生效,但仍然有大量 HTTPS 请求失败。
2. 抓包工具只能看到少量前置请求,后续请求断开。
3. 日志或抓包工具中出现 connection reset、certificate verify failed 等提示。
4. so 中存在 Cronet、BoringSSL、QUIC 相关符号或字符串。

4. IDA 中的定位流程

4.1 导入符号搜索

用 IDA 打开 libsscronet.so 后,优先搜索以下导入:

SSL_CTX_set_custom_verify
SSL_get0_peer_certificates
SSL_get0_ocsp_response
SSL_get0_signed_cert_timestamp_list
SSL_set_enforce_rsa_key_usage
CRYPTO_BUFFER_data
CRYPTO_BUFFER_len

其中最关键的是:

SSL_CTX_set_custom_verify

对它查看 xrefs,通常会看到两个调用点:

TLS 路径  -> 注册普通 HTTPS/TLS 证书验证回调
QUIC 路径 -> 注册 QUIC 握手证书验证回调

这也是很多初次 Patch 失败的原因:只处理 TLS 不处理 QUIC,或者只处理 QUIC 不处理 TLS,都可能导致请求仍然失败。

4.2 字符串辅助定位

可以继续搜索以下字符串:

Cronet_CertVerify_DoVerifyV2
Cronet_VerifyParamsV2
Cronet_VerifyResult
Cert chain verification failed:
SSL_PINNED_KEY_NOT_IN_CERT_CHAIN
CERT_AUTHORITY_INVALID
CERT_COMMON_NAME_INVALID
QUIC_SESSION_CERTIFICATE_VERIFY_FAILED
AndroidCertVerifyResult

其中 Cert chain verification failed: 通常能帮助定位 QUIC 证书链验证函数。

5. 一次实际版本的调用链示例

以下地址来自一次具体版本分析。不同版本偏移会变化,不应直接照搬地址,应该照搬定位方法。

5.1 TLS 路径

TLS 初始化函数中可以看到类似调用:

SSL_CTX_set_custom_verify(ctx, 1, tls_custom_verify_callback);

在某一版中定位结果为:

SSL_CTX_set_custom_verify 调用点: 0x3D61A8
TLS verify callback:        0x3D58B8
结果转换函数:                0x3D5C58
最终失败返回点:              0x3D5D70

tls_custom_verify_callback 内部会做很多事情:

1. 通过 SSL_get0_peer_certificates 提取服务端证书链。
2. 读取 OCSP response。
3. 读取 Signed Certificate Timestamp list。
4. 构造 Cronet VerifyParams。
5. 调用内部 CertVerifier。
6. 将内部验证结果转换成 BoringSSL 需要的 verify result。

真正适合 Patch 的位置不是 callback 开头,而是结果转换函数里的失败返回点。

原始指令:

MOV W0, #1

含义是返回 ssl_verify_invalid

Patch 后:

MOV W0, #0

含义是失败路径在完成证书链解析、错误对象构造、日志和内部状态更新后,最终向 BoringSSL 返回 ssl_verify_ok

机器码:

00 00 80 52

5.2 QUIC 路径

QUIC 也会注册 custom verify callback:

SSL_CTX_set_custom_verify(ctx, 1, quic_custom_verify_callback);

某一版中定位结果为:

SSL_CTX_set_custom_verify 调用点: 0x52BC08
QUIC verify callback:       0x52C098
QUIC 证书链验证函数:          0x59928C
最终失败返回点:              0x599488

QUIC 证书链验证函数中能看到典型日志:

Cert chain verification failed:

失败分支原始指令:

MOV W21, #1

Patch 后:

MOV W21, #0

机器码:

15 00 80 52

这里不是直接改 W0,因为该函数最后通过 MOV W0, W21 或等价逻辑返回结果。要结合上下文判断实际承载返回值的寄存器。

6. 稳定 Patch 原则:不要入口早退

一开始很容易想到直接把 verify callback 改成:

PACIASP
MOV W0, #0
AUTIASP
RET

这确实能让证书验证立即返回成功,但它存在明显风险。

Cronet 的 verify callback 不只是返回一个值,它还会填充后续逻辑依赖的内部状态:

证书链对象
OCSP 数据
SCT 数据
host / cert verify params
cert status
is_issued_by_known_root
pinning 判断结果
错误对象和日志信息
异步 verify request 状态

如果入口直接早退,这些字段没有初始化,App 可能短时间内能抓到一点包,但后续请求会闪退或断开。常见表现是:

-4 Connection reset by peer

更稳的方式是:

1. 保留 verify callback 原始主体。
2. 让它正常解析证书链并填充状态。
3. 只在最终失败返回点把 invalid 改成 ok。

也就是只改最后的结果,不破坏中间副作用。

7. 通用分析步骤总结

每次遇到新版本 libsscronet.so,不要直接套旧偏移,按下面流程重新定位:

7.1 找 custom verify 安装点

在 IDA 中搜索导入:

SSL_CTX_set_custom_verify

查看 xrefs。通常能看到两个调用点:

TLS:  SSL_CTX_set_custom_verify(ctx, 1, callback_a)
QUIC: SSL_CTX_set_custom_verify(ctx, 1, callback_b)

第三个参数就是回调函数。

7.2 分析 TLS callback

进入 TLS callback 后,重点看:

SSL_get0_peer_certificates
SSL_get0_ocsp_response
SSL_get0_signed_cert_timestamp_list
内部 CertVerifier 调用
结果转换函数

如果 callback 里有一个函数专门把内部结果转换成 0/1/2,它通常就是最适合 Patch 的位置。

7.3 分析 QUIC callback

QUIC callback 可能很短,只是取出上下文后跳到真正的证书链验证函数。

可以用字符串辅助定位:

Cert chain verification failed:
../../net/third_party/quiche/src/quiche/quic/core/tls_handshaker.cc
QUIC_SESSION_CERTIFICATE_VERIFY_FAILED

找到失败分支后,确认最终返回寄存器。它不一定是直接 W0,也可能先写入 W21,函数退出前再把 W21 放入 W0

7.4 Patch 最终失败返回

常见修改:

MOV W0, #1  -> MOV W0, #0
MOV W21,#1  -> MOV W21,#0

常见机器码:

MOV W0,  #0 = 00 00 80 52
MOV W21, #0 = 15 00 80 52

不同寄存器的机器码不同,不能只记机器码,要结合当前函数上下文判断。

8. Patch 后验证

Patch 后建议做三类验证。

8.1 验证入口没有早退

证书 verify callback 的函数入口应保持原始 prologue。例如:

PACIASP
SUB SP, SP, #...
STP X29, X30, ...

不要看到:

PACIASP
MOV W0, #0
AUTIASP
RET

8.2 验证失败返回点已改

例如某一版最终验证字节为:

003D58B8: 3F 23 03 D5 FF C3 06 D1 FD 7B 15 A9 FC 6F 16 A9
0052C098: 3F 23 03 D5 FD 7B BE A9 F3 0B 00 F9 FD 03 00 91
003D5D70: 00 00 80 52
00599488: 15 00 80 52

其中前两行说明 TLS/QUIC callback 入口保持原始逻辑,后两行说明最终失败返回点已改成 OK。

8.3 验证运行状态

替换 so 后启动 App,观察:

1. App 是否能稳定运行,不再几秒后闪退。
2. HTTPS 请求是否能持续出现在抓包工具中。
3. QUIC 请求是否仍然失败。
4. 是否还有完整性校验或二次 pinning 判断。

9. 替换 so 的注意事项

如果是在 Root 设备上直接替换应用目录中的 so,基本流程是:

# 备份原始 so
mv /data/app/.../lib/arm64/libsscronet.so /data/app/.../lib/arm64/libsscronet.so.bak

# 复制 patched so
cp /sdcard/libsscronet.so /data/app/.../lib/arm64/libsscronet.so

# 设置权限
chmod 755 /data/app/.../lib/arm64/libsscronet.so

注意事项:

1. 替换前必须备份原文件。
2. 文件权限错误会导致 so 加载失败。
3. App 如果做了完整性校验,物理替换 so 可能触发崩溃。
4. 物理替换失败时,可以考虑运行时 Hook 或在加载后内存 Patch。

10. 常见问题排查

10.1 App 打开几秒后闪退

优先检查是否做了入口早退 Patch。

如果 verify callback 入口被改成:

MOV W0, #0
RET

建议恢复入口,改为只 Patch 最终失败返回点。

同时抓取 crash log:

adb logcat -c
adb logcat -v time | findstr /i "fatal signal crash abort sscronet cronet ttnet linker libc"

10.2 只能抓到一点点包,后续连接断开

可能原因:

1. 只 Patch 了 TLS,没有 Patch QUIC。
2. Patch 了 callback 入口,导致内部状态未填充。
3. App 还有请求层 pinning 结果二次判断。
4. 服务端或客户端启用了 QUIC,抓包工具未正确处理。

处理顺序:

1. 确认 TLS 和 QUIC 两条路径都定位。
2. 确认 callback 入口没有早退。
3. 继续搜索 SSL_PINNED_KEY_NOT_IN_CERT_CHAIN 等错误分支。
4. 必要时临时关闭 QUIC,再判断是否只剩 TLS 问题。

10.3 新版本偏移全部变化

这是正常情况。不要依赖旧地址。

每次升级后重新按以下关键点定位:

SSL_CTX_set_custom_verify
SSL_get0_peer_certificates
Cert chain verification failed:
Cronet_CertVerify_DoVerifyV2
SSL_PINNED_KEY_NOT_IN_CERT_CHAIN

偏移会变,但调用模式通常稳定。

11. 小结

libsscronet.so 的证书校验绕过不能只停留在“搜索 VerifyCert,然后改返回值”的层面。Cronet/BoringSSL 的实际结构更复杂,至少要区分:

TLS custom verify callback
QUIC custom verify callback
内部 CertVerifier
最终 verify result 转换函数

稳定 Patch 的核心原则是:

保留证书链解析和状态填充,只改最终失败返回值。

这样比入口早退更稳,也更适合应对“能抓一点包但后面闪退”这类问题。

最终,分析新版本时应记住一句话:

不要套旧偏移,套定位方法。

只要 SSL_CTX_set_custom_verify 还在,TLS/QUIC 的证书校验路径通常就能重新追出来。

文章作者:Linso

文章链接:https://linso.pro//archives/dyy

版权声明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0 许可协议,转载请注明出处!


评论