【Frida Android】实战篇5:SSL Pinning 证书绑定绕过 Hook 教程(二)

文章目录

⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。

1. 前言

在上一章里,我们学习了如何绕过通过X509TrustManager实现的证书绑定------简单说就是直接"修改"了负责检查证书的"保安",让它对所有证书都放行。但在实际开发中,企业更常用另一种更规范的证书绑定方式:通过network_security_config.xml配置文件来实现。这种方式是Android系统在7.0版本后专门推出的"官方规范",某些大厂会用它来保障App的网络安全。本章就来学习如何绕过这种更"标准"的证书绑定,帮你搞懂其中的原理和操作方法。

本章节使用的示例 APK 和 APK 源码如下:

链接: https://pan.baidu.com/s/1dhKQqjcle1tLv-q_C6KQlg?pwd=b65b

提取码: b65b

2. 方案简介

network_security_config.xml是Android 7.0及以上系统提供的"网络安全配置文件",它的核心作用可以简单理解为:给系统自带的证书检查规则"加额外条件"

打个比方:系统默认的证书检查(由TrustManager负责)就像小区保安,会核对访客的"身份证"(证书)是否有效;而network_security_config.xml就像小区新增的规定------比如"只允许住在某几栋楼的人进入",保安在核对身份证后,还要额外检查是否符合这个新规定。

其中最常用的"额外规定"就是"证书指纹绑定(PinSet)",比如这样配置:

xml 复制代码
<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <base-config>
        <pin-set>
            <!-- 配置服务器证书的指纹,只有匹配的证书才被信任 -->
            <pin digest="SHA-256">abc123...</pin>
        </pin-set>
    </base-config>
</network-security-config>

这段配置的意思是:无论证书本身是否有效,只有指纹是abc123...的证书才会被信任。

它的工作逻辑很简单:

  1. App依然用系统自带的"保安"(TrustManager)来检查证书;
  2. 但这个"保安"会多做一步:检查服务器返回的证书指纹是否和network_security_config.xml里配置的一致;
  3. 不一致?直接"拒之门外"(断开连接并报错)。

3. 应用修改

要给App加上这种证书绑定,步骤很简单,分两步:

步骤1:在清单文件中启用配置

打开AndroidManifest.xml(App的"身份清单"),在application标签里加一行配置,告诉系统"我要用自定义的网络安全规则":

xml 复制代码
<application
             ...
    android:networkSecurityConfig="@xml/network_security_config"> <!-- 新增这行 -->
</application>

这里的@xml/network_security_config就是告诉系统:配置文件在res/xml文件夹下,名叫network_security_config.xml

步骤2:创建具体的配置文件

res/xml文件夹下新建network_security_config.xml,填入具体的绑定规则,比如:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- 针对特定域名的配置 -->
    <domain-config cleartextTrafficPermitted="false"> <!-- 禁止明文传输(只允许HTTPS) -->
        <domain includeSubdomains="true">x.x.x.x</domain> <!-- 要绑定的域名(包括子域名) -->
        <pin-set expiration="2035-12-31"> <!-- 证书指纹配置,过期时间到2035年 -->
            <pin digest="SHA-256">xxxxxx</pin> <!-- 服务器证书的SHA-256指纹 -->
        </pin-set>
    </domain-config>
</network-security-config>

配置项说明:

  • domain:指定要生效的域名(比如api.example.com),includeSubdomains="true"表示子域名也生效;
  • cleartextTrafficPermitted="false":禁止该域名的明文HTTP请求,只能用HTTPS;
  • pin-set:证书指纹绑定的核心,digest="SHA-256"表示指纹算法,xxxxxx是具体的证书指纹;
  • expiration:配置的过期时间,到期后该绑定规则失效。

校验不通过的表现

启动本地服务的 python 脚本与上一章相同,本章不再重复。

如果App连接的服务器证书指纹和配置不匹配,会直接报错,比如:

查看日志(logcat)会看到类似这样的错误信息:

shell 复制代码
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
                                                                                                    	at com.android.org.conscrypt.TrustManagerImpl.verifyChain(TrustManagerImpl.java:674)
                                                                                                    	at com.android.org.conscrypt.TrustManagerImpl.checkTrustedRecursive(TrustManagerImpl.java:549)
                                                                                                    	at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:505)
                                                                                                    	at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:425)
                                                                                                    	at com.android.org.conscrypt.TrustManagerImpl.getTrustedChainForServer(TrustManagerImpl.java:353)
                                                                                                    	at android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted(NetworkSecurityTrustManager.java:94)
2025-11-15 09:39:07.056  3956-4041  NetworkService          com.example.fridaapk                 E  	at android.security.net.config.RootTrustManager.checkServerTrusted(RootTrustManager.java:90)
                                                                                                    	at com.android.org.conscrypt.ConscryptEngineSocket$2.checkServerTrusted(ConscryptEngineSocket.java:163)
                                                                                                    	at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:255)
                                                                                                    	at com.android.org.conscrypt.ConscryptEngine.verifyCertificateChain(ConscryptEngine.java:1638)
                                                                                                    	at com.android.org.conscrypt.NativeCrypto.ENGINE_SSL_read_direct(Native Method)
                                                                                                    	at com.android.org.conscrypt.NativeSsl.readDirectByteBuffer(NativeSsl.java:569)
                                                                                                    	at com.android.org.conscrypt.ConscryptEngine.readPlaintextDataDirect(ConscryptEngine.java:1095)
                                                                                                    	at com.android.org.conscrypt.ConscryptEngine.readPlaintextData(ConscryptEngine.java:1079)

从日志能看出证书检查的流程(简单理解):

shell 复制代码
服务器返回证书 → 系统检查证书是否有效(TrustManagerImpl) → 检查是否符合network配置(NetworkSecurityTrustManager) → 不匹配则报错

4. Hook 流程

启动 Frida_Sever 服务、Python 启动脚本与《实战篇4》的步骤相同,本章聚焦 Hook 脚本的编写,之前的步骤不再重复。

要绕过这种证书绑定,核心是"让系统的检查规则失效"。下面两种方案都能实现,原理不同但目标一致。

方案一:直接"跳过"关键检查步骤

这种方案的思路是:找到证书检查流程中的关键方法,直接修改它们的逻辑,让检查"通过"。

这里通过上一章的 Logcat 日志和 Android 源码(需要网络较好):

shell 复制代码
# AOSP 源码

# TrustManagerImpl:getTrustedChainForServer 有4个重载方法
https://cs.android.com/android/platform/superproject/main/+/main:external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/TrustManagerImpl.java;l=1?q=com%2Fandroid%2Forg%2Fconscrypt%2FTrustManagerImpl.java&sq=&hl=zh-cn

# NetworkSecurityTrustManager:checkPins 方法中调用 isPinningEnforced 方法
https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/security/net/config/NetworkSecurityTrustManager.java;l=1?q=frameworks%2Fbase%2Fcore%2Fjava%2Fandroid%2Fsecurity%2Fnet%2Fconfig%2FNetworkSecurityTrustManager.java&hl=zh-cn

核心逻辑

  1. 绕开系统的基础证书校验(TrustManagerImplgetTrustedChainForServer方法);
  2. 绕开network_security_config.xml的绑定检查(NetworkSecurityTrustManagerisPinningEnforcedcheckPins方法)。

完整脚本

语法技巧 :前面说到getTrustedChainForServer有4个重载方法,一个个hook太麻烦,如何一次性绕过所有重载方法。

  1. TrustManagerImpl.getTrustedChainForServer 是 Android 系统中负责对服务器提供的证书链进行校验的关键方法。
  2. 核心操作 : - 使用 overloads.forEach 遍历该方法的所有重载版本。
  3. 对每个重载版本都替换其实现(implementation),即 hook 操作。
  4. Hook 后的行为 : - 当应用调用 getTrustedChainForServer 方法时,不执行原本的证书校验逻辑,而是直接将服务器传入的证书数组(arguments[0])通过 arrayToList 转换为 List 并返回。
