篇幅有限 完整内容及源码关注公众号:ReverseCode,发送 冲
HttpURLConnection adb install -r -t network-debug.apk
启动frida
1 2 adb shell ./data/local/tmp/fs128arm64
内存漫游
1 2 3 4 5 6 pyenv local 3.8.0 objection -g com.example.network explore -P ~/.objection/plugins android hooking list classes 查看所有可hook的类 android hooking search classes URL android hooking watch class java.net.URL 由于hookURL类遗漏构造函数需要手动hook $init android hooking watch class_method java.net.URL.$init --dump-args --dump-backtrace --dump-return hook构造函数并打印
点击HTTP图片获取按钮,实现自吐第一步,并拿到上层实现类HttpURLConnectionImpl
1 2 3 android hooking search classes HttpURLConnectionImpl android hooking watch class com.android.okhttp.internal.huc.HttpURLConnectionImpl hook类所有方法并打印方法 android hooking watch class_method com.android.okhttp.internal.huc.HttpURLConnectionImpl.setRequestProperty --dump-args --dump-backtrace --dump-return hook类指定方法并打印出入参及调用栈
点击HTTP图片获取按钮,实现自吐第二步
plugin wallbreaker objectsearch com.android.okhttp.internal.huc.HttpURLConnectionImpl 存在多个实例说明每次点击生成新的对象且不释放
plugin wallbreaker objectdump –fullname 0x2972 打印其中一个对象在内存中的结构
android heap search instances com.android.okhttp.internal.huc.HttpURLConnectionImpl 获取内存中的实例地址
android heap execute 0x21e6 defaultUserAgent 手动调用defaultUserAgent
自吐 frida -U -f com.cz.babySister -l hook_HttpUrlConnection.js --no-pause
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 function hook_HttpUrlConnection(){ Java.perform(function(){ // java.net.URL.URL ($init) (得到URL) Java.use("java.net.URL").$init.overload('java.lang.String').implementation = function (str){ var result = this.$init(str) console.log("result , str => ",result,str); return result; } //HttpURLConnection setRequestProperty 得到各种请求头、属性等,不能hook抽象类HttpURLConnection,只能hook抽象类的实现类HttpURLConnectionImpl Java.use("com.android.okhttp.internal.huc.HttpURLConnectionImpl").setRequestProperty.implementation = function(str1,str2){ var result = this.setRequestProperty(str1,str2); console.log(".setRequestProperty result,str1,str2->",result,str1,str2); return result; } Java.use("com.android.okhttp.internal.huc.HttpURLConnectionImpl").setRequestMethod.implementation = function(str1){ var result = this.setRequestMethod(str1); console.log(".setRequestMethod result,str1,str2->",result,str1); return result; } }) } setImmediate(hook_HttpUrlConnection)
OkHttp3 搭建抓包环境 默认创建 Okhttp框架帮我们默认所有配置,因此无法自定义添加用户拦截器。
as新建Ok3Demo项目,创建页面button布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <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>
build.gradle引入ok3依赖
1 2 // 增加对Okhttp3的依赖 implementation("com.squareup.okhttp3:okhttp:3.12.0")
AndroidManifest.xml配置网络权限
1 2 <!-- 申请网络请求权限 --> <uses-permission android:name="android.permission.INTERNET" />
创建异步请求线程,在RealCall.newRealCall()中,创建了一个新的RealCall对象,RealCall对象是Okhttp3.Call接口的一个实现,也是Okhttp3中Call的唯一实现。它表示一个等待执行的请求,它只能被执行一次,但实际上,到这一步,请求依然可以被取消。因此只有Hook 了execute()和enqueue(new Callback())才能真正保证每个从Okhttp出去的请求都能被Hook到
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 public class example { // TAG即为日志打印时的标签 private static String TAG = "learnokhttp"; // 新建一个Okhttp客户端 OkHttpClient client = new OkHttpClient(); void run(String url) throws IOException { // 构造request Request request = new Request.Builder() .url(url) .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()); } } ); } }
MainActivity中调用网络请求线程
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 public class MainActivity extends AppCompatActivity { private static String TAG = "learnokhttp"; @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/"; example myexample = new example(); try { myexample.run(requestUrl); } catch (IOException e) { e.printStackTrace(); } } }); } }
建造者(Builder)模式 新建LoggingInterceptor类,实现Interceptor接口,这代表它是一个拦截器,接下来实现intercept方法,我们的拦截器会打印URL和请求headers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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; } }
拦截器是Okhttp中重要的一个概念,Okhttp通过Interceptor来完成监控管理、重写和重试请求。Okhttp本身存在五大拦截器,每个网络请求,不管是GET还是PUT/POST或者其他,都必须经过这五大拦截器。拦截器可以对request做出一定修改,同时对返回的Response做出一定修改,因此Interceptor是一个绝佳的Hook点,可以同时打印输出请求和相应。
自定义配置所有参数
1 2 3 4 5 6 7 // 此为原先的client OkHttpClient client = new OkHttpClient(); // 基于原先的client创建新的client OkHttpClient newClient = client.newBuilder() .addNetworkInterceptor(new LoggingInterceptor()) .build();
将example中代码转移到MainActivity中
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 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()); } } ); } }); } }
hook adb shell && ./data/local/tmp/fs128arm64
启动frida
pyenv local 3.8.0
切换python环境
objection -g com.onejane.ok3demo explore -P ~/.objection/plugins
加载所有插件,点击发送请求并开启内存漫游
1 2 3 4 5 plugin wallbreaker classsearch OkHttpClient 内存搜索OkHttpClient类 plugin wallbreaker classdump --fullname okhttp3.OkHttpClient 打印该类结构 plugin wallbreaker objectsearch okhttp3.OkHttpClient 获取该类的内存地址 plugin wallbreaker objectdump --fullname 0x2592 打印内存中该地址的类结构 plugin wallbreaker objectsearch okhttp3.OkHttpClient 内存中存在多个OkHttpClient,默认不回收对象实例
okhttp3Logging 新增okhttp3Logging类
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 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"); } }
打包编译后取出dex改名为okhttp3logging.dex
,push
到/data/locol/tmp
目录下
编写frida进行hook,frida -U -f com.onejane.ok3demo -l hookOkhttp3.js --no-pause
并通过adb logcat 查看系统log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hook_okhttp3_logging() { // 1. frida Hook java层的代码必须包裹在Java.perform中,Java.perform会将Hook Java相关API准备就绪。 Java.perform(function () { Java.openClassFile("/data/local/tmp/okhttp3logging.dex").load(); // 只修改了这一句,换句话说,只是使用不同的拦截器对象。 var MyInterceptor = Java.use("com.onejane.ok3demo.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..."); }); }
Retrofit git clone https://github.com/peiniwan/Ganhuo.git 编译源码编译安装apk
修改build.gradle,buildscript.repositories和allprojects.repositories添加google()
1 classpath 'com.android.tools.build:gradle:3.5.3'
frida -U -f ganhuo.ly.com.ganhuo -l hookOkhttp3.js --no-pause
调用hook_okhttp3_logging()通过adb logcat查看后台log
git clone https://github.com/siyujie/OkHttpLogger-Frida.git 获取Frida 实现拦截okhttp的脚本,首先将 okhttpfind.dex
拷贝到 /data/local/tmp/
目录下,执行命令启动frida -UF -l okhttp_poker.js -f ganhuo.ly.com.ganhuo --no-pause
可追加 -o [output filepath]
保存到文件
原理:
由于所有使用的okhttp
框架的App发出的请求都是通过RealCall.java
发出的,那么我们可以hook此类拿到request
和response
, 也可以缓存下来每一个请求的call
对象,进行再次请求,所以选择了此处进行hook。 find
前新增check
,根据特征类寻找是否使用了okhttp3
库,如果没有特征类,则说明没有使用okhttp
; 找到特征类,说明使用了okhttp
的库,并打印出是否被混淆。
1 2 3 4 5 `find()` 要等完全启动并执行过网络请求后再进行调用,检查是否使用了Okhttp & 是否可能被混淆 & 寻找okhttp3关键类及函数 `switchLoader(\"okhttp3.OkHttpClient\")` 参数:静态分析到的okhttpclient类名 `hold()` 要等完全启动再进行调用,开启HOOK拦截 `history()` 打印可重新发送的请求 `resend(index)` 重新发送请求
baseUrl自吐
Hook RetrofitUtils 中的new Retrofit.Builder().baseUrl(baseurl)的baseUrl
1 2 3 4 5 objection -g ganhuo.ly.com.ganhuo explore android hooking search classes retrofit android hooking list class_methods retrofit2.Retrofit android hooking list class_methods retrofit2.Retrofit$Builder 发现只有baseUrl()无参构造,可能在app启动时就执行了baseUrl(baseurl) objection -g ganhuo.ly.com.ganhuo explore --startup-command "android hooking list class_methods retrofit2.Retrofit$Builder" 没有反应
通过编写frida脚本实现hook有参构造baseUrl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function hookbaseurl(){ Java.perform(function(){ Java.use("retrofit2.Retrofit$Builder").baseUrl.overload('java.lang.String').implementation = function(str){ var result = this.baseUrl(str) console.log("result1,str=>",result,str) return result } Java.use("retrofit2.Retrofit$Builder").baseUrl.overload('okhttp3.HttpUrl').implementation = function(str){ var result = this.baseUrl(str) console.log("result2,str=>",result,str) return result } }) } setImmediate(hookbaseurl)