1.TCP请求

当APP端和服务端基于TCP直接发送请求时,无法抓包。

1.1 示例-服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# server.bind(("0.0.0.0", 9002))
server.bind(("192.168.3.136", 9002))
server.listen(5)

while True:
client, addr = server.accept() # 等待客户端来链接(卡主)
data = client.recv(1024) # 读取客户端发送的消息
print(data)
client.send("ok".encode('utf-8')) # 给安卓端回复 ok
client.close()

1.2 示例-客户端

示例代码:TCP001

1
2
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
1
<uses-permission android:name="android.permission.INTERNET" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送请求" />
</LinearLayout>
</LinearLayout>
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
package com.nb.tcp001;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;

public class MainActivity extends AppCompatActivity {
private Button btn1;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

btn1 = findViewById(R.id.btn1);
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doRequest();
}
});

}

private void doRequest() {
new Thread() {
@Override
public void run() {
try {
//Socket socket = new Socket("127.0.0.1", 9001);
Socket socket = new Socket("192.168.43.252", 9001);

// java.net.SocketOutputStream
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello".getBytes());
//Log.e("outputStream的类 => ", outputStream.getClass().getName());

// java.net.SocketInputStream
InputStream inputStream = socket.getInputStream();
//Log.e("inputStream的类 => ", inputStream.getClass().getName());
while (true) {
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer, 0, buffer.length);
if (len == -1) {
break;
}
Log.e("读取相应内容 =>", new String(Arrays.copyOf(buffer, len)));
}
socket.close();

} catch (Exception ex) {
Log.e("Main", "网络请求异常" + ex);
}
}
}.start();
}

}

1.3 无法抓包

image-20240517201523886

1.4 Hook抓包【java】

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
Java.perform(function () {
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
var HexDump = Java.use("com.android.internal.util.HexDump");

var SocketOutputStream = Java.use('java.net.SocketOutputStream');
SocketOutputStream.write.overload('[B').implementation = function (arr) {
// console.log(arr); // 104,101,108,108,111
// console.log(ByteString.of(arr).hex()); // 68656c6c6f
// console.log(ByteString.of(arr).utf8()); // hello
// console.log(HexDump.dumpHexString(arr))

return this.write(arr);
};

var SocketInputStream = Java.use('java.net.SocketInputStream');
SocketInputStream.read.overload('[B', 'int', 'int').implementation = function (b, off, len) {
var res = this.read(b, off, len);

// console.log(b);
// console.log(ByteString.of(b).hex());
// console.log(ByteString.of(b).utf8());
// console.log(HexDump.dumpHexString(b, off, len))

return res;

};
});

// frida -UF -l 1.hook.js

1.发送请求

请求数据的写入是调用的SocketOutputStreamwrite方法。

1
2
3
// java.net.SocketOutputStream
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());

image-20240517201538381

image-20240517201545246

image-20240517201555297

所以,底层发送的http请求的数据就是执行 java.net.SocketOutputStream 中的 socketWrite0方法。

那么,如果我们对这个方法进行Hook,是不是就可以获取到所有http请求的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function () {
var SocketOutputStream = Java.use('java.net.SocketOutputStream');
var HexDump = Java.use("com.android.internal.util.HexDump");
var ByteString = Java.use("com.android.okhttp.okio.ByteString");

SocketOutputStream.socketWrite0.overload('java.io.FileDescriptor', '[B', 'int', 'int').implementation = function (fd, b, off, len) {
// console.log("参数:", fd, b, off, len);
// console.log(HexDump.dumpHexString(b, off, len), "\n")

console.log(ByteString.of(b).utf8(), "\n");

return this.socketWrite0(fd, b, off, len);
};
});

// frida -UF -l hook.js
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

image-20240517201603702

2.获取响应

1
2
3
4
5
6
7
8
9
10
11
// java.net.SocketInputStream
InputStream inputStream = socket.getInputStream();
Log.e("inputStream的类 => ", inputStream.getClass().getName());
while (true) {
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer, 0, buffer.length);
if (len == -1) {
break;
}
Log.e("读取相应内容 =>", new String(Arrays.copyOf(buffer, len)));
}

image-20240517201610790

image-20240517201617946

image-20240517201627375

