Android抓包时遇到的一些问题

Android开发中查看HTTP/HTTPS通信的请求、返回数据是一个常见debug需求,通常有两种实现方式:

  • 网络日志输出到控制台,在LogCat中查看。
  • 使用网络通信抓包工具,抓取产生的所有通信数据。

LogCat

以最常用的OkHttp为例,只需在构造OkHttpClient时添加日志拦截器

// build.gradle添加依赖
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'

// 添加日志拦截器
val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(HttpLoggingInterceptor()
            .setLevel(HttpLoggingInterceptor.Level.BODY))
        .build()

添加拦截器后,使用此OkHttpClient进行的网络请求,包括与RxJava配合使用的Retrofit和其它基于OkHttpLibrary,都会把通信数据输出到LogCat里,对于一些简单的表单请求这种方式十分方便有效。

但对一些非文本数据的上传和下载,输出到LogCat里是一堆乱码,无法查看具体二进制数据,比如图片是无法直接打开的。此外,测试的同事通常也不习惯查看AndroidLogCat,所以提交的测试版应该有一种方式能够直观获取到通信数据,即开放抓包

抓包

抓包有两种:

  • 使用Wireshark抓取传输层的TCP/UDP通信包。
  • 使用Charles抓取应用层的HTTP/HTTPS通信包。

大部分场景下只需要抓取应用层的HTTP/HTTPS数据包,抓包操作很简单。PCCharles监听一个端口,比如8888,Android测试机连接同一个局域网,配置网络代理指向该PC的该端口。这样测试机所有网络通信都会被转发到PC的8888端口,进而被Charles捕获,然后即可进行数据分析。

HTTP协议本身是明文传输,可以直接看到数据报文,除非对传输的明文二次加密,但那是另一件事。

而双向加密的HTTPS,正常情况下即使能以中间人的方式拿到通信报文,但因为没有密钥并不能看到具体内容。基于HTTPS加密通信的建立过程和密钥交换方式,如果在加密通信建立之前截取服务端发送的包含证书的报文,伪装成服务端,把自己的证书发给客户端,然后拿到客户端返回的包含对称加密通信密钥的报文。这样双向加密通信依然得以建立,而中间人实际拿到了通信的密钥,可以查看、修改HTTPS通信报文,这就是典型的Man-in-the-middle attackMITM中间人攻击

实现MITM的关键是中间人要把服务端证书替换成自己的证书发给客户端,让客户端相信自己就是服务端,可问题是客户端为什么会相信呢?HTTPS之所以安全,是因为用来建立加密通信的证书是由权威CA机构签发的,受信CA根证书会被内嵌在WindowsLinuxmacOSAndroidiOS这些操作系统里,用来对服务端发来的证书进行验证。CA当然不可能随便给一个中间人签发不属于它的域名证书,那么就只能把中间人根证书导入客户端的操作系统,以此来完成建立加密通信时对中间人证书的验证。

所以,在一定情况下HTTPS通信可以被监听,抓包的实现基础是Android测试机导入Charles根证书

新问题

以上配置后确实可以看到HTTPS的通信内容,但当targetSDK设置到24以上,在Android 7.0以上的设备上测试时,发现又看不到了。原因是GoogleAndroid 7.0时更改了App对操作系统本地根证书的信任机制。

Android 7.0之前默认信任系统预置根证书用户自导入根证书Android 7.0之后为保障App的通信安全避免被第三方抓包,App默认只信任系统预置根证书而不再信任用户自导入根证书,所以自然看不到HTTPS密文。

知道原因,解决方法有四种:

  • Root测试机或自编译系统,把Charles根证书设置为系统预置根证书
  • Android 7.0以下的测试机中抓包。
  • targetSDK版本设置为24以下。
  • 修改App的AndroidManifest网络安全配置,信任用户自导入根证书

只有第四种是合理的解决方案。

参考Google官方文档/res/xml/中新建network_security_config.xml文件,写入以下内容:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- 支持网络通信的明文传输,在Android 9.0即targetSDK >= 28时,当webView访问http站点时,需要配置此项 -->
    <base-config cleartextTrafficPermitted="true">
        <!-- 只有在debug模式下才会覆写的属性,以支持在Android 7.0即targetSDK >= 24时使用用户自导入CA根证书抓包 -->
        <debug-overrides>
            <trust-anchors>
                <!-- 信任系统根证书 -->
                <certificates src="system" />
                <!-- 信任用户根证书 -->
                <certificates src="user" />
            </trust-anchors>
        </debug-overrides>
    </base-config>
</network-security-config>

ModuleAndroidManifest文件中导入:

<application
    android:name=".CusApp"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="${appName}"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    android:networkSecurityConfig="@xml/network_security_config"
    tools:ignore="UnusedAttribute"/>

这样配置后,debug模式的App就会信任用户自导入的根证书。如果依然抓不到数据,可能是项目中使用的HTTP工具没有信任这些根证书,以OkHttp为例,设置其信任所有根证书

private fun getUnsafeOkHttpClient(): OkHttpClient? {
    return try {
        // 创建一个不验证证书链的TrustManager
        val trustAllCerts = arrayOf<TrustManager>(
                object : X509TrustManager {
                    @Throws(CertificateException::class)
                    override fun checkClientTrusted(chain: Array<X509Certificate?>?, authType: String?) {

                    }
                    @Throws(CertificateException::class)
                    override fun checkServerTrusted(chain: Array<X509Certificate?>?, authType: String?) {

                    }
                    override fun getAcceptedIssuers(): Array<X509Certificate?>? {
                        return arrayOf()
                    }
                }
        )
        // 使用该TrustManager
        val sslContext = SSLContext.getInstance("SSL")
        sslContext.init(null, trustAllCerts, SecureRandom())
        // 创建SSLSocketFactory
        val sslSocketFactory = sslContext.socketFactory
        // 创建OkHttpClient
        OkHttpClient.Builder()
                .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
                .hostnameVerifier { hostname, session -> true }
                .build()
    } catch (e: Exception) {
        throw RuntimeException(e)
    }
}

安全

测试时使用以上配置方便抓包,要上架的正式版必须遵循Android安全策略,关闭抓包途径以保障通信安全。

arrow_upward