HttpURLConnection&OK3&Retrofit自吐通杀

篇幅有限

完整内容及源码关注公众号: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)

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

okhttp3logging

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

Retrofit基于ok3

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此类拿到requestresponse, 也可以缓存下来每一个请求的call对象,进行再次请求,所以选择了此处进行hook。 find前新增check,根据特征类寻找是否使用了okhttp3库,如果没有特征类,则说明没有使用okhttp; 找到特征类,说明使用了okhttp的库,并打印出是否被混淆。

1
2
3
4
5
`find()`                                         要等完全启动并执行过网络请求后再进行调用,检查是否使用了Okhttp & 是否可能被混淆 & 寻找okhttp3关键类及函数	
`switchLoader(\"okhttp3.OkHttpClient\")` 参数:静态分析到的okhttpclient类名
`hold()` 要等完全启动再进行调用,开启HOOK拦截
`history()` 打印可重新发送的请求
`resend(index)` 重新发送请求

OkHttpLogger-Frida抓Retrofit

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)

hookbaseurl

文章作者: J
文章链接: http://onejane.github.io/2021/03/02/HttpURLConnection&OK3&Retrofit自吐通杀/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