Http(s)网络框架分析

HttpURLConnection

开发流程

配置

需在AndroidManifest.xml文件赋予权限:

1
<uses-permission android:name="android.permission.INTERNET"/>

布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center|center_horizontal|center_vertical"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center|center_horizontal|center_vertical"
        android:id="@+id/mybtn"
        android:text="发送请求"
        android:textSize="45sp">
    </Button>

</LinearLayout>

代码

  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
package com.example.luodst;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;

public class MainActivity extends AppCompatActivity {
    private static String TAG = "XiaLuoHun";
    private Handler handler = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initHandler();
        // 定位发送请求按钮
        Button btn = findViewById(R.id.mybtn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getResponse("http://www.baidu.com");
            }
        });
    }

    private void httpUrlConnection(String strUrl){
        try {
            URL url = new URL(strUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            //设置Http请求方法
            connection.setRequestMethod("GET");
            //设置请求参数
            connection.setRequestProperty("token","LuoHun");
            //设置连接超时时间
            connection.setConnectTimeout(8000);
            //设置接收超时时间
            connection.setReadTimeout(8000);
            // 开始连接
            connection.connect();
            //得到响应码
            //int responseCode = connection.getResponseCode();
 /*                       if(responseCode == HttpURLConnection.HTTP_OK){
                            //...
                        }*/
            //获取服务器返回的输入流
            InputStream in = connection.getInputStream();
            //if(in.available() > 0){

            // 每次写入1024字节
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            StringBuffer sb = new StringBuffer();
            while ((in.read(buffer)) != -1) {
                sb.append(new String(buffer));
            }
            sendMsg(sb.toString());
            //Log.d("LuoHun", sb.toString());
            //关闭Http连接
            connection.disconnect();
            // }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void getResponse(String strUrl) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                httpUrlConnection(strUrl);
            }
        }).start();
    }

    private void initHandler() {
        handler = new Handler(getMainLooper(), new Handler.Callback() {
            @Override
            public boolean handleMessage(@NonNull Message msg) {
                if (msg.what == 1){
                    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                    builder.setTitle("NOTICE");
                    builder.setMessage((String) msg.obj);
                    builder.setPositiveButton("Confirm",null);
                    builder.create().show();
                }
                return false;
            }
        });
    }

    private void sendMsg(String message){
        Message msg = new Message();
        msg.what = 1;
        msg.obj = message;
        handler.sendMessage(msg);
    }
}

HttpURLConnectionDemo.7z

“自吐"脚本开发

观察上面的开发流程,我们发现几个关键函数:

  • URL类的构造函数,其包含了目标网址的字符串.
  • setRequestMethod和setRequestProperty函数设置请求头和请求参数等信息.
  • getInputStream函数获取response.

Hook URL

  1. 使用Objection来Hook URL类的构造函数,测试下.
1
2
android hooking watch class_method java.net.URL.$init --dump-args --dump-backtrace
 --dump-return

URL构造函数

可以看到出现了网址.

  1. 编写"自吐"脚本.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function main(){
    Java.perform(function(){
        var URL = Java.use('java.net.URL')
        URL.$init.overload('java.lang.String').implementation = function(urlstr){
            console.log('url => ', urlstr)
            var result = this.$init(urlstr)
            return result
        }
    })
}
setImmediate(main)

HttpURLConnectionUrl自吐

Hook HttpURLConnection

前期研究
  1. 使用Objection Hook HttpURLConnection整个类和构造函数来看下.
1
2
3
android hooking watch class java.net.HttpURLConnection
android hooking watch class_method java.net.HttpURLConnection.$init --dump-args --
dump-backtrace --dump-return(agent)

HttpURLConnection整个类

HttpURLConnection构造函数

发现只有java.net.HttpURLConnection.getFollowRedirects()和HttpURLConnection的构造函数被调用了.

  1. 使用Objection在堆上寻找HttpURLConnection实例.
1
android heap search instances java.net.HttpURLConnection

寻找HttpURLConnection实例

发现不存在任何实例.

查询Android源码可以知道,HttpURLConnection类是一个抽象类,在开发中虽然可以直接使用抽象类去表示,但是在运行过程中是抽象类的具体实现类在工作.

HttpURLConnection源码

  1. 确定HttpURLConnection的具体实现类.

