本篇文章接上篇。

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());}// 添加真正发起网络请求的Interceptorinterceptors.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 exploreA 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/UpdatingUsing USB device `OXF AN10`
Agent injected and responds ok!_   _         _   ____| |_|_|___ ___| |_|_|___ ___
| . | . | | -_|  _|  _| | . |   |
|___|___| |___|___|_| |_|___|_|_||___|(object)inject(ion) v1.8.4Runtime Mobile Explorationby: @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

怎么样?如果大家对安卓逆向感兴趣,想学到更多的知识,或者想与肉丝姐进一步交流的话,欢迎加入肉丝姐的星球来学习。

这里我跟肉丝姐还申请到了专属的半价(原价 50 元)优惠,一杯咖啡的钱大家就能学到更多关于安卓逆向的知识,感兴趣的朋友来扫码加入吧。

精品连载丨安卓 App 逆向课程之四 frida 注入 Okhttp 抓包中篇相关推荐

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

    本篇内容是「肉丝姐教你安卓逆向之 frida 注入 Okhttp 抓包系列的第三篇,建议配合前两篇一起阅读,效果更佳. 精品连载丨安卓 App 逆向课程之三 frida 注入 Okhttp 抓包上篇 ...

  2. 精品连载丨安卓 App 逆向课程之二逆向神器 frida 的介绍

    " 阅读本文大概需要 8 分钟. " 前面我们介绍了精品连载丨安卓 App 逆向课程一之环境配置,下面我们来接着介绍一个安卓 App 逆向大杀器-- frida. 前阵子受< ...

  3. 《APP逆向学习》课程介绍和什么是安卓app逆向?

    来源[小肩膀 零基础一站式安卓app逆向安全(2021版)]:aHR0cHM6Ly93d3cuYmlsaWJpbGkuY29tL3ZpZGVvL0JWMUFWNDExSjd6aw== 1.课程介绍 安 ...

  4. 知物由学 | 干货!一文了解安卓APP逆向分析与保护机制

    "知物由学"是网易云易盾打造的一个品牌栏目,词语出自汉·王充<论衡·实知>.人,能力有高下之分,学习才知道事物的道理,而后才有智慧,不去求问就不会知道."知物 ...

  5. 安卓APP破解利器之FRIDA

    本文讲的是安卓APP破解利器之FRIDA,在我去年参加RadareCon大会的时候,我了解到了一个动态的二进制插桩框架--Frida.起初我觉得它似乎只有一丁点趣味,后来经过实践才发现它原来是如此的有 ...

  6. APP渗透—MobSF安全评估、frida、r0capture抓包

    APP渗透-MobSF安全评估.frida.r0capture抓包 1. 前言 2. 补充AppInfoScanner相关内容 3. MobSF安全评估 3.1. 下载MobSF 3.2. 安装Doc ...

  7. fiddler设置中文版本_突破安卓7.0以上版本WX小程序抓包篇

    工作上经常需要使用brupsuite抓取APP和WX小程序的包,所以会在安卓上安装burp的证书.但是你会发现安卓7.0之后有了network-security-config这个选项,可以让app只信 ...

  8. Android安卓进阶之——一文带你了解抓包和反抓包

    今天主要跟大家介绍一下Android的抓包和防止抓包 介绍两款抓包工具,Profiter和Charles. 工具环境: Android Studio 4.2.2 手机Google Pixel 3XL ...

  9. 安卓APP逆向百集完整版

    课程介绍 Android逆向这玩意在日常工作中用的比较少,但是作为一个Android开发者,还是要学会的,比如我们可以破解反编译别人的APP,然后进行学习分析,甚至还可以反编译后进行修改再次打包等等, ...

最新文章

  1. php怎么看数据化,3.2.11 查看和判断数据类型
  2. IOS SEL (@selector) 原理及使用总结(一)
  3. bzoj2006 NOI2010 数据结构+堆维护区间和最大
  4. 微软Exchange Server 2013 CU11更新已发布
  5. H3C 三种生成树协议特性的比较
  6. PHP实现中文字符串截取无乱码
  7. 强力推荐几种多媒体播放器方案(jQuery、Flash、HTML5)
  8. linux dd 光标在闪,linux dd详解
  9. Cannot start process,the working directory 'F:\hello\hello'does not exit 问题解决
  10. golang-go mod版本等相关内容:
  11. 算法设计与分析基础 课后习题答案(第一章)
  12. 改进YOLOv7系列:26.CVPR2022. ConvNeXt结合YOLOv7 | 基于ConvNeXt结构 构建 CNeB 模块
  13. QQ被盗恢复原来QQ的好友有妙招
  14. ACM与IEEE双Fellow、华人女计算机科学家周以真:可信 AI,未来可期
  15. 微信分享网页链接自定义图片和文字描述
  16. Tensorflow2.*教程之使用Tensorflow Hub 对IMDB电影评论数据集进行文本分类(2)
  17. 台式您想使用系统还原计算机吗,系统还原功能已关闭。如果继续安装,将不能使用系统还原随您的计算机运行的Windows操作系统提供的Windows - Microsoft Community...
  18. 国密SM1、SM2、SM3、SM4算法资料大全
  19. u盘中的android文件夹图标不显示了,u盘里的文件夹不显示,u盘文件夹不见了
  20. 搜狗输入法5.0_马化腾又出手!将全资收购搜狗,后者盘前疯涨超45%

热门文章

  1. HTML5七夕情人节表白网页(新年倒计时-红色雪花) HTML+CSS+JS 求婚 html生日快乐祝福代码网页 520情人节告白代码 程序员表白源码 抖音3D旋转相册 js烟花代码 css爱
  2. 数据结构 作业答案 第1章 绪论
  3. 计算机函数公式的使用VLOOKUP,EXCEL函数公式大全使用VLOOKUP函数IF函数数据验证来自动获取价格...
  4. 幼师计算机能力自我评价,幼师工作能力自我评价
  5. HTML5+CSS大作业——明星个人主页(15页) 创作主页
  6. 程序员读《马云全转》
  7. python爬虫实战之爬取知乎帖子
  8. docker构建hadoop镜像、docker-compose启动hdfs
  9. 请问我这表该如何遍历呢?
  10. STP理论04-RSTP相对STP的改进