关于无ROOT抓Https请求的探索

背景

从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等。连接地址(需要翻墙)
但是这种方案仅适用于有源码了进行修改,但是在开发阶段还需要注意存在这些配置的包不能流传到外部,否则造成泄露。这种方案仅适用于开发和测试人员或者内部使用。

  1. 修改targetSdkVersion
    将APP的targetSdkVersion修改在24以下就行。但是这样对APP的影响太大,兼容性、上架等因素,所以这种方案不可取。
  2. ROOT
    首先这个方案需要ROOT,当然,既然Root了,有很多方案可以抓包。
    (1).复制证书到系统证书层面
    这个方案网上方法很多,就不一一赘述(因为我也没试过,搞起来麻烦,我都是用第二种方案),举个例子:例子
    (2).Xposed
    有个大佬写了一个JustTrustMe,当然这个框架是基于Xposed(ROOT版的)。安装这个软件可以直接抓取Https的包。

不常见的方案

以上的方案不是需要ROOT就是需要改源码,这个操作对于普通线下用户来说还是有点难度的,所以针对上面的方案对一下拓展,如何实现免ROOT的抓包。

  1. 官方的方法
    (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相关的校验和安全相关的配置,我在这方面比较薄弱,欢迎大家踊跃留言。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值