我们注意到第一次出现HttpURLConnection的定义是通过URL类的openConnection()函数完成的,这个函数返回值的类名就是HttpURLConnection的具体实现类,那么我们可以使用Frida来打印openConnection()函数返回值的类名,从而获取HttpURLConnection的具体实现类.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function main(){
    Java.perform(function(){
        var URL = Java.use('java.net.URL')
        URL.openConnection.overload().implementation = function(){
            var result = this.openConnection()
            console.log('openConnection returnType => ', result.$className)
            return result
        }
    })
}
setImmediate(main)

打印HttpURLConnection具体实现类

最终得到HttpURLConnection抽象类的具体实现类为com.android.okhttp.internal.huc.HttpURLConnectionImpl

  1. 使用Objection来对HttpURLConnection具体实现类整体进行Hook测试下.
1
android hooking watch class com.android.okhttp.internal.huc.HttpURLConnectionImpl

ObjectionHookHttpURLConnection具体实现类

可以发现,这下上述Demo中使用的每个函数都被调用到了.

“自吐"脚本
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function main(){
    Java.perform(function(){
        var HttpsURLConnectionImpl = Java.use('com.android.okhttp.internal.huc.HttpURLConnectionImpl')
        //打印请求参数
        HttpsURLConnectionImpl.setRequestProperty.implementation = function(key, value){
            var result = this.setRequestProperty(key, value)
            console.log('setRequestProperty => ',key,': ', value)
            return result
        }
    })
}
setImmediate(main)

HttpURLConnection请求参数自吐

okhttp3

开发流程

配置

  1. 添加权限.

需在AndroidManifest.xml文件赋予权限:

1
<uses-permission android:name="android.permission.INTERNET"/>
  1. 在build.gradle文件中添加okhttp3支持.
1
implementation("com.squareup.okhttp3:okhttp:3.12.0")

添加okhttp3支持

布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center|center_horizontal|center_vertical"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center|center_horizontal|center_vertical"
        android:id="@+id/mybtn"
        android:text="发送请求"
        android:textSize="45sp">
    </Button>

</LinearLayout>

代码

  1. okHttpExample.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
package com.example.luodst;

import android.util.Log;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class okHttpExample {
    private static final String TAG = "XiaLuoHun";

    // 新建一个Okhttp客户端
     OkHttpClient client = new OkHttpClient();

    // 新建一个Okhttp客户端
 /*   OkHttpClient client = new OkHttpClient.Builder()
            //新建一个拦截器
            // .addNetworkInterceptor(new LoggingInterceptor())
            //设置读超时
            .readTimeout(5, TimeUnit.SECONDS)
            //设置写超时
            .writeTimeout(5, TimeUnit.SECONDS)
            //设置连接超时
            .connectTimeout(15, TimeUnit.SECONDS)
            //设置是否自动重连
            .retryOnConnectionFailure(false)
            .build();*/

    void run(String url) throws IOException {
        // 构造request
        Request request = new Request.Builder()
                .url(url)
                .header("token","LuoHun")
                .build();

        // 发起异步请求
        client.newCall(request).enqueue(
                new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        call.cancel();
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {

                        //打印输出
                        Log.d(TAG, response.body().string());

                    }
                }
        );
    }
}
  1. MainActivity.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
package com.example.luodst;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;

public class MainActivity extends AppCompatActivity {
    private static String TAG = "XiaLuoHun";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 定位发送请求按钮
        Button btn = findViewById(R.id.mybtn);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 访问百度首页
                String requestUrl = "https://www.baidu.com/";
                okHttpExample myExample = new okHttpExample();
                try {
                    myExample.run(requestUrl);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

    }
}

“自吐"脚本开发

初步研究

观察上述开发流程,假如我们想要获取请求的数据,我们可以Hook okhttp3.OkHttpClient.newCall()函数.

  1. 先使用Objection进行快速Hook,测试下.

ObjectionHookokhttp3newcall

  1. JavaScript代码.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function main(){
    Java.perform(function(){
        var OkHttpClient = Java.use('okhttp3.OkHttpClient')
        OkHttpClient.newCall.implementation = function(request){
            var result = this.newCall(request)
            console.log(request.toString())
            return result
        }
    })
}
setImmediate(main)

这里需要提一点,上述Hook点是有问题的,可能出现遗漏或多出部分请求,因为存在"Call"后没有发出实际请求的情况.观察上述开发流程,发现只有Hook execute()和enqueue(new Callback())才能真正保证每个从okhttp出去的请求都能被获取.但即便如此,按这个流程走,只能看到request,无法同时看到返回的响应,这是有问题的.我们可以采用okhttp拦截器Interceptor来解决这个问题.

