一、前置知識
-
默認你了解了HTTPS中間人攻擊的流程,再來回顧一下這個圖:
-
網絡在傳輸過程中一般不會被監控解密,但是通過中間人進行攻擊(也就是抓包)可以解密這個傳輸流程。
-
其實如果不進行客戶端安裝證書,也是可以抓包的,但是沒有信任的證書相當于就是在路由上走一次,并沒有加解密過程,實際上還是客戶端與服務器端進行加解密通信
-
-
這個中間人自己生成的證書在中間進行加解密與雙方進行通信。
二、核心知識點
2.1 證書安裝
-
了解了中間人的攻擊流程,也就知道了關鍵是這個證書,證書被校驗成功即可以進行雙向的傳輸解密
-
安卓7以后安裝的證書是放在用戶目錄的,并不能被系統信任,所以無法加解密流量。
-
-
大多數抓不到包的原因就是證書安裝了,但是在用戶目錄
-
解決方法:
-
1
使用MT管理器
把用戶目錄的證書移動到系統證書目錄(據說可以支持到安卓10)-
用戶證書文件目錄
/data/misc/user/0/cacerts-added/
-
系統證書目錄
/etc/security/cacerts/
-
-
2 由于 Android 10 采用了某些安全策略,將系統分區
/system掛載為只讀
,就算你 root 了也沒用,無法寫入系統分區也就無法導入系統證書-
解決:使用Move Certificates模塊
-
https://github.com/Magisk-Modules-Repo/movecert
-
-
3 修改源碼默認信任用戶證書(提供幾個檢測的源碼定位代碼)
-
-
-
到此安裝完證書 使用抓包工具(charles、fidder、Burpsuit等)就可以進行抓包了
-
但是我們使用的證書是中間人的證書 不是服務器直接下發的證書所以只能解決80%的HTTPS抓包問題。
三、SSLPinning環境下如何抓包
3.1 證書校驗——SSL證書綁定
上文可以了解到從 HTTP 到 HTTPS 數據在傳輸過程中添加了一層 加密(SSL/TLS),讓我們數據流量處于加密狀態,不再是明文可見。一旦 app 校驗了證書的指紋信息。我們的證書不再受信任了。自然而然就無法建立連接,所以必須想辦法讓 app 信任,才能繼續抓包。當然這個分為兩種情況:
3.1.1客戶端校驗服務器端的證書
上篇文件提到了一個證書包含了很多信息,那么客戶端校驗的原理就是:
在APP中預先設置好證書的信息,在證書校驗階段時與服務器返回的證書信息進行比較。
-
一般會有以下整數的信息會被校驗,每種校驗的方式這里就不展開了,下一篇文件在詳細研究。
-
公鑰校驗
-
證書校驗
-
Host校驗
-
-
如何繞過?(未混淆的情況)
探索開發邏輯
1 公鑰校驗
-
這里我把52pj的證書公鑰寫進app進行校驗實驗
private void doRequest(){
new Thread(){
@Override
public void run() {
final String CA_PUBLIC_KEY = "sha256/kO7OP94daK9P8+X52s00RvJLU0SiCXA9KAg9PelfwIw=";
final String CA_DOMAIN = "www.52pojie.cn";
//校驗公鑰
CertificatePinner buildPinner = new CertificatePinner.Builder()
.add(CA_DOMAIN, CA_PUBLIC_KEY)
.build();
OkHttpClient client = new OkHttpClient.Builder().certificatePinner(buildPinner).build();
Request req = new Request.Builder().url("https://www.52pojie.cn/forum.php")
.build();
Call call = client.newCall(req);
try {
Response res = call.execute();
Log.e("請求成功", "狀態碼:" + res.code());
} catch (IOException e) {
e.printStackTrace();
Log.e("請求失敗", "異常" + e);
}
}
}.start();
}
-
安裝好charles的證書后發現抓包就不好使了
-
證書公鑰校驗的代碼 一般來說是使用同的網絡請求框架,大多都是Okhttp3
CertificatePinner buildPinner = new CertificatePinner.Builder()
.add(CA_DOMAIN, CA_PUBLIC_KEY)
.build();
//將buildPinner 傳給OkHttpclient
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(buildPinner)
.build();
-
查看
certificatePinner(buildPinner)
的代碼邏輯,使用frida hook把返回值空即可
public void check(String hostname, List peerCertificates)
throws SSLPeerUnverifiedException {
List pins = findMatchingPins(hostname);
if (pins.isEmpty()) return;
if (certificateChainCleaner != null) {
peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname);
}
for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
// Lazily compute the hashes for each certificate.
ByteString sha1 = null;
ByteString sha256 = null;
for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
Pin pin = pins.get(p);
if (pin.hashAlgorithm.equals("sha256/")) {
if (sha256 == null) sha256 = sha256(x509Certificate);
if (pin.hash.equals(sha256)) return; // Success!
} else if (pin.hashAlgorithm.equals("sha1/")) {
if (sha1 == null) sha1 = sha1(x509Certificate);
if (pin.hash.equals(sha1)) return; // Success!
} else {
throw new AssertionError("unsupported hashAlgorithm: " + pin.hashAlgorithm);
}
}
}
-
frida 腳本片段
// Bypass OkHTTPv3 {1}
var okhttp3_Activity_1 = Java.use('okhttp3.CertificatePinner');
okhttp3_Activity_1.check.overload('java.lang.String', 'java.util.List').implementation = function(a, b) {
console.log('[+] Bypassing OkHTTPv3 {1}: ' + a);
return;
2 證書校驗
private void doRequest2(){
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//服務器返回的證書
X509Certificate cf = chain[0];
//轉換為RSA的公鑰
RSAPublicKey rsaPublicKey = (RSAPublicKey) cf.getPublicKey();
//Base64 encode
String ServerPubkey = Base64.encodeToString(rsaPublicKey.getEncoded(), 0);
Log.e("服務器端返回的證書",ServerPubkey);
//讀取客戶端資源目錄中的證書
InputStream client_input = getResources().openRawResource(R.raw.pojie);
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate realCertificate = (X509Certificate) certificateFactory.generateCertificate(client_input);
String realPubkey = Base64.encodeToString(realCertificate.getPublicKey().getEncoded(), 0);
Log.e("客戶端資源目錄中的證書",realPubkey);
cf.checkValidity();
final boolean expected = realPubkey.equalsIgnoreCase(ServerPubkey);
Log.e("eq = ",String.valueOf(expected));
if (!expected){
throw new CertificateException("證書不一致");
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
SSLSocketFactory factory = null;
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null,new TrustManager[]{trustManager},new SecureRandom());
factory = sslContext.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
SSLSocketFactory finalFactory = factory;
new Thread(){
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient.Builder().sslSocketFactory(finalFactory, trustManager).build();
Request req = new Request.Builder().url("https://www.52pojie.cn/forum.php").build();
Call call = client.newCall(req);
Response res = call.execute();
Log.e("請求發送成功","狀態碼:" + res.code());
} catch (IOException e) {
Log.e("請求發送失敗","網絡異常" + e);
}
}
}.start();
}
X509TrustManager trustManager = new X509TrustManager() {
...
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
...
}
....
}
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var SSLContext = Java.use('javax.net.ssl.SSLContext');
// TrustManager (Android < 7) //
////////////////////////////////
var TrustManager = Java.registerClass({
// Implement a custom TrustManager
name: 'dev.asd.test.TrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function(chain, authType) {},
checkServerTrusted: function(chain, authType) {},
getAcceptedIssuers: function() {return []; }
}
});
// Prepare the TrustManager array to pass to SSLContext.init()
var TrustManagers = [TrustManager.$new()];
// Get a handle on the init() on the SSLContext class
var SSLContext_init = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom');
try {
// Override the init method, specifying the custom TrustManager
SSLContext_init.implementation = function(keyManager, trustManager, secureRandom) {
console.log('[+] Bypassing Trustmanager (Android < 7) pinner');
SSLContext_init.call(this, keyManager, TrustManagers, secureRandom);
};
} catch (err) {
console.log('[-] TrustManager (Android < 7) pinner not found');
//console.log(err);
}
-
-
-
不用的請求框架 代碼不一樣需要看情況編寫
-
-
內置證書到資源文件目錄
-
抓包測試
-
校驗的核心邏輯:自定義的
trustManager
類實現的checkServerTrusted接口
-
繞過:
-
這個一般是自定義的類然后實現了這個trustManager的接口 ,所以不確定這個類在哪,不容易hook
-
但是定義好trustManager會傳入以下倆地方
-
思路是這樣:實例化一個trustManager類,然后里面什么都不寫,當上面兩處調用到這個類時hook這兩個地方,把自己定義的空trustManager類放進去,這樣就可以繞過,參考下面的frida腳本
-
3host(域名)校驗
-
背景:一個證書可能對應有很多域名都可以使用,但是開發者只想讓當前的應用證書校驗通過后只能往指定的域名發送請求,而不想證書校驗通過后往其他可以校驗證書通過的域名發送請求。(證書允許往很多域名發送請求,但是app限制只能往特定域名發送請求)
private void doRequest3(){
HostnameVerifier verifier = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
if ("www.52pojie.cn".equalsIgnoreCase(hostname)){
return true;
}
return false;
}
};
new Thread() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient.Builder().hostnameVerifier(verifier).build();
Request req = new Request.Builder().url("https://www.52pojie.cn/forum.php").build();
Call call = client.newCall(req);
Response res = call.execute();
Log.e("請求發送成功", "狀態碼:" + res.code());
} catch (IOException ex) {
Log.e("Main", "網絡請求異常" + ex);
}
}
}.start();
}
-
-
繞過方式和證書校驗思路一樣 自己創建一個HostnameVerifier類實現的接口直接返回true,哪里調用傳給哪里即可。加好友hackctf55進交流群。
-
3.1.2 服務器端證書校驗
在客戶端放入證書(p12/bks),客戶端向服務端發送請求時,攜帶證書信息,在服務端會校驗客戶端攜帶過來的證書合法性
private void doRequest4(){
X509TrustManager trustManager = new 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];
}
};
HostnameVerifier verify = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
new Thread(){
@Override
public void run() {
try {
InputStream client_input = getResources().openRawResource(R.raw.client);
Log.e("x",client_input.getClass().toString());
SSLContext sslContext = SSLContext.getInstance("TLS");
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(client_input, "demoli666".toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "demoli666".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), new TrustManager[]{trustManager}, new SecureRandom());
SSLSocketFactory factory = sslContext.getSocketFactory();
OkHttpClient client = new OkHttpClient.Builder().sslSocketFactory(factory, trustManager).hostnameVerifier(verify).build();
Request req = new Request.Builder().url("https://xxx.xxx.xxx.xxx:443/index").build();
Call call = client.newCall(req);
Response res = call.execute();
Log.e("請求發送成功","狀態碼:" + res.code());
} catch (Exception e) {
Log.e("請求發送失敗","網絡異常" + e);
}
}
}.start();
}
-
開發邏輯
-
在APK打包時,將證書放入assets或raw目錄
-
在開發代碼時,發送請求讀取證書文件內容+證書密碼 攜帶發送到服務器端
-
-
標志
解決方法
-
找到證書文件(bsk/p12)
-
通過hook獲取證書相關密碼
Java.perform(function () {
function uuid(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [], i;
radix = radix || chars.length;
if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
// rfc4122, version 4 form
var r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
function storeP12(pri, p7, p12Path, p12Password) {
var X509Certificate = Java.use("java.security.cert.X509Certificate")
var p7X509 = Java.cast(p7, X509Certificate);
var chain = Java.array("java.security.cert.X509Certificate", [p7X509])
var ks = Java.use("java.security.KeyStore").getInstance("PKCS12", "BC");
ks.load(null, null);
ks.setKeyEntry("client", pri, Java.use('java.lang.String').$new(p12Password).toCharArray(), chain);
try {
var out = Java.use("java.io.FileOutputStream").$new(p12Path);
ks.store(out, Java.use('java.lang.String').$new(p12Password).toCharArray())
} catch (exp) {
console.log(exp)
}
}
//在服務器校驗客戶端的情形下,幫助dump客戶端證書,并保存為p12的格式,證書密碼為r0ysue
Java.use("java.security.KeyStore$PrivateKeyEntry").getPrivateKey.implementation = function () {
var result = this.getPrivateKey()
var packageName = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext().getPackageName();
storeP12(this.getPrivateKey(), this.getCertificate(), '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12', 'r0ysue');
return result;
}
Java.use("java.security.KeyStore$PrivateKeyEntry").getCertificateChain.implementation = function () {
var result = this.getCertificateChain()
var packageName = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext().getPackageName();
storeP12(this.getPrivateKey(), this.getCertificate(), '/sdcard/Download/' + packageName + uuid(10, 16) + '.p12', 'r0ysue');
return result;
}
});
-
-
將證書導入charles即可正常抓包
-
charles只支持p12證書,若是在app中獲取了bks證書 需要 轉換p12
-
可以使用https://keystore-explorer.org/downloads.html來做證書的轉換。
-
在使用py構造請求時也需要攜帶這個證書和密碼
-
四、混淆代碼
1 關于混淆
-
只需要了解到安卓開發中,系統包是無法混淆的,例如
java.security.KeyStore
不會被混淆,但是第三方的包都會被混淆為a.b.c.v
類似的形式 -
所以在這樣的情況下之前的hook第三方包的腳本都是不通用的
-
客戶端證書校驗的frida腳本【不通用】
Java.use('okhttp3.CertificatePinner');
Java.use('com.square.okhttp.internal.tls.OkHostnamaVerifier');
-
服務端證書校驗的frida腳本【通用】
Java.use("java.security.KeyStore");
-
遇到混淆的情況下,這些hook代碼或多或少會失效一些,所以我們要尋找一個像服務端證書校驗的系統函數來繞過客戶端證書校驗。
2 解決方法
-
服務端的證書校驗對于我們來說已經不是問題,混淆對于
java.security.KeyStore
沒有作用。 -
客戶端的證書校驗,我們先來捋一下開發時的邏輯
-
1 調用證書校驗
X509TrustManager trustManager = new X509TrustManager() { public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} }
-
2 主機校驗
HostnameVerifier verify = new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } };
-
3 pinner 公鑰校驗這個校驗主要是調用了CertificatePinner類中check方法
CertificatePinner buildPinner = new CertificatePinner.Builder() .add(CA_DOMAIN, CA_PUBLIC_KEY) .build(); OkHttpClient client = new OkHttpClient.Builder().certificatePinner(buildPinner).build();
-
-
可以打調用棧或者hook查看這三個方法走的調用邏輯,結論就是都走了:
okhttp3.internal.connection.RealConnection
類中的connectTls
方法:
Java.perform(function () {
var NativeSsl = Java.use('com.android.org.conscrypt.NativeSsl');
NativeSsl.doHandshake.overload('java.io.FileDescriptor', 'int').implementation = function (a, b) {
console.log("參數:", a, b);
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
return this.doHandshake(a, b);
};
});
// frida -UF -l 1.hook_check.js
-
找到第三方調用棧 注意客戶端的證書校驗順序
-
1 證書校驗
Java.perform(function () { var Platform = Java.use('com.android.org.conscrypt.Platform'); Platform.checkServerTrusted.overload('javax.net.ssl.X509TrustManager', '[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'com.android.org.conscrypt.AbstractConscryptSocket').implementation = function (x509tm, chain, authType, socket) { console.log(' [+] checkServer ',x509tm,JSON.stringify(x509tm) ); // 這里會去調用客戶端證書校驗的方法,不執行,就是不去校驗(直接通過)。 //return this.checkServerTrusted(x509tm, chain, authType, socket); }; });
-
2 hostname校驗
很少遇到 不寫了 Java.perform(function () { function getFieldValue(obj, fieldName) { var cls = obj.getClass(); var field = cls.getDeclaredField(fieldName); field.setAccessible(true); var name = field.getName(); var value = field.get(obj); return value; } function getMethodValue(obj, methodName) { var res; var cls = obj.getClass(); var methods = cls.getDeclaredMethods(); methods.forEach(function (method) { var method_name = method.getName(); console.log(method_name, method); if (method_name === methodName) { method.setAccessible(true); res = method; return; } }) return res; } var RealConnection = Java.use('uk.c'); RealConnection.f.implementation = function (a, b, c, d) { try { console.log("==============="); var route = getFieldValue(this, "c"); console.log('route=', route); var address = getFieldValue(route, 'a'); console.log('address=', address); var hostnameVerifier = getFieldValue(address, 'hostnameVerifier'); console.log('hostnameVerifier=', hostnameVerifier); console.log(' [+] hostnameVerifier', hostnameVerifier); } catch (e) { console.log(e); } return this.f(a, b, c, d); }; });
-
3 公鑰pinner校驗
connectTls中就能找到他的類和方法被混淆后的名稱,可以直接Hook
-
五、禁用代理的場景
-
現在的很多的app都是用禁止網絡代理來防止抓包。在請求的時候都使用了Proxy.NO_PROXY
-
解決方法:
-
傳輸層的vpn進行流量轉發
-
使用charles + Postern
-
postern是在傳輸層久把流量轉發指定的中間人(代理/抓包軟件)
-
不理解傳輸層什么意思?稍后來填坑
-
六、特殊框架 flutter 環境下如何抓包
-
框架特點:自實現SSL庫 + 單向證書綁定 + 禁用代理
-
https://bbs.kanxue.com/thread-261941.htm
-
注意自己的系統是32位還是64位
-
拖到IDA中 ---> shift + f12 搜索ssl_server
-
找到這個地方后 往上找函數開始的地方
-
然后你需要這樣操作才能把字節顯示出來
-
菜單欄->Options->General->Disassembly->Display disassambly line parts->number ofo opcode bytes(non-graph)->填入每行顯示的字節數即可
-
-
也可以直接找該函數的函數地址
function hook_ssl_verify_result(address) { Interceptor.attach(address, { onEnter: function(args) { console.log("Disabling SSL validation") }, onLeave: function(retval) { console.log("Retval: " + retval) retval.replace(0x1); } }); } function disablePinning(){ // Change the offset on the line below with the binwalk result // If you are on 32 bit, add 1 to the offset to indicate it is a THUMB function: .add(0x1) // Otherwise, you will get 'Error: unable to intercept function at ......; please file a bug' // 0x393DA4 換成你找到的函數地址 var address = Module.findBaseAddress('libflutter.so').add(0x393DA4) hook_ssl_verify_result(address); } setTimeout(disablePinning, 1000)
七、非root環境如何抓包
-
場景:應用對root環境進行校驗
-
暫時不展開
八、自實現SSL/TLS框架
九、其他抓包技巧
-
https://www.52pojie.cn/thread-1405917-1-1.html
-
hook得到 sslkey,可以解釋原理
-
https://bbs.kanxue.com/thread-277996.htm
-
服務器
+關注
關注
12文章
9184瀏覽量
85477 -
APP
+關注
關注
33文章
1574瀏覽量
72514 -
SSL
+關注
關注
0文章
125瀏覽量
25743
原文標題:APP抓不到包?
文章出處:【微信號:哆啦安全,微信公眾號:哆啦安全】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論