所以,底层发送的http响应的的数据就是执行 java.net.SocketInputStream 中的 socketRead0方法。

那么,如果我们对这个方法进行Hook,是不是就可以获取到所有http请求的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function () {
var SocketInputStream = Java.use('java.net.SocketInputStream');
var HexDump = Java.use("com.android.internal.util.HexDump");
var ByteString = Java.use("com.android.okhttp.okio.ByteString");

SocketInputStream.socketRead0.overload('java.io.FileDescriptor', '[B', 'int', 'int', 'int').implementation = function (fd, b, off, len, timeout) {

var res = this.socketRead0(fd, b, off, len, timeout);
// console.log(HexDump.dumpHexString(b, off, len), "\n")
console.log(ByteString.of(b).utf8(), "\n");
return res;

};
});

// frida -UF -l hook.js
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

1.5 Hook抓包【so】

上述的请求和写入,都是在java层面进行hook的获取,实际上真正的请求会调用so层的代码,最终将请求发送过去。

image-20240517201637263

image-20240517201644727

1.寻找so文件

寻找socketRead0socketWrite0的hook的脚本:

1
2
3
4
5
6
7
8
Java.perform(function () {
const apiResolver = new ApiResolver('module');
apiResolver.enumerateMatches('exports:*!*socket*0*').forEach(function (v) {
console.log(v.name)
});
});

// frida -UF -l hook.js

image-20240517201652451

1
2
/apex/com.android.runtime/lib64/libopenjdk.so!SocketInputStream_socketRead0
/apex/com.android.runtime/lib64/libopenjdk.so!SocketOutputStream_socketWrite0

2.请求和响应

/apex/com.android.runtime/lib64/libopenjdk.so

image-20240517201700316

image-20240517201707948

image-20240517201716980

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
Java.perform(function () {
const NET_Send = Module.getExportByName('libopenjdk.so', 'NET_Send');
const NET_Read = Module.getExportByName('libopenjdk.so', 'NET_Read');


Interceptor.attach(NET_Send, {
onEnter(args) {
console.log('write call'); // jni,jobj,data,off,len
//console.log(hexdump(args[1], {length: args[2].toInt32()}));
console.log(Memory.readByteArray(args[1], args[2].toInt32()));
}
});

Interceptor.attach(NET_Read, {
onEnter(args) {
console.log('read call');
this.buf = args[1];
},
onLeave: function (retval) {
retval |= 0; // Cast retval to 32-bit integer.
if (retval <= 0) {
return;
}
console.log(Memory.readByteArray(this.buf, retval));
}
});
});

// frida -UF -l hook.js

image-20240517201726242

2.Http请求

在app开发时,发送请求会使用okhttp3等第三发模块,但这些模块发送http请求的等调用是基于TCP实现,例如:

1
2
3
4
5
6
7
8
9
10
11
12
OkHttpClient client = new OkHttpClient.Builder().build();
Request req = new Request.Builder().url("http://www.chinatelecom.com.cn/corp/01/").build();
Call call = client.newCall(req);
try {
Response res = call.execute();
ResponseBody resBody = res.body();
String dataString = resBody.string();
Log.e("=======>", dataString);

} catch (IOException ex) {
Log.e("----->", "请求失败");
}

基于TCP底层发送的本质:

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
// http://www.chinatelecom.com.cn/corp/01/
Socket socket = new Socket("www.chinatelecom.com.cn", 80);

// 1.构造请求头
StringBuilder sb = new StringBuilder();
sb.append("GET /corp/01/ HTTP/1.1\r\n");
sb.append("host: www.chinatelecom.com.cn\r\n");
sb.append("user-Agent: test\r\n");
sb.append("\r\n");

// 2.写入数据(发送数据)
// java.net.SocketOutputStream
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());
Log.e("outputStream的类 => ", outputStream.getClass().getName());

// 3.读取数据(获取数据)
// java.net.SocketInputStream
InputStream inputStream = socket.getInputStream();
Log.e("inputStream的类 => ", inputStream.getClass().getName());

while (true) {
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer, 0, buffer.length);
if (len == -1) {
break;
}
Log.e("读取相应内容 =>", new String(Arrays.copyOf(buffer, len)));
}

socket.close();

2.1 示例-服务端