javascript 复制代码
import Java from "frida-java-bridge";

Java.perform(function() {
    console.log("[*] 开始绕过证书绑定...");
    
    // 获取系统中负责基础证书校验的类(TrustManagerImpl)
    var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
    // 获取负责network配置检查的类(NetworkSecurityTrustManager)
    var NetworkSecurityTrustManager = Java.use('android.security.net.config.NetworkSecurityTrustManager');
    // 辅助工具类(将数组转成List)
    var ArrayList = Java.use('java.util.ArrayList');
    
    // 辅助函数:把证书数组转成系统需要的List格式
    function arrayToList(array) {
        var list = ArrayList.$new();
        if (array != null) {
            for (var i = 0; i < array.length; i++) {
                list.add(array[i]);
            }
        }
        return list;
    }
    
    // 绕过基础证书校验:不管证书是否有效,直接返回服务器的证书链
    if (TrustManagerImpl.getTrustedChainForServer) {
        TrustManagerImpl.getTrustedChainForServer.overloads.forEach(function(overload) {
            overload.implementation = function() {
                console.log("[*] 已绕过基础证书校验");
                // 直接把服务器返回的证书数组转成List返回(假装校验通过)
                return arrayToList(arguments[0]);
            };
        });
    }
    
    // 关闭证书绑定强制检查:告诉系统"不需要检查绑定规则"
    if (NetworkSecurityTrustManager.isPinningEnforced) {
        NetworkSecurityTrustManager.isPinningEnforced.implementation = function() {
            console.log("[*] 已关闭证书绑定强制检查");
            return false; // 返回false表示"不强制检查绑定"
        };
    }
    
    // 跳过证书绑定的具体检查:即使需要检查,也直接"通过"
    if (NetworkSecurityTrustManager.checkPins) {
        NetworkSecurityTrustManager.checkPins.implementation = function() {
            console.log("[*] 已跳过证书绑定检查");
            // 空实现:不做任何检查,相当于"通过"
        };
    }
    
    console.log("[+] 证书绑定绕过完成");
});

为什么这样有效?

  • getTrustedChainForServer是系统判断"证书是否有效"的核心方法,修改后直接返回服务器的证书(不管是否有效),相当于"基础检查通过";
  • isPinningEnforced控制是否启用network_security_config的绑定规则,返回false就是"不启用";
  • checkPins是具体对比证书指纹的方法,空实现就是"不对比,直接过"。

方案二:用"自己人"替换系统的"保安"

这种方案更彻底:直接创建一个"不做任何检查"的信任管理器(TrustManager),然后替换系统原来的,让所有证书检查都由"自己人"负责(自然就通过了)。

核心逻辑

  1. 自定义一个"无校验"的TrustManager(不管证书是否有效、是否匹配绑定规则,都放行);
  2. 拦截系统初始化信任管理器的过程(SSLContext.init方法),用自定义的替换原来的。

完整脚本

javascript 复制代码
import Java from "frida-java-bridge";

Java.perform(function () {
    console.log('[*] 开始绕过 SSL 证书校验...');

    // 创建自定义的"无校验"信任管理器
    const trustManager = Java.registerClass({
        name: 'com.example.CustomTrustManager', // 自定义类名
        implements: [Java.use("javax.net.ssl.X509TrustManager")], // 实现系统的TrustManager接口
        methods: {
            // 客户端证书校验:空实现(不检查)
            checkClientTrusted: function (chain, authType) {},
            // 服务器证书校验:空实现(不检查,直接放行)
            checkServerTrusted: function (chain, authType) {},
            // 信任的CA列表:返回空(不限制任何CA)
            getAcceptedIssuers: function () { return []; }
        }
    }).$new(); // 创建实例

    // 拦截SSLContext的初始化方法,替换信任管理器
    Java.use("javax.net.ssl.SSLContext").init.overload(
        "[Ljavax.net.ssl.KeyManager;",
        "[Ljavax.net.ssl.TrustManager;",
        "java.security.SecureRandom"
    ).implementation = function (keyManagers, trustManagers, secureRandom) {
        console.log("[+] 已拦截SSLContext初始化,替换为自定义TrustManager");
        // 忽略系统原来的trustManagers,传入我们自己的"无校验"管理器
        this.init(keyManagers, [trustManager], secureRandom);
    };

    console.log('[*] SSL 证书校验绕过完成');
});

