绕过 Android 7.0+ 网络安全配置

在 Android 7.0 以上的系统中,Google 引入了一种名为网络安全配置(Network Security Configuration)的功能。据官方文档所说,这个功能可以让开发者在一个安全的声明性 XML 配置文件中自定义应用的网络安全设置,而无需修改应用代码。也可以针对特定域和特定应用配置这些设置。
可以参考官方文档原文
https://developer.android.google.cn/training/articles/security-config.html

当然这篇文章并不是介绍 Network Security Configuration 的具体用法的,本篇文章主要讲如何绕过这种在 Android 7.0+ 的默认行为。

如果你是一个网络安全从业者,或者了解过相关的知识,对于这个新增的功能,最直观的感觉可能就是,在运行着 Android 7.0 的手机上无法使用 Fiddler 或类似工具抓到 https 连接的包了,只有一些 https 的握手请求,无法查看到实际的数据,根本原因就是应用不再信任用户导入的 Fiddler 证书了。

想要研究如何绕过这项功能,就必须先了解如何正常使用。据官方文档描述,需要在 res/xml 下创建一个 XML 文件来自定义网络配置:
下面给出几个样例,可参照注释

配置该应用的所有 HTTPS 链接

1
2
3
4
5
6
7
8
9
<?xml version="1.0"encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors> <!-- 信任锚点集合 -->
<certificates src="system"/> <!-- 信任系统自带的证书 -->
<certificates src="user"/> <!-- 信任用户导入的证书 -->
</trust-anchors>
</base-config>
</network-security-config>

配置该应用的自定义 CA

1
2
3
4
5
6
7
8
<?xml version="1.0"encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="@raw/my_custom_ca"/> <!-- 放在 res/raw 下的自定义 CA 文件 -->
</trust-anchors>
</base-config>
</network-security-config>

根据域名配置 HTTPS 可信域

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config> <!-- 与上文的 base-config 不同 -->
<domain includeSubdomains="true">example1.iacn.me</domain> <!-- 过滤域名,可配置多个 -->
<domain includeSubdomains="true">example2.iacn.me</domain> <!-- 一般会将 CDN 配置在此 -->
<trust-anchors>
<certificates src="system"/>
</trust-anchors>
</domain-config>
</network-security-config>

开发阶段的配置

仅在 android:debuggable=”true” 时生效

1
2
3
4
5
6
7
8
<?xml version="1.0"encoding="utf-8"?>
<network-security-config>
<debug-overrides>
<trust-anchors>
<certificates src="system"/>
</trust-anchors>
</debug-overrides>
</network-security-config>

除此之外,还需要在 AndroidManifest.xml 中引用自定义的网络安全配置

1
2
3
4
5
6
7
<?xml version="1.0"encoding="utf-8"?>
<manifest>
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
</application>
</manifest>


上面是 Network Security Configuration 的使用简介,那么如何绕过该功能呢?

重编译 APK 文件

如上文介绍,需要将目标 APK 文件反编译,然后修改 XML 配置文件,在 trust-anchors 中信任用户导入的证书,之后重新打包即可。

运行时 Hook

在某些情况下,第一种方法也许是不可行的。比如说,需要保留目标应用原始的签名文件。这是无法做到的,因为你不可能拿到开发者的原始证书去给重编译后的应用签名。这里就需要用到 Hook 技术,可以在不修改应用代码的前提下修改应用的行为。

查看 Android 7.1.2_r36 源码,android.security.net.config.ManifestConfigSource 类 用于加载和处理网络安全配置 XML 文件的相关信息
getConfigSource() 方法下,如果应用未配置网络信息,它将会加载默认配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class NetworkSecurityConfig {
...
private ConfigSource getConfigSource() {
...
ConfigSource source;
if (mConfigResourceId != 0) {
...
source = new XmlConfigSource(mContext, mConfigResourceId, debugBuild,
mTargetSdkVersion, mTargetSandboxVesrsion);
} else {
...
source = new DefaultConfigSource(usesCleartextTraffic, mTargetSdkVersion,
mTargetSandboxVesrsion);
}
mConfigSource = source;
return mConfigSource;
}
}

DefaultConfigSource 类是 ManifestConfigSource 类中一个内部类,即上文代码中返回的默认网络配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class NetworkSecurityConfig {
...
private static final class DefaultConfigSource implements ConfigSource {
...
public DefaultConfigSource(boolean usesCleartextTraffic, int targetSdkVersion,
int targetSandboxVesrsion) {
mDefaultConfig = NetworkSecurityConfig.getDefaultBuilder(targetSdkVersion,
targetSandboxVesrsion)
.setCleartextTrafficPermitted(usesCleartextTraffic)
.build();
}
}
}

在其构造方法中,调用了 android.security.net.config.NetworkSecurityConfig 类中的 getDefaultBuilder() 去构造一个默认配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class NetworkSecurityConfig {
...
public static final Builder getDefaultBuilder(int targetSdkVersion, int targetSandboxVesrsion) {
Builder builder = new Builder()
.setHstsEnforced(DEFAULT_HSTS_ENFORCED)
.addCertificatesEntryRef(
new CertificatesEntryRef(SystemCertificateSource.getInstance(), false));
...
if (targetSdkVersion <= Build.VERSION_CODES.M) {
builder.addCertificatesEntryRef(
new CertificatesEntryRef(UserCertificateSource.getInstance(), false));
}
return builder;
}
}

可以看到,在应用的 targetSdkVersion <= M 时(Android 6.0 及以下),有一个 addCertificatesEntryRef(UserCertificateSource),系统将默认信任用户导入的证书。

那么以 Xposed 为例,Hook getDefaultBuilder(),在调用前将第一个参数 targetSdkVersion 改为 Android N 以下就可以了。这里给出核心代码:

1
2
3
4
5
6
7
8
9
10
11
public void initZygote(StartupParam startupParam) throws Throwable {
Class<?> targetClass = findClass("android.security.net.config.NetworkSecurityConfig", null);
if (targetClass != null) {
XposedBridge.hookAllMethods(targetClass, "getDefaultBuilder", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
param.args[0] = Build.VERSION_CODES.M;
}
});
}
}

这样处理之后,抓包工具已经可以正常抓取 https 流量了