1
pip install flask
1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/api/info")
def info():
return jsonify({"code": 1000, 'data': "success"})


if __name__ == '__main__':
app.run(host="0.0.0.0", port=9003)

2.2 示例-客户端

示例代码:HTTP002

1
2
3
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
implementation "com.squareup.okhttp3:okhttp:3.4.2"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nb.http002">
<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.HTTP002">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!--禁用掉明文流量请求的检查-->
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送请求1" />

<Button
android:id="@+id/btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送请求2" />
</LinearLayout>
</LinearLayout>
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
package com.nb.http002;

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 java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;

import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class MainActivity extends AppCompatActivity {
private Button btn1;
private Button btn2;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);


btn1 = findViewById(R.id.btn1);
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doRequest1();
}
});


btn2 = findViewById(R.id.btn2);
btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doRequest2();
}
});
}


private void doRequest1() {
new Thread() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient.Builder().build();

Request req = new Request.Builder().url("http://192.168.3.136:9003/api/info").build();
Call call = client.newCall(req);
try {
Response res = call.execute();
ResponseBody resBody = res.body();
String dataString = resBody.string();
Log.e("=======>", dataString);

} catch (IOException ex) {
Log.e("----->", "请求失败" + ex.toString());
}

} catch (Exception ex) {
Log.e("Main", "网络请求异常" + ex);
}
}
}.start();
}

private void doRequest2() {
new Thread() {
@Override
public void run() {
try {
Socket socket = new Socket("192.168.3.136", 9003);

// 1.构造请求头
StringBuilder sb = new StringBuilder();
sb.append("GET /api/info HTTP/1.1\r\n");
sb.append("user-Agent: test\r\n");
sb.append("\r\n");

// 2.写入数据(发送数据)
// java.net.SocketOutputStream
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());
//Log.e("outputStream的类 => ", outputStream.getClass().getName());

// 3.读取数据(获取数据)
// java.net.SocketInputStream
InputStream inputStream = socket.getInputStream();
//Log.e("inputStream的类 => ", inputStream.getClass().getName());

while (true) {
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer, 0, buffer.length);
if (len == -1) {
break;
}
Log.e("读取响应内容 =>", new String(Arrays.copyOf(buffer, len)));
}

socket.close();

} catch (Exception ex) {
Log.e("Main", "网络请求异常" + ex);
}
}
}.start();
}
}

2.2 Hook抓包

Http底层就是基于上述TCP协议的方式。所以,业界基于上述TCP的方式进行抓包。

1.java层

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
Java.perform(function () {
var HexDump = Java.use("com.android.internal.util.HexDump");
var ByteString = Java.use("com.android.okhttp.okio.ByteString");


var SocketOutputStream = Java.use('java.net.SocketOutputStream');
SocketOutputStream.socketWrite0.overload('java.io.FileDescriptor', '[B', 'int', 'int').implementation = function (fd, b, off, len) {
// console.log("参数:", fd, b, off, len);
// console.log(HexDump.dumpHexString(b, off, len), "\n")
console.log("------------请求--------------")
console.log(ByteString.of(b).utf8(), "\n");

return this.socketWrite0(fd, b, off, len);
};


var SocketInputStream = Java.use('java.net.SocketInputStream');
SocketInputStream.socketRead0.overload('java.io.FileDescriptor', '[B', 'int', 'int', 'int').implementation = function (fd, b, off, len, timeout) {

var res = this.socketRead0(fd, b, off, len, timeout);

console.log("------------响应-------------")
// console.log(HexDump.dumpHexString(b, off, len), "\n")
console.log(ByteString.of(b).utf8(), "\n");

return res;

};
});


// frida -UF -l hook1.js
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

image-20240517201743025

2.so层

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
Java.perform(function () {
const NET_Send = Module.getExportByName('libopenjdk.so', 'NET_Send');
const NET_Read = Module.getExportByName('libopenjdk.so', 'NET_Read');


Interceptor.attach(NET_Send, {
onEnter(args) {
console.log('write call'); // jni,jobj,data,off,len
//console.log(hexdump(args[1], {length: args[2].toInt32()}));
console.log(Memory.readByteArray(args[1], args[2].toInt32()));
}
});

Interceptor.attach(NET_Read, {
onEnter(args) {
console.log('read call');
this.buf = args[1];
}, onLeave: function (retval) {
retval |= 0; // Cast retval to 32-bit integer.
if (retval <= 0) {
return;
}
console.log(Memory.readByteArray(this.buf, retval));
}
});
});