okhttp拦截器机制

拦截器机制

  1. 拦截器可以对request做出修改.在数据返回时再对request做出修改.
  2. 整个拦截器机制实际上是一个链条,在网络请求传输过程中,最上层的拦截器首先向下传递一个request,并请求下层拦截器返回一个response,下层的拦截器收到request继续向下传递,并请求返回response,直到传递到最后一个拦截器,它对这个request进行处理并返回一个response,然后这个response开始层层向上传递,直到传递到最上层.这样最上层的拦截器就得到了response,整个过程形成一个拦截器的完整递归调用链.

上述流程可以查看okhttp源码中的getResponseWithInterceptorChain()函数.

进一步研究

接下来我们修改上述Demo,添加一个拦截器来打印URL和请求头.

  1. 新建一个类LoggingInterceptor继承Interceptor.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.luodst;

import android.util.Log;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class LoggingInterceptor implements Interceptor {
    private static String TAG = "XiaLuoHun";
    
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Log.i(TAG, "请求URL: " + String.valueOf(request.url()) + "\n");
        Log.i(TAG, "请求头: " + "\n" + String.valueOf(request.headers()) + "\n");
        
        Response response = chain.proceed(request);
        return  response;
    }
}
  1. 在okHttpExample.java中添加一个拦截器.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//方式一
// 新建一个Okhttp客户端
OkHttpClient client = new OkHttpClient.Builder()
        //新建一个拦截器
        .addNetworkInterceptor(new LoggingInterceptor())
        //设置读超时
        .readTimeout(5, TimeUnit.SECONDS)
        //设置写超时
        .writeTimeout(5, TimeUnit.SECONDS)
        //设置连接超时
        .connectTimeout(15, TimeUnit.SECONDS)
        //设置是否自动重连
        .retryOnConnectionFailure(false)
        .build();
//方式二
OkHttpClient client = new OkHttpClient();
OkHttpClient newClient = client.newBuilder()
        .addInterceptor(new LoggingInterceptor())
        .build();
  1. 查看日志输出.

拦截日志1

注意
上述只打印了请求信息,并没有打印response.是因为此时response对象还需进一步处理.这些前人已经替我们完成了,okhttp官方还提供了一个日志拦截打印器(okhttp3:logging-interceptor).接下来对官方的代码稍作修改,并替换上述的LoggingInterceptor类即可.
  1. 下方的代码就是对头部信息进行处理,并添加对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
 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
package com.example.luodst;

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 class LoggingInterceptor 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");
    }
}
  1. 查看日志输出.

拦截日志2

Hook方案

为了将上述方案推广到所有使用okhttp框架的App上,我们有两种方案:

  1. 将上述LoggingInterceptor.java中的代码翻译成JavaScript代码.
  2. 将上述LoggingInterceptor.java中的代码编译成Dex文件,再通过Frida将Dex注入到其他应用中.

这里我们使用第二种方案,使用AS编译上述源码,切换到项目工程的app/build/intermediates/apk/release目录下,有一个app-release-unsigned.apk文件,将其解压拿classes.dex文件,然后更名为okhttp3logging.dex,并将其推送到手机的/data/local/tmp目录下.

okhttp3logging.dex

成果展示

这里提供给一个测试apk(包名为ganhuo.ly.com.ganhuo).

retrofit2-demo.apk

1
2
//使用下述命令安装
adb install -t .\retrofit2-demo.apk

Hook代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//LuoHook.js
function hook_okhttp3(){
    Java.perform(function(){
        //加载目标Dex
        Java.openClassFile("/data/local/tmp/okhttp3logging.dex").load()

        var myInterceptor = Java.use("com.example.luodst.LoggingInterceptor")
        var myInterceptorObj = myInterceptor.$new()
        //利用建造者模式在Interceptor链中添加自定义链
        var Builder = Java.use("okhttp3.OkHttpClient$Builder")
        Builder.build.implementation = function(){
            this.networkInterceptors().add(myInterceptorObj);
            return this.build();
        }
        console.log("hook_okhttp3...")
    })
}

function main(){
    hook_okhttp3()
}

setImmediate(main)

用Frida以spawn模式进行测试.

1
frida -U -f ganhuo.ly.com.ganhuo -l .\LuoHook.js --no-pause

使用下述命令查看日志输出.

1
adb logcat |grep okhttpGET

拦截日志3

终极"自吐” Socket

理论基础

网络模型

Socket抓包理论基础

