背景
从Android7.0之后系统不再信任用户CA证书。主要限制在应用的targetSdkVersion >= 24时生效,如果targetSdkVersion < 24即使系统是7.0+依然会信任(用户证书)。也就是说即使安装了用户CA证书,在Android 7.0+的机器上,targetSdkVersion >= 24的应用的HTTPS包就抓不到了。对于普通的HTTP请求,可以使用一些抓包工具进行抓包,对于targetSdkVersion >= 24的Https请求,只需要信任相关的证书,也可以抓取Https请求(抓包相关的配置不是本文的重点)。
可行性
常见的解决方案
1.官方的方案
网络安全配置功能使用一个 XML 文件,您可以在该文件中指定应用的设置。您必须在应用的清单中添加一个指向该文件的条目。以下代码摘自一个清单文件,演示了如何创建此条目:
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:networkSecurityConfig="@xml/network_security_config"
... >
...
</application>
</manifest>
自定义可信 CA
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<trust-anchors>
<certificates src="@raw/my_ca"/>
</trust-anchors>
</domain-config>
</network-security-config>
其中可配置自定义CA,限制可信CA,信任其他CA等。连接地址(需要翻墙)
但是这种方案仅适用于有源码了进行修改,但是在开发阶段还需要注意存在这些配置的包不能流传到外部,否则造成泄露。这种方案仅适用于开发和测试人员或者内部使用。
- 修改targetSdkVersion
将APP的targetSdkVersion修改在24以下就行。但是这样对APP的影响太大,兼容性、上架等因素,所以这种方案不可取。 - ROOT
首先这个方案需要ROOT,当然,既然Root了,有很多方案可以抓包。
(1).复制证书到系统证书层面
这个方案网上方法很多,就不一一赘述(因为我也没试过,搞起来麻烦,我都是用第二种方案),举个例子:例子
(2).Xposed
有个大佬写了一个JustTrustMe,当然这个框架是基于Xposed(ROOT版的)。安装这个软件可以直接抓取Https的包。
不常见的方案
以上的方案不是需要ROOT就是需要改源码,这个操作对于普通线下用户来说还是有点难度的,所以针对上面的方案对一下拓展,如何实现免ROOT的抓包。
- 官方的方法
(1)热修复
官方要求在manifest和xml中都进行修改,原本准备以热修复的形式将代码插入APP中,但是很多热修复框架不支持Manifest文件的修复,所以热修复的方案放弃
(2)重打包
这个方案就比较简单了,将APP进行重打包,思路:XPatch
将APP解包,对manifest,resource.arsc和资源目录进行修改,最后重新签名的过程。实现起来唯一有难度的地方就是resource.arsc文件的修改,不过网上也有比较成熟的方案。
这个方案比较麻烦,因为Xpatch是给Xpoased用的,所以用在这个地方有点杀鸡焉用牛刀的感觉。
2.修改targetSdkVersion
官方说只有targetSdkVersion的限制且经过修改manifest后就可以进行抓包,说明源码肯定有过修改,通过官方的配置可知:
android:networkSecurityConfig="@xml/network_security_config"
相关配置必定和networkSecurityConfig相关,不出意外,搜到一个类:NetworkSecurityConfig文件(同名,谷歌还是个讲究人),果然在里面发现了关于Android M版本的相关校验:
/** 基于Android 7.1.1_r6的源码
* Return a {@link Builder} for the default {@code NetworkSecurityConfig}.
*
* <p>
* The default configuration has the following properties:
* <ol>
* <li>Cleartext traffic is permitted.</li>
* <li>HSTS is not enforced.</li>
* <li>No certificate pinning is used.</li>
* <li>The system certificate store is trusted for connections.</li>
* <li>If the application targets API level 23 (Android M) or lower then the user certificate
* store is trusted by default as well.</li>
* </ol>
*
* @hide
*/
public static final Builder getDefaultBuilder(int targetSdkVersion) {
Builder builder = new Builder()
.setCleartextTrafficPermitted(DEFAULT_CLEARTEXT_TRAFFIC_PERMITTED)
.setHstsEnforced(DEFAULT_HSTS_ENFORCED)
// System certificate store, does not bypass static pins.
.addCertificatesEntryRef(
new CertificatesEntryRef(SystemCertificateSource.getInstance(), false));
// Applications targeting N and above must opt in into trusting the user added certificate
// store.
if (targetSdkVersion <= Build.VERSION_CODES.M) {
// User certificate store, does not bypass static pins.
builder.addCertificatesEntryRef(
new CertificatesEntryRef(UserCertificateSource.getInstance(), false));
}
return builder;
}
这里有个argetSdkVersion <= Build.VERSION_CODES.M的判断,如果校验成功的话,会 添加UserCertificateSource.getInstance(),这就是用户证书的实例。这样看起来我们只需要hook这个方法就可以实现了修改targetSdkVersion的值,下面开始动手实现:
try {
findAndHookMethod("android.security.net.config.NetworkSecurityConfig",
lpparam.classLoader,"getDefaultBuilder",Integer.class,Integer.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log(TAG + " >> getDefaultBuilder:2:" + param.args.length);
}
});
} catch (Exception e) {
e.printStackTrace();
}
很遗憾,方法没有走进来,我有一个合理的猜测是:
无ROOT的Xposed方案采用的是XPatch的方案:也就是解包->修改源码->重新签名的过程,但是在这一步与完整版的Xposed有一个非常大的区别在于HOOK的时机。
完整版的Xposed完整,不需要对APP做任何修改,因为他的插桩点在ActivityThread上,当进程创建的时候就已经开始拦截方法,所以理论上可以拦截APP上所有的方法。
但是Xpatch的方案无法修改ActivityThread的方法(这个APP中不存在这个方法),他的插桩点在Application方法上,attach方法后,所以针对APP启动流程的相关方法当然就拦截不到了,所以有可能这个方法被初始化一遍之后就被缓存了起来(这也是很常见的,常用的配置不用每次用的时候都去初始化),所以只会初始化一遍,且在manifest解析的时候,所以上面的Hook行为也就拦截不上了。
那么问题来了,这样我们就没有办法了吗?当然不是,我们从这个方法向上找,总能找到蛛丝马迹或者说一个好的Hook的地方,让我们放手去做吧(PS:以下源码是从androidxref网站获取:AndroidXREF):
//frameworks/base/core/java/android/security/net/config/ManifestConfigSource$DefaultConfigSource.java
private static final class DefaultConfigSource implements ConfigSource {
private final NetworkSecurityConfig mDefaultConfig;
public DefaultConfigSource(boolean usesCleartextTraffic, int targetSdkVersion) {
mDefaultConfig = NetworkSecurityConfig.getDefaultBuilder(targetSdkVersion)
.setCleartextTrafficPermitted(usesCleartextTraffic)
.build();
}
@Override
public NetworkSecurityConfig getDefaultConfig() {
return mDefaultConfig;
}
@Override
public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() {
return null;
}
}
//frameworks/base/core/java/android/security/net/config/ManifestConfigSource.java
public ManifestConfigSource(Context context) {
mContext = context;
// Cache values because ApplicationInfo is mutable and apps do modify it :(
ApplicationInfo info = context.getApplicationInfo();
mApplicationInfoFlags = info.flags;
mTargetSdkVersion = info.targetSdkVersion;
mConfigResourceId = info.networkSecurityConfigRes;
}
@Override
public NetworkSecurityConfig getDefaultConfig() {
return getConfigSource().getDefaultConfig();
}
private ConfigSource getConfigSource() {
synchronized (mLock) {
if (mConfigSource != null) {
return mConfigSource;
}
ConfigSource source;
if (mConfigResourceId != 0) {
boolean debugBuild = (mApplicationInfoFlags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
if (DBG) {
Log.d(LOG_TAG, "Using Network Security Config from resource "
+ mContext.getResources().getResourceEntryName(mConfigResourceId)
+ " debugBuild: " + debugBuild);
}
source = new XmlConfigSource(mContext, mConfigResourceId, debugBuild,
mTargetSdkVersion);
} else {
if (DBG) {
Log.d(LOG_TAG, "No Network Security Config specified, using platform default");
}
boolean usesCleartextTraffic =
(mApplicationInfoFlags & ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC) != 0;
source = new DefaultConfigSource(usesCleartextTraffic, mTargetSdkVersion);
}
mConfigSource = source;
return mConfigSource;
}
}
从上向下看,一个内部类初始化了NetworkSecurityConfig并传入了targetSdkVersion,而内部类在getConfigSource里面被初始化的,而targetSdkVersion则是ManifestConfigSource在初始化的时候从ApplicationInfo中取出来的,那么,事情简单了,看情况只需要修改targetSdkVersion就可以了,现在我们决定在getConfigSource这个方法的前面将targetSdkVersion修改掉,不过很遗憾,方法并未执行,不用气馁,我们继续修改他的上层函数:getDefaultConfig。可喜可贺,这个方法执行了,我们在这里做一个HOOK:
findAndHookMethod("android.security.net.config.ManifestConfigSource",
lpparam.classLoader, "getDefaultConfig", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
XposedHelpers.setIntField(param.thisObject, "mTargetSdkVersion", 23);
}
});
现在看起来还不错,你怀着满腔热血去运行,不好意思,无效,这时候你仔细观察代码就会发现,还有一个mConfigSource这个变量,是一个懒加载的模式,所以这个东西在之前肯定加载过了,所以我们就算修改了mTargetSdkVersion的值,也无效,我们看下抓包工具的反馈:
很遗憾,这样不行,但是这时候肯定有童鞋想问,你怎么评估出来没有效果,很简单啊,将证书输出一下就不知道了。
我们观察NetworkSecurityConfig这个类,会发现有一个变量list:mCertificatesEntryRefs,里面存放的是证书的实例,理论上6.0以后这里只有一个长度,如果他的targetSdkVersion小于M,则这个的长度是2,我们通过这个特征值去判断我们的修改有没有作用。现在又有疑问了,NetworkSecurityConfig这个类的方法都不执行,怎么取出里面的变量,这个问题就很简单了,因为这个类在DefaultConfigSourc中被使用了,DefaultConfigSource就是ManifestConfigSource的mConfigSource,所以我们可以在getDefaultConfig方法执行后,拿到mConfigSource相关的信息,这里面就是XposedHelpers反射相关的知识灵活运用了(当然你用java的反射类也行,但是写起来麻烦),好了,我们修改一下代码,将长度输出:
findAndHookMethod("android.security.net.config.ManifestConfigSource",
lpparam.classLoader, "getDefaultConfig", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
XposedHelpers.setIntField(param.thisObject, "mTargetSdkVersion", 23);
XposedBridge.log(TAG + " >> getDefaultBuilder:0022:" + XposedHelpers.getIntField(param.thisObject, "mTargetSdkVersion"));
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
//检查生成的数据
Object mConfigSource = XposedHelpers.getObjectField(param.thisObject, "mConfigSource");
if (mConfigSource != null) {//DefaultConfigSource
XposedBridge.log(TAG + " >> getDefaultBuilder:有值:");
Object mDefaultConfig = XposedHelpers.getObjectField(mConfigSource, "mDefaultConfig");
if (mDefaultConfig != null) {
XposedBridge.log(TAG + " >> mDefaultConfig:有值:");
List mCertificatesEntryRefs = (List) XposedHelpers.getObjectField(mDefaultConfig, "mCertificatesEntryRefs");
if (mCertificatesEntryRefs != null) {
XposedBridge.log(TAG + " >> mCertificatesEntryRefs:有值:" + mCertificatesEntryRefs.size());
} else {
XposedBridge.log(TAG + " >> mCertificatesEntryRefs:为空");
}
} else {
XposedBridge.log(TAG + " >> mDefaultConfig:为空");
}
} else {
XposedBridge.log(TAG + " >> getDefaultBuilder:为空");
}
}
});
OK,我们输出一下结果,如果不出意外的话:
很完美,和我们想的一样,现在怎么解决呢,很简单,改mConfigSource的值:
XposedHelpers.setObjectField(param.thisObject, "mConfigSource", null);
现在我们看下输出的结果:
OK,这就很棒,现在可以开开心心的看看看抓包工具了:
看起来是OK的。
残留的问题
从上小结可以看到我们针对Android7.1.1_r6的源码层面上的抓包已经解决,但是Android会这么一成不变吗?NONONO,经过检查源码
一直到Android8.1.0_r33的版本,这地方是不变的,但是在Android9.0.0_r3的源码中,这里的处理出现了质的变化,我们先看一下NetworkSecurityConfig:
private final ApplicationInfo mApplicationInfo;
/**
* Return a {@link Builder} for the default {@code NetworkSecurityConfig}.
*
* <p>
* The default configuration has the following properties:
* <ol>
* <li>If the application targets API level 27 (Android O MR1) or lower then cleartext traffic
* is allowed by default.</li>
* <li>Cleartext traffic is not permitted for ephemeral apps.</li>
* <li>HSTS is not enforced.</li>
* <li>No certificate pinning is used.</li>
* <li>The system certificate store is trusted for connections.</li>
* <li>If the application targets API level 23 (Android M) or lower then the user certificate
* store is trusted by default as well for non-privileged applications.</li>
* <li>Privileged applications do not trust the user certificate store on Android P and higher.
* </li>
* </ol>
*
* @hide
*/
public static Builder getDefaultBuilder(ApplicationInfo info) {
Builder builder = new Builder()
.setHstsEnforced(DEFAULT_HSTS_ENFORCED)
// System certificate store, does not bypass static pins.
.addCertificatesEntryRef(
new CertificatesEntryRef(SystemCertificateSource.getInstance(), false));
final boolean cleartextTrafficPermitted = info.targetSdkVersion < Build.VERSION_CODES.P
&& info.targetSandboxVersion < 2;
builder.setCleartextTrafficPermitted(cleartextTrafficPermitted);
// Applications targeting N and above must opt in into trusting the user added certificate
// store.
if (info.targetSdkVersion <= Build.VERSION_CODES.M && !info.isPrivilegedApp()) {
// User certificate store, does not bypass static pins.
builder.addCertificatesEntryRef(
new CertificatesEntryRef(UserCertificateSource.getInstance(), false));
}
return builder;
}
可以看到这里的入参变成了ApplicationInfo,但是内部的逻辑没有变,依然是判断targetSdkVersion,但在这里的targetSdkVersion是从info开始获取的,所以上层肯定也有少许的改动,但是问题不大:
public ManifestConfigSource(Context context) {
mContext = context;
// Cache the info because ApplicationInfo is mutable and apps do modify it :(
mApplicationInfo = new ApplicationInfo(context.getApplicationInfo());
}
@Override
public NetworkSecurityConfig getDefaultConfig() {
return getConfigSource().getDefaultConfig();
}
private ConfigSource getConfigSource() {
synchronized (mLock) {
if (mConfigSource != null) {
return mConfigSource;
}
int configResource = mApplicationInfo.networkSecurityConfigRes;
ConfigSource source;
if (configResource != 0) {
boolean debugBuild =
(mApplicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
if (DBG) {
Log.d(LOG_TAG, "Using Network Security Config from resource "
+ mContext.getResources()
.getResourceEntryName(configResource)
+ " debugBuild: " + debugBuild);
}
source = new XmlConfigSource(mContext, configResource, mApplicationInfo);
} else {
if (DBG) {
Log.d(LOG_TAG, "No Network Security Config specified, using platform default");
}
// the legacy FLAG_USES_CLEARTEXT_TRAFFIC is not supported for Ephemeral apps, they
// should use the network security config.
boolean usesCleartextTraffic =
(mApplicationInfo.flags & ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC) != 0
&& mApplicationInfo.targetSandboxVersion < 2;
source = new DefaultConfigSource(usesCleartextTraffic, mApplicationInfo);
}
mConfigSource = source;
return mConfigSource;
}
}
private static final class DefaultConfigSource implements ConfigSource {
private final NetworkSecurityConfig mDefaultConfig;
DefaultConfigSource(boolean usesCleartextTraffic, ApplicationInfo info) {
mDefaultConfig = NetworkSecurityConfig.getDefaultBuilder(info)
.setCleartextTrafficPermitted(usesCleartextTraffic)
.build();
}
@Override
public NetworkSecurityConfig getDefaultConfig() {
return mDefaultConfig;
}
@Override
public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() {
return null;
}
}
}
看到了吧,mApplicationInfo也是初始化的时候建立的,不过也很简单,取出这个mApplicationInfo,将里面的targetSdkVersion直接改成23以下就行:
Object o= XposedHelpers.getObjectField(param.thisObject, "mApplicationInfo");
if (o instanceof ApplicationInfo){
//如果匹配的上说明存在这个变量,修改这个变量,9.0新增
// http://androidxref.com/9.0.0_r3//xref/frameworks/base/core/java/android/security/net/config/ManifestConfigSource.java
ApplicationInfo info= (ApplicationInfo) o;
info.targetSdkVersion=23;
XposedHelpers.setObjectField(param.thisObject, "mApplicationInfo", info);
}
这样Android9就适配好了(Android10的源码没找到,还没来得及看异同),原理很简单,多实践两次就行。
说明
在本文我们主要介绍关于无ROOT情况下怎么使用Xposed抓包的原理,看到这里的同学想必有所体会。需要说明的是本次实践是针对无ROOT场景下的抓HTTPS,工具使用的是太极(阴),如果是相同原理的APP均可实现。
另外本文仅针对普通场景下的Https的应用,相较于证书校验等大家可以参考JustTrustMe的源码,里面有相关证书校验的HOOK,针对SSL PINGNING 技术,这个一般情况下找到HOOK的点即可,其他Https相关的校验和安全相关的配置,我在这方面比较薄弱,欢迎大家踊跃留言。