文章目录
- [1. 前言](#1. 前言)
- [2. 方案简介](#2. 方案简介)
- [3. 应用修改](#3. 应用修改)
- [4. Hook 流程](#4. Hook 流程)
- [5. 本章总结](#5. 本章总结)
- [6. 下章预告](#6. 下章预告)
⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。
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...的证书才会被信任。
它的工作逻辑很简单:
- App依然用系统自带的"保安"(
TrustManager)来检查证书; - 但这个"保安"会多做一步:检查服务器返回的证书指纹是否和
network_security_config.xml里配置的一致; - 不一致?直接"拒之门外"(断开连接并报错)。
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
核心逻辑
- 绕开系统的基础证书校验(
TrustManagerImpl的getTrustedChainForServer方法); - 绕开
network_security_config.xml的绑定检查(NetworkSecurityTrustManager的isPinningEnforced和checkPins方法)。
完整脚本
语法技巧 :前面说到getTrustedChainForServer有4个重载方法,一个个hook太麻烦,如何一次性绕过所有重载方法。
TrustManagerImpl.getTrustedChainForServer是 Android 系统中负责对服务器提供的证书链进行校验的关键方法。- 核心操作 : - 使用
overloads.forEach遍历该方法的所有重载版本。 - 对每个重载版本都替换其实现(
implementation),即 hook 操作。 - 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),然后替换系统原来的,让所有证书检查都由"自己人"负责(自然就通过了)。
核心逻辑
- 自定义一个"无校验"的TrustManager(不管证书是否有效、是否匹配绑定规则,都放行);
- 拦截系统初始化信任管理器的过程(
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配置的证书绑定及其绕过方法:
network_security_config.xml是Android 7+的系统级配置,作用是给证书检查加"额外规则"(如证书指纹绑定);- 绕过方案一:直接Hook关键检查方法(
TrustManagerImpl.getTrustedChainForServer、NetworkSecurityTrustManager的相关方法),跳过校验; - 绕过方案二:创建"无校验"的
TrustManager,替换系统默认的,从根源上关闭所有检查。
两种方案各有优势:方案一更针对性(只改必要步骤),方案二更通用(覆盖所有场景)。
6. 下章预告
证书绑定不仅可以通过系统配置实现,很多App还会用第三方网络库(如OkHttp)自己实现证书绑定。下一章我们将学习如何绕过基于OkHttp库的证书绑定,进一步完善证书校验绕过的技能。