抓包检测绕过分析
常见的抓包检测
- 正常抓包不走代理
HttpsURLConnection设置不走代理
okhttp3设置不走代理
- 代理检测与VPN检测
Hook设置代理、在模拟器中可以使用httpv7来抓包、使用VPN抓包
- 单向验证与双向验证
单向验证:客户端检测服务器 (比如客户端去检测服务器的证书也就是公钥,通过比对公钥来查看是否有抓包工具的出现(因为中间人抓包工具没有对应网页的私钥,所以要给客户端签发的证书也就是自己有私钥的伪造的公钥,才能进行数据交互之间的解密。)
抓包解密SSL流量,需要伪造证书
通常使用抓包工具的根证书,来签发一个服务器实体证书,这时可以证书检测
双向验证:
客户端校验服务器证书:
通常利用系统相关函数来校验证书,这时可以通过Hook相关系统函数来绕过
X509TrustManager HostnameVerifier okhttp3证书锁定 okhttp3证书校验
服务器也会去检测客户端,
- HOOK抓包
利用HOOK函数去实现抓取系统函数来实现抓包
不用需理会各种抓包检测
抓到的可能没有抓包工具全,会被Hook检测
代理检测:
代理检测需要设置代理,函数里面通过System.setProperty来实现设置代理,而代理检测则是通过
System.getProperty()
VPN检测:
常见的VPN检测代码:
- java.net.NetworkInterface.getName()
public static void getNetworkName() {
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
int count = 0;
while (networkInterfaces.hasMoreElements()) {//使用 while 循环遍历每一个网络接口。hasMoreElements() 方法检查是否还有网络接口可以处理,nextElement() 方法返回下一个网络接口对象。
NetworkInterface next = networkInterfaces.nextElement();
logOutPut("getName获得网络设备名称=" + next.getName());
logOutPut("getDisplayName获得网络设备显示名称=" + next.getDisplayName());
logOutPut("getIndex获得网络接口的索引=" + next.getIndex());
logOutPut("isUp是否已经开启并运行=" + next.isUp());
logOutPut("isBoopback是否为回调接口=" + next.isLoopback());
logOutPut("**********************" + count++);
}
} catch (SocketException e) {
e.printStackTrace();
}
// "getName获得网络设备名称=tun0" <----- 可能出现的VPN情况 这样就是有问题的
//正常的是 ---> "getName获得网络设备名称=rmnet_data0-rmnet_data999"
这里检测的函数也就是NetworkInterface类对象的getName()方法,所以要过检测就直接去HOOK这里的函数,然后判断是否有tun的字眼,有就返回别的
function hook_vpn(){
Java.perform(function() {
var NetworkInterface = Java.use("java.net.NetworkInterface");
NetworkInterface.getName.implementation = function() {
var name = this.getName();
console.log("name: " + name);
if(name == "tun0" || name == "ppp0"){
return "rmnet_data0";
}else {
return name;
}
}
})
}
- android.net.ConnectivityManager.getNetworkCapabilities
检测代码:
public void networkCheck() {
try {
ConnectivityManager connectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);//获得网络连接管理器
Network network = connectivityManager.getActiveNetwork();//getActiveNetwork() 方法返回当前设备上正在使用的网络对象 Network,表示当前活动的网络连接。可能返回 null,这表示当前没有活跃网络连接。
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
Log.i("TAG", "networkCapabilities -> " + networkCapabilities.toString());//getNetworkCapabilities(Network network) 方法根据网络对象获取 NetworkCapabilities 实例,这个实例包含了当前网络的各种特性,比如网络类型(WiFi、移动网络等)、连接速度、传输能力等。
} catch (Exception e) {
e.printStackTrace();
}
}
//检测到VPN的字眼:networkCapabilities -> [ Transports: WIFI|VPN Capabilities: INTERNET&NOT_RESTRICTED&TRUSTED&VALIDATED&NOT_ROAMING&FOREGROUND&NOT_CONGESTED&NOT_SUSPENDED LinkUpBandwidth>=1048576Kbps LinkDnBandwidth>=1048576Kbps]
当检测到提前的网络连接时,会出现添加vpn的字眼,具体是在toString()的地方进行的添加的
public @NonNull String toString() {
final StringBuilder sb = new StringBuilder("[");
if (0 != mTransportTypes) {
sb.append(" Transports: ");
appendStringRepresentationOfBitMaskToStringBuilder(sb, mTransportTypes,
NetworkCapabilities::transportNameOf, "|");
}
具体也就是在这里添加上的VPN的字眼的,所以我们可以直接去HOOK这里
NetworkCapabilities.appendStringRepresentationOfBitMaskToStringBuilder.implementation = function (sb, bitMask, nameFetcher, separator) {
if (bitMask == 18) {
console.log("bitMask", bitMask);
sb.append("WIFI");
}else {
console.log(sb, bitMask);
this.appendStringRepresentationOfBitMaskToStringBuilder(sb, bitMask, nameFetcher, separator);
}
}
HttpsURLConnection的GET和POST请求发送
package com.chen_chen_chen.myapplication;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Proxy;
import java.net.URL;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class HttpUtil {
public static void Post()
{
new Thread(){
public void run(){
String str =getHttpsURLConnection("POST","https://baidu.com/","user");
if (str!=null) Log.d("HTTP input:", str);
else Log.d("HTTP input:", "error");
}
}.start();
}
private static Context getSSLContext() {
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, null, null);
} catch (Exception e) {
e.printStackTrace();
}
return sslContext;
}
public static String getHttpsURLConnection(String method, String url, String outputStr) {
//System.setProperty("https.proxyHost", "192.168.10.1");
//System.setProperty("https.proxyPort", "8888");
//Proxy.NO_PROXY
//Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.10.1", 8888));
try {
SSLContext sslContext = getSSLContext();
if (sslContext != null) {
URL u = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) u.openConnection(Proxy.NO_PROXY);
conn.setRequestMethod("GET");
conn.setDoInput(true);//输出
conn.setUseCaches(false);
conn.setConnectTimeout(30000);
if(method.equals("POST")){
conn.setRequestMethod("POST");
conn.setDoOutput(true);//输入
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
}
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
conn.connect();
InputStream inputStream = conn.getInputStream();//获取输出流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
conn.disconnect();
return buffer.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
HttpsURLConnection框架代码获取GET和POST请求
从上面得知,我们利用HttpsURLConeection申请的get和post请求的结果是通过这一部分来确定和指定的初始化数据
if (sslContext != null) {
URL u = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) u.openConnection(Proxy.NO_PROXY);
conn.setRequestMethod("GET");
conn.setDoInput(true);//输出
conn.setUseCaches(false);
conn.setConnectTimeout(30000);
if(method.equals("POST")){
conn.setRequestMethod("POST");
conn.setDoOutput(true);//输入
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
}
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
conn.connect();
其中主要是通过的conn对象也就是HttpsURLConnection类对象来进行初始化的,其中包含了请求方式conn.setRequestMethod("GET"),以及请求的输入,这里的输入流应该是同步的,先获取输入流对象,然后直接写入就可以写好请求包了
我们获取抓包请求和响应其实也就是通过getOutputStream()以及getInputStream()来获取的,所以其实可以去HOOK这些函数来实现我们的框架下的抓包HOOK
getOutputStream()是在com.android.okhttp.internal.huc.HttpsURLConnectionImpl
javax.net.ssl.HttpsURLConnection
java.net.URLConnection
我们可以通过objection来实现快速的定位,其实发现这个函数是在com.android.okhttp.internal.huc.HttpsURLConnectionImpl类下真正实现的
HttpsURLConnection的证书检测
首先是利用HttpsURLConnection的框架提交POST或者Get请求时初始化证书检测
SSLContext sslContext = getSSLContext();
if (sslContext != null) {
URL u = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) u.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
conn.setRequestMethod(method);
conn.setDoInput(true); // 允许输入流
conn.setUseCaches(false);
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setSSLSocketFactory(sslContext.getSocketFactory());//这里就是初始化证书检测的地方
conn.setHostnameVerifier(DO_NOT_VERIFY);
conn.setSSLSocketFactory(sslContext.getSocketFactory()假如这里的 HttpsURLConnection的对象里面没有去单独设置证书初始化的话,那么这里就是利用的默认的证书检测,全部的证书都能过
百度证书:
-----BEGIN CERTIFICATE-----
MIIJ7DCCCNSgAwIBAgIMTkADpl62gfh/S9jrMA0GCSqGSIb3DQEBCwUAMFAxCzAJ
BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSYwJAYDVQQDEx1H
bG9iYWxTaWduIFJTQSBPViBTU0wgQ0EgMjAxODAeFw0yNDA3MDgwMTQxMDJaFw0y
NTA4MDkwMTQxMDFaMIGAMQswCQYDVQQGEwJDTjEQMA4GA1UECBMHYmVpamluZzEQ
MA4GA1UEBxMHYmVpamluZzE5MDcGA1UEChMwQmVpamluZyBCYWlkdSBOZXRjb20g
U2NpZW5jZSBUZWNobm9sb2d5IENvLiwgTHRkMRIwEAYDVQQDEwliYWlkdS5jb20w
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1wFMskJ2dseOqoHptNwot
FOhdBERsZ4VQnRNKXEEXMQEfgbNtScQ+C/Z+IpRAt1EObhYlifn74kt2nTsCQLng
jfQkRVBuO/6PNGKdlCYGBeGqAL7xR+LOyHnpH9mwCBJc+WVt2zYM9I1clpXCJa+I
tsq6qpb1AGoQxRDZ2n4K8Gd61wgNCPHDHc/Lk9NPJoUBMvYWvEe5lKhHsJtWtHe4
QC3y58Vi+r5R0PWn2hyTBr9fCo58p/stDiRqp9Irtmi95YhwkNkmgwpMB8RhcGoN
h+Uw5TkPZVj4AVaoPT1ED/GMKZev0+ypmp0+nmjVg2x7yUfLUfp3X7oBdI4TS2hv
AgMBAAGjggaTMIIGjzAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADCBjgYI
KwYBBQUHAQEEgYEwfzBEBggrBgEFBQcwAoY4aHR0cDovL3NlY3VyZS5nbG9iYWxz
aWduLmNvbS9jYWNlcnQvZ3Nyc2FvdnNzbGNhMjAxOC5jcnQwNwYIKwYBBQUHMAGG
K2h0dHA6Ly9vY3NwLmdsb2JhbHNpZ24uY29tL2dzcnNhb3Zzc2xjYTIwMTgwVgYD
VR0gBE8wTTBBBgkrBgEEAaAyARQwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93d3cu
Z2xvYmFsc2lnbi5jb20vcmVwb3NpdG9yeS8wCAYGZ4EMAQICMD8GA1UdHwQ4MDYw
NKAyoDCGLmh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vZ3Nyc2FvdnNzbGNhMjAx
OC5jcmwwggNhBgNVHREEggNYMIIDVIIJYmFpZHUuY29tggxiYWlmdWJhby5jb22C
DHd3dy5iYWlkdS5jboIQd3d3LmJhaWR1LmNvbS5jboIPbWN0LnkubnVvbWkuY29t
ggthcG9sbG8uYXV0b4IGZHd6LmNuggsqLmJhaWR1LmNvbYIOKi5iYWlmdWJhby5j
b22CESouYmFpZHVzdGF0aWMuY29tgg4qLmJkc3RhdGljLmNvbYILKi5iZGltZy5j
b22CDCouaGFvMTIzLmNvbYILKi5udW9taS5jb22CDSouY2h1YW5rZS5jb22CDSou
dHJ1c3Rnby5jb22CDyouYmNlLmJhaWR1LmNvbYIQKi5leXVuLmJhaWR1LmNvbYIP
Ki5tYXAuYmFpZHUuY29tgg8qLm1iZC5iYWlkdS5jb22CESouZmFueWkuYmFpZHUu
Y29tgg4qLmJhaWR1YmNlLmNvbYIMKi5taXBjZG4uY29tghAqLm5ld3MuYmFpZHUu
Y29tgg4qLmJhaWR1cGNzLmNvbYIMKi5haXBhZ2UuY29tggsqLmFpcGFnZS5jboIN
Ki5iY2Vob3N0LmNvbYIQKi5zYWZlLmJhaWR1LmNvbYIOKi5pbS5iYWlkdS5jb22C
EiouYmFpZHVjb250ZW50LmNvbYILKi5kbG5lbC5jb22CCyouZGxuZWwub3JnghIq
LmR1ZXJvcy5iYWlkdS5jb22CDiouc3UuYmFpZHUuY29tgggqLjkxLmNvbYISKi5o
YW8xMjMuYmFpZHUuY29tgg0qLmFwb2xsby5hdXRvghIqLnh1ZXNodS5iYWlkdS5j
b22CESouYmouYmFpZHViY2UuY29tghEqLmd6LmJhaWR1YmNlLmNvbYIOKi5zbWFy
dGFwcHMuY26CDSouYmR0anJjdi5jb22CDCouaGFvMjIyLmNvbYIMKi5oYW9rYW4u
Y29tgg8qLnBhZS5iYWlkdS5jb22CESoudmQuYmRzdGF0aWMuY29tghEqLmNsb3Vk
LmJhaWR1LmNvbYISY2xpY2suaG0uYmFpZHUuY29tghBsb2cuaG0uYmFpZHUuY29t
ghBjbS5wb3MuYmFpZHUuY29tghB3bi5wb3MuYmFpZHUuY29tghR1cGRhdGUucGFu
LmJhaWR1LmNvbTAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHwYDVR0j
BBgwFoAU+O9/8s14Z6jeb48kjYjxhwMCs+swHQYDVR0OBBYEFK3KAFTK2OWUto+D
2ieAKE5ZJDsYMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdgCvGBoo1oyj4KmK
TJxnqwn4u7wiuq68sTijoZ3T+bYDDQAAAZCQAGzzAAAEAwBHMEUCIFwF5Jc+zyIF
Gnpxchz9fY1qzlqg/oVrs2nnuxcpBuuIAiEAu3scD6u51VOP/9aMSqR2yKHZLbHw
Fos9U7AzSdLIZa8AdgAS8U40vVNyTIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAA
AZCQAG3iAAAEAwBHMEUCIBBYQ6NP7VUDgfktWRg5QxT23QAbTqYovtV2D9O8Qc0T
AiEA2P7+44EvQ5adwL1y56oyxv/m+Gujeia7wpo7+Xbhv6MAdwAN4fIwK9MNwUBi
EgnqVS78R3R8sdfpMO8OQh60fk6qNAAAAZCQAGy+AAAEAwBIMEYCIQDU7Hxtx4c9
p9Jd+cr+DCMtyRYSc0b8cktCcbMmtDE9ygIhAIpJd4yb7jtxnaEC8oLWDushbK1v
0BIuZu6YrQvsf1nQMA0GCSqGSIb3DQEBCwUAA4IBAQCh9DfewC012/+fHZpmSpCn
y+h3/+ClAZ8cJVO+LCmYz9r6bkyhcFquJ5qUpyoW8AYtU0oUFlqH6zLIyujW+7lq
wFxB6NsXKKdwBKmMbmnZr2Fca5f+TtwD/GDJgG/egr7fI1u8194j9KEl8cK8Fujm
+UsoWklEzd1It9xkLazJR/6SwbhSR4k610pvj8rQrS4wAewuYFDaDOfqsHtDIsx1
tZfIfoB/O1wGWZQJU2M9wC8uYq0jQ2Q0MQJXuyJz04MFiGrPAS1Uk8mWd8M+3p65
Xy4iAf8uWzs1M+fcwBE8BNBghkQgE+FSUsldm+5ZBCazU0joJswzldWisXMLTagI
-----END CERTIFICATE-----
X509TrustManager trustManager = new X509TrustManager() {
@ Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
if (chain == null) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate array is null");
}
//检测是否为空
if (!(chain.length > 0)) {
throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");
}
//检测是否长度负数
if (!(!TextUtils.isEmpty(authType) && authType.toUpperCase().contains("RSA"))) {
throw new CertificateException("checkServerTrusted: AuthType is not RSA");
}
Log.d("xiaojianbang","authType: " + authType);
X509Certificate cf = chain[0];
//获取证书
RSAPublicKey pubkey = (RSAPublicKey)cf.getPublicKey();
String encoded = Base64.encodeToString(pubkey.getEncoded(),0);
//得到的证书进行base64编码
CertificateFactory finalcf = CertificateFactory.getInstance("X.509");
X509Certificate PUB_KEY = (X509Certificate)finalcf.generateCertificate(new ByteArrayInputStream(certificate.getBytes()));
String realPubKey = Base64.encodeToString(PUB_KEY.getPublicKey().getEncoded(),0);
//这里是去获取的真实的服务器证书同时进行base64编码
cf.checkValidity();
Log.d("xiaojianbang", "IssuerDN: " + cf.getIssuerDN().toString());
Log.d("xiaojianbang", "SubjectDN: " + cf.getSubjectDN().toString());
Log.d("xiaojianbang", "证书版本: "+ cf.getVersion());
final boolean expected = realPubKey.equalsIgnoreCase(encoded);
if (!expected) {
throw new CertificateException("checkServerTrusted: got error public key: " + encoded);
}
Log.d("xiaojianbang","证书公钥验证正确");
//证书比对
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@ Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
这里是对于我们证书检测的单独设置的位置,其中包含了证书链chain的检测以及证书的获取和公钥证书的比对(细节进行了批注)
同样的conn.setHostnameVerifier(DO_NOT_VERIFY);这个函数也设置了一个检测点HostnameVerifier类下面的verify函数也要返回true才行
HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
@ Override
public boolean verify(String hostname, SSLSession session) {
Log.d("xiaojianbang", hostname);
return true;
}
};//注意在session里面同样可以获取证书进行比对
绕过
所以我们可以直接来看看之前过HttpURLConnection的抓包检测的代码了
var HttpsURLConnection = Java.use("com.android.okhttp.internal.huc.HttpsURLConnectionImpl");
HttpsURLConnection.setSSLSocketFactory.implementation = function(SSLSocketFactory) {
quiet_send("HttpsURLConnection.setSSLSocketFactory invoked");
};
HttpsURLConnection.setHostnameVerifier.implementation = function(hostnameVerifier) {
quiet_send("HttpsURLConnection.setHostnameVerifier invoked");
};
var RequestParams = Java.use('org.xutils.http.RequestParams');
RequestParams.setSslSocketFactory.implementation = function(sslSocketFactory) {
sslSocketFactory = EmptySSLFactory;
return null;
}
RequestParams.setHostnameVerifier.implementation = function(hostnameVerifier) {
hostnameVerifier = TrustHostnameVerifier.$new();
return null;
}
//直接去HOOK的setSSLSocketFactory和setHostnameVerifier
至于这里其实是过不了检测的,因为这个走的是com.android.okhttp.internal.huc.HttpsURLConnectionImpl类下的setSslSocketFactory和setHostnameVerifier,所以过不了
okhttps提交POST和GET请求
GET请求提交的过程:
public class okHttp3Utils {
public static OkHttpClient client = new OkHttpClient.Builder().build();//直接构造一个客户端请求对象
public static void POST() {
new Thread() {
public void run() {
// OkHttpClient client = MySSLSocketFactory.createClient();
Request request = new Request.Builder()//创建请求对象
.url("https://www.baidu.com/")
.get()
.addHeader(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 UBrowser/6.2.4098.3 Safari/537.36"
)
.build();
try {
Response response = client.newCall(request).execute();//这里是提交过程中的重点:cilent.newCall()
Log.d("chen_chen_chen", "response: " + response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
Post请求包发送:
FormBody builder = new FormBody.Builder().add("请求键", "请求值").add("请求键1", "请求值1").build();
// OkHttpClient client = MySSLSocketFactory.createClient();
Request request = new Request.Builder()
.url("https://www.baidu.com/")
.post(builder)
也就是在这里多了这些东西
LoggingInterceptor拦截器
这个函数是加载在请求之前和响应之后的函数,和HOOK一样,都可以获取过程中的信息
class LoggingInterceptor implements Interceptor{
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long time1 = System.nanoTime();
Log.d("chen_chen_chen",String.format("Sending request %s on %s %n %s",request.url(),chain.connection(),request.headers()));
//请求前
Response response = chain.proceed(request);
long time2 = System.nanoTime();
Log.d("chen_chen_chen",String.format("Sending response %s on %s %n %s",response.request().url(),(time2-time1)/1e6d,response.headers()));
return response;
//响应后
}
}
/*Sending request https://www.baidu.com/ on null
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 UBrowser/6.2.4098.3 Safari/537.36
2024-10-31 16:52:40.388 18715-18888 chen_chen_chen com.chen_chen_chen.myapplication D Sending response https://www.baidu.com/ on 29636.590765
*/
loggingInterceptor拦截器
package com.chen_chen_chen.myapplication;
import android.util.Log;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
import okhttp3.Connection;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http.HttpHeaders;
import okio.Buffer;
import okio.BufferedSource;
import okio.GzipSource;
import okhttp3.Interceptor;
public class okhttp3Logging implements Interceptor {
private static final String TAG = "okhttpGET";
private static final Charset UTF8 = Charset.forName("UTF-8");
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RequestBody requestBody = request.body();
boolean hasRequestBody = requestBody != null;
Connection connection = chain.connection();
String requestStartMessage = "--> "
+ request.method()
+ ' ' + request.url();
Log.e(TAG, requestStartMessage);
if (hasRequestBody) {
// Request body headers are only present when installed as a network interceptor. Force
// them to be included (when available) so there values are known.
if (requestBody.contentType() != null) {
Log.e(TAG, "Content-Type: " + requestBody.contentType());
}
if (requestBody.contentLength() != -1) {
Log.e(TAG, "Content-Length: " + requestBody.contentLength());
}
}
Headers headers = request.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
String name = headers.name(i);
// Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
Log.e(TAG, name + ": " + headers.value(i));
}
}
if (!hasRequestBody) {
Log.e(TAG, "--> END " + request.method());
} else if (bodyHasUnknownEncoding(request.headers())) {
Log.e(TAG, "--> END " + request.method() + " (encoded body omitted)");
} else {
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
Log.e(TAG, "");
if (isPlaintext(buffer)) {
Log.e(TAG, buffer.readString(charset));
Log.e(TAG, "--> END " + request.method()
+ " (" + requestBody.contentLength() + "-byte body)");
} else {
Log.e(TAG, "--> END " + request.method() + " (binary "
+ requestBody.contentLength() + "-byte body omitted)");
}
}
long startNs = System.nanoTime();
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
Log.e(TAG, "<-- HTTP FAILED: " + e);
throw e;
}
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
Log.e(TAG, "<-- "
+ response.code()
+ (response.message().isEmpty() ? "" : ' ' + response.message())
+ ' ' + response.request().url()
+ " (" + tookMs + "ms" + (", " + bodySize + " body:" + "") + ')');
Headers myheaders = response.headers();
for (int i = 0, count = myheaders.size(); i < count; i++) {
Log.e(TAG, myheaders.name(i) + ": " + myheaders.value(i));
}
if (!HttpHeaders.hasBody(response)) {
Log.e(TAG, "<-- END HTTP");
} else if (bodyHasUnknownEncoding(response.headers())) {
Log.e(TAG, "<-- END HTTP (encoded body omitted)");
} else {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
Long gzippedLength = null;
if ("gzip".equalsIgnoreCase(myheaders.get("Content-Encoding"))) {
gzippedLength = buffer.size();
GzipSource gzippedResponseBody = null;
try {
gzippedResponseBody = new GzipSource(buffer.clone());
buffer = new Buffer();
buffer.writeAll(gzippedResponseBody);
} finally {
if (gzippedResponseBody != null) {
gzippedResponseBody.close();
}
}
}
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (!isPlaintext(buffer)) {
Log.e(TAG, "");
Log.e(TAG, "<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
return response;
}
if (contentLength != 0) {
Log.e(TAG, "");
Log.e(TAG, buffer.clone().readString(charset));
}
if (gzippedLength != null) {
Log.e(TAG, "<-- END HTTP (" + buffer.size() + "-byte, "
+ gzippedLength + "-gzipped-byte body)");
} else {
Log.e(TAG, "<-- END HTTP (" + buffer.size() + "-byte body)");
}
}
return response;
}
/**
* Returns true if the body in question probably contains human readable text. Uses a small sample
* of code points to detect unicode control characters commonly used in binary file signatures.
*/
static boolean isPlaintext(Buffer buffer) {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted()) {
break;
}
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false;
}
}
return true;
} catch (EOFException e) {
return false; // Truncated UTF-8 sequence.
}
}
private boolean bodyHasUnknownEncoding(Headers myheader s) {
String contentEncoding = myheaders.get("Content-Encoding");
return contentEncoding != null
&& !contentEncoding.equalsIgnoreCase("identity")
&& !contentEncoding.equalsIgnoreCase("gzip");
}
}
OKhttp3
证书检测:
首先是设置证书的函数:
public static OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(createSSLSocketFactory(new ByteArrayInputStream(certificate.getBytes())),trustManager)
.hostnameVerifier(new TrustAllHostnameVerifier())
.certificatePinner(CPinner)
.build();
这几个OkHttpClient类下的方法都会使得证书检测到,同时这里的sslSocketFactory也就是在设置对应的证书的位置了,也是算法进行之后直接去比对证书的位置,
certificatePinner(CPinner)这里的方法其实是对于证书进行SHA1或者是SHA256的算法进行一个计算之后的证书校验,比对的是算法之后的SHA值的校验
hostnameVerifier(new TrustAllHostnameVerifier())这里通过也可以进行证书的校验,因为其获取的参数可以得到证书的值
绕过sslSocketFactory:
.sslSocketFactory(createSSLSocketFactory(new ByteArrayInputStream(certificate.getBytes())),trustManager)
这里同时是去HOOK对应类下的sslContext对象的init()这里规定了对于init的TrustManager的位置
private static SSLSocketFactory newSslSocketFactory(X509TrustManager trustManager) {
try {
SSLContext sslContext = Platform.get().getSSLContext();
sslContext.init(null, new TrustManager[] { trustManager }, null);
return sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError("No System TLS", e); // The system has no TLS. Just give up.
}
}
HOOK:
自定义TrustManager
TrustManager = Java.registerClass({//创建自定义的 TrustManager
name: 'org.wooyun.TrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function(chain, authType) {},//checkClientTrusted 和 checkServerTrusted 方法被重写以不执行任何检查。
checkServerTrusted: function(chain, authType) {},
getAcceptedIssuers: function() {
// var certs = [X509Certificate.$new()];
// return certs;
return [];
}
}
});
var SSLContext_init = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom');
// Override the init method, specifying our new TrustManager
SSLContext_init.implementation = function(keyManager, trustManager, secureRandom) {
quiet_send('Overriding SSLContext.init() with the custom TrustManager');
SSLContext_init.call(this, null, TrustManagers, null);
};
上面通过去HOOK对应的SSLContext初始化的位置,实现了对应的自写的TrustManager来接受所以的证书
绕过 .hostnameVerifier(new TrustAllHostnameVerifier())
这里的证书检测也很简单了,不过需要返回的是对应的类对象,这里我们采用了自写的类对象进行赋值
var OkHttpClient$Builder = Java.use('okhttp3.OkHttpClient$Builder');
quiet_send('OkHttpClient$Builder Found');
console.log("hostnameVerifier", OkHttpClient$Builder.hostnameVerifier);
OkHttpClient$Builder.hostnameVerifier.implementation = function () {
quiet_send('OkHttpClient$Builder hostnameVerifier() called. Not throwing an exception.');
return this;
}
var myHostnameVerifier = Java.registerClass({
name: 'com.chenchenchen.MyHostnameVerifier',
implements: [HostnameVerifier],
methods: {
verify: function (hostname, session) {
return true;
}
}
});
var OkHttpClient = Java.use('okhttp3.OkHttpClient');
OkHttpClient.hostnameVerifier.implementation = function () {
quiet_send('OkHttpClient hostnameVerifier() called. Not throwing an exception.');
return myHostnameVerifier.$new();
}
绕过 .certificatePinner(CPinner)
这里的绕过也是一样的,在CertificatePinner类下的check方法去比对了得到的公钥和证书SHA值的对比,我们HOOK这里去直接绕过
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
quiet_send('OkHTTP 3.x Found');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function() {
quiet_send('OkHTTP 3.x check() called. Not throwing an exception.');
}
混淆之后的HOOK定位
在混淆之后我们要去定位这些证书校验的函数怎么去找?
首先是 .certificatePinner(CPinner):
这里是在对应check()方法内部的findMatchingPins(),会去比对的是假如的pins的是否匹配,所以我们可以去HOOK对应的系列函数来实现打印堆栈的情况,最后实现函数定位
这里的系统函数ArrayList的add()方法,去直接HOOK,打印堆栈
List<Pin> findMatchingPins(String hostname) {
List<Pin> result = Collections.emptyList();
for (Pin pin : pins) {
if (pin.matches(hostname)) {
if (result.isEmpty()) result = new ArrayList<>();
result.add(pin);
}
}
return result;
}
MessageDigest.digest check函数的SHA算法
在这里的SHA算法之后的证书比对之前,肯定会去调用对应的SHA算法的加密,所以
private ByteString digest(String algorithm) {
try {
return ByteString.of(MessageDigest.getInstance(algorithm).digest(data));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
也可以去HOOK这里的MessageDigest.digest
网络无法访问会抛出错误,直接Hook错误比如 javax.net.ssl.SSLHandshakeException.$init
这里会报错,那么直接去HOOK对应的提示错误的地方
OKhttps3算法源码分析
okhttps3的算法源码的分析在现在貌似都被全部的Native化了,进不了Java层的函数题内部,大概的内容是Interceptor的处理,同时是对于Socket进行了connect,这里的Socket包含了java.net.Socket 包下面的socket 以com.android.org.conscrypt.Java8FileDescriptorSocket下的 sslSocket两种Socket,而且我们则是要去查看的在数据传输过程中的sslSocket的加密数据。
最终可以定位到com.android.org.conscrypt.Java8FileDescriptorSocket的getOutputStream的位置,这里是对于请求的进入的位置
最终可以定位到的位置是/src/main/java/org/conscrypt/[NativeSsl.java] 下的 write函数
406 void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
407 throws IOException {
408 lock.readLock().lock();
409 try {
410 if (isClosed() || fd == null || !fd.valid()) {
411 throw new SocketException("Socket is closed");
412 }
413 NativeCrypto
414 .SSL_write(ssl, this, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
415 } finally {
416 lock.readLock().unlock();
417 }
418 }
到了NativeCrypto类下的SSL_write函数 (这个函数被Native化了)
1088 static native void SSL_write(long ssl, NativeSsl ssl_holder, FileDescriptor fd,
1089 SSLHandshakeCallbacks shc, byte[] b, int off, int len, int writeTimeoutMillis)
1090 throws IOException;
以及可能在响应包里面的read,读取服务器的响应
391 int read(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
392 throws IOException {
393 lock.readLock().lock();
394 try {
395 if (isClosed() || fd == null || !fd.valid()) {
396 throw new SocketException("Socket is closed");
397 }
398 return NativeCrypto
399 .SSL_read(ssl, this, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
400 } finally {
401 lock.readLock().unlock();
402 }
403 }
同时还要SSL_read函数同样Native化了
1082 static native int SSL_read(long ssl, NativeSsl ssl_holder, FileDescriptor fd, SSLHandshakeCallbacks shc,
1083 byte[] b, int off, int len, int readTimeoutMillis) throws IOException;
OKHttps3自吐算法Java层
Java.perform(function () {
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
function toBase64(tag, data) {
console.log(tag + " Base64: \n", ByteString.of(data).base64());
}
function toHex(tag, data) {
console.log(tag + " Hex: \n", ByteString.of(data).hex());
}
function toUtf8(tag, data) {
console.log(tag + " Utf8: \n", ByteString.of(data).utf8());
}
var NativeCrypto = Java.use("com.android.org.conscrypt.NativeCrypto");
NativeCrypto.SSL_write.implementation = function (ssl, nativeCrypto, fd, handshakeCallbacks, buf, offset, len, timeoutMillis) {
console.log(offset, len);
toUtf8("chen_chen SSL_write: ", buf);
console.log("=======================================================");
this.SSL_write(ssl, nativeCrypto, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
}
NativeCrypto.SSL_read.implementation = function (ssl, nativeCrypto, fd, handshakeCallbacks, buf, offset, len, timeoutMillis) {
console.log(offset, len);
toUtf8("chen_chen SSL_read", buf);
console.log("=======================================================");
return this.SSL_read(ssl, nativeCrypto, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
}
JNI层源码的分析:
Naitve化之后的函数名为对应的类名_函数比如SSL_write ——>在NaitveCrypto的类里——>名为 NaitveCrypto_SSL_write
由此来开始对应的源码分析过程:
/external/conscrypt/common/src/jni/main/cpp/conscrypt/native_crypto.cc
static void NativeCrypto_SSL_write
(JNIEnv* env, jclass, jlong ssl_address, CONSCRYPT_UNUSED jobject ssl_holder, jobject fdObject,
8286 jobject shc, jbyteArray b, jint offset, jint len,
8287 jint write_timeout_millis)
/external/conscrypt/common/src/jni/main/cpp/conscrypt/native_crypto.cc
static int sslWrite(JNIEnv* env, SSL* ssl, jobject fdObject, jobject shc, const char* buf, jint len,
8146 SslError* sslError, int write_timeout_millis)
boringssl是谷歌从openssl改过来的,ssl_lib.cc会编译到libssl.so中
/external/boringssl/src/ssl/ssl_lib.cc
int SSL_write(SSL *ssl, const void *buf, int num)
int SSL_read(SSL *ssl, void *buf, int num)
以上两个函数就是r0capture的hook点
/external/boringssl/src/ssl/s3_pkt.cc
int ssl3_write_app_data(SSL *ssl, bool *out_needs_handshake, const uint8_t *in,
130 int len)
/external/boringssl/src/ssl/s3_pkt.cc
static int do_ssl3_write(SSL *ssl, int type, const uint8_t *in, unsigned len)
在这之前,数据是明文
========================================================================================================
在这之后,数据是密文
/external/boringssl/src/ssl/s3_pkt.cc
static int ssl3_write_pending(SSL *ssl, int type, const uint8_t *in,
205 unsigned int len)
/external/boringssl/src/ssl/ssl_buffer.cc
int ssl_write_buffer_flush(SSL *ssl)
static int dtls_write_buffer_flush(SSL *ssl)
/external/boringssl/src/crypto/bio/bio.c
int BIO_write(BIO *bio, const void *in, int inl)
——————————————————————————————————————————————————————————————————————
libcrypto.so
/external/boringssl/src/crypto/bio/socket.c
static int sock_read(BIO *b, char *out, int outl) {
108 int ret = 0;
109
110 if (out == NULL) {
111 return 0;
112 }
113
114 bio_clear_socket_error();
115 #if defined(OPENSSL_WINDOWS)
116 ret = recv(b->num, out, outl, 0);
117 #else
118 ret = read(b->num, out, outl);
119 #endif
120 BIO_clear_retry_flags(b);
121 if (ret <= 0) {
122 if (bio_fd_should_retry(ret)) {
123 BIO_set_retry_read(b);
124 }
125 }
126 return ret;
127 }
128
129 static int sock_write(BIO *b, const char *in, int inl) {
130 int ret;
131
132 bio_clear_socket_error();
133 #if defined(OPENSSL_WINDOWS)
134 ret = send(b->num, in, inl, 0);
135 #else
136 ret = write(b->num, in, inl);
137 #endif
138 BIO_clear_retry_flags(b);
139 if (ret <= 0) {
140 if (bio_fd_should_retry(ret)) {
141 BIO_set_retry_write(b);
142 }
143 }
144 return ret;
145 }
最终交给libc.so中的 write函数
SSL自吐HOOK
通常情况下HOOK点:在加密之前可以HOOK
int SSL_write(SSL *ssl, const void *buf, int num)
int SSL_read(SSL *ssl, void *buf, int num)
这两个函数其实是在libssl.so的系统so库中,但是开发中其实可以把对应实现的函数copy下来,放到别的so中,这样就找不到了
这种情况下,我们只能去HOOK加密之后的函数,然后打印对应的堆栈信息,看走的是哪个函数
HOOK点:在加密之后可以HOOK
这时候就可以去更底层看libc.so中(打印堆栈)
ssize_t write(int fd, const void * buf, size_t count)ssize_t read(int fd, void * buf, size_t count)
自吐代码:
Java.perform(function () {
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
function toBase64(tag, data) {
console.log(tag + " Base64: \n", ByteString.of(data).base64());
}
function toHex(tag, data) {
console.log(tag + " Hex: \n", ByteString.of(data).hex());
}
function toUtf8(tag, data) {
console.log(tag + " Utf8: \n", ByteString.of(data).utf8());
}
});
var SSL_write_addr = Module.findExportByName("libssl.so", "SSL_write");
var SSL_read_addr = Module.findExportByName("libssl.so", "SSL_read");
console.log(SSL_write_addr, SSL_read_addr);
Interceptor.attach(SSL_write_addr, {
onEnter: function (args) {
console.log("SSL_write_addr: ", Process.getCurrentThreadId() + '\n' +
Thread.backtrace(this.context, Backtracer.FUZZY)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log("SSL_write arg[1]"+hexdump(args[1], {length: args[2].toInt32()}));
}, onLeave: function (retval) {
}
});
Interceptor.attach(SSL_read_addr, {
onEnter: function (args) {
this.args1 = args[1];
}, onLeave: function (retval) {
var nums = retval.toInt32();
if (nums > 0) {
console.log("SSL_read_addr: ", Process.getCurrentThreadId() + '\n' +
Thread.backtrace(this.context, Backtracer.FUZZY)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log("SSL_read arg[1]"+hexdump(this.args1, {length: nums}));
}
}
});
var write_addr = Module.findExportByName("libc.so", "write");
var read_addr = Module.findExportByName("libc.so", "read");
console.log(write_addr, read_addr);
Interceptor.attach(write_addr, {
onEnter: function (args) {
console.log("write_addr: ", Process.getCurrentThreadId() + '\n' +
Thread.backtrace(this.context, Backtracer.FUZZY)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log("write arg[1]"+hexdump(args[1], {length: args[2].toInt32()}));
}, onLeave: function (retval) {
}
});
Interceptor.attach(read_addr, {
onEnter: function (args) {
this.args1 = args[1];
}, onLeave: function (retval) {
var nums = retval.toInt32();
if (nums > 0) {
console.log("read_addr: ", Process.getCurrentThreadId() + '\n' +
Thread.backtrace(this.context, Backtracer.FUZZY)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log("SSL_read arg[1]"+hexdump(this.args1, {length: nums}));
}
}
});
这里的int SSL_write(SSL *ssl, const void *buf, int num) 和 int SSL_read(SSL *ssl, void *buf, int num)的结果都是解密过程的
而ssize_t write(int fd, const void * buf, size_t count) 和 ssize_t read(int fd, void * buf, size_t count)都是加密过程的
单向检测和双向检测的细节处理以及绕过
单向检测是客户端校验服务器的证书
在客户端申请了SSL/TLS连接之后,服务器会发送SSL/TLS证书给客户端,这里便是客户端去校验服务器发送的证书了,这里的校验可以通过HOOK证书派发,实现全部证书的通过。
双向检测是服务器校验客户端的证书
客户端发起连接:客户端请求与服务器建立 SSL/TLS 连接。服务器发送证书服务器首先发送其 SSL/TLS 证书给客户端。客户端验证服务器证书的有效性(如前面提到的单向验证步骤);客户端发送证书:如果服务器要求,客户端也需要发送其证书给服务器进行校验,这里就是对应的服务器校验客户端了。
双向检测绕过:
对于可能出现的双向检测,假如要去绕过服务器校验客户端,则需要去获取到对应的客户端的证书,这样的证书可能会以文件的形式存放在对应的apk的压缩文件里,也能是在内存加载,我们可以通过dump下对应的内存读取。
而在证书双向验证的时候 Keystore.load()是对应证书进行加载使用的( 通常有证书密码 )我们可以去HOOK这里的代码去实现证书的获取
public final void load(InputStream stream, char[] password)
传入的是InputStream对象(证书)和证书密码
不过也可能这里的证书为null,然后在之后去设置
keyStore.setCertificateEntry(certificateAlias, certificate);
HOOK代码:
Java.perform(function () {
var KeyStore = Java.use("java.security.KeyStore");
var str = Java.use("java.lang.String");
KeyStore.load.overload("java.io.InputStream", "[C").implementation = function (input, pwdStr) {
if (input) {
console.log("pwdStr: ", str.$new(pwdStr));
var file = Java.use("java.io.File").$new("/data/data/com.xh.xinghe/xiaojianbang.p12");
// File file = new File("/data/data/com.xh.xinghe/xiaojianbang.p12");
var output = Java.use("java.io.FileOutputStream").$new(file);
//FileOutputStream output = new FileOutputStream(file)
var r, myArr = [];
for (var i = 0; i < 1024; i++) {
myArr[i] = 0;
}
var buffer = Java.array("byte", myArr);
//数组转byte array
while((r = input.read(buffer)) > 0) {
output.write(buffer, 0, r);
}
//读证书写入文件
console.log("save");
output.close();
}
return this.load(input, pwdStr);
}});
得到的证书通过cp 到 /sdcard/下然后导入抓包工具 这样就可以实现服务器校验客户端了