专栏首页进击的Coder安卓 App 逆向课程之四 frida 注入 Okhttp 抓包中篇

安卓 App 逆向课程之四 frida 注入 Okhttp 抓包中篇

本篇文章接上篇。

2. Okhttp3 自吐抓包

我们将一次请求的request大致结构罗列如下。

•请求方法 GET、POST、PUT、DELETE、HEAD 等•URL•使用的协议版本 HTTP/1/1.1/2•多个请求 Header•回车、换行符•请求 Body 数据

如果通过Hook的方式实现另类的“抓包”,我们的需求是保留URL,请求Body,以及headers。至于协议版本等可有可无。目前国内应用使用的 HTTP 协议版本大多数都是 HTTP/1.1,HTTP/2 虽然出来有段时间了,但国内使用的还比较少,这些字段对我们无帮助。

2.1 Hook request的时机

先不考虑response如何获取,单纯想一下如何hook request,即使不深入了解Okhttp框架,通过断点调试一步步走,大概也能找到几个看着不错的点。

2.1.1 requests 构建过程

// 构造request
Request request = new Request.Builder()
    .url(url) // Hook url()方法,此处只能得到单纯url,不妥
    .header(key, value) // Hook header()方法,一个request会多次调用此方法,会造成干扰,不妥。
    .header(key, value)
    ...
    .build();// Hook build()方法,一个request只可能调用一次,且它是构造完成时调用,不会遗漏信息。

DEBUG断点调试

DEBUG url()

可以得到url,其余均获取不到。

DEBUG header()

大家会发现,header会被调用数次,尽管我们并没有配置header,但Okhttp会检测并帮我们填补许多header字段,因此这个点也行不通。

诸如此类,DEBUG build()方法,会发现每次请求,build方法都会被调用两次,同样不适合。

上述问题是由Okhttp的拦截器机制造成,后续会有篇幅讲解,除此之外,构建一个request并不代表发送一次请求,这不是一个概念,因此我们要继续往下找。

查看OkHttpClient.newCall(request)方法源码

  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }

直接Hook newCall方法行不行,当然是可以的。

直接上frida脚本

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

});

启动Frida server

C:\Users\Lenovo>adb shell
root@OXF-AN10:/ # cd /data/local/tmp
root@OXF-AN10:/data/local/tmp # ./frida-server

Hook结果如下:

C:\Users\Lenovo>frida -U com.r0ysue.learnokhttp explore -l C:\Users\Lenovo\Desktop\抓包\teach\1.js
     ____
    / _  |   Frida 12.8.14 - A world-class dynamic 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/