为什么这样有效?

  • 所有HTTPS连接的证书检查,最终都由SSLContext里的TrustManager负责;
  • 我们创建的CustomTrustManager对所有校验都"放行了事";
  • 拦截SSLContext.init后,系统无论原来用什么TrustManager,都会被换成我们的"自己人",自然就绕过了所有检查(包括network_security_config的绑定)。

方案总结

方法1和方法2本质上都是为了绕过证书绑定,但思路不同:

  • 方法1是"针对性破解":它精准定位network_security_config.xml配置生效的关键环节(比如NetworkSecurityTrustManager的绑定检查、TrustManagerImpl的证书链校验),直接"跳过"这些具体步骤,相当于"堵住了这个配置的生效路径",但不影响系统其他默认的证书校验逻辑。
  • 方法2是"釜底抽薪":它不关心具体的配置细节,而是直接替换了整个证书校验的"执行者"------SSLContext中的TrustManager。因为所有证书校验(包括系统默认规则、network_security_config.xml的额外配置)最终都要通过TrustManager来执行 ,当我们用"无校验"的自定义TrustManager替换系统默认的管理器后,原本依赖系统管理器才能生效的network_security_config.xml配置,自然就失去了作用(相当于换了一个"完全不看规则"的保安,原来的规定也就无效了)。

简单说:方法1是"拆零件",只针对network_security_config.xml相关的校验步骤;方法2是"换机器",直接让所有依赖原校验机制的规则(包括该配置)都失效,适用范围更广。

5. 本章总结

本章学习了通过network_security_config.xml配置的证书绑定及其绕过方法:

  1. network_security_config.xml是Android 7+的系统级配置,作用是给证书检查加"额外规则"(如证书指纹绑定);
  2. 绕过方案一:直接Hook关键检查方法(TrustManagerImpl.getTrustedChainForServerNetworkSecurityTrustManager的相关方法),跳过校验;
  3. 绕过方案二:创建"无校验"的TrustManager,替换系统默认的,从根源上关闭所有检查。

两种方案各有优势:方案一更针对性(只改必要步骤),方案二更通用(覆盖所有场景)。

6. 下章预告

证书绑定不仅可以通过系统配置实现,很多App还会用第三方网络库(如OkHttp)自己实现证书绑定。下一章我们将学习如何绕过基于OkHttp库的证书绑定,进一步完善证书校验绕过的技能。

相关推荐
2501_937193142 小时前
PLB-TV 影视!无广告 + 4K 高清
android·源码·源代码管理·机顶盒
阿斌_bingyu7092 小时前
uniapp实现android/IOS消息推送
android·ios·uni-app
Android系统攻城狮3 小时前
Android内核进阶之周期更新PCM状态snd_pcm_period_elapsed:用法实例(九十二)
android·pcm·android内核·音频进阶
Cola可洛3 小时前
修复Flyme移植BUG
android·bug
消失的旧时光-19433 小时前
Kotlinx.serialization 使用指南
android·kotlin·json
麦烤楽鸡翅4 小时前
pdf(攻防世界)
网络安全·pdf·ctf·misc·杂项·攻防世界·信息竞赛
消失的旧时光-19435 小时前
Kotlinx.serialization 项目集成
android·kotlin·json
梦里不知身是客115 小时前
datax如何做增量导入
android
我是好小孩6 小时前
【Android】RecyclerView的高度问题、VH复用概念、多子项的实现;
android·java·网络