okhttp3比URLConnection
更常见,而且基于okhttp3还衍生出很多web框架
使用okhttp3发送GET和POST请求 DataPost.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class DataPost { 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(); Log.d("DemoJohn" , "response: " + response.body().string()); } catch (IOException e) { e.printStackTrace(); } } }.start(); } }
MySSLSocketFactory.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 package com.DemoJohn.okhttp3;import ...;public class MySSLSocketFactory { public static String certificate = "-----BEGIN CERTIFICATE-----\n" + "MIIKLjCCCRagAwIBAgIMclh4Nm6fVugdQYhIMA0GCSqGSIb3DQEBCwUAMGYxCzAJ\n" + "5OzP+PKupV9VKw8x8mQKU6vr\n" + "-----END CERTIFICATE-----\n" ; public static X509TrustManager trustManager = null ; public static OkHttpClient createClient () { CertificatePinner CPinner = new CertificatePinner .Builder() .add("www.baidu.com" , "sha256//558pd1Y5Vercv1ZoSqOrJWDsh9sTMEolM6T8csLucQ=" ) .build(); OkHttpClient client = new OkHttpClient .Builder() .proxy(Proxy.NO_PROXY) .certificatePinner(CPinner) .sslSocketFactory(createSSLSocketFactory(new ByteArrayInputStream (certificate.getBytes())), trustManager) .hostnameVerifier(new TrustAllHostnameVerifier ()) .build(); return client; } public static SSLSocketFactory createSSLSocketFactory (InputStream in) { SSLSocketFactory sSLSocketFactory = null ; try { trustManager = trustManagerForCertificates(in); SSLContext sc = SSLContext.getInstance("TLS" ); sc.init(null , new TrustManager []{trustManager}, new SecureRandom ()); sSLSocketFactory = sc.getSocketFactory(); } catch (Exception e) { } return sSLSocketFactory; } private static X509TrustManager trustManagerForCertificates (InputStream in) throws GeneralSecurityException { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509" ); Collection<? extends Certificate > certificates = certificateFactory.generateCertificates(in); if (certificates.isEmpty()) { throw new IllegalArgumentException ("expected non-empty set of trusted certificates" ); } char [] password = "password" .toCharArray(); KeyStore keyStore = newEmptyKeyStore(password); int index = 0 ; for (Certificate certificate : certificates) { String certificateAlias = Integer.toString(index++); keyStore.setCertificateEntry(certificateAlias, certificate); } KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, password); TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); if (trustManagers.length != 1 || !(trustManagers[0 ] instanceof X509TrustManager)) { throw new IllegalStateException ("Unexpected default trust managers:" + Arrays.toString(trustManagers)); } return (X509TrustManager) trustManagers[0 ]; } private static KeyStore newEmptyKeyStore (char [] password) throws GeneralSecurityException { try { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null , password); return keyStore; } catch (IOException e) { throw new AssertionError (e); } } public static class TrustAllHostnameVerifier implements HostnameVerifier { @Override public boolean verify (String hostname, SSLSession sslSession) { Log.d("DemoJohn" ,"HostnameVerifier: " + hostname); javax.security.cert.X509Certificate cf = null ; CertificateFactory finalcf = null ; X509Certificate PUB_KEY = null ; String realPubKey = null ; String encoded = null ; try { cf = sslSession.getPeerCertificateChain()[0 ]; RSAPublicKey pubkey = (RSAPublicKey)cf.getPublicKey(); encoded = Base64.encodeToString(pubkey.getEncoded(),0 ); finalcf = CertificateFactory.getInstance("X.509" ); PUB_KEY = (X509Certificate)finalcf.generateCertificate(new ByteArrayInputStream (certificate.getBytes())); realPubKey = Base64.encodeToString(PUB_KEY.getPublicKey().getEncoded(),0 ); cf.checkValidity(); } catch (Exception e) { return false ; } Log.d("DemoJohn" , "IssuerDN: " + cf.getIssuerDN().toString()); Log.d("DemoJohn" , "SubjectDN: " + cf.getSubjectDN().toString()); Log.d("DemoJohn" , "证书版本: " + cf.getVersion()); final boolean expected = realPubKey.equalsIgnoreCase(encoded); if (!expected) { Log.d("DemoJohn" ,"证书公钥验证错误" ); return false ; } Log.d("DemoJohn" ,"证书公钥验证正确" ); return true ; } } }
okhttp3的拦截器 def 也就是hook发包的时候的函数,把包给hook下来
Okhttp 的拦截器采用了责任链模式,具体提供了两种拦截器:
1 2 应用拦截器:专注于业务层的逻辑处理,比如 添加日志 ... 网络拦截器:专注于网络层请求的处理,比如 重定向、重试 ...
在网络请求提交之前就会经过拦截器,先执行拦截器里的代码,返回的数据包也一样,都会先执行拦截器里的代码
先定义一个拦截器类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class LoggingInterceptor implements Interceptor { @Override public Response intercept (Interceptor.Chain chain) throws IOException { Request request = chain.request(); long t1 = System.nanoTime(); logger.info(String.format("Sending request %s on %s%n%s" , request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime(); logger.info(String.format("Received response for %s in %.1fms%n%s" , response.request().url(), (t2 - t1) / 1e6d , response.headers())); return response; } }
应用拦截器 1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testApplicationInterceptors () throws IOException { OkHttpClient client = new OkHttpClient .Builder().addInterceptor(new LoggingInterceptor ()).build(); Request request = new Request .Builder().url("http://www.publicobject.com/helloworld.txt" ) .header("User-Agent" , "OkHttp Example" ) .build(); Response response = client.newCall(request).execute(); Objects.requireNonNull(response.body()).close(); }
网络拦截器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testNetworkInterceptors () throws IOException { OkHttpClient client = new OkHttpClient .Builder() .addNetworkInterceptor(new LoggingInterceptor ()) .build(); Request request = new Request .Builder() .url("http://www.publicobject.com/helloworld.txt" ) .header("User-Agent" , "OkHttp Example" ) .build(); Response response = client.newCall(request).execute(); Objects.requireNonNull(response.body()).close(); }
okhttp3的自吐及快速定位 根据上面拦截器的原理,其实可以hook new OkHttpClient.Builder().build();
方法,当该方法被调用的时候先去addNetworkInterceptor(new LoggingInterceptor())
,Builder
是OkHttpClient
的内部类
把如下详细输出数据包信息的java代码编译成dex文件,也就是编译成app之后把dex文件拖出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 package com.DemoJohn.test;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;public final 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) { 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); 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 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; } 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 ; } } private boolean bodyHasUnknownEncoding (Headers myheaders) { String contentEncoding = myheaders.get("Content-Encoding" ); return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity" ) && !contentEncoding.equalsIgnoreCase("gzip" ); } }
然后进行frida hook,加载该dex文件,然后重写内部类Builder
的build()
方法
1 2 3 4 5 6 7 8 9 10 11 Java .perform (function ( ) { Java .openClassFile ("/data/local/tmp/DemoJohn.dex" ).load (); var okhttp3Logging = Java .use ("com.example.demo.okhttp3Logging" ); var Builder = Java .use ("okhttp3.OkHttpClient$Builder" ); Builder .build .implementation = function ( ) { console .log ("okhttp3.OkHttpClient$Builder is called!" ); return this .addNetworkInterceptor (okhttp3Logging.$new()).build (); } });
这样做的好处是,绕过证书检测,但是需要面对hook检测和frida检测,而且如果类名被混淆了,Java.use("okhttp3.OkHttpClient$Builder");
就失效了,需要找出混淆后的类名
okhttp3的证书检测 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 package com.DemoJohn.test;import ;public class Okhttp3Utils { public static String certificate = "-----BEGIN CERTIFICATE-----\n" + "1z5MXsKSeOQbTpsoNp8Yd/K79WpkcXgP6tVofxFXtP8PsORz\n" + "-----END CERTIFICATE-----\n" ; public static X509TrustManager trustManager = new X509TrustManager () { @Override public void checkClientTrusted (X509Certificate[] chain, String authType) throws CertificateException { } @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("DemoJohn" ,"authType: " + authType); X509Certificate cf = chain[0 ]; RSAPublicKey pubkey = (RSAPublicKey)cf.getPublicKey(); String encoded = Base64.encodeToString(pubkey.getEncoded(),0 ); CertificateFactory finalcf = CertificateFactory.getInstance("X.509" ); X509Certificate realCertificate = (X509Certificate)finalcf.generateCertificate(new ByteArrayInputStream (certificate.getBytes())); String realPubKey = Base64.encodeToString(realCertificate.getPublicKey().getEncoded(),0 ); cf.checkValidity(); Log.d("DemoJohn" , "IssuerDN: " + cf.getIssuerDN().toString()); Log.d("DemoJohn" , "SubjectDN: " + cf.getSubjectDN().toString()); Log.d("DemoJohn" , "证书版本: " + cf.getVersion()); final boolean expected = realPubKey.equalsIgnoreCase(encoded); if (!expected) { throw new CertificateException ("checkServerTrusted: got error public key: " + encoded); } Log.d("DemoJohn" ,"证书公钥验证正确" ); } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate [0 ]; } }; public static HostnameVerifier VERIFY = new HostnameVerifier () { @ Override public boolean verify (String hostname, SSLSession session) { try { javax.security.cert.X509Certificate[] peerCertificateChain = session.getPeerCertificateChain(); javax.security.cert.X509Certificate cf = peerCertificateChain[0 ]; RSAPublicKey pubkey = (RSAPublicKey)cf.getPublicKey(); String encoded = Base64.encodeToString(pubkey.getEncoded(),0 ); CertificateFactory finalcf = CertificateFactory.getInstance("X.509" ); X509Certificate realCertificate = (X509Certificate)finalcf.generateCertificate(new ByteArrayInputStream (certificate.getBytes())); String realPubKey = Base64.encodeToString(realCertificate.getPublicKey().getEncoded(),0 ); cf.checkValidity(); Log.d("DemoJohn" , "HostnameVerifier IssuerDN: " + cf.getIssuerDN().toString()); Log.d("DemoJohn" , "HostnameVerifier SubjectDN: " + cf.getSubjectDN().toString()); Log.d("DemoJohn" , "HostnameVerifier 证书版本: " + cf.getVersion()); Log.d("DemoJohn" , "HostnameVerifier pubkey: " + realPubKey); final boolean expected = realPubKey.equalsIgnoreCase(encoded); if (!expected) { throw new CertificateException ("HostnameVerifier: got error public key: " + encoded); } Log.d("DemoJohn" ,"HostnameVerifier 证书公钥验证正确" ); } catch (Exception e) { e.printStackTrace(); return false ; } return true ; } }; public static OkHttpClient client = new OkHttpClient .Builder() .sslSocketFactory(getSSLSocketFactory(new ByteArrayInputStream (certificate.getBytes())), trustManager) .hostnameVerifier(VERIFY) .certificatePinner(new CertificatePinner .Builder().add("www.baidu.com" , "sha1/P6gCFiWqW5mhUGKcOK8ENmbVUJA=" ).build()) .build(); public static void doRequest () { new Thread (){ public void run () { FormBody formBody = new FormBody .Builder().add("user" , "DemoJohn" ).add("pass" , "DemoJohn" ).build(); Request request = new Request .Builder() .url("https://www.baidu.com/" ) .post(formBody) .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(); Log.d("DemoJohn" , "response: " + response.body().string()); } catch (IOException e) { e.printStackTrace(); } } }.start(); } private static SSLContext getSSLContext () { SSLContext sslContext = null ; try { sslContext = SSLContext.getInstance("TLS" ); sslContext.init(null , new TrustManager []{trustManager}, null ); } catch (Exception e) { e.printStackTrace(); } return sslContext; } public static SSLSocketFactory getSSLSocketFactory (InputStream in) { SSLSocketFactory sSLSocketFactory = null ; try { trustManager = trustManagerForCertificates(in); SSLContext sc = SSLContext.getInstance("TLS" ); sc.init(null , new TrustManager []{trustManager}, new SecureRandom ()); sSLSocketFactory = sc.getSocketFactory(); } catch (Exception e) { } return sSLSocketFactory; } private static X509TrustManager trustManagerForCertificates (InputStream in) throws GeneralSecurityException { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509" ); Collection<? extends Certificate > certificates = certificateFactory.generateCertificates(in); if (certificates.isEmpty()) { throw new IllegalArgumentException ("expected non-empty set of trusted certificates" ); } char [] password = "password" .toCharArray(); KeyStore keyStore = newEmptyKeyStore(password); int index = 0 ; for (Certificate certificate : certificates) { String certificateAlias = Integer.toString(index++); keyStore.setCertificateEntry(certificateAlias, certificate); } KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, password); TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); if (trustManagers.length != 1 || !(trustManagers[0 ] instanceof X509TrustManager)) { throw new IllegalStateException ("Unexpected default trust managers:" + Arrays.toString(trustManagers)); } return (X509TrustManager) trustManagers[0 ]; } private static KeyStore newEmptyKeyStore (char [] password) throws GeneralSecurityException { try { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null , password); return keyStore; } catch (IOException e) { throw new AssertionError (e); } } } class LoggingInterceptor implements Interceptor { @Override public Response intercept (Interceptor.Chain chain) throws IOException { Request request = chain.request(); long t1 = System.nanoTime(); Log.d("DemoJohn" , String.format("Sending request %s on %s%n%s" , request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime(); Log.d("DemoJohn" , String.format("Received response for %s in %.1fms%n%s" , response.request().url(), (t2 - t1) / 1e6d , response.headers())); return response; } }
证书检测的绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 try { 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.' ); } 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.DemoJohn.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 (); } } catch (err) { if (err.message.indexOf('ClassNotFoundException' ) === 0 ) { throw new Error (err); } }
okhttp3混淆后的方法定位 okhttp3打包之后会到apk中,所以类名和方法名是可以混淆的,证书校验的CertificatePinner
的check
方法是关键点,这个函数的原型
1 public void check(String hostname, List<Certificate> peerCertificates) throws SSLPeerUnverifiedException
不管函数类名怎么混淆,但是参数的类型一般是不会改的,可以尝试搜索List<Certificate>
,从而找到CertificatePinner
类所在的位置
更通用的是去hook底层的方法,或者去hook报错信息的类,因为报错的类一般都是在证书校验失败的时候报错,方法比较多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 Java.perform(function () { function showStacks () { console.log( Java.use("android.util.Log" ) .getStackTraceString( Java.use("java.lang.Throwable" ).$new () ) ); } var ByteString = Java.use("com.android.okhttp.okio.ByteString" ); function toBase64 (tag, data) { console.log(tag + " Base64: " , ByteString.of(data).base64()); } function toHex (tag, data) { console.log(tag + " Hex: " , ByteString.of(data).hex()); } function toUtf8 (tag, data) { console.log(tag + " Utf8: " , ByteString.of(data).utf8()); } var CertificatePinner = Java.use('okhttp3.CertificatePinner' ); CertificatePinner.check.overload('java.lang.String' , 'java.util.List' ).implementation = function(a, b) { console.log('OkHTTP 3.x check() called. Not throwing an exception.' ); console.log("=======================================================" ); return this .check(a, b); } var arrayList = Java.use("java.util.ArrayList" ); arrayList.add.overload('java.lang.Object' ).implementation = function (a) { try { console.log("realStr: " , a.toString()); if (a.toString().startsWith("sha1/" )) { showStacks(); }else if (a.toString().startsWith("sha1/" )) { showStacks(); } }catch (e) { } return this .add(a); } });
Java层ssl自吐 之前抓包的思路是绕过抓包相关检测,让抓包工具能够正常抓包,缺点是要逆向去hook掉检测证书的位置,而且不同框架的检测方法不同,好处是数据包更全面
通过hook直接获取通信过程中的明文数据包,通信过程中的数据会一步步交给系统函数来进行加密,Java层和JNI层,比如使用libssl.so文件,可以hook固定的系统相关函数,如果开发人员使用自定义的ssl进行加密,需要逆向找到相关加密函数来hook
也就是 hook com.android.org.conscrypt.NativeCrypto
的native方法SSL_write
和 SSL_read
,这俩方法是底层调用的,一个是往SSL socket写数据,一个是读数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 ("DemoJohn 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 ("DemoJohn SSL_read" , buf); console .log ("=======================================================" ); return this .SSL_read (ssl, nativeCrypto, fd, handshakeCallbacks, buf, offset, len, timeoutMillis); } });
JNI层ssl自吐 so层hook的时候,要么在数据加密之前去hook,要么在数据加密之后即将发送出去之前hook
底层就去hook libssl.so和libc.so中的函数,libssl.so的底层是调用的libc.so,但是一般俩都会hook一下,因为libssl.so可能被混淆或者自写一个so去实现逻辑,但是几乎底层都会调用libc.so
1 2 3 4 5 6 7 libssl.so int SSL_write(SSL *ssl, const void *buf, int num) int SSL_read(SSL *ssl, void *buf, int num) libc.so ssize_t write(int fd, const void * buf, size_t count) ssize_t read(int fd, void * buf, size_t count)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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 (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 (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 (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 (hexdump (this .args1 , {length : nums})); } } });