按类名定位的证书固定脚本,在普通 APK 上有效,在混淆版上却可能完全没有命中。策略仍在执行,丢失的是 okhttp3.CertificatePinner 这类名称线索;参数类型、输入数据和对象构造时机则提供了另一条定位路径。
Jeroen Beckers 的 OkHttp 实验使用 Android 7.1.2、Objection 1.4.3 与 Apktool 2.3.4,对比普通构建和默认 ProGuard 混淆构建。以下结论限定于这组环境,重点是从类型描述符恢复构造方法的身份,而非依赖跨版本固定类名。
先拆开三条请求路径
测试应用以三种方式请求同一服务的 robots.txt:平台客户端、未显式配置 pinner 的 OkHttp,以及配置了 CertificatePinner 的 OkHttp。与此同时,network_security_config.xml 还为目标域及其子域设置了 SHA-256 固定值。
因此,没有 OkHttp pinner,不等于整条连接没有固定策略。平台配置与库内策略可以叠加;同样,代理 CA 被系统信任,也不意味着它符合应用附加的固定值。
| 条件 | 材料中的记录 | 排查意义 |
|---|---|---|
| 设备 | Android 7.1.2(终端截图) | 固定平台验证实现 |
| 工具 | Objection 1.4.3;Apktool 2.3.4 | 避免把旧版行为推广到新版 |
| 代理 CA | 已加入系统信任存储 | 尽量排除一般证书链不受信任 |
| 配置有效期 | expiration="2022-01-01" |
保留 2019 年实验的时间条件 |
| 对照变量 | 同一应用的普通版与默认 ProGuard 混淆版 | 优先区分类名定位失效与请求逻辑变化 |
工具命中与请求成功是两类证据
Objection 的输出显示它找到了 OkHttp 和 TrustManagerImpl。以下节选省略重复调用、会话标识、应用包名和本地路径:
$ android sslpinning disable
Custom, Empty TrustManager ready
OkHTTP 3.x Found
TrustManagerImpl“找到类”证明定位阶段成功,不证明所有验证入口均被覆盖。应用的三个结果标签给出了请求层面的对照:
| 界面标签 | 普通版:仅 Objection 默认处理 | 混淆版:平台处理与 Builder 处理同时生效 |
|---|---|---|
| SecurityPolicy | ERROR | OK |
| OKHTTP | OK | OK |
| Pinned OKHTTP | OK | OK |
两列不是同一构建上只切换一个 Hook 的严格 A/B 实验,而是流程中的不同阶段:普通版先用默认处理,平台路径仍失败;补充平台处理后,再引入混淆;最后处理混淆后的 Builder。表中直接呈现两个端点状态,未展示各中间阶段的完整记录。
在该设备上,默认工具入口与实际平台检查入口的覆盖范围不同。把这个观察推广成“Android 7 全部小版本都如此”,会超出材料提供的证据。
参数描述符比短类名更有用
混淆后,按已知类名查找的脚本不再识别 OkHttp,配置额外固定值的请求再次失败。此时应先检查定位链,而不是假定应用换用了新的密码学机制。
目标是策略构造点 CertificatePinner.Builder.add():它接收主机名匹配模式和固定值数组,写入 Builder 的集合,再返回 Builder 以支持链式调用。第一个参数是 hostname/pattern,不是任意完整 URL。
String... 在字节码中表现为 String[]。用于筛选候选的描述符形状是:
(Ljava/lang/String;[Ljava/lang/String;)Lreturn/type;其中 Lreturn/type; 是示意性的返回对象类型。[ 表示数组;标准库类型描述符往往在默认名称混淆后继续保留。但同样的签名可以出现在业务方法中,所以它只是候选过滤器,不是函数身份证明。
从搜索结果确认构造点
Apktool 2.3.4 解包 obfuscated.apk 后,搜索结果包含调用点与方法声明。下面是候选方法的声明行:
.method public varargs a(Ljava/lang/String;[Ljava/lang/String;)Lokhttp3/g$a;复核时可使用固定字符串搜索,避免正则语法误读 [。下面是操作命令,不是执行日志:
apktool d obfuscated.apk -o application
rg -n -F 'Ljava/lang/String;[Ljava/lang/String;)L' application/smali*范围应覆盖所有 smali 目录,而非仅第一个 DEX。调用指令与方法声明也要去重,避免把同一个方法记成多个候选。
- 1
记录身份
保存类名、方法名、完整参数描述符、返回类型及所属 DEX。该样本的名称是
okhttp3.g$a与a,只对该 APK 有意义。 - 2
观察实参
确认首参符合目标主机模式,第二个数组包含预期固定值,而不是普通业务字符串。
- 3
检查调用关系
查看调用者与调用栈,确认这是构造证书固定策略的过程,并核对 Builder 的集合写入。
- 4
检查时机
确认观察点在客户端与策略对象构造之前生效,再记录请求结果。
返回 Builder 为什么能改变策略
在这个样本中,方法既追加规则,又返回自身。保留返回对象、略过那次追加,可以让链式调用继续,但不加入本次固定值。下面保留历史样本的混淆名:
Java.perform(function () {
const Builder = Java.use('okhttp3.g$a');
const add = Builder.a.overload(
'java.lang.String', '[Ljava.lang.String;'
);
add.implementation = function (hostPattern, pins) {
console.log('Observed pin configuration: ' + hostPattern);
return this;
};
});- 同时限定两个参数类型,避免选中同名的另一重载。
- 保持链式返回值,但不调用原方法,因此跳过这次规则追加;它不会删除已有客户端中的固定规则。
这个时序条件很重要:客户端已经构造完成后,再处理 Builder 并不会追溯更新旧实例。静态提前初始化、不同类加载器、另一重载、直接构造策略对象及构建器内联,也都可能使这条路径失去覆盖。
最终成功结果依赖既有平台处理与新增 Builder 处理共同生效。它证明的是该测试样本的配置入口与后续请求有关,不是一个适用于所有 OkHttp 构建的通用结论。
保留可复核的结论
混淆可以抹去方便的名字,但类型形状、参数语义和对象构造关系仍能组合成证据。可靠的分析顺序是:建立各请求的基线,核对工具命中,再引入混淆,从描述符缩小候选,最后用实参与构造时机确认身份。
客户端上的观察与行为调整,也不替代服务端的身份鉴别和数据授权检查。开发阶段宜提供独立调试配置;评估证书策略时,分别记录平台信任、库内固定与服务端权限,避免用“最后抓到了包”概括全部结论。