[OXF AN10::com.r0ysue.learnokhttp]-> 
Request{method=GET, url=http://www.kuaidi100.com/query?type=yuantong&postid=11111111111, tags={}}
Request{method=GET, url=http://www.kuaidi100.com/query?type=yuantong&postid=11111111111, tags={}}
Request{method=GET, url=http://www.kuaidi100.com/query?type=yuantong&postid=11111111111, tags={}}
Request{method=GET, url=http://www.kuaidi100.com/query?type=yuantong&postid=11111111111, tags={}}
Request{method=GET, url=http://www.kuaidi100.com/query?type=yuantong&postid=11111111111, tags={}}

实际上,Hook request的问题远没有完美解决,此Hook点同样可能遗漏或多出部分请求,因为存在"call"后没有发出实际请求的情况。

newCall(Request)方法调用了RealCall.newRealCall()方法:

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}

在RealCall.newRealCall()中,创建了一个新的RealCall对象,RealCall对象是Okhttp3.Call接口的一个实现,也是Okhttp3中Call的唯一实现。它表示一个等待执行的请求,它只能被执行一次,但实际上,到这一步,请求依然可以被取消。因此只有Hook 了execute()和enqueue(new Callback())才能真正保证每个从Okhttp出去的请求都能被Hook到,不多也不少。

除此之外,割裂开请求和相应,分开去找Hook点的想法是有问题的,只能看到request,但无法同时看到返回的相应,无法对爬虫工程师或协议分析过程产生有益的帮助。

2.2 Okhttp拦截器

拦截器是Okhttp中重要的一个概念,Okhttp通过Interceptor来完成监控管理、重写和重试请求。Okhttp本身存在五大拦截器,每个网络罗请求,不管是GET还是PUT/POST或者其他,都必须经过这五大拦截器。拦截器可以对request做出一定修改,同时对返回的Response做出一定修改,因此Interceptor是一个绝佳的Hook点,可以同时打印输出请求和相应。

为了帮助大家理解Interceptor机制,我们从三个方面去窥其一角。

2.2.1 拦截器整体概览

拦截器可以对request做出修改,在数据返回时,再对response做出修改,这种说法可能会让人不知所云,引用刘望舒的演示:

引用yuashuai[1] 所举的例子,他表述的生动且贴切。

老张有很多干面条,但是他想吃汤面,可是自己又不会做,但是碰巧村里大郎会做,于是老张拿一包干面条让大郎做成了汤面。但是老张发现他做面不好吃,盐都没放,连个青菜叶子都没有。 这时候老张正好碰到隔壁老王,老王说了这东西我也会做,比他做的好吃多了。于是老张又拿着一包干面条给了老王,老王说老张你等着,我马上回家给你做,做好了就给你送过去。但是老王回家并没有做,而是去家里拿了一包盐,然后去找隔壁老李了,原来老王并不会做面,但是他知道隔壁老李会做,而且做得比较好吃。于是他把干面条和盐都交给了老李。老李对老王说你回去等着吧,做好了马上给你送过去。可是老李同样不会做,但是他知道村里的大郎会做,这时老李首先回厨房拿了两个生菜叶子,然后带着老王给的干面条和盐去找大郎了,对大郎说,生菜叶子,盐,面条都给你了,你快给我做一碗面。大郎对老李说好嘞,3分钟就好了,3分钟后,老李拿着做好了的放了盐和生菜叶子的一碗面回去了。本来打算直接给老王,但是一想,自己放了两个生菜叶子,不吃点这个面吃不是有点亏,于是老李偷偷了吃了几根面。然后老李去找老王说你的面做好了并把面交给了老王。老王一看这面只有两个青菜叶子,营养是不是不够呀!于是老王又买了半斤熟牛肉,切切放了进去。然后老王去找老张说你的面做好了,还说道这么大一碗你也吃不完吧,让小张也吃点。最后老张吃着老王送来的红烧牛肉面感动的肉牛满面。 这里的干面条就可以看做一个最原始的request,到老王哪里被加了点盐,到老李哪里被加了生菜叶子,于是大郎才能把这个request做成放了盐和生菜叶子的response,这个response回到老李那里又被啃了几口,到老王那又被放了点牛肉。于是最后回到老张哪里收到的response就是被啃了几口并且加了牛肉的response。这样整个链条是不是就清楚了!

在Okhttp代码中,由getResponseWithInterceptorChain方法展示这个过程:

Response getResponseWithInterceptorChain() throws IOException {
    // 空的拦截器容器
    List<Interceptor> interceptors = new ArrayList<>();
    // 添加用户自定义的应用拦截器集合(可能0,1或多个)
    interceptors.addAll(client.interceptors());
    // 添加retryAndFollowUpInterceptor拦截器,该拦截器用于取消、失败重试、重定向
    interceptors.add(retryAndFollowUpInterceptor);
    // 添加BridgeInterceptor拦截器,对于Request而言,该拦截器把用户请求转换为 HTTP 请求;对于Resposne,把 HTTP 响应转换为用户友好的响应
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // 添加CacheInterceptor拦截器,该拦截器读写缓存、根据策略决定是否使用
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //该拦截器实现和服务器建立连接
    interceptors.add(new ConnectInterceptor(client));
    //如果不是WebSocket请求,添加用户自定义的网络拦截器集合(可能0,1或多个)
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    // 添加真正发起网络请求的Interceptor
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }
}

参照下图理解整体理解拦截器链:

2.2.2 动手添加用户拦截器

新建LoggingInterceptor类,实现Interceptor接口,这代表它是一个拦截器,接下来实现intercept方法,我们的拦截器会打印URL和请求headers,完整代码如下。

LoggingInterceptor.java

package com.r0ysue.learnokhttp;

import android.util.Log;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

class LoggingInterceptor implements Interceptor {

    // TAG即为日志打印时的标签
    private static String TAG = "learnokhttp";

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {

        Request request = chain.request();
        Log.i(TAG, "请求URL:"+String.valueOf(request.url())+"\n");
        Log.i(TAG, "请求headers:"+"\n"+String.valueOf(request.headers())+"\n");

        Response response = chain.proceed(request);

        return response;
    }

}

接下来需要将我们自定义的LoggingInterceptor拦截器添加到拦截器链中,这部分需要顺着1.3.1 中的OkhttpClient对象继续讲。用户自定义的拦截器就是在此处添加。

用户拦截器有两种

•应用拦截器 Application Interceptors•网络拦截器 Network Interceptors

具体区别可以看官网[2],对于我们的需求而言,两者都可,但网络拦截器更好,具体原因有心的读者可以去对照一下。看一下相应代码的修改:

在example.java中

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

// 新建一个拦截器
OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(new LoggingInterceptor())
        .build();

大家会发现,OKhttpclient的创建方式改变了,这里讲一下Okhttpclient三种创建方式,之所以存在这三种创建方式,和Okhttpclient本身的原则息息相关。

创建方法一

OkHttpClient client = new OkHttpClient();

其内部即默认配置大量参数

public OkHttpClient() {
   this(new Builder());
 }

public Builder() {
      dispatcher = new Dispatcher();
      protocols = DEFAULT_PROTOCOLS;
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      eventListenerFactory = EventListener.factory(EventListener.NONE);
      proxySelector = ProxySelector.getDefault();
      cookieJar = CookieJar.NO_COOKIES;
      //...
      //...
      //...
    }
#此即Okhttp默认配置
OkHttpClient(Builder builder) {
    this.dispatcher = builder.dispatcher;
    this.proxy = builder.proxy;
    this.protocols = builder.protocols;
    this.connectionSpecs = builder.connectionSpecs;
    this.interceptors = Util.immutableList(builder.interceptors);
    this.networkInterceptors = Util.immutableList(builder.networkInterceptors);
    this.eventListenerFactory = builder.eventListenerFactory;
    this.proxySelector = builder.proxySelector;
    //...
    //...
    //...
  }

Okhttp框架帮我们默认所有配置,因此无法自定义添加用户拦截器。

创建方法二

OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

此即采用建造者(Builder)模式,可以自定义配置所有参数,我们在这儿添加了一个拦截器。

我们使用方式二验证一下拦截器是否生效,然后再讲解为什么需要第三种创建方式。

输出内容有三行,第一行是URL,第二行是headers,需要注意,第三行的response并非由我们拦截器中打印出来,而是之前代码的作用,之所以不在拦截器中顺带打印response,是因为在此时,response对象的打印稍有不便,我们在后续提它。

接下来讨论第三种client创建方式,它会在原先的client基础上创建一个新的okhttp客户端。

创建方式三

// 此为原先的client
OkHttpClient client = new OkHttpClient();

// 基于原先的client创建新的client
OkHttpClient newClient = client.newBuilder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

newclient和client共享连接池、线程池,且newclient继承client原先的配置。为什么我们需要它呢?

———通过这种方式,我们可以配置出一个用于处理某一类特定需求的client。

打个比方,一个新闻客户端,主要提供如下三个接口:

•/xxx/xxx/news ——浏览新闻•/xxx/xxx/comments ——查看评论•/xxx/xxx/login ——登录

可以将Okhttpclient想象成网络通信工厂,根据需求不断生产这三类请求,但登录接口由于涉及到用户信息,我们需要额外的防护,最好通过一个拦截器给request增加一个sign验证。在这种情况下,我们就可以使用第三种方式后创建loginClient,此工厂会为每个请求加上sign验证。那为什么不重新创建一个全新的client?一是因为每个App的网络通信都有许多默认配置,比如host,固定的sign,或者延迟等待时间等等。如果重新创建一个client,同样需要设置和添加这些配置,比较繁琐。二是为了提升性能,减少内存消耗。通过newBuilder方式创建的新client,和原client共享连接池、线程池等“基础设施”。

你可能会有疑问,为什么要在意这一点消耗呢?从我们的DEMO代码可以发现,我们每次点击“发送请求”按钮,都会创建一个新的client,既然每点击一次都创建一个新的客户端,何必在意newbuilder省下来的那点性能呢?

其实不然,在演示DEMO时,我们忽略了性能的问题,其实Okhttpclient应该被设置为单例模式,即App全局只使用一个共享的OkHttpClient 实例,将所有的网络请求都通过这个实例处理。因为每个OkHttpClient 实例都有自己的连接池和线程池,重用这个实例能降低延时,减少内存消耗,而重复创建新实例则会浪费资源。

换而言之,我们的DEMO每次点击都创建一个新的实例,相当于每个生产车间只生产一个布娃娃,因此会造成极大的浪费,甚至会造成内存的溢出。

Okhttp官方并没有在框架中强制OkhttpClient全局单例(可能是出于让开发者更灵活和自由的缘故),但强烈建议非必要的情况下,全局共享一个OkHttpclient(网络访问框架一般都需要单例模式)。

接下来我们使用Objection来验证,DEMO中是否存在滥创Okhttpclient的现象。(显而易见,我们单纯熟悉一下Objection操作)。首先关闭并重新打开App,不做任何操作,创造一个干净的环境。

objection是一个基于Frida开发的命令行工具,它可以很方便的Hook Java函数和类,并输出参数,调用栈,返回值。只需一行命令就可以完成Hook。Objection的简单使用可以看这篇[3]。

开启server,安装objection后,使用Objection Hook 我们的DEMO app:

C:\Users\Lenovo>objection -g com.r0ysue.learnokhttp explore

正常情况展示如下:

C:\Users\Lenovo>objection -g com.r0ysue.learnokhttp explore


A newer version of objection is available!
You have v1.8.4 and v1.9.2 is ready for download.

Upgrade with: pip3 install objection --upgrade
For more information, please see: https://github.com/sensepost/objection/wiki/Updating

Using USB device `OXF AN10`
Agent injected and responds ok!

     _   _         _   _
 ___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_|  _|  _| | . |   |
|___|___| |___|___|_| |_|___|_|_|
      |___|(object)inject(ion) v1.8.4

     Runtime Mobile Exploration
        by: @leonjza from @sensepost

[tab] for command suggestions
com.r0ysue.learnokhttp on (HUAWEI: 5.1.1) [usb] #

使用Objection搜索堆中okhttp3.OkHttpClient实例,命令如下:

android heap search instances okhttp3.OkHttpClient
com.r0ysue.learnokhttp on (HUAWEI: 5.1.1) [usb] # android heap search instances okhttp3.OkHttpClient
Class instance enumeration complete for [32mokhttp3.OkHttpClient[39m

结果为空,点击数次“发送请求”按钮后,再次尝试

每次click后,堆中就会多出一个新的client实例,这是极大的浪费,除此之外,如果读者显示结果有误差,堆中实例并无增加,添加 --fresh参数重试。

2.2.3 BridgeInterceptor拦截器讲解

BridgeInterceptor为例,截取一段代码:

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

如果我们并不对header中的相应字段做设置,Bridge拦截器会为其添加一些默认值,从之前的抓包对比也可以看出,当我们没有添加user-Agent、Host,Accept-Encoding等字段时,Okhttp会为我们自动添加这些信息。

本篇文章先到这里,下一篇我们会介绍另外两个拦截器—— Yang Okhttp 拦截器和天外飞仙拦截器的实现,敬请期待。

References

[1] yuashuai: https://blog.csdn.net/qq_16445551/article/details/79008433 [2] 官网: https://square.github.io/okhttp/interceptors/#application-interceptors [3] 这篇: https://www.anquanke.com/post/id/197657

本文分享自微信公众号 - 进击的Coder(FightingCoder)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Scrapy-Redis分布式爬虫源码解析

    Scrapy-Redis库已经为我们提供了Scrapy分布式的队列、调度器、去重等功能,其GitHub地址为:https://github.com/rmax/s...

    崔庆才
  • ScrapyRedis源码解析

    ScrapyRedis 这个库已经为我们提供了 Scrapy 分布式的队列、调度器、去重等功能,其 GitHub 地址为:https://github.com/...

    崔庆才
  • TensorFlow layers模块用法

    TensorFlow 中的 layers 模块提供用于深度学习的更高层次封装的 API,利用它我们可以轻松地构建模型,这一节我们就来看下这个模块的 API 的具...

    崔庆才
  • 【SpringBoot专题】监控健康状况前言监控插件:actuatorSpring Boot Admin:可视化后台管理系统总结

    想一想,我们以前是如何监控一个Java应用的监控状况的?一般而言,我们会在Linux服务器上通过一些命令,比如通过jstat来统计堆内存的使用情况/垃圾回收的情...

    用户2890438
  • kafka连接器两种部署模式详解

    一 kafka Connector介绍 Kafka Connect是一个用于在Apache Kafka和其他系统之间进行可扩展和可靠数据流传输的工具。这使得快速...

    Spark学习技巧
  • 持续集成之钉钉通知

    在使用持续集成中,经常会涉及到通知的功能,比如自动化测试代码执行完成后,希望能够得到反馈,依据反馈的结果来看具体的结果。在Jenkins中可以使用Em...

    无涯WuYa
  • 2018腾讯广告算法大赛总结/0.772229/Rank11

    liupengsay/2018-Tencent-social-advertising-algorithm-contest​github.com

    Coggle数据科学
  • 前端开发者指南(2017)

    参与者(排名不分先后):blueken; brucecham; cfanlife; DDU1222; LittlePineapple; MatildaJin;...

    iKcamp
  • 源码分析Qt窗口标题中文乱码的问题

    Qt君
  • Android设备屏幕方向的一二事

    orientation的取值为: - 横屏:Configuration.ORIENTATION_LANDSCAPE - 竖屏:Configuration.O...

    fdroid

扫码关注云+社区

领取腾讯云代金券