// frida -UF -l hook2.js

image-20240517201754075

3.Https请求

HTTPS请求的底层也是基于TCP发送请求,只不过其中有加入了SSL证书相关。

所以,在基于TCP进行读写时候相关的模块不一样。

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
SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket("api.luffycity.com", 443);

// 1.构造请求头
StringBuilder sb = new StringBuilder();
sb.append("GET /api/v1/course/category/actual/?courseType=actual HTTP/1.1\r\n");
sb.append("host: api.luffycity.com\r\n");
sb.append("user-Agent: test\r\n");
sb.append("\r\n");

// 2.写入数据(发送数据)
// com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());
Log.e("outputStream的类 => ", outputStream.getClass().getName());

// 3.读取数据(获取数据)
// com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream
InputStream inputStream = socket.getInputStream();
Log.e("inputStream的类 => ", inputStream.getClass().getName());

while (true) {
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer, 0, buffer.length);
if (len == -1) {
break;
}
Log.e("读取响应内容 =>", new String(Arrays.copyOf(buffer, len)));
}

socket.close();

3.1 示例-客户端

示例:HTTPS003

1
2
3
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
implementation "com.squareup.okhttp3:okhttp:3.4.2"
1
<uses-permission android:name="android.permission.INTERNET" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送请求1" />

<Button
android:id="@+id/btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送请求2" />
</LinearLayout>
</LinearLayout>
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
package com.nb.https003;


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 java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class MainActivity extends AppCompatActivity {
private Button btn1;
private Button btn2;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);


btn1 = findViewById(R.id.btn1);
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doRequest1();
}
});


btn2 = findViewById(R.id.btn2);
btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doRequest2();
}
});
}


private void doRequest1() {
new Thread() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient.Builder().build();

Request req = new Request.Builder().url("https://api.luffycity.com/api/v1/course/category/actual/?courseType=actual").build();
Call call = client.newCall(req);
try {
Response res = call.execute();
ResponseBody resBody = res.body();
String dataString = resBody.string();
Log.e("=======>", dataString);

} catch (IOException ex) {
Log.e("----->", "请求失败" + ex.toString());
}

} catch (Exception ex) {
Log.e("Main", "网络请求异常" + ex);
}
}
}.start();
}

private void doRequest2() {
new Thread() {
@Override
public void run() {
try {
// https://api.luffycity.com/api/v1/course/actual/?limit=1&offset=0&category_id=9999
SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket("api.luffycity.com", 443);

// 1.构造请求头
StringBuilder sb = new StringBuilder();
sb.append("GET /api/v1/course/category/actual/?courseType=actual HTTP/1.1\r\n");
sb.append("host: api.luffycity.com\r\n");
sb.append("user-Agent: test\r\n");
sb.append("\r\n");

// 2.写入数据(发送数据)
// com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());
Log.e("outputStream的类 => ", outputStream.getClass().getName());

// 3.读取数据(获取数据)
// com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream
InputStream inputStream = socket.getInputStream();
Log.e("inputStream的类 => ", inputStream.getClass().getName());

while (true) {
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer, 0, buffer.length);
if (len == -1) {
break;
}
Log.e("读取响应内容 =>", new String(Arrays.copyOf(buffer, len)));
}

socket.close();

} catch (Exception ex) {
Log.e("Main", "网络请求异常" + ex);
}
}
}.start();
}
}

3.2 Hook抓包【java】

1.发送请求

1
2
3
4
// com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());
Log.e("outputStream的类 => ", outputStream.getClass().getName());

安卓源码地址:

1
http://aospxref.com/android-10.0.0_r47/xref/external/conscrypt/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java

image-20240517201811793

image-20240517201820812

image-20240517201831218

image-20240517201842454

image-20240517201850917

http://aospxref.com/android-10.0.0_r47/xref/external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/NativeCrypto.java#1185

