了解了HTTPS的传输流程,再来了解一下抓包相关知识。
一、前置知识
二、核心知识点
2.1 证书安装
三、SSLPinning环境下如何抓包
3.1 证书校验——SSL证书绑定
上文可以了解到从 HTTP 到 HTTPS 数据在传输过程中添加了一层 加密(SSL/TLS),让我们数据流量处于加密状态,不再是明文可见。一旦 app 校验了证书的指纹信息。我们的证书不再受信任了。自然而然就无法建立连接,所以必须想办法让 app 信任,才能继续抓包。当然这个分为两种情况:
3.1.1 客户端校验服务器端的证书
上篇文件提到了一个证书包含了很多信息,那么客户端校验的原理就是:在APP中预先设置好证书的信息,在证书校验阶段时与服务器返回的证书信息进行比较。
探索开发逻辑
1 公钥校验
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();
}
CertificatePinner buildPinner = new CertificatePinner.Builder()
.add(CA_DOMAIN, CA_PUBLIC_KEY)
.build();
//将buildPinner 传给OkHttpclient
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(buildPinner)
.build();
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
// 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) {
...
}
....
}
绕过:
这个一般是自定义的类然后实现了这个trustManager的接口 ,所以不确定这个类在哪,不容易hook
但是定义好trustManager会传入以下俩地方
思路是这样:实例化一个trustManager类,然后里面什么都不写,当上面两处调用到这个类时hook这两个地方,把自己定义的空trustManager类放进去,这样就可以绕过,参考下面的frida脚本
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var SSLContext = Java.use('javax.net.ssl.SSLContext');
// TrustManager (Android
3 host(域名)校验
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();
}
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();
}
标志
解决方法
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
四、混淆代码
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 没有作用。
客户端的证书校验,我们先来捋一下开发时的逻辑
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
}
HostnameVerifier verify = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
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
找到第三方调用栈 注意客户端的证书校验顺序
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('\n[+] checkServer ',x509tm,JSON.stringify(x509tm) );
// 这里会去调用客户端证书校验的方法,不执行,就是不去校验(直接通过)。
//return this.checkServerTrusted(x509tm, chain, authType, socket);
};
});
很少遇到 不写了
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('\n[+] hostnameVerifier', hostnameVerifier);
} catch (e) {
console.log(e);
}
return this.f(a, b, c, d);
};
});
connectTls中就能找到他的类和方法被混淆后的名称,可以直接Hook
五、禁用代理的场景
现在的很多的app都是用禁止网络代理来防止抓包。在请求的时候都使用了Proxy.NO_PROXY
解决方法:
传输层的vpn进行流量转发
使用charles + Postern
postern是在传输层久把流量转发指定的中间人(代理/抓包软件)
六、特殊框架 flutter 环境下如何抓包
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环境如何抓包
八、自实现SSL/TLS框架
九、其他抓包技巧