前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >精品连载丨安卓 App 逆向课程之五 frida 注入 Okhttp 抓包下篇

精品连载丨安卓 App 逆向课程之五 frida 注入 Okhttp 抓包下篇

作者头像
崔庆才
发布2020-07-28 14:26:52
7.3K0
发布2020-07-28 14:26:52
举报
本篇内容是「肉丝姐教你安卓逆向之 frida 注入 Okhttp 抓包系列的第三篇,建议配合前两篇一起阅读,效果更佳。

“ 阅读本文大概需要 8 分钟。”

2.3 Yang Okhttp 拦截器思路讲解

接下来我们分析Yang大佬的Frida实现okhttp3.Interceptor[1]。

代码完整如下,建议使用该份代码测试:


function hook_okhttp3() {
    // 1. frida Hook java层的代码必须包裹在Java.perform中,Java.perform会将Hook Java相关API准备就绪。
    Java.perform(function () {

        // 2. 准备相应类库,用于后续调用,前两个库是Android自带类库,后三个是使用Okhttp网络库的情况下才有的类
        var ByteString = Java.use("com.android.okhttp.okio.ByteString");
        var Buffer = Java.use("com.android.okhttp.okio.Buffer");
        var Interceptor = Java.use("okhttp3.Interceptor");
        var ArrayList = Java.use("java.util.ArrayList");
        var OkHttpClient = Java.use("okhttp3.OkHttpClient");

        //  注册一个Java类
        var MyInterceptor = Java.registerClass({
            name: "okhttp3.MyInterceptor",
            implements: [Interceptor],
            methods: {
                intercept: function (chain) {
                    var request = chain.request();
                    try {
                        console.log("MyInterceptor.intercept onEnter:", request, "\nrequest headers:\n", request.headers());
                        var requestBody = request.body();
                        var contentLength = requestBody ? requestBody.contentLength() : 0;
                        if (contentLength > 0) {
                            var BufferObj = Buffer.$new();
                            requestBody.writeTo(BufferObj);
                            try {
                                console.log("\nrequest body String:\n", BufferObj.readString(), "\n");
                            } catch (error) {
                                try {
                                    console.log("\nrequest body ByteString:\n", ByteString.of(BufferObj.readByteArray()).hex(), "\n");
                                } catch (error) {
                                    console.log("error 1:", error);
                                }
                            }
                        }
                    } catch (error) {
                        console.log("error 2:", error);
                    }
                    var response = chain.proceed(request);
                    try {
                        console.log("MyInterceptor.intercept onLeave:", response, "\nresponse headers:\n", response.headers());
                        var responseBody = response.body();
                        var contentLength = responseBody ? responseBody.contentLength() : 0;
                        if (contentLength > 0) {
                            console.log("\nresponsecontentLength:", contentLength, "responseBody:", responseBody, "\n");

                            var ContentType = response.headers().get("Content-Type");
                            console.log("ContentType:", ContentType);
                            if (ContentType.indexOf("video") == -1) {
                                if (ContentType.indexOf("application") == 0) {
                                    var source = responseBody.source();
                                    if (ContentType.indexOf("application/zip") != 0) {
                                        try {
                                            console.log("\nresponse.body StringClass\n", source.readUtf8(), "\n");
                                        } catch (error) {
                                            try {
                                                console.log("\nresponse.body ByteString\n", source.readByteString().hex(), "\n");
                                            } catch (error) {
                                                console.log("error 4:", error);
                                            }
                                        }
                                    }
                                }

                            }

                        }

                    } catch (error) {
                        console.log("error 3:", error);
                    }
                    return response;
                }
            }
        });

        OkHttpClient.$init.overload('okhttp3.OkHttpClient$Builder').implementation = function (Builder) {
            console.log("OkHttpClient.$init:", this, Java.cast(Builder.interceptors(), ArrayList));
            this.$init(Builder);
        };

        var MyInterceptorObj = MyInterceptor.$new();
        var Builder = Java.use("okhttp3.OkHttpClient$Builder");
        console.log(Builder);
        Builder.build.implementation = function () {
            this.interceptors().clear();
            this.interceptors().add(MyInterceptorObj);
            var result = this.build();
            return result;
        };

        Builder.addInterceptor.implementation = function (interceptor) {
            this.interceptors().clear();
            this.interceptors().add(MyInterceptorObj);
            return this;
        };

        console.log("hook_okhttp3...");
    });
}