1
2
3
4
5
6
7
java.lang.Throwable
at com.android.org.conscrypt.NativeCrypto.SSL_write(Native Method)
at com.android.org.conscrypt.NativeSsl.write(NativeSsl.java:426)
at com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write(ConscryptFileDescriptorSocket.java:626)
at java.io.OutputStream.write(OutputStream.java:75)
at com.nb.ssldemo2.MainActivity$2.run(MainActivity.java:55)

直接hook来进行抓包:

image-20240517201902277

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java.perform(function () {
var NativeCrypto = Java.use('com.android.org.conscrypt.NativeCrypto');
var HexDump = Java.use("com.android.internal.util.HexDump");
var ByteString = Java.use("com.android.okhttp.okio.ByteString");

NativeCrypto.SSL_write.implementation = function (ssl, ssl_holder, fd, shc, b, off, len, timeout) {
//console.log(HexDump.dumpHexString(b, off, len), "\n")
console.log(ByteString.of(b).utf8(), "\n");

return this.SSL_write(ssl, ssl_holder, fd, shc, b, off, len, timeout);
};
});

// frida -UF -l 3.hook.js
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

2.响应获取

1
2
3
4
5
6
7
8
9
10
11
12
// com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream
InputStream inputStream = socket.getInputStream();
Log.e("inputStream的类 => ", inputStream.getClass().getName());

while (true) {
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer, 0, buffer.length);
if (len == -1) {
break;
}
Log.e("读取相应内容 =>", new String(Arrays.copyOf(buffer, len)));
}

安卓源码地址:

1
http://aospxref.com/android-10.0.0_r47/xref/external/conscrypt/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java

image-20240517201911918

image-20240517201919999

image-20240517201936369

image-20240517201944726

image-20240517201951213

http://aospxref.com/android-10.0.0_r47/xref/external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/NativeCrypto.java#1185

1
2
3
4
5
6
java.lang.Throwable
at com.android.org.conscrypt.NativeCrypto.SSL_read(Native Method)
at com.android.org.conscrypt.NativeSsl.read(NativeSsl.java:411)
at com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read(ConscryptFileDescriptorSocket.java:549)
at com.nb.ssldemo2.MainActivity$2.run(MainActivity.java:65)

直接hook来进行抓包:

image-20240517202001036

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function () {
var NativeCrypto = Java.use('com.android.org.conscrypt.NativeCrypto');
var HexDump = Java.use("com.android.internal.util.HexDump");
var ByteString = Java.use("com.android.okhttp.okio.ByteString");

NativeCrypto.SSL_read.implementation = function (ssl, ssl_holder, fd, shc, b, off, len, timeout) {
var res = this.SSL_read(ssl, ssl_holder, fd, shc, b, off, len, timeout);

//console.log(HexDump.dumpHexString(b, off, len), "\n")
console.log(ByteString.of(b).utf8(), "\n");

return res;
};
});

// frida -UF -l 4.hook.js
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

3.3 Hook抓包【so】

上述的请求和写入,都是在java层面进行hook的获取,实际上真正的请求会调用so层的代码,最终将请求发送过去。

image-20240517202010461

1.寻找so文件

一般情况下底层是基于openssl将https请求发送出去,而openssl基于的是libssl.so的文件。

寻找SSL_writeSSL_read的hook的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.perform(function () {
const apiResolver = new ApiResolver('module');
// 包含:libttboringssl.so 或 libssl.so
// 'exports:*lib*ssl*!SSL_*'
apiResolver.enumerateMatches('exports:*lib*ssl*!SSL_*').forEach(function (v) {
if (v.name.indexOf('SSL_write') > 0) {
// SSL_write = v.address;
console.log(v.name);
} else if (v.name.indexOf('SSL_read') > 0) {
// SSL_read = v.address;
console.log(v.name);
}
});
});

image-20240517202023556

2.请求和响应

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
Java.perform(function () {
const SSL_write = Module.getExportByName('libssl.so', 'SSL_write');
const SSL_read = Module.getExportByName('libssl.so', 'SSL_read');

Interceptor.attach(SSL_write, {
onEnter(args) {
console.log('write call');
//console.log(args[0]); // ssl
//console.log(args[1]); // buffer
//console.log(args[2]); // len
//console.log(hexdump(args[1], {length: args[2].toInt32()}));
console.log(Memory.readByteArray(args[1], parseInt(args[2])));
}
});

Interceptor.attach(SSL_read, {
onEnter(args) {
console.log('read call');
this.buf = args[1];
}, onLeave: function (retval) {
retval |= 0; // Cast retval to 32-bit integer.
if (retval <= 0) {
return;
}
console.log(Memory.readByteArray(this.buf, retval));
}
});
});

