关于无Root进行Https抓包的可行性实践与探索

不羁的心 提交于 2020-08-15 07:20:50

关于无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;
    }

这样看起来我们只需要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的地方,让我们放手去做吧:



//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
    @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);                    
                        }
                    });
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!