Android 抓包时遇到的一些问题
立泉Android 开发中查看 HTTP/HTTPS 的通信数据是一个常见 debug 需求,通常有两种实现方式:
- 网络日志输出到控制台,在 LogCat 中查看。
- 使用网络抓包工具抓取产生的通信数据。
LogCat
以常用 OkHttp 为例,只需在构造OkHttpClient时添加日志Interceptor拦截器:
// 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 和其它基于 OkHttp 的 Library,都会把通信数据输出到 LogCat 里,这种方式适合简单的表单请求。
但非文本数据的上传和下载输出到 LogCat 里是一堆乱码,无法查看具体二进制数据,比如图片无法直接打开。此外测试的同事通常不习惯查看 Android 的 LogCat,所以提交的测试版应该有一种方式能够直观获取到通信数据,即开放抓包。
抓包
抓包有两种:
- 使用 Wireshark 抓取传输层 TCP/UDP 通信包。
- 使用 Charles 抓取应用层 HTTP/HTTPS 通信包。
大部分场景只需要抓取应用层 HTTP/HTTPS,抓包时先设置 PC 的 Charles 监听一个端口,然后 Android 测试机连接同一个局域网,配置网络代理指向该 PC 的该端口。这样测试机的所有网络通信都会被定向转发给监听的 Charles,即可进行数据分析。
除非有二次加密,明文传输的 HTTP 协议可直接看到数据报文,而双向加密的 HTTPS 正常情况下即使能以中间人方式拿到通信数据,但因为没有密钥并不能看到具体内容。基于 HTTPS 加密通信的建立过程和密钥交换方式,如果在加密通信建立之前截取服务端发送的证书报文,伪装成服务端把自己的证书发给客户端,就能拿到返回的包含对称加密通信密钥的报文。这样双向加密通信依然能够建立,中间人也可通过密钥查看、修改 HTTPS 通信报文,即是典型的 Man-in-the-middle attack / MITM 中间人攻击。
实现 MITM 的关键是中间人替换服务端证书发给客户端,让客户端相信自己是服务端,可问题是客户端为什么相信?HTTPS 之所以安全是因为用来建立加密通信的证书均由权威 CA 机构签发,受信 CA 的根证书会被内置在操作系统里对服务端发来的证书进行核验。CA 不可能给未验证身份的中间人签发不属于它的域名证书,只能设法把中间人的根证书导入客户操作系统,以此完成建立加密通信时对中间人证书的验证。
所以在一定情况下 HTTPS 通信可以被监听,抓包的实现基础是 Android 测试机导入 Charles 的根证书。
新问题
依照以上配置即可获取 HTTPS 通信内容,但当targetSDK设置到 24 并在 Android 7.0 之后的设备上测试时则会失效,原因是 Google 在新版本 Android 中更改了 App 对操作系统根证书的信任机制。Android 7.0 之前默认信任系统预置根证书和用户导入根证书,之后为保障通信安全避免被第三方抓包,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>
在 Module 的 AndroidManifest 文件中导入:
<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 安全策略关闭抓包途径以保障通信安全。