一、前置知识
-
默认你了解了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 修改源码默认信任用户证书(提供几个检测的源码定位代码)
-
/frameworks/base/core/java/android/security/net/config/NetworkSecurityConfig.java
-
/frameworks/base/core/java/android/security/net/config/XmlConfigSource.java
-
libcore/ojluni/src/main/java/java/net/NetworkInterface.java
-
libcore/ojluni/src/main/java/java/lang/System.java
-
到此安装完证书 使用抓包工具(charles、fidder、Burpsuit等)就可以进行抓包了
-
但是我们使用的证书是中间人的证书 不是服务器直接下发的证书 所以只能解决80%的HTTPS抓包问题。
三、SSLPinning环境下如何抓包
3.1 证书校验——SSL证书绑定
上文可以了解到从 HTTP 到 HTTPS 数据在传输过程中添加了一层 加密(SSL/TLS),让我们数据流量处于加密状态,不再是明文可见。一旦 app 校验了证书的指纹信息。我们的证书不再受信任了。自然而然就无法建立连接,所以必须想办法让 app 信任,才能继续抓包。当然这个分为两种情况:
3.1.1 客户端校验服务器端的证书
上篇文件提到了一个证书包含了很多信息,那么客户端校验的原理就是:
在APP中预先设置好证书的信息,在证书校验阶段时与服务器返回的证书信息进行比较。
-
一般会有以下整数的信息会被校验,每种校验的方式这里就不展开了,下一篇文件在详细研究。
-
公钥校验
-
证书校验
-
Host校验
-
如何绕过?(未混淆的情况)
-
因为本质上校验是在客户端中完成的,所以客户端中肯定会有相关的校验代码,我们找到校验的地方进行HOOK,使得校验的地方直接PASS。这里方法列举,暂时不展开,下一篇文件在详细研究。加好友hackctf55进交流群。
-
frida hook(见下面)
-
JustTrustMe(xposed的Hook脚本)
-
https://github.com/LSPosed/LSPosed/releases
-
https://github.com/Fuzion24/JustTrustMe/releases/tag/v.2
-
安装后无图标
-
去手机的
/data/adb/lspd/
目录下找apk包,然后再点击安装即可。
探索开发逻辑
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<Certificate> peerCertificates)
throws SSLPeerUnverifiedException {
List<Pin> 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;
-
不用的请求框架 代码不一样 需要看情况编写
-
内置证书到资源文件目录
-
抓包测试
-
校验的核心逻辑:自定义的
trustManager
类实现的checkServerTrusted接口 -
绕过:
-
这个一般是自定义的类然后实现了这个trustManager的接口 ,所以不确定这个类在哪,不容易hook
-
但是定义好trustManager会传入以下俩地方
-
思路是这样:实例化一个trustManager类,然后里面什么都不写,当上面两处调用到这个类时hook这两个地方,把自己定义的空trustManager类放进去,这样就可以绕过,参考下面的frida脚本
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);
}
3 host(域名)校验
-
背景:一个证书可能对应有很多域名都可以使用,但是开发者只想让当前的应用证书校验通过后只能往指定的域名发送请求,而不想证书校验通过后往其他可以校验证书通过的域名发送请求。(证书允许往很多域名发送请求,但是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),客户端向服务端发送请求时,携带证书信息,在服务端会校验客户端携带过来的证书合法性
-
开发逻辑
-
在APK打包时,将证书放入assets或raw目录
-
在开发代码时,发送请求读取证书文件内容+证书密码 携带发送到服务器端
-
标志
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();
}
-
找到证书文件(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('n[+] 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('n[+] 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
原文始发于微信公众号(哆啦安全):APP抓不到包?
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论