~/posts/mobile/obfuscated-okhttp-certificate-pinning.md

混淆后的 OkHttp:用参数类型定位证书固定

在 Android 7.1.2 的普通与混淆构建之间比较三条请求路径,区分平台策略和 OkHttp 固定,通过 Smali 参数描述符、实参语义与 Builder 构造时机定位混淆方法。

date[31:24]
read[23:16]
5 分钟
cat[15:8]
移动端
目录
  1. 0x00先拆开三条请求路径
  2. 0x01工具命中与请求成功是两类证据
  3. 0x02参数描述符比短类名更有用
  4. 0x03从搜索结果确认构造点
  5. 0x04返回 Builder 为什么能改变策略
  6. 0x05保留可复核的结论
  7. 0x06参考资料

按类名定位的证书固定脚本,在普通 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 被系统信任,也不意味着它符合应用附加的固定值。

历史测试的控制条件5 行
条件 材料中的记录 排查意义
设备 Android 7.1.2(终端截图) 固定平台验证实现
工具 Objection 1.4.3;Apktool 2.3.4 避免把旧版行为推广到新版
代理 CA 已加入系统信任存储 尽量排除一般证书链不受信任
配置有效期 expiration="2022-01-01" 保留 2019 年实验的时间条件
对照变量 同一应用的普通版与默认 ProGuard 混淆版 优先区分类名定位失效与请求逻辑变化

工具命中与请求成功是两类证据

Objection 的输出显示它找到了 OkHttp 和 TrustManagerImpl。以下节选省略重复调用、会话标识、应用包名和本地路径:

Objection 1.4.3 · 历史输出节选
$ android sslpinning disable
Custom, Empty TrustManager ready
OkHTTP 3.x Found
TrustManagerImpl

“找到类”证明定位阶段成功,不证明所有验证入口均被覆盖。应用的三个结果标签给出了请求层面的对照:

三个请求路径的结果对照3 行
界面标签 普通版:仅 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[]。用于筛选候选的描述符形状是:

JVM method descriptortext
(Ljava/lang/String;[Ljava/lang/String;)Lreturn/type;

其中 Lreturn/type; 是示意性的返回对象类型。[ 表示数组;标准库类型描述符往往在默认名称混淆后继续保留。但同样的签名可以出现在业务方法中,所以它只是候选过滤器,不是函数身份证明。

从搜索结果确认构造点

Apktool 2.3.4 解包 obfuscated.apk 后,搜索结果包含调用点与方法声明。下面是候选方法的声明行:

okhttp3/g$a.smali · 方法声明smali
.method public varargs a(Ljava/lang/String;[Ljava/lang/String;)Lokhttp3/g$a;

复核时可使用固定字符串搜索,避免正则语法误读 [。下面是操作命令,不是执行日志:

Locate descriptor candidatesbash
apktool d obfuscated.apk -o application
rg -n -F 'Ljava/lang/String;[Ljava/lang/String;)L' application/smali*

范围应覆盖所有 smali 目录,而非仅第一个 DEX。调用指令与方法声明也要去重,避免把同一个方法记成多个候选。

确认候选的语义4 步
  1. 1

    记录身份

    保存类名、方法名、完整参数描述符、返回类型及所属 DEX。该样本的名称是 okhttp3.g$a 与 a,只对该 APK 有意义。

  2. 2

    观察实参

    确认首参符合目标主机模式,第二个数组包含预期固定值,而不是普通业务字符串。

  3. 3

    检查调用关系

    查看调用者与调用栈,确认这是构造证书固定策略的过程,并核对 Builder 的集合写入。

  4. 4

    检查时机

    确认观察点在客户端与策略对象构造之前生效,再记录请求结果。

返回 Builder 为什么能改变策略

在这个样本中,方法既追加规则,又返回自身。保留返回对象、略过那次追加,可以让链式调用继续,但不加入本次固定值。下面保留历史样本的混淆名:

builder-hook.jsjavascript
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;
  };
});
  1. 同时限定两个参数类型,避免选中同名的另一重载。
  2. 保持链式返回值,但不调用原方法,因此跳过这次规则追加;它不会删除已有客户端中的固定规则。

这个时序条件很重要:客户端已经构造完成后,再处理 Builder 并不会追溯更新旧实例。静态提前初始化、不同类加载器、另一重载、直接构造策略对象及构建器内联,也都可能使这条路径失去覆盖。

最终成功结果依赖既有平台处理与新增 Builder 处理共同生效。它证明的是该测试样本的配置入口与后续请求有关,不是一个适用于所有 OkHttp 构建的通用结论。

保留可复核的结论

混淆可以抹去方便的名字,但类型形状、参数语义和对象构造关系仍能组合成证据。可靠的分析顺序是:建立各请求的基线,核对工具命中,再引入混淆,从描述符缩小候选,最后用实参与构造时机确认身份。

客户端上的观察与行为调整,也不替代服务端的身份鉴别和数据授权检查。开发阶段宜提供独立调试配置;评估证书策略时,分别记录平台信任、库内固定与服务端权限,避免用“最后抓到了包”概括全部结论。

参考资料

NORMAL~/posts/mobile/obfuscated-okhttp-certificate-pinning.md§--
0%zh-CN