以Http为例,Http数据从应用层发送之后,依次通过传输层、网络层、链路层,在经过每一层时都会被包裹上头部数据,以保证数据在传输过程中的完整性,然后传输给接收方;接收方以相反的过程依次去除头部数据从而获取真实传输的Http数据.因此如果对App进行抓包,那么不仅仅是应用层,在传输层、网络层等应用层往下的所有层级都可以获取传输的全部数据.这正是传输层进行Socket终极抓包的理论基础.

之所以选择在传输层进行抓包,是因为不管App是使用系统自带的Http(s)收发包框架还是第三方的Http(s)收发包框架,都不可避免的会经过系统的Socket相关类,而且Socket都是系统完成的,因此相关类一定不会被混淆.

Http

这里以上方使用的HttpURLConnectionDemo为例,开发Http的Socket"自吐"脚本.

前期研究

  1. 点击App界面上的发送请求按钮后,使用Objection搜索内存中Socket相关的类
1
2
objection.exe -g com.example.luodst explore
android hooking search classes socket

搜索Socket相关类

  1. 将搜索出来的结果复制到1.txt文件中,并在每一行前添加android hooking watch class

类前添加androidhooking

  1. 利用objection -c参数批量Hook Socket相关类(如果出现崩溃现象,可以删除或将导致崩溃的类移到另一个文件中待下次执行即可)
1
objection.exe -g com.example.luodst explore -c .\1.txt

objection批量hookSocket相关类

  1. 点击App界面上的"发送请求"按钮,会发现下图中的几个与Socket相关的类被调用了,如此可进一步缩小Socket相关类的范围

缩小Socket相关类范围

  1. 此时分别对上图中一些可疑的函数进行进一步的Hook并打印调用栈进行观察.这里观察到比较可疑的函数是java.net.AbstractPlainSocketImpl.acquireFD
1
android hooking watch class_method java.net.AbstractPlainSocketImpl.acquireFD --dump-args --dump-backtrace --dump-return

hookacquireFD

上述调用栈中发现了已经了HttpURLConnection 的发包函数com.android.okhttp.internal.huc.HttpURLConnectionImpl.getInputStream,此时再通过Android源码查看下java.net.AbstractPlainSocketImpl.acquireFD的上层函数java.net.SocketOutputStream.socketWrite

http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/SocketOutputStream.java

android源码socketWrite