hook_okhttp3();
2.3.1 使用效果

接下来的演示效果均由Pixel展示

Yang大佬使用在Okhttp中添加用户自定义拦截器的方式达到抓包效果,先前我们说过,App不可能发送一次请求就创建一个client客户端,往往是全局一个client,我们先修改DEMO代码,使之更接近真实App。

package com.r0ysue.learnokhttp;

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 okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {

    private static String TAG = "learnokhttp";

    public static final String requestUrl = "http://www.kuaidi100.com/query?type=yuantong&postid=11111111111";

    // 全局只使用这一个拦截器
    public static final OkHttpClient client = new OkHttpClient.Builder()
            .addNetworkInterceptor(new LoggingInterceptor())
            .build();

    Request request = new Request.Builder()
            .url(requestUrl)
            .build();


    @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) {
                // 发起异步请求
                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());

                    }
                                                }
                );

            }
        });
    }

}

Frida有spawn和attach两种启动方式,接下来使用Spawn模式和Attach模式分别测试,attach模式下,Frida会附加到当前的目标进程中,即需要App处于启动状态,这也意味着只能从当前时机往后Hook,而spawn模式下,Frida会自行启动并注入进目标App,Hook的时机非常早,好处在于不会错过App中相对较早(比如App启动时产生的参数),缺点是假如想要Hook的时机点偏后,则会带来大量干扰信息,严重甚至会导致server崩溃。

之前我们提过,App全局只有一个client,因此它在App启动的较早时机被创建,如果采用attach模式Hook OkhttpClient,大概率会一无所获。六月天想看樱花——你来晚了。

因此只能用Spawn模式启动,对应frida命令即必须使用-f参数:

frida -U -f com.r0ysue.learnokhttp -l C:\Users\Lenovo\Desktop\抓包\teach\yang1.js --no-pause
C:\Users\Lenovo>frida -U -f com.r0ysue.learnokhttp -l C:\Users\Lenovo\Desktop\抓包\teach\yang1.js --no-pause
     ____
