現象與原因
Android 對於 Http 和 Https 兩類網絡請求
Http
因爲沒有加密,屬於明文傳輸,是可以抓包的。
但是從 Android 9.0 開始,默認是禁止 App 使用 Http 這種使用所有未加密的連接,使用 Http 會導致程序報錯。
java.net.UnknownServiceException: CLEARTEXT communication
但還是可以通過寫一段關於網絡安全的配置 network_security_config ,讓系統允許繼續使用 Http 協議。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
這裏先跳過,下面會詳細講怎麼讓它生效。
可以認爲 Android9.0 以後不推薦使用 Http,要求開發者轉移到 Https。
Https
Https 是一種使用了加密傳輸的協議,防止了 App 和 服務器之間的中間人進來攔截、僞造、篡改問題。
但是如果是手機持有人,主動在手機裏安裝 charles 的根證書,實現了認證環節,是可以實現抓包的。
然而 Android 也發現了這種漏洞,爲了保護應用開發公司的通訊安全,在 Android7.0 以後,只信任 Android 設備的系統根證書。也就是如果你安裝的 charles 根證書這類屬於「用戶證書」分類的證書,Android 系統是不認的,照樣不讓 Https 請求正常通過。
程序會報錯:
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
而此時的 charles 顯示如下:
ps:一般在 Android 手機的這個打開路徑下「設置 - 某個“安全”設置子頁面 - 加密與憑證 - 信任的憑證」,可以看到該設備的所有根證書。
分爲「系統」和「用戶」兩個分類。
——————————————————————————————
解決辦法
一、開發者抓第三方的 Https 包
如果第三方只是使用默認的 Android Https 配置,那麼可以使用這兩種方法可以抓到它們的請求包。
1. 使用 Android7.0 以下的手機安裝應用,然後抓包
很好理解,上面講過了只有 Android7.0 以後,纔開始不信任用戶根證書。
2. 想辦法把 你的 charles 證書或者其他證書,變成設備的根證書
比如你是手機設備廠家,或者你可以編輯一套ROM出來,當然可以把任何個人證書給搞成是系統證書。
另一種方法是,需要一部有Root權限的手機,安裝 Xposed 的 JustTrustMe 模塊來信任所有的證書。
二、開發者抓自己 App 的 Https 包
0.應該不會有人爲了抓包,把 targetSdkVersion 強行改成低於24(Android 7.0)的版本吧。孩子睡覺老是踢被子,幸好被我及時發現打斷了腿,否則肯定感冒。
1. 使用 Android 提供的「網絡安全配置(Network security configuration)」
官方講解文檔:Android 開發者官網 網絡安全配置
步驟一:在 manifest 文件中配置一個 android:networkSecurityConfig 屬性,填寫一個 xml 文件。
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:networkSecurityConfig="@xml/network_security_config"
... >
...
</application>
</manifest>
步驟二:在 res/xml 文件夾裏創建一個 network_security_config.xml 文件,裏面配置如下。
配置的意思是在 debug 模式下,信任用戶證書。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<debug-overrides>
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
一個很重要的知識點是,區分是不是 debug 包是通過 module 的 build.gralde 文件,在 buildType 裏面的 debuggable 字段來決定的。
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//debuggable true 如果打生產包時候忘了關,就玩脫了
}
preRelease {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
debuggable true //推薦這種做法。創建一個預發佈的 buildType,打開 debuggable=true 專門給測試人員能在正式域名環境裏抓包檢查用。
}
debug {
minifyEnabled false
shrinkResources false
}
}
在 buildTypes 裏配置這個 debuggable 屬性,最終會被合併到 manifest 文件裏面的 <application 結點下,增加一個 android:debuggable=“true” 屬性。
2. 配置 OkHttp 信任所有證書
在使用 builder 模式構建 OkHttpClient 的時候,增加 sslSocketFactory 和 hostnameVerifier 配置項。下面的演示代碼裏這兩個配置項裏面會信任所有證書。爲了避免玩脫也記得只在 BuildConfig.DEBUG 條件下才使用這個配置。這個 BuildConfig.DEBUG 的值跟前面講的 debuggable 是一致的。
public class ZhihuHttp {
public static final String ZHIHU_BASE_URL = "https://news-at.zhihu.com/api/";
private static final ZhihuHttp zhihuHttp = new ZhihuHttp();
private OkHttpClient okHttpClient;
private static SSLSocketFactory createSSLSocketFactory() {
SSLSocketFactory sSLSocketFactory = null;
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[]{new TrustAllManager()},
new SecureRandom());
sSLSocketFactory = sc.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
return sSLSocketFactory;
}
private static class TrustAllManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
private static class TrustAllHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
private ZhihuHttp() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(10, TimeUnit.SECONDS);
if (BuildConfig.DEBUG) {
builder.sslSocketFactory(createSSLSocketFactory(), new TrustAllManager());
builder.hostnameVerifier(new TrustAllHostnameVerifier());
}
okHttpClient = builder.build();
}
public static ZhihuHttp getZhihuHttp() {
return zhihuHttp;
}
public void getDailiesWithCallback() {
Request request = new Request.Builder()
.url(ZHIHU_BASE_URL + "4/news/latest")
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.e("YAO", "ZhihuHttp.java - onFailure() ----- e:" + e.toString());
e.printStackTrace();
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
Log.e("YAO", "ZhihuHttp.java - onResponse() ----- :" + response.toString());
}
});
}
}
三、怎麼防止被其他開發者抓包
前面講到,默認的 Android Https 配置下,只要使用 Android7.0 以下的手機、或者找個 Root 設備安裝把用戶證書(比如charles證書)想辦法搞進系統證書那部分,就可以抓包了。這對於黑產來說也忒忒忒簡單了。那麼怎麼防止呢?
答案是配置你信任的網站證書或者配置信任的認證鏈
1.Android 官方配置信息證書
比如你可以像這樣把你信任的網站的證書給搞下來
步驟①:點擊域名旁邊鎖的圖標,彈出框裏面點「證書」
mac系統,對着證書那個圖標拖動到某個文件夾裏。這樣你就能得到一個HTTPS的裏面的SSL裏面的非對稱加密算法的公鑰。
步驟②:放進 res/raw 文件夾裏,在network_security_config.xml 裏寫上相關配置
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- 這個是全局的基礎的配置 -->
<base-config>
<trust-anchors>
<!-- 如果整個base-config都不寫,就等於是<certificates src="system" /> -->
<!-- 這裏寫全局基礎配置,只信任下面某幾個證書 -->
<certificates src="@raw/zhihu" />
<certificates src="@raw/baidu" />
</trust-anchors>
</base-config>
<!-- 如果只對某些涉及數據安全的私密域名進行保護,可以針對某個域名,只信任某幾個證書 -->
<domain-config>
<domain includeSubdomains="true">zhihu.com</domain>
<trust-anchors>
<certificates src="@raw/zhihu" />
<certificates src="@raw/tencent" />
</trust-anchors>
</domain-config>
<debug-overrides>
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
以上就是 Android 官方推薦的做法。
——————————————————————————————
2. OkHttp 配置信任認證鏈
參照這部分的 OkHttp官方介紹 以及 stack overflow,可以學到這部分的使用方法
步驟①:寫一個 CertificatePinner 的配置
其中的 add方法兩個參數。第一個參數是網址的域名host,第二個是sha256的證書。證書我們目前不清楚,先輸入這麼一段字符串。使用這麼一段錯誤的配置運行後將會報錯,然後在日誌裏得到正確的配置信息。
注意第一個參數不要包含協議,也不要省略部分域名,錯誤示例 「https://news-at.zhihu.com」、「zhihu.com」。
第二個參數是個假證書識別串,但是有效的,我測試時候亂輸入了一串「sha256/wrong」沒有觸發到搜想要的結果。
public class ZhihuHttp {
public static final String ZHIHU_BASE_URL = "https://news-at.zhihu.com/api/";
private static final ZhihuHttp zhihuHttp = new ZhihuHttp();
private OkHttpClient okHttpClient;
private ZhihuHttp() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(10, TimeUnit.SECONDS);
// 只信任網站對應的證書
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("news-at.zhihu.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
builder.certificatePinner(certificatePinner);
okHttpClient = builder.build();
}
public static ZhihuHttp getZhihuHttp() {
return zhihuHttp;
}
public void getDailiesWithCallback() {
Request request = new Request.Builder()
.url(ZHIHU_BASE_URL + "4/news/latest")
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.e("YAO", "ZhihuHttp.java - onFailure() ----- e:" + e.toString());
e.printStackTrace();
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
Log.e("YAO", "ZhihuHttp.java - onResponse() ----- :" + response.toString());
}
});
}
}
步驟②:執行代碼後報錯。搜索關鍵字,我們能得到這麼一串報錯
Subscriber onError() : javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain:
sha256/f5fNYvDJUKFsO51UowKkyKAlWXZXpaGK6Bah4yX9zmI=: CN=*.zhihu.com,OU=IT,O=智者四海(北京)技術有限公司,L=北京市,C=CN
sha256/zUIraRNo+4JoAYA7ROeWjARtIoN4rIEbCpfCRQT6N6A=: CN=GeoTrust RSA CA 2018,OU=www.digicert.com,O=DigiCert Inc,C=US
sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=: CN=DigiCert Global Root CA,OU=www.digicert.com,O=DigiCert Inc,C=US
Pinned certificates for news-at.zhihu.com:
sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
這報錯的意思是。現在訪問這個鏈接的認證鏈是 Peer certificate chain 下面的3個sha256。
「sha256/f5fNYvDJUKFsO51UowKkyKAlWXZXpaGK6Bah4yX9zmI=」
「sha256/zUIraRNo+4JoAYA7ROeWjARtIoN4rIEbCpfCRQT6N6A=」
「sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=」
鏈的含義是,第一個sha256對應的證書由第二個sha256對應的證書認證,第二個sha256對應的證書又第三個sha256對應的證書認證。
在代碼裏配置的用於「news-at.zhihu.com」域名的認證 sha256 期望是
「sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=」
因爲兩者對應不上,所以請求失敗。然而通過這個方法,我們得到正確的sha256,下面就拿這幾個正確的sha256來配置。
步驟③:配置正確 sha256
CertificatePinner certificatePinner = new CertificatePinner.Builder()
//正常請求下的證書驗證鏈路
.add("news-at.zhihu.com", "sha256/f5fNYvDJUKFsO51UowKkyKAlWXZXpaGK6Bah4yX9zmI=")//CN=*.zhihu.com,OU=IT,O=智者四海(北京)技術有限公司,L=北京市,C=CN
.add("news-at.zhihu.com", "sha256/zUIraRNo+4JoAYA7ROeWjARtIoN4rIEbCpfCRQT6N6A=")//CN=GeoTrust RSA CA 2018,OU=www.digicert.com,O=DigiCert Inc,C=US
.add("news-at.zhihu.com", "sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=")//CN=DigiCert Global Root CA,OU=www.digicert.com,O=DigiCert Inc,C=US
.build();
配置後,這個域名的請求就不能被抓包了。因爲開啓抓包後,根證書是 charles 的公鑰,跟期望的 sha256 匹配不上。
一個很重要的點是,其實我們可以不用把3個 sha256 都加上。匹配邏輯是任意一個 sha256 匹配上請求就可以通過了。所以其實可以這麼寫。
CertificatePinner certificatePinner = new CertificatePinner.Builder()
//正常請求下的證書驗證鏈路
.add("news-at.zhihu.com", "sha256/f5fNYvDJUKFsO51UowKkyKAlWXZXpaGK6Bah4yX9zmI=")//CN=*.zhihu.com,OU=IT,O=智者四海(北京)技術有限公司,L=北京市,C=CN
.build();
域名hostname 支持通配符,可以參考 OkHttp CertificatePinner 裏面的「Wildcard pattern rules」部分
驗證結果
按照 OkHttp 官方指導配置完後,使用 charles 抓包看看還能不能在 Android 7.0 以下系統抓到包。
驗證結果,不能抓包,會出現一個報錯:
Subscriber onError() : javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain:
sha256/dVUJFtUhQtJki5t0/j+hMYzTgtVkETqjsogUuyquPPo=: CN=*.zhihu.com,OU=IT,O=智者四海(北京)技術有限公司,L=北京市,C=CN
sha256/54ZQa+M6vq6DhdR7DLkc1X6fWmVEZ6wLZaaYwoR4Uvw=: C=NZ,ST=Auckland,L=Auckland,O=XK72 Ltd,OU=https://charlesproxy.com/ssl,CN=Charles Proxy CA (2 十月 2017\, YaodeMacBook-Pro.local)
Pinned certificates for news-at.zhihu.com:
sha256/f5fNYvDJUKFsO51UowKkyKAlWXZXpaGK6Bah4yX9zmI=
sha256/zUIraRNo+4JoAYA7ROeWjARtIoN4rIEbCpfCRQT6N6A=
sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=
可以看到現在訪問這個鏈接的認證鏈是 Peer certificate chain 下面的兩個sha256。
「sha256/dVUJFtUhQtJki5t0/j+hMYzTgtVkETqjsogUuyquPPo=」
「sha256/54ZQa+M6vq6DhdR7DLkc1X6fWmVEZ6wLZaaYwoR4Uvw=」
報錯的信息裏,第二個 sha256 冒號後面,顯示這串字符來自我的 charles 公鑰。
對比 未使用 和 使用 抓包的報錯信息,發現第一個 sha256 都是來自於知乎的公鑰,但兩個的 sha256 是不一樣的。
//未開啓抓包
sha256/f5fNYvDJUKFsO51UowKkyKAlWXZXpaGK6Bah4yX9zmI=: CN=*.zhihu.com,OU=IT,O=智者四海(北京)技術有限公司,L=北京市,C=CN
//開啓抓包
sha256/dVUJFtUhQtJki5t0/j+hMYzTgtVkETqjsogUuyquPPo=: CN=*.zhihu.com,OU=IT,O=智者四海(北京)技術有限公司,L=北京市,C=CN
一開始我一直以爲這一串東西是SSL公鑰進行一次hash算法得到的字符串。後面觀察後發現不是。
這是兩個公鑰在不同 「中間證書認證」-「中間證書認證」……「根證書認證」這種認證體系下的一個hash串。
所以如果開啓抓包,那麼他對應的根證書是 charles 的公鑰,得出的第一個來自知乎證書的 sha256 就會有所不同。
warning: 配置信任某個具體證書一定要與服務器開發或運維溝通好,因爲如果服務器進行了證書替換而App沒有更新到最新證書,App的請求將會失效。如果開啓的是全部域名的證書配置,意味着你連應用內升級或者熱更新都用不了,絕對是重大事故。
四、還有什麼騷操作
上面講到我們配置到工程代碼裏的是 網站的公鑰(任何人都可以隨意下載)
根據HTTPS的原理,公鑰和私鑰的原理,其實完全可以在代碼裏配置上開發者的 charles 公鑰(只針對某臺具體的筆記本,charles 爲它生成的一對公鑰私鑰)。因爲沒人能根據公鑰能破解出對應的私鑰。
所以如果在在 app 裏配上我們某部電腦 charles 公鑰,以後就可以用那個電腦抓正式環境正式域名的請求了。比如工程加上公司的公用開發機的 charles 公鑰,或者核心App測試大佬的 charles 公鑰。
使用 Android 官方配置信任證書可以這些寫
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- 這個是全局的基礎的配置 -->
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<!-- 如果整個base-config都不寫,就等於是<certificates src="system" /> -->
<!-- 這裏寫全局基礎配置,只信任下面某幾個證書 -->
<certificates src="@raw/zhihu" />
<certificates src="@raw/yao_charles" />
</trust-anchors>
</base-config>
<debug-overrides>
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
使用OkHttp的配置的方法,這麼寫
// 只信任網站對應的證書
CertificatePinner certificatePinner = new CertificatePinner.Builder()
//正常請求下的證書驗證鏈路
.add("news-at.zhihu.com", "sha256/f5fNYvDJUKFsO51UowKkyKAlWXZXpaGK6Bah4yX9zmI=")//CN=*.zhihu.com,OU=IT,O=智者四海(北京)技術有限公司,L=北京市,C=CN
.add("news-at.zhihu.com", "sha256/zUIraRNo+4JoAYA7ROeWjARtIoN4rIEbCpfCRQT6N6A=")//CN=GeoTrust RSA CA 2018,OU=www.digicert.com,O=DigiCert Inc,C=US
.add("news-at.zhihu.com", "sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=")//CN=DigiCert Global Root CA,OU=www.digicert.com,O=DigiCert Inc,C=US
//charles 抓包下的配置
.add("news-at.zhihu.com", "sha256/dVUJFtUhQtJki5t0/j+hMYzTgtVkETqjsogUuyquPPo=")//CN=*.zhihu.com,OU=IT,O=智者四海(北京)技術有限公司,L=北京市,C=CN
.add("news-at.zhihu.com", "sha256/54ZQa+M6vq6DhdR7DLkc1X6fWmVEZ6wLZaaYwoR4Uvw=")//C=NZ,ST=Auckland,L=Auckland,O=XK72 Ltd,OU=https://charlesproxy.com/ssl,CN=Charles Proxy CA (2 十月 2017\, YaodeMacBook-Pro.local)
.build();
——————————————————————————————
附贈一份看過的比較好的 HTTPS 文章《從0到1講解HTTPS設計流程》,幫助解決95%關於 HTTPS 方面的疑問。