// frida -UF -l 7.hook.js
// frida -U -f com.nb.ssldemo2 -l 7.hook.js --no-pause

image-20240517202034350

4 其他-案例

1.抽屉新热榜

版本:v3.5.8

1.无法抓包

登录页面,无法抓包。

image-20240517202227226

2.NO_PROXY

image-20240517202236295

image-20240517202244780

3.底层抓包

如果是发送的https请求,底层会执行:NativeCrypto.SSL_writeNativeCrypto.SSL_read

image-20240517202255593

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Java.perform(function () {
var NativeCrypto = Java.use('com.android.org.conscrypt.NativeCrypto');
var HexDump = Java.use("com.android.internal.util.HexDump");
var ByteString = Java.use("com.android.okhttp.okio.ByteString");

NativeCrypto.SSL_write.implementation = function (ssl, ssl_holder, fd, shc, b, off, len, timeout) {
//console.log(HexDump.dumpHexString(b, off, len), "\n")
console.log(ByteString.of(b).utf8(), "\n");
return this.SSL_write(ssl, ssl_holder, fd, shc, b, off, len, timeout);
};

NativeCrypto.SSL_read.implementation = function (ssl, ssl_holder, fd, shc, b, off, len, timeout) {
var res = this.SSL_read(ssl, ssl_holder, fd, shc, b, off, len, timeout);

//console.log(HexDump.dumpHexString(b, off, len), "\n")
console.log(ByteString.of(b).utf8(), "\n");

return res;
};
});

2.B站

2.1 无法抓包

直播间弹幕页面,Charles抓不到包。

image-20240517202406918

2.2 猜测,是否TCP?

SocketInputStream中读取内容,发现有数据不断返回,应该是TCP请求返回的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java.perform(function () {
var HexDump = Java.use("com.android.internal.util.HexDump");

var SocketInputStream = Java.use('java.net.SocketInputStream');
SocketInputStream.socketRead0.overload('java.io.FileDescriptor', '[B', 'int', 'int', 'int').implementation = function (fd, b, off, len, timeout) {
var res = this.socketRead0(fd, b, off, len, timeout);
console.log("------------响应-------------")
try{
console.log(HexDump.dumpHexString(b, off, len), "\n")
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}catch (e) {
console.log(e, "\n");
}
return res;
};
});

// frida -UF -l hook1.js

image-20240517202418120

1
2
3
4
5
6
7
8
9
10
11
12
13
java.lang.Throwable
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:119)
at java.net.SocketInputStream.read(SocketInputStream.java:176)
at java.net.SocketInputStream.read(SocketInputStream.java:144)
at okio.l$b.j5(BL:4)
at okio.a$b.j5(BL:2)
at okio.q.request(BL:3)
at okio.q.c4(BL:1)
at okio.q.readInt(BL:1)
at com.bilibili.bililive.infra.socket.core.codec.reader.a.t(BL:1) 【看这里】
at com.bilibili.bililive.infra.socket.core.codec.reader.a.s(BL:1)
at com.bilibili.bililive.infra.socketclient.SocketClient.J(BL:2)

image-20240517202427166

基于Hook响应内容:

image-20240517202435463

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function () {
// c2.f.j.g.k.h.a.a(bArr);
var c = Java.use("c2.f.j.g.h.b.d.c.c");
c.$init.implementation = function (v1, v2) {
console.log("---->", v2);
//console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
var res = this.$init(v1, v2);
return res;
};
});
// frida -UF -l hook2.js

2.3 猜测,socket连接

如果是TCP请求,那应该会有socket对象的常见去连接远程服务器。

1
2
3
4
5
6
Socket socket = new Socket("192.168.43.252", 9002);

Socket socket = new Socket();
InetAddress serverAddress = InetAddress.getByName("192.168.43.252");
SocketAddress address = new InetSocketAddress(serverAddress, 8080);
socket.connect(address,10000)