可以发现java.net.SocketOutputStream.socketWrite这个函数的第一个参数实际上就是网络传输的数据内容.

  1. 同样的在获取response相关函数上找到了java.net.SocketInputStream.read([B, int, int)函数

“自吐"脚本

 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
function jhexdump(array) {
    var ptr = Memory.alloc(array.length);
    for(var i = 0; i < array.length; ++i)
        Memory.writeS8(ptr.add(i), array[i]);
    //console.log(hexdump(ptr, { offset: off, length: len, header: false, ansi: false }));
    console.log(hexdump(ptr, { offset: 0, length: array.length, header: false, ansi: false }));
}

function hookSocket() {
    Java.perform(function () {
        
        // java.net.SocketOutputStream.write
        // java.net.SocketOutputStream.socketWrite
        Java.use('java.net.SocketOutputStream').socketWrite.overload('[B', 'int', 'int').implementation = function (bytearray1, int1, int2) {
            var result = this.socketWrite(bytearray1, int1, int2)

            console.log('socketWrite result,bytearray1,int1,int2=>', result, bytearray1, int1, int2)

            var ByteString = Java.use("com.android.okhttp.okio.ByteString");
           // console.log('contents: => ', ByteString.of(bytearray1).hex())

            jhexdump(bytearray1)
            return result
        }
        
        // java.net.SocketInputStream.read
        // java.net.SocketInputStream.socketRead0
        Java.use('java.net.SocketInputStream').read.overload('[B', 'int', 'int').implementation = function (bytearray1, int1, int2) {
            var result = this.read(bytearray1, int1, int2)

            console.log('read result,bytearray1,int1,int2=>', result, bytearray1, int1, int2)
            var ByteString = Java.use("com.android.okhttp.okio.ByteString");

            //console.log('contents: => ', ByteString.of(bytearray1).hex())
            jhexdump(bytearray1)
            return result
        }
    })

}

function main(){
    hookSocket()
}

setImmediate(main)

httpfrida展示1

httpfrida展示2

Https

前期研究

将上方使用的HttpURLConnectionDemo中的URL由http://www.baidu.com修改为https://www.baidu.com,然后重新按照上面的流程进行trace和Hook,最终发现在进行Https连接时一定会经过下图中的函数.

HttpsSocket关键函数

其中比较关键的两个函数如下,并且它们的第一个参数永远是明文的request或response数据.

1
2
3
//http://androidxref.com/8.1.0_r33/xref/external/conscrypt/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write([B, int, int)
com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read([B, int, int)

“自吐"脚本

 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
function jhexdump(array) {
    var ptr = Memory.alloc(array.length);
    for(var i = 0; i < array.length; ++i)
        Memory.writeS8(ptr.add(i), array[i]);
    //console.log(hexdump(ptr, { offset: off, length: len, header: false, ansi: false }));
    console.log(hexdump(ptr, { offset: 0, length: array.length, header: false, ansi: false }));
}
function hookSSLSocketAndroid8(){
    Java.perform(function () {
        
         // com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write
        Java.use('com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream').write.overload('[B', 'int', 'int').implementation = function (bytearray1, int1, int2) {
            var result = this.write(bytearray1, int1, int2)
            console.log('write result,bytearray1,int1,int2=>', result, bytearray1, int1, int2)

            var ByteString = Java.use("com.android.okhttp.okio.ByteString");
            //console.log('contents: => ', ByteString.of(bytearray1).hex())
            jhexdump(bytearray1)
            return result
        }
    
        // com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read
        Java.use('com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream').read.overload('[B', 'int', 'int').implementation = function (bytearray1, int1, int2) {
            var result = this.read(bytearray1, int1, int2)
            console.log('read result,bytearray1,int1,int2=>', result, bytearray1, int1, int2)

            var ByteString = Java.use("com.android.okhttp.okio.ByteString");
            //console.log('contents: => ', ByteString.of(bytearray1).hex())
            jhexdump(bytearray1)
            return result
        }
    })
}
function main(){
    hookSSLSocketAndroid8()
}

setImmediate(main)

httpsfrida展示1

httpsfrida展示2

hookAddress

在上面的流程中,针对Http(s)收发包内容的"自吐"脚本已经开发完毕,但是还缺少一个关键的内容:获取收发包的IP地址和端口.

前期研究

这里仍然使用上方HttpURLConnectionDemo为例,重新按照上面的流程进行trace和Hook,不过需要注意的是这次测试的是所有Socket相关类的构造函数.

  1. 点击App界面上的发送请求按钮后,使用Objection搜索内存中Socket相关的类.
1
2
objection.exe -g com.example.luodst explore
android hooking search classes socket

搜索Socket相关类

  1. 将搜索出来的结果复制到1.txt文件中,并在每一行前添加android hooking watch class_method,在每一行的行尾添加.$init –dump-args –dump-backtrace –dump-return.

这里可以使用NotePad++,来批量完成在行尾添加数据.(Ctrl+H打开替换窗口)

NotePad++小技巧

  1. 利用objection -c参数批量HookSocket相关类构造函数.(如果出现崩溃现象,可以删除或将导致崩溃的类构造函数移到另一个文件中待下次执行即可)
1
objection.exe -g com.example.luodst explore -c .\1.txt
  1. 点击App界面上的"发送请求"按钮,会发现不管是Http还是Https都会调用到一些相同的函数,这里最终关注的函数是java.net.InetSocketAddress.InetSocketAddress

下方是Http协议Hook的结果.

Http地址和端口

下方是Https协议Hook的结果.

Https地址和端口

“自吐"脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function hookAddress(){
    Java.perform(function(){
        // java.net.InetSocketAddress.InetSocketAddress(java.net.InetAddress, int)
        Java.use('java.net.InetSocketAddress').$init.overload('java.net.InetAddress', 'int').implementation = function(addr,port){
            var result = this.$init(addr,port)
            console.log('addr,port =>',addr.toString(),port)
            return result
        }
    })
}

function main(){
    hookAddress()
}

setImmediate(main)

hookaddr结果

从上方结果中可以看到不仅仅有访问的网址信息,还存在本地的IP信息.那么如何区分本地地址和远程地址信息呢?这里我们可以看下java.net.InetAddress类的源码中的相关函数.

http://androidxref.com/8.1.0_r33/xref/libcore/ojluni/src/main/java/java/net/InetAddress.java

InetAddress类相关函数

故最终的"自吐"脚本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function hookAddress(){
    Java.perform(function(){
        // java.net.InetSocketAddress.InetSocketAddress(java.net.InetAddress, int)
        Java.use('java.net.InetSocketAddress').$init.overload('java.net.InetAddress', 'int').implementation = function(addr,port){
            var result = this.$init(addr,port)

            //console.log('addr,port =>',addr.toString(),port)
            if(addr.isSiteLocalAddress()){
                console.log('Local address =>',addr.toString(),', port is ',port)
            }else{
                console.log('Server address =>',addr.toString(),', port is ',port)
            }

            return result
        }
    })
}

function main(){
    hookAddress()
}

setImmediate(main)

hookaddr结果2

hookSocket汇总

 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
function jhexdump(array) {
    var ptr = Memory.alloc(array.length);
    for(var i = 0; i < array.length; ++i)
        Memory.writeS8(ptr.add(i), array[i]);
    //console.log(hexdump(ptr, { offset: off, length: len, header: false, ansi: false }));
    console.log(hexdump(ptr, { offset: 0, length: array.length, header: false, ansi: false }));
}
function hookAddress(){
    Java.perform(function(){
        // java.net.InetSocketAddress.InetSocketAddress(java.net.InetAddress, int)
        Java.use('java.net.InetSocketAddress').$init.overload('java.net.InetAddress', 'int').implementation = function(addr,port){
            var result = this.$init(addr,port)
            //console.log('addr,port =>',addr.toString(),port)
            
            if(addr.isSiteLocalAddress()){
                console.log('Local address =>',addr.toString(),', port is ',port)
            }else{
                console.log('Server address =>',addr.toString(),', port is ',port)
            }

            return result
        }
    })
}
function hookSocket() {
    Java.perform(function () {
        // java.net.SocketOutputStream.write
        // java.net.SocketOutputStream.socketWrite
        Java.use('java.net.SocketOutputStream').socketWrite.overload('[B', 'int', 'int').implementation = function (bytearray1, int1, int2) {
            var result = this.socketWrite(bytearray1, int1, int2)
            console.log('socketWrite result,bytearray1,int1,int2=>', result, bytearray1, int1, int2)

            var ByteString = Java.use("com.android.okhttp.okio.ByteString");
           // console.log('contents: => ', ByteString.of(bytearray1).hex())
            jhexdump(bytearray1)
            return result
        }
        
        // java.net.SocketInputStream.read
        // java.net.SocketInputStream.socketRead0
        Java.use('java.net.SocketInputStream').read.overload('[B', 'int', 'int').implementation = function (bytearray1, int1, int2) {
            var result = this.read(bytearray1, int1, int2)
            console.log('read result,bytearray1,int1,int2=>', result, bytearray1, int1, int2)

            var ByteString = Java.use("com.android.okhttp.okio.ByteString");
            //console.log('contents: => ', ByteString.of(bytearray1).hex())
            jhexdump(bytearray1)
            return result
        }
    })

}
function hookSSLSocketAndroid8(){
    Java.perform(function () {
         // com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write
        Java.use('com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream').write.overload('[B', 'int', 'int').implementation = function (bytearray1, int1, int2) {
            var result = this.write(bytearray1, int1, int2)
            console.log('write result,bytearray1,int1,int2=>', result, bytearray1, int1, int2)

            var ByteString = Java.use("com.android.okhttp.okio.ByteString");
            //console.log('contents: => ', ByteString.of(bytearray1).hex())
            jhexdump(bytearray1)
            return result
        }
    
        // com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read
        Java.use('com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream').read.overload('[B', 'int', 'int').implementation = function (bytearray1, int1, int2) {
            var result = this.read(bytearray1, int1, int2)
            console.log('read result,bytearray1,int1,int2=>', result, bytearray1, int1, int2)

            var ByteString = Java.use("com.android.okhttp.okio.ByteString");
            //console.log('contents: => ', ByteString.of(bytearray1).hex())
            jhexdump(bytearray1)
            return result
        }
    })
}
function main(){
    hookAddress()
    //hookSocket()
   // hookSSLSocketAndroid8()
}

setImmediate(main)

总结

虽然上述"自吐"脚本是基于HttpURLConnection开发的,但经过测试,使用okhttp3和Retrofit框架的App同样能完成网络数据包的抓包工作.从理论上讲上述"自吐"脚本应该可以通杀所有使用系统Socket进行收发包的App.但需要注意的是上述脚本还存在一些问题,比如URL信息和收发包的对应关系、request和response如何更加精确的一对一等.

参考链接

<安卓Frida逆向与抓包实战>


相关内容

0%