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();

//Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.10.1", 8888));
OkHttpClient client = new OkHttpClient.Builder()
.proxy(Proxy.NO_PROXY)
.certificatePinner(CPinner)
.sslSocketFactory(createSSLSocketFactory(new ByteArrayInputStream(certificate.getBytes())), trustManager)
.hostnameVerifier(new TrustAllHostnameVerifier())
.build();
//okhttp3.internal.tls.OkHostnameVerifier
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");
}

// Put the certificates a key store.
char[] password = "password".toCharArray(); // Any password will work.
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}

// Use it to build an X509 trust manager.
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())BuilderOkHttpClient的内部类

把如下详细输出数据包信息的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) {
// 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 myheaders) {
String contentEncoding = myheaders.get("Content-Encoding");
return contentEncoding != null
&& !contentEncoding.equalsIgnoreCase("identity")
&& !contentEncoding.equalsIgnoreCase("gzip");
}
}

然后进行frida hook,加载该dex文件,然后重写内部类Builderbuild()方法

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");
}

// Put the certificates a key store.
char[] password = "password".toCharArray(); // Any password will work.
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}

// Use it to build an X509 trust manager.
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
/*** okhttp3.x unpinning ***/


// Wrap the logic in a try/catch as not all applications will have
// okhttp as part of the app.
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 we dont have a ClassNotFoundException exception, raise the
// problem encountered.
if (err.message.indexOf('ClassNotFoundException') === 0) {

throw new Error(err);
}
}

okhttp3混淆后的方法定位

okhttp3打包之后会到apk中,所以类名和方法名是可以混淆的,证书校验的CertificatePinnercheck方法是关键点,这个函数的原型

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 messageDigest = Java.use("java.security.MessageDigest");
// messageDigest.digest.overload('[B').implementation = function (data) {
// console.log("MessageDigest.digest('[B') is called!");
// showStacks();
// var algorithm = this.getAlgorithm();
// var tag = algorithm + " digest data";
// toUtf8(tag, data);
// toHex(tag, data);
// toBase64(tag, data);
// var result = this.digest(data);
// var tags = algorithm + " digest result";
// toHex(tags, result);
// toBase64(tags, result);
// console.log("=======================================================");
// return result;
// }

// var SSLHandshakeException = Java.use("javax.net.ssl.SSLHandshakeException");
// SSLHandshakeException.$init.implementation = function (args) {
// showStacks();
// console.log(args);
// return this.$init(args);
// }

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_writeSSL_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) {
// read函数里的buf参数只有在read执行完毕之后才有值
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}));
}
}
});