所以,可以尝试编写Hook脚本去获取IP地址和端口等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(function () {
var Socket = Java.use("java.net.Socket");
Socket.$init.overload().implementation = function () {
console.log("1.创建socket对象");
var res = this.$init();
return res;
};
Socket.connect.overload('java.net.SocketAddress', 'int').implementation = function (address, timeout) {
console.log("2.执行连接",address, timeout);
// address = zj-cn-live-comet.chat.bilibili.com/119.167.206.12:2243 timeout = 10000
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
var res = this.connect(address, timeout);
return res;
};
});
// frida -UF -l hook3.js

image-20240517202446307

1
2
3
4
5
6
7
8
java.lang.Throwable
at java.net.Socket.connect(Native Method)
at com.bilibili.bililive.infra.socketclient.g.a.d(BL:4)
at com.bilibili.bililive.infra.socketclient.g.a.c(BL:3)
at com.bilibili.bililive.infra.socketclient.SocketClient$a.run(BL:6)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)

2.4 猜测,发送数据

当连接成功后,手机端会向后端发送请求(告诉后端进入的是那个直播间),然后后端才会源源不断的返回数据。

image-20240517202454867

image-20240517202503249

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
Java.perform(function () {
// c2.f.j.g.k.h.a.a(bArr);
var Socket = Java.use("java.net.Socket");

Socket.$init.overload().implementation = function () {
console.log("1.创建socket对象");
var res = this.$init();
return res;
};

Socket.connect.overload('java.net.SocketAddress', 'int').implementation = function (address, timeout) {
console.log("2.执行connect", address, timeout);
// address = zj-cn-live-comet.chat.bilibili.com/119.167.206.12:2243 timeout = 10000
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
var res = this.connect(address, timeout);
return res;
};


Socket.getOutputStream.implementation = function () {
var res = this.getOutputStream();
console.log("3.写对象", JSON.stringify(res))
return res;
}

var SocketOutputStream = Java.use("java.net.SocketOutputStream");
SocketOutputStream.write.overload('[B', 'int', 'int').implementation = function (v1, v2, v3) {
console.log("4.写入数据:", v1)
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
var res = this.write(v1, v2, v3);
return res;
}


var c = Java.use("c2.f.j.g.h.b.d.c.c");
c.$init.implementation = function (v1, v2) {
console.log("5.读取数据", v2);
//console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
var res = this.$init(v1, v2);
return res;
};

});
// frida -UF -l hook5.js

接下来,根据写入数据时的调用栈,找到数据加密的算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java.lang.Throwable
at java.net.SocketOutputStream.write(Native Method)
at okio.l$a.A3(BL:5)
at okio.a$a.A3(BL:6)
at okio.p.flush(BL:3)
at com.bilibili.bililive.infra.socketclient.f.b(BL:2)
at com.bilibili.bililive.infra.socketclient.f.c(BL:2) 【看这里】
at com.bilibili.bililive.infra.socketclient.SocketClient$b.run(BL:3)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)

image-20240517202515795

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上图a函数的调用栈
java.lang.Throwable
at com.bilibili.bililive.infra.socketclient.f.a(Native Method)
at com.bilibili.bililive.infra.socketclient.SocketClient.O(BL:2) 【看这里】
at com.bilibili.bililive.infra.socket.plugins.a.f(BL:2)
at c2.f.j.g.h.b.c.f(BL:4)
at com.bilibili.bililive.infra.socketclient.SocketClient.K(BL:4)
at com.bilibili.bililive.infra.socketclient.SocketClient.z(BL:8)
at com.bilibili.bililive.infra.socketclient.SocketClient.q(BL:1)
at com.bilibili.bililive.infra.socketclient.SocketClient$a$a.c(BL:1)
at com.bilibili.bililive.infra.socketclient.g.a.c(BL:6)
at com.bilibili.bililive.infra.socketclient.SocketClient$a.run(BL:6)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)

image-20240517202525356

1.bVar类型

可以Hook找到bVar到底是谁的对象?

1
"<instance: com.bilibili.bililive.infra.socketclient.b, $className: c2.f.j.g.i.d>"

image-20240517202534751

2.fvar.a

image-20240517202543960

3.N相关

image-20240517202553667

image-20240517202601081

