Frida实现Android应用HTTP和HTTPS抓包

通过分析APP代码找到它的通信方法也可,但是只可一点点。也就是说,每个APP都不一样,需要针对每个APP单独分析,各种框架,各种接口。想要找到一个通用方法抓取http包,就需要从Android系统源码入手。

Frida Hook Android APP笔记(三)中,已经找到HttpURLConnection的具体实现类为com.android.okhttp.internal.huc.HttpURLConnectionImpl,并且已经

阅读源码能够发现通信过程与服务端的connect、disconnect、getRequestBody、response等等过程都需要通过HttpEngine对象实现。该类在源码中的路径为:external/okhttp/repackaged/okhttp/src/main/java/com/android/okhttp/internal/http/HttpEngine.java

HttpEngine类的关键代码:

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 final class HttpEngine {
/**
* The original application-provided request. Never modified by OkHttp. When
* follow-up requests are necessary, they are derived from this request.
*/
private final Request userRequest;
/**
* The user-visible response. This is derived from either the network
* response, cache response, or both. It is customized to support OkHttp
* features like compression and caching.
*/
@android.compat.annotation.UnsupportedAppUsage
private Response userResponse;

public Request getRequest() {
return userRequest;
}

/** Returns the engine's response. */
// TODO: the returned body will always be null.
public Response getResponse() {
if (userResponse == null) throw new IllegalStateException();
return userResponse;
}
}

进行网络通信时,首先需要初始化HttpURLConnection对象,进行一系列配置,之后调用connect()方法。connect()方法在HttpURLConnectionImpl中的实现如下:

1
2
3
4
5
6
7
@Override public final void connect() throws IOException {
initHttpEngine();
boolean success;
do {
success = execute(false);
} while (!success);
}

可以看到,先调用initHttpEngine();初始化httpEngine对象,然后调用execute(boolean readResponse)方法,此方法负责发送request和读取response。我们把hook的点选在这里,执行完原方法之后,直接获取HttpURLConnection中的httpEngine属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var HttpURLConnectionImpl = Java.use('com.android.okhttp.internal.huc.HttpURLConnectionImpl');    
HttpURLConnectionImpl.execute.implementation = function(readResponse){
var result = this["execute"](readResponse);
var httpEngine = getFieldValue(this, "httpEngine");
return result;
}

function getFieldValue(object, fieldName) {
var field = object.class.getDeclaredField(fieldName);
field.setAccessible(true);
var fieldValue = field.get(object);
if (null == fieldValue) {
return null;
}
var FieldClazz = Java.use(fieldValue.$className);
return Java.cast(fieldValue, FieldClazz);
}

获取到httpEngine之后,就可以获取request和response了。可以通过上面getFieldValue方法根据变量名获取变量值,也可以直接调用HttpEngine中的方法。示例代码如下:

1
2
3
4
5
   var userRequest = getFieldValue(httpEngine, "userRequest");

httpEngine["readResponse"]();
var userResponse = httpEngine.getResponse();
// var userResponse = getFieldValue(httpEngine, "userResponse");

在获取response时,需要先调用readResponse()方法,flush需要处理的request,并解析response header和body。否则,userResponse就是null。

到这里我们已经获取到Request和Response对象了,后续只需解析它们就可以了。从HttpEngine类点进去找到Request和Response路径:

  • external/okhttp/repackaged/okhttp/src/main/java/com/android/okhttp/Request.java
  • external/okhttp/repackaged/okhttp/src/main/java/com/android/okhttp/Response.java

PS:测试时发现,由于request和response是异步的,最终呈现时request和response不能一一对应。因此,将request的获取从httpEngine改为从Response

解析request和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
47
48
function parseResponse(userResponse){
if(null == userResponse){
return;
}
// 解析Request
var request = userResponse.request();
var request_method = request.method();
var request_url = request.urlString();
var request_headers = getFieldValue(request, "headers");
var request_headerStr = request_headers.toString();

// 解析Response
var code = userResponse.code()
var message = userResponse.message()

var headers = getFieldValue(userResponse, "headers");
var headerStr = headers.toString();

var bodyObj = userResponse.body();
var mediaTypeStr = "";
var bodyLength = 0;
var bodyStr = "";
if(bodyObj != null){
var body = Java.cast(bodyObj, Java.use(bodyObj.$className));
var mediaType = body.contentType()
if(mediaType != null){
mediaTypeStr = mediaType.toString();
}
bodyLength = body.contentLength();
var bodyBytes = body.bytes();
var stringclazz = Java.use("java.lang.String");
bodyStr = stringclazz.$new(bodyBytes);

}

console.log("----------------------------------------------- START-------------------------------------------------------------");
console.log("REQUEST:");
console.log("METHOD: " , request_method,"\nURL: ", request_url, "\nHEADERS:\n" , request_headerStr);
console.log("==REQUEST END==");
console.log("\n");

console.log("RESPONSE:");
console.log("\nSTATUS CODE: ", code, "\nMESSAGE: ", message, "\nHEADERS:\n" , headerStr,
"\nRESPONSE BODY:\n mediaType: ", mediaTypeStr, "\n length: ", bodyLength, "\n body: " , bodyStr, "\n");
console.log("==RESPONSE END==\n");
console.log("------------------------------------------------ END--------------------------------------------------------------");

}

结果呈现:

完整代码后续会放在github上。


关于https:

Frida Hook Android APP笔记(三)中,我们已经分析过,具体到通信过程HttpsURLConnectionImpl使用的是HttpURLConnectionImpl对象,只不过另外增加了一些安全验证方法。具体代码如下:

HttpsURLConnectionImpl初始化:

1
2
3
4
5
6
7
8
public final class HttpsURLConnectionImpl extends DelegatingHttpsURLConnection {
@android.compat.annotation.UnsupportedAppUsage
private final HttpURLConnectionImpl delegate;
public HttpsURLConnectionImpl(HttpURLConnectionImpl delegate) {
super(delegate);
this.delegate = delegate;
}
}

DelegatingHttpsURLConnection类的关键方法:

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
abstract class DelegatingHttpsURLConnection extends HttpsURLConnection {
private final HttpURLConnection delegate;

public DelegatingHttpsURLConnection(HttpURLConnection delegate) {
super(delegate.getURL());
this.delegate = delegate;
}

@Override public void connect() throws IOException {
connected = true;
delegate.connect();
}

@Override public void disconnect() {
delegate.disconnect();
}

@Override public InputStream getErrorStream() {
return delegate.getErrorStream();
}

@Override public String getRequestMethod() {
return delegate.getRequestMethod();
}

@Override public int getResponseCode() throws IOException {
return delegate.getResponseCode();
}

@Override public String getResponseMessage() throws IOException {
return delegate.getResponseMessage();
}
......
}

因此,https抓包就不需要再做额外配置了。