/ _  |   Frida12.8.14- A world-classdynamic instrumentation toolkit
| (_| |
> _  |   Commands:
/_/ |_|       help      -> Displays the help system
. . . .       object?   -> Display information about 'object'
. . . .       exit/quit -> Exit
. . . .
. . . .   More info at https://www.frida.re/docs/home/
Spawned`com.r0ysue.learnokhttp`. Resuming main thread!
TypeError: cannot read property'apply' of undefined
    at [anon] (../../../frida-gum/bindings/gumjs/duktape.c:56618)
    at frida/runtime/core.js:55
[Pixel::com.r0ysue.learnokhttp]-> <class: okhttp3.OkHttpClient>
<class: okhttp3.OkHttpClient$Builder>
hook_okhttp3...
OkHttpClient.$init: okhttp3.OkHttpClient@ba4f0a3[okhttp3.MyInterceptor@79e4fa0]
MyInterceptor.intercept onEnter: Request{method=GET, url=http://www.kuaidi100.com/query?type=yuantong&postid=11111111111, tags={}}
request headers:

MyInterceptor.intercept onLeave: Response{protocol=http/1.1, code=200, message=OK, url=http://www.kuaidi100.com/query?type=yuantong&postid=11111111111}
response headers:
Server: nginx
Date: Mon, 25May202015:59:33 GMT
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
P3P: CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
Cache-Control: no-cache
vary: accept-encoding

打印出了Request的信息以及Response的部分内容,但缺少响应体,整体似乎有不少可以补充的地方,具体看一下代码。

2.3.2 代码分析

代码虽然才几十行,但对于新手来说,可能会略显复杂,我们解构一下,从无到有重新实现一下:

STEP 1

function hook_okhttp3() {
    // frida Hook java层的代码必须包裹在Java.perform中,Java.perform会将Hook Java相关API准备就绪。
    Java.perform(function () {
        console.log("hook_okhttp3...");
    });
}
hook_okhttp3();

Java.perform(fn)主要用于当前线程附加到Java VM并且调用fn方法。frida Hook java层的代码必须包裹在Java.perform中,Java.perform会将Hook Java相关API准备就绪。

STEP 2 实现自定义interceptor

// 获取interceptor类
var Interceptor = Java.use("okhttp3.Interceptor");
//  注册一个Java类
var MyInterceptor = Java.registerClass({
    name: "okhttp3.MyInterceptor",
    implements: [Interceptor],
    methods: {
        intercept: function (chain) {
            var request = chain.request();
            try {
                console.log("MyInterceptor.intercept onEnter:", request, "\nrequest headers:\n", request.headers());
                var requestBody = request.body();
                var contentLength = requestBody ? requestBody.contentLength() : 0;
                if (contentLength > 0) {
                    var BufferObj = Buffer.$new();
                    requestBody.writeTo(BufferObj);
                    try {
                        console.log("\nrequest body String:\n", BufferObj.readString(), "\n");
                    } catch (error) {
                        try {
                            console.log("\nrequest body ByteString:\n", ByteString.of(BufferObj.readByteArray()).hex(), "\n");
                        } catch (error) {
                            console.log("error 1:", error);
                        }
                    }
                }
            } catch (error) {
                console.log("error 2:", error);
            }
            var response = chain.proceed(request);
            try {
                console.log("MyInterceptor.intercept onLeave:", response, "\nresponse headers:\n", response.headers());
                var responseBody = response.body();
                var contentLength = responseBody ? responseBody.contentLength() : 0;
                if (contentLength > 0) {
                    console.log("\nresponsecontentLength:", contentLength, "responseBody:", responseBody, "\n");

                    var ContentType = response.headers().get("Content-Type");
                    console.log("ContentType:", ContentType);
                    if (ContentType.indexOf("video") == -1) {
                        if (ContentType.indexOf("application") == 0) {
                            var source = responseBody.source();
                            if (ContentType.indexOf("application/zip") != 0) {
                                try {
                                    console.log("\nresponse.body StringClass\n", source.readUtf8(), "\n");
                                } catch (error) {
                                    try {
                                        console.log("\nresponse.body ByteString\n", source.readByteString().hex(), "\n");
                                    } catch (error) {
                                        console.log("error 4:", error);
                                    }
                                }
                            }
                        }

                    }

                }

            } catch (error) {
                console.log("error 3:", error);
            }
            return response;
        }
    }
});

这部分代码量比较多,但实际上根本不用慌,我们拆解一下。首先,它的意图是在App中注册一个Java类,在第二节我们演示过自定义拦截器,换而言之,STEP2相当于在我们正向开发中,新建了一个类,实现了interceptor接口,是一个正儿八经的用户自定义拦截器。

看一下API Java.registerClass:创建一个新的Java类并返回一个包装器,规范如下:

name:指定类名称的字符串。

superClass:(可选)父类。要从 java.lang.Object 继承的省略。

implements:(可选)由此类实现的接口数组。

fields:(可选)对象,指定要公开的每个字段的名称和类型。

methods:(可选)对象,指定要实现的方法。

在此处:

name: "okhttp3.MyInterceptor",  //全类名:okhttp3.MyInterceptor,类名:MyInterceptor
implements: [Interceptor], // 实现Interceptor接口,即为一个拦截器
   methods: {
       // 该类中只有一个方法,即实现了Interceptor的interceptor方法
        intercept: function (chain) {
            // 具体逻辑
        }
    }

换而言之,上述Frida中的操作,与如下JAVA类等价:

package okhttp3;

import java.io.IOException;

public class MyInterceptor implements Interceptor{

    @Override
    public Response intercept(Chain chain) throws IOException {
        // 具体逻辑
        return null;
    }
}

接下来看interceptor中的具体实现,看一下其对Request的处理:

var request = chain.request();
try {
    console.log("MyInterceptor.intercept onEnter:", request, "\nrequest headers:\n", request.headers());
    var requestBody = request.body();
    var contentLength = requestBody ? requestBody.contentLength() : 0;
    if (contentLength > 0) {
        var BufferObj = Buffer.$new();
        requestBody.writeTo(BufferObj);
        try {
            console.log("\nrequest body String:\n", BufferObj.readString(), "\n");
        } catch (error) {
            try {
                console.log("\nrequest body ByteString:\n", ByteString.of(BufferObj.readByteArray()).hex(), "\n");
            } catch (error) {
                console.log("error 1:", error);
            }
        }
    }
} catch (error) {
    console.log("error 2:", error);
}

我将其翻译成java,可以一一对照:MyInterceptor.java

package com.r0ysue.learnokhttp;

import android.util.Log;

import java.io.IOException;
import java.nio.charset.Charset;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.ByteString;

public class MyInterceptor implements Interceptor {

    private static String TAG = "learnokhttp";
    private final Charset UTF8 = Charset.forName("UTF-8");

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        try{
            // 打印GET/POST,URL,HEADERS
            Log.i(TAG, "MyInterceptor.intercept onEnter:"+request+"\nrequest headers:\n"+request.headers());

            // 如果请求方式为POST,下面的逻辑负责打印RequestBody
            RequestBody requestBody = request.body();
            if(requestBody != null){
                long contentLength = requestBody.contentLength();
                if(contentLength != 0){
                    Buffer BufferObj = new Buffer();
                    requestBody.writeTo(BufferObj);
                    try {
                        Log.i(TAG, "\nrequest body String:\n"+BufferObj.readString(UTF8)+"\n");
                    }catch (Exception e){
                        try {
                            Log.i(TAG, "\nrequest body ByteString:\n"+ByteString.of(BufferObj.readByteArray()).hex()+"\n");
                        }catch (Exception e1){
                            Log.i(TAG, "error 1:");
                        }
                    }
                }
            }
        }catch (Exception e2){
            Log.i(TAG, "error 2:");
        }

        Response response = chain.proceed(request);
        return response;
    }
}

将此拦截器放入client,测试效果:MainActivity.java中:

publicstaticfinalOkHttpClient client = newOkHttpClient.Builder()
.addNetworkInterceptor(newMyInterceptor())
.build();

运行结果正常:

Response的逻辑也类似,在此不做额外讲解。

因此我们可以理解成,STEP1+STEP2后,好似在原App中添加了一个自定义拦截器链,那剩下的工作应该就是将我们的自定义拦截器添加到拦截器链里,在开发中,我们只用如下一行代码,但逆向中似乎不是这么容易。

.addNetworkInterceptor(newMyInterceptor())

STEP 3 添加拦截器 这部分代码可能存在一些问题,我问yang神,他说也忘记当时为啥这么写了:

OkHttpClient.$init.overload('okhttp3.OkHttpClient$Builder').implementation = function (Builder) {
            console.log("OkHttpClient.$init:", this, Java.cast(Builder.interceptors(), ArrayList));
            this.$init(Builder);
        };

var MyInterceptorObj = MyInterceptor.$new();
var Builder = Java.use("okhttp3.OkHttpClient$Builder");
console.log(Builder);
Builder.build.implementation = function () {
    this.interceptors().clear();
    this.interceptors().add(MyInterceptorObj);
    var result = this.build();
    return result;
};

Builder.addInterceptor.implementation = function (interceptor) {
    this.interceptors().clear();
    this.interceptors().add(MyInterceptorObj);
    return this;
};

简而言之,一共选择了三个Hook点,我们在正向开发中标出它们的位置,需要注意,Builder类是Okhttpclient中的内部类,Java编译器会将内部类编译成外部类名内部类名格式,因此不论Frida还是Xposed中,如果我们想对内部类进行操作,都应该使用连接符。

Hook点1——Okhttpclient的有参构造函数,参数为Builder

OkHttpClient(Builder builder) {
    this.dispatcher = builder.dispatcher;
    this.proxy = builder.proxy;
    this.protocols = builder.protocols;
}

先前我们讲过三种client创建的方式,每一种都必经过此构造函数,因此可以避免遗漏,yang大佬在此选择了简单打印对象。

Hook点2和3:

如果采用默认方式创建Okhttpclient,这两个Hook点就会失效,且在大佬的hook代码逻辑中,会将原拦截器数组清空,这可能会造成App本身拦截器失效或者无法访问网络,我们不妨做一些修改。

首先选择Hook点,我们使用Hook点2,开发中很少会使用默认方式创建client。

将源代码中STEP 3做删减,如下:

var MyInterceptorObj = MyInterceptor.$new();
var Builder = Java.use("okhttp3.OkHttpClient$Builder");
console.log(Builder);
Builder.build.implementation = function () {
    this.interceptors().add(MyInterceptorObj);
    return this.build();
};

测试后打印内容与原先不变,只做到这里多少有些不够味儿,下面开始整活儿。

2.4 天外飞仙拦截器

可以从2.3看出,通过Hook新增拦截器来实现打印内容是有效果的,但脚本远远称不上完善,多少有点鸡肋,除此之外,Java层面的拦截器逻辑在Frida中编写多少有些不自在,有隔靴搔痒之感。

Frida提供了如下API用于将DEX加载进内存,从而使用DEX中的方法和类,因为DEX是外来之物,因此称为天外飞仙。(需要注意的是,无法加载JAR包):

Java.openClassFile(dexPath).load();

2.3 中依照yang的Hook脚本,编写了对应的MyInterceptor.java类(有所阉割,只实现了request部分的逻辑处理)

2.4.1 加载自定义DEX

接下来我们取出App中的DEX,如果有多DEX,则用JADX查看想要使用的类在哪一个DEX中,最后push到手机,最后调用。

检查MyInterceptor.java是否编写正确,编译:

在DEMO APK项目目录中找到如下位置

C:\xxx\xxx\learnokhttp\app\build\outputs\apk\debug

(1).解压app-debug.apk取出classes1.dex文件(其中有目标类)

(2).push到/data/local/tmp 下

C:\Users\Lenovo>adb push C:\xxx\learnokhttp\app\build\outputs\apk\debug\classes.dex /data/local/tmp
C:\Users\Lenovo\Desktop\teach\AndroidProj\learnokhttp\app\...ile pushed, 0 skipped. 15.0 MB/s (2485932 bytes in0.158s)

(3).修改Frida Hook代码:

function hook_okhttp3() {
    Java.perform(function () {

        Java.openClassFile("/data/local/tmp/classes.dex").load();
        var MyInterceptor = Java.use("com.r0ysue.learnokhttp.MyInterceptor");

        var MyInterceptorObj = MyInterceptor.$new();
        var Builder = Java.use("okhttp3.OkHttpClient$Builder");
        console.log(Builder);
        Builder.build.implementation = function () {
            this.interceptors().add(MyInterceptorObj);
            return this.build();
        };


        console.log("hook_okhttp3...");
    });
}

hook_okhttp3();

可以发现整体代码量大减,这是因为原先创建拦截器类的逻辑被写在了dex中。(4).Frida Hook,在此之前将DEMO中OKhttpclient的添加拦截器代码注销,以防干扰,查看Android Studio日志,拦截器是否生效。

Frida 端:

 C:\Users\Lenovo>frida -U -f com.r0ysue.learnokhttp -l C:\Users\Lenovo\Desktop\抓包\teach\yang2.js  --no-pause
     ____
/ _  |   Frida12.8.14- A world-classdynamic instrumentation toolkit
| (_| |
> _  |   Commands:
/_/ |_|       help      -> Displays the help system
. . . .       object?   -> Display information about 'object'
. . . .       exit/quit -> Exit
. . . .
. . . .   More info at https://www.frida.re/docs/home/
Spawned`com.r0ysue.learnokhttp`. Resuming main thread!
[Pixel::com.r0ysue.learnokhttp]->
[Pixel::com.r0ysue.learnokhttp]-> <okhttp3.OkHttpClient$Builder>
hook_okhttp3...

Android Studio 日志查看:

红色框中即为我们拦截器输出的内容,可以发现,headers为空,这是因为我们拦截器添加的位置,我们在Frida代码中为应用添加的是Application Interceptor,这个拦截器在BridgeInterceptor等拦截器前,因此如果是BridgeInterceptor中添加的headers字段等,无法通过Application Interceptor打印出来,修改Frida代码,改成Network Application。

修改代码,将interceptor修改成如下:

Builder.build.implementation = function() {
// 原先添加到interceptors(即Application Interceptor)
// 修改为添加至networkInterceptors
this.networkInterceptors().add(MyInterceptorObj);
returnthis.build();
};

重新测试,结果符合预期:

2.4.2 加载Okhttp logging-interceptor

Okhttp 官方也提供了一款简单易用的日志打印拦截器——okhttp3:logging-interceptor

对其稍作修改,完整Java代码如下

package com.r0ysue.learnokhttp;

/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

同2.4.1 操作,编译——取出dex改名为okhttp3logging.dexpush/data/locol/tmp目录下,frida代码修改如下:

function hook_okhttp3() {
    // 1. frida Hook java层的代码必须包裹在Java.perform中,Java.perform会将Hook Java相关API准备就绪。
    Java.perform(function () {

        Java.openClassFile("/data/local/tmp/okhttplogging.dex").load();
        // 只修改了这一句,换句话说,只是使用不同的拦截器对象。
        var MyInterceptor = Java.use("com.r0ysue.learnokhttp.okhttp3Logging");

        var MyInterceptorObj = MyInterceptor.$new();
        var Builder = Java.use("okhttp3.OkHttpClient$Builder");
        console.log(Builder);
        Builder.build.implementation = function () {
            this.networkInterceptors().add(MyInterceptorObj);
            return this.build();
        };
        console.log("hook_okhttp3...");
    });
}

hook_okhttp3();

打印结果十分好,几乎和抓包能得到的信息一样多。

小总结:

在本篇文章中,我们学习了安卓中应用最为基本的网络库Okhttp,并通过小Demo学习其基本开发方法,进一步探索定位拦截位置,最后通过Frida构造一个拦截器并挂载,打印出通过Okttp传输的所有内容。

下一篇会关注这几个要点:

1.当前的okhttp3logging够好了吗?有没有办法让信息更清晰?或者功能更强大。2.一定要用Spawn模式启动吗?Attach方式常常更方便,是否能在Attach模式下也添加拦截器。3.混淆怎么办?混淆是否会对Hook产生影响?面对一般混淆是否有办法自识别?4.App加固是否会对Hook产生影响?

敬请期待。

References

[1] Frida实现okhttp3.Interceptor: https://bbs.pediy.com/thread-252129.htm

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 进击的Coder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 本篇内容是「肉丝姐教你安卓逆向之 frida 注入 Okhttp 抓包系列的第三篇,建议配合前两篇一起阅读,效果更佳。
  • 2.3 Yang Okhttp 拦截器思路讲解
  • 2.3.1 使用效果
  • 2.3.2 代码分析
  • 2.4 天外飞仙拦截器
  • 2.4.1 加载自定义DEX
  • 2.4.2 加载Okhttp logging-interceptor
  • References
相关产品与服务
日志服务
日志服务(Cloud Log Service,CLS)是腾讯云提供的一站式日志服务平台,提供了从日志采集、日志存储到日志检索,图表分析、监控告警、日志投递等多项服务,协助用户通过日志来解决业务运维、服务监控、日志审计等场景问题。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档