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