image-20240517202608955

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java.lang.Throwable
at java.net.SocketOutputStream.write(Native Method)
at okio.l$a.A3(BL:5)
at okio.a$a.A3(BL:6)
at okio.p.flush(BL:3)
at com.bilibili.bililive.infra.socketclient.f.b(BL:2)
at com.bilibili.bililive.infra.socketclient.f.c(BL:2) 【看这里】
at com.bilibili.bililive.infra.socketclient.SocketClient$b.run(BL:3)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)

4.谁创建了bVar

image-20240517202619495

image-20240517202627517

image-20240517202635402

image-20240517202642958

image-20240517202653155

image-20240517202701963

5 笔记

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
抓包-底层通信相关

今日概要:
- TCP协议,安卓开发
- Http协议,安卓开发
- Https协议,安卓开发
- 案例:抽屉+B站

1.关于TCP和Http协议

2.TCP安卓开发:app和后端

2.1 Python后端

2.2 安卓客户端

2.3 别人家的APP
安卓手机 TCP 后端 基于Charlse无法抓包
安卓手机 http/https 后端(charlse)

2.4 如何抓包:基于Hook进行抓包

发送消息:
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello".getBytes());

- 你的Hook脚本:java.io.OutputStream.write方法【错误】
- 真正Hook脚本:java.net.SocketOutputStream.write方法

接受返回值:
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer, 0, buffer.length);

- 你的Hook脚本:java.io.InputStream.read方法【错误】
- 真正Hook脚本:java.net.SocketInputStream.read方法

问题:Hook是固定的吗?
问题:获取IP和端口

2.5 深入的Hook脚本(java层)
- 真正Hook脚本:java.net.SocketOutputStream.write方法
- socketWrite
- socketWrite0
private native void socketWrite0(FileDescriptor fd, byte[] b, int off,int len) throws IOException;
- libopenjdk.so
- SocketOutputStream_socketWrite0
- NET_Send

- 真正Hook脚本:java.net.SocketInputStream.read方法
- socketRead
- socketRead0
private native int socketRead0(FileDescriptor fd,byte b[], int off, int len,int timeout)
- libopenjdk.so
- SocketInputStream_socketRead0
- NET_Read

2.6 深入的Hook脚本(so层)

private native void socketWrite0(FileDescriptor fd, byte[] b, int off,int len) throws IOException;
private native int socketRead0(FileDescriptor fd,byte b[], int off, int len,int timeout)

1.在那个so文件中
Java.perform(function () {
const apiResolver = new ApiResolver('module');

apiResolver.enumerateMatches('exports:*!*socket*0*').forEach(function (v) {
console.log(v.name)
});
});

/apex/com.android.runtime/lib64/libopenjdk.so!SocketInputStream_socketRead0
/apex/com.android.runtime/lib64/libopenjdk.so!SocketOutputStream_socketWrite0

2.ida打开so

3.HTTP安卓开发:app和后端

3.1 后端

3.2 app端(okhttp3、底层)

3.3 架设没有charlse等工具

4.HTTPS安卓开发:app + 后端路飞学城

4.1 安卓端(HTTPS003)
- 基于okhttp3发送https请求
- 基于底层TCP协议,Socket是模块+SSL

4.2 假如charlse不存在了,如何抓包?

4.3 Hook抓包
- 发送
com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write
com.android.org.conscrypt.NativeCrypto.SSL_write 【native】

- 获取
com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read
com.android.org.conscrypt.NativeCrypto.SSL_read 【 native】

4.4 Hook抓包深入

小结:
- TCP、Http、Https协议
- TCP协议请求 -> 固定Hook脚本
- Http和Https抓包
- 基于工具
- 基于源码和Hook【tcp=http固定】【https固定】

5.案例

5.1 抽屉新热榜 v3.5.8
- 登录页面
- 手机代理 & charles抓包 -> 无法抓包

解决方案:
- NO_PROXY,可以抓包。【https】
- 底层Hook抓包x

5.3 B站APP v6.24.0
- 手机代理 & charles抓包
- 页面加载、登录
- 直播页面

- 猜想:实时显示
- http或https,无状态短连接。
- TCP协议,不断开连接实时展示。

- 验证猜想:TCP请求(Hook脚本)

__END__