移动安全_抓包__底层通信_http和SSL请求的本质
- iOS破解
- 2025-01-24
- 52热度
- 0评论
http请求的本质
例如:你在Python中使用requests模块发送一个http请求,其底层就是使用socket模块+TCP实现发送的请求。
import requests
res = requests.get("http://192.168.1.37:8080/index")
print(res.text)
如果基于底层的socket模块+TCP协议实现如下:
import socket
client = socket.socket()
client.connect(('192.168.1.37', 8080))
content_list = []
content_list.append("GET /index HTTP/1.1\r\n")
content_list.append("host: wiki.mikecrm.com\r\n")
content_list.append("user-Agent: test\r\n")
content_list.append("\r\n")
content_string = "".join(content_list)
client.sendall(content_string.encode('utf-8'))
response = b""
while True:
chunk = client.recv(1024)
if not chunk:
break
response += chunk
print(response.decode('utf-8'))
client.close()
Http请求DEMO
2.1 http请求
详见示例 --- net.demo2.netd10_ssl01
public class MainActivity extends AppCompatActivity {
private Button btn1; // 定义一个按钮
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this); // 启用边到边显示
setContentView(R.layout.activity_main);// 设置布局文件
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {// 处理窗口内边距, 适应系统栏
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 初始化按钮
btn1 = findViewById(R.id.btn1); //通过 ID 找到按钮
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doRequest();
}
});
}
private void doRequest() {
new Thread() {
@Override
public void run() {
try {
// http://wiki.mikecrm.com/index?ajax=1&page=2
Socket socket = new Socket("192.168.1.37", 8080);
// 1.构造请求头
StringBuilder sb = new StringBuilder();
sb.append("GET /index HTTP/1.1\r\n");
sb.append("host: wiki.mikecrm.com\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();
}
}
http请求写入
请求数据的写入是调用的`SocketOutputStream`的`write`方法。
// java.net.SocketOutputStream
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());
http请求写入_调用路径
package java.net;
class SocketOutputStream extends FileOutputStream {
private native void socketWrite0(FileDescriptor var1, byte[] var2, int var3, int var4) throws IOException; // 底层 0
private void socketWrite(byte[] b, int off, int len) throws IOException { // 底层 1 +
if (len > 0 && off >= 0 && len <= b.length - off) {
FileDescriptor fd = this.impl.acquireFD();
try {
this.socketWrite0(fd, b, off, len); // 调用 socketWrite0 Native层
} catch (SocketException var9) {
if (this.impl.isClosedOrPending()) {
throw new SocketException("Socket closed");
}
throw var9;
} finally {
this.impl.releaseFD();
}
} else if (len != 0) {
throw new ArrayIndexOutOfBoundsException("len == " + len + " off == " + off + " buffer length == " + b.length);
}
}
public void write(int b) throws IOException { // 底层 2 +
this.temp[0] = (byte)b;
this.socketWrite(this.temp, 0, 1);
}
}
所以,底层发送的http请求的数据就是执行 `java.net.SocketOutputStream` 中的 `socketWrite0`方法。
那么,如果我们对这个方法进行Hook,是不是就可以获取到所有http请求的数据了。
http请求写入_hook验证
Java.perform(function () {
// 使用Java.use获取Java类的引用
var SocketOutputStream = Java.use('java.net.SocketOutputStream'); // 获取SocketOutputStream类的引用
var HexDump = Java.use("com.android.internal.util.HexDump"); // 获取HexDump类的引用,用于十六进制转储
var ByteString = Java.use("com.android.okhttp.okio.ByteString"); // 获取ByteString类的引用,用于处理字节数据
// 重载SocketOutputStream类的socketWrite0方法
SocketOutputStream.socketWrite0.overload('java.io.FileDescriptor', '[B', 'int', 'int').implementation = function (fd, b, off, len) {
// 打印传入的参数
console.log("参数:", fd, b, off, len);
// 使用HexDump.dumpHexString方法将字节数组转换为十六进制字符串并打印
console.log(HexDump.dumpHexString(b, off, len), "\n");
// 使用ByteString.of方法将字节数组转换为UTF-8字符串并打印
console.log(ByteString.of(b).utf8(), "\n");
// 调用原始的socketWrite0方法并返回结果
return this.socketWrite0(fd, b, off, len);
};
});
// frida -UF -l 1.hook.js
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
// frida -D 10.10.10.200:5555 -f net.demo2.netd10_ssl01 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_JavaHook.js'
// (.venv) calleng@tuxedo-os:~/p9/Mikrom2.0/season5/day36 抓包-服务端证书$ frida -D 10.10.10.200:5555 -f net.demo2.netd10_ssl01 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_JavaHook.js'
// ____
// / _ | Frida 16.1.3 - A world-class dynamic instrumentation toolkit
// | (_| |
// > _ | Commands:
// /_/ |_| help -> Displays the help system
// . . . . object? -> Display information about 'object'
// . . . . exit/quit -> Exit
// . . . .
// . . . . More info at https://frida.re/docs/home/
// . . . .
// . . . . Connected to MI 8 (id=10.10.10.200:5555)
// Spawned `net.demo2.netd10_ssl01`. Resuming main thread!
// [MI 8::net.demo2.netd10_ssl01 ]-> 参数: java.io.FileDescriptor@8c550c7 71,69,84,32,47,105,110,100,,120,32,72,84,84,80,47,49,46,49,13,10,104,111,115,116,58,32,119,105,107,105,46,109,105,107,101,99,114,109,46,99,111,109,13,10,117,115,101,114,45,65,103,101,110,116,58,32,116,101,115,116,13,10,13,10 0 65
//
// 0x00000000 47 45 54 20 2F 69 6E 64 65 78 20 48 54 54 50 2F GET./index.HTTP/
// 0x00000010 31 2E 31 0D 0A 68 6F 73 74 3A 20 77 69 6B 69 2E 1.1..host:.wiki.
// 0x00000020 6D 69 6B 65 63 72 6D 2E 63 6F 6D 0D 0A 75 73 65 mikecrm.com..use
// 0x00000030 72 2D 41 67 65 6E 74 3A 20 74 65 73 74 0D 0A 0D r-Agent:.test...
// 0x00000040 0A .
//
// GET /index HTTP/1.1
// host: wiki.mikecrm.com
// user-Agent: test
http响应获取_实现
// 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)));
}
http响应获取_调用路径
package java.net;
class SocketInputStream extends FileInputStream {
private native int socketRead0(FileDescriptor fd,
byte b[], int off, int len,
int timeout)
throws IOException;
private int socketRead(FileDescriptor fd,
byte b[], int off, int len,
int timeout)
throws IOException {
return socketRead0(fd, b, off, len, timeout);
}
public int read(byte b[], int off, int length) throws IOException {
return read(b, off, length, impl.getTimeout());
}
int read(byte b[], int off, int length, int timeout) throws IOException {
int n;
// EOF already encountered
if (eof) {
return -1;
}
// connection reset
if (impl.isConnectionReset()) {
throw new SocketException("Connection reset");
}
// bounds check
if (length <= 0 || off < 0 || length > b.length - off) {
if (length == 0) {
return 0;
}
throw new ArrayIndexOutOfBoundsException("length == " + length
+ " off == " + off + " buffer length == " + b.length);
}
// acquire file descriptor and do the read
FileDescriptor fd = impl.acquireFD();
try {
// Android-added: Check BlockGuard policy in read().
BlockGuard.getThreadPolicy().onNetwork();
n = socketRead(fd, b, off, length, timeout);
if (n > 0) {
return n;
}
} catch (ConnectionResetException rstExc) {
impl.setConnectionReset();
} finally {
impl.releaseFD();
}
/*
* If we get here we are at EOF, the socket has been closed,
* or the connection has been reset.
*/
if (impl.isClosedOrPending()) {
throw new SocketException("Socket closed");
}
if (impl.isConnectionReset()) {
throw new SocketException("Connection reset");
}
eof = true;
return -1;
}
}
所以,底层发送的http响应的的数据就是执行 `java.net.SocketInputStream` 中的 `socketRead0`方法。
那么,如果我们对这个方法进行Hook,是不是就可以获取到所有http请求的数据了。
http响应获取_hook验证
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");
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
return res;
};
});
// frida -UF -l 2.hook.js
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
// frida -D 10.10.10.200:5555 -f net.demo2.netd10_ssl01 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_JavaHook_响应获取.js'
// (.venv) calleng@tuxedo-os:~/p9/Mikrom2.0/season5/day36 抓包-服务端证书$ frida -D 10.10.10.200:5555 -f net.demo2.netd10_ssl01 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_JavaHook_响应获取.js'
// ____
// / _ | Frida 16.1.3 - A world-class dynamic instrumentation toolkit
// | (_| |
// > _ | Commands:
// /_/ |_| help -> Displays the help system
// . . . . object? -> Display information about 'object'
// . . . . exit/quit -> Exit
// . . . .
// . . . . More info at https://frida.re/docs/home/
// . . . .
// . . . . Connected to MI 8 (id=10.10.10.200:5555)
// HTTP/1.1 200 OK
// Server: Werkzeug/3.1.3 Python/3.9.13
// Date: Thu, 23 Jan 2025 11:34:45 GMT
// Content-Type: application/json
// Content-Length: 34
// Connection: close
//
//
//
// 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 net.demo2.netd10_ssl01.MainActivity$2.run(MainActivity.java:73)
//
// {"code":1000,"message":"success"}
//
//
// 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 net.demo2.netd10_ssl01.MainActivity$2.run(MainActivity.java:73)
//
//
//
// 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 net.demo2.netd10_ssl01.MainActivity$2.run(MainActivity.java:73)
so级别
上述的请求和写入,都是在java层面进行hook的获取,实际上真正的请求会调用so层的代码,最终将请求发送过去。
底层调用路径_发送_接收
// ==========================================> 一个发送
package java.net;
class SocketOutputStream extends FileOutputStream {
private native void socketWrite0(FileDescriptor var1, byte[] var2, int var3, int var4) throws IOException; // 底层 0
}
//============================================> 一个接收
package java.net;
class SocketInputStream extends FileInputStream {
private native int socketRead0(FileDescriptor fd,byte b[], int off, int len,int timeout) throws IOException;
}
libopenjdk.so
`socketRead0`和`socketWrite0`
OkHttp3_底层_JavaHook__寻找socketRead0和socketWrite0的hook的脚本
Java.perform(function (){
const apiResolver = new ApiResolver('module');
apiResolver.enumerateMatches('exports:*!*socket*0*').forEach(function (v){
console.log(v.name);
});
});
// frida -D 10.10.10.200:5555 -f net.demo2.netd10_ssl01 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_JavaHook__socketRead0和socketWrite0的hook的脚本.js'
// frida -D 10.10.10.200:5555 -f net.demo2.netd10_ssl01 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_JavaHook__socketRd0和socketWrite0的hook的脚本.js's'
// ____
// / _ | Frida 16.1.3 - A world-class dynamic instrumentation toolkit
// | (_| |
// > _ | Commands:
// /_/ |_| help -> Displays the help system
// . . . . object? -> Display information about 'object'
// . . . . exit/quit -> Exit
// . . . .
// . . . . More info at https://frida.re/docs/home/
// . . . .
// . . . . Connected to MI 8 (id=10.10.10.200:5555)
// Spawned `net.demo2.netd10_ssl01`. Resuming main thread!
// [MI 8::net.demo2.netd10_ssl01 ]-> /apex/com.android.runtime/lib64/libopenjdk.so!SocketInputStream_socketRead0
// /apex/com.android.runtime/lib64/libopenjdk.so!Java_sun_nio_ch_Net_socket0
// /apex/com.android.runtime/lib64/libopenjdk.so!SocketOutputStream_socketWrite0
http请求和响应_hook
/apex/com.android.runtime/lib64/libopenjdk.so
Java.perform(function () { // 使用Frida的Java.perform函数在Java运行时中执行代码
const NET_Send = Module.getExportByName('libopenjdk.so', 'NET_Send'); // 获取libopenjdk.so中的NET_Send函数地址
const NET_Read = Module.getExportByName('libopenjdk.so', 'NET_Read'); // 获取libopenjdk.so中的NET_Read函数地址
Interceptor.attach(NET_Send, { // 拦截NET_Send函数
onEnter(args) { // 当进入NET_Send函数时执行
console.log('write call'); // 打印日志,表示进入NET_Send函数
//console.log(hexdump(args[1], {length: args[2].toInt32()})); // 打印内存十六进制数据(注释状态)
console.log(Memory.readByteArray(args[1], args[2].toInt32())); // 读取并打印内存中的字节数组
}
});
Interceptor.attach(NET_Read, { // 拦截NET_Read函数
onEnter(args) { // 当进入NET_Read函数时执行
console.log('read call'); // 打印日志,表示进入NET_Read函数
this.buf = args[1]; // 保存读取的缓冲区地址到this.buf
}, onLeave: function (retval) { // 当离开NET_Read函数时执行
retval |= 0; // 将retval强制转换为32位整数
if (retval <= 0) { // 如果返回值小于等于0,直接返回
return;
}
console.log(Memory.readByteArray(this.buf, retval)); // 读取并打印缓冲区中的数据
}
});
});
// frida -D 10.10.10.200:5555 -f net.demo2.netd10_ssl01 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_SoHook_LibOpenJDK_请求和响应.js'
// ____
// / _ | Frida 16.1.3 - A world-class dynamic instrumentation toolkit
// | (_| |
// > _ | Commands:
// /_/ |_| help -> Displays the help system
// . . . . object? -> Display information about 'object'
// . . . . exit/quit -> Exit
// . . . .
// . . . . More info at https://frida.re/docs/home/
// . . . .
// . . . . Connected to MI 8 (id=10.10.10.200:5555)
// Spawned `net.demo2.netd10_ssl01`. Resuming main thread!
// [MI 8::net.demo2.netd10_ssl01 ]->
// write call ============>
// 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
// 00000000 47 45 54 20 2f 69 6e 64 65 78 20 48 54 54 50 2f GET /index HTTP/
// 00000010 31 2e 31 0d 0a 68 6f 73 74 3a 20 77 69 6b 69 2e 1.1..host: wiki.
// 00000020 6d 69 6b 65 63 72 6d 2e 63 6f 6d 0d 0a 75 73 65 mikecrm.com..use
// 00000030 72 2d 41 67 65 6e 74 3a 20 74 65 73 74 0d 0a 0d r-Agent: test...
// 00000040 0a .
// read call ============>
// 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
// 00000000 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d HTTP/1.1 200 OK.
// 00000010 0a 53 65 72 76 65 72 3a 20 57 65 72 6b 7a 65 75 .Server: Werkzeu
// 00000020 67 2f 33 2e 31 2e 33 20 50 79 74 68 6f 6e 2f 33 g/3.1.3 Python/3
// 00000030 2e 39 2e 31 33 0d 0a 44 61 74 65 3a 20 54 68 75 .9.13..Date: Thu
// 00000040 2c 20 32 33 20 4a 61 6e 20 32 30 32 35 20 31 32 , 23 Jan 2025 12
// 00000050 3a 31 31 3a 31 31 20 47 4d 54 0d 0a 43 6f 6e 74 :11:11 GMT..Cont
// 00000060 65 6e 74 2d 54 79 70 65 3a 20 61 70 70 6c 69 63 ent-Type: applic
// 00000070 61 74 69 6f 6e 2f 6a 73 6f 6e 0d 0a 43 6f 6e 74 ation/json..Cont
// 00000080 65 6e 74 2d 4c 65 6e 67 74 68 3a 20 33 34 0d 0a ent-Length: 34..
// 00000090 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 63 6c 6f 73 Connection: clos
// 000000a0 65 0d 0a 0d 0a e....
// read call ============>
// 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
// 00000000 7b 22 63 6f 64 65 22 3a 31 30 30 30 2c 22 6d 65 {"code":1000,"me
// 00000010 73 73 61 67 65 22 3a 22 73 75 63 63 65 73 73 22 ssage":"success"
// 00000020 7d 0a }.
// read call ============>
SSL请求的本质
SSL请求DEMO
详见示例:net.demo2.netd11_ssl02
public class MainActivity extends AppCompatActivity {
private Button btn1; // 定义一个按钮
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this); // 启用边到边显示
setContentView(R.layout.activity_main);// 设置布局文件
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {// 处理窗口内边距, 适应系统栏
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 初始化按钮
btn1 = findViewById(R.id.btn1); //通过 ID 找到按钮
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doRequest();
}
});
}
private void doRequest() {
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/actual/?limit=1&offset=0&category_id=9999 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();
}
}
SSL发送_接收_路径 A
// com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());
Log.e("outputStream的类 => ", outputStream.getClass().getName());
调用路径_[请求写入]_1_Step_A , org.conscrypt.ConscryptFileDescriptorSocket.$SSLOutputStream.$write
调用路径_[响应获取]_2_Step_A, org.conscrypt.ConscryptFileDescriptorSocket.$SSLInputStream.$read
安卓源码地址:[updated]
http://xrefandroid.com/android-10.0.0_r47/xref/external/conscrypt/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java
/*
* Copyright (C) 2017 The Android Open Source Project
*/
package org.conscrypt;
class ConscryptFileDescriptorSocket extends OpenSSLSocketImpl
implements NativeCrypto.SSLHandshakeCallbacks, SSLParametersImpl.AliasChooser,
SSLParametersImpl.PSKCallbacks {
/**
* This inner class provides input data stream functionality
* for the OpenSSL native implementation. It is used to
* read data received via SSL protocol.
*/
private class SSLInputStream extends InputStream {
/**
* OpenSSL only lets one thread read at a time, so this is used to
* make sure we serialize callers of SSL_read. Thread is already
* expected to have completed handshaking.
*/
private final Object readLock = new Object();
SSLInputStream() {
}
/**
* Reads one byte. If there is no data in the underlying buffer,
* this operation can block until the data will be
* available.
*/
@Override
public int read() throws IOException {
byte[] buffer = new byte[1];
int result = read(buffer, 0, 1);
return (result != -1) ? buffer[0] & 0xff : -1;
}
/**
* Method acts as described in spec for superclass.
* @see java.io.InputStream#read(byte[],int,int)
*/
@Override
public int read(byte[] buf, int offset, int byteCount) throws IOException {
Platform.blockGuardOnNetwork();
checkOpen();
ArrayUtils.checkOffsetAndCount(buf.length, offset, byteCount);
if (byteCount == 0) {
return 0;
}
synchronized (readLock) {
synchronized (ssl) {
if (state == STATE_CLOSED) {
throw new SocketException("socket is closed");
}
if (DBG_STATE) {
assertReadableOrWriteableState();
}
}
int ret = ssl.read(
Platform.getFileDescriptor(socket), buf, offset, byteCount, getSoTimeout());
if (ret == -1) {
synchronized (ssl) {
if (state == STATE_CLOSED) {
throw new SocketException("socket is closed");
}
}
}
return ret;
}
}
void awaitPendingOps() {
if (DBG_STATE) {
synchronized (ssl) {
if (state != STATE_CLOSED) {
throw new AssertionError("State is: " + state);
}
}
}
synchronized (readLock) {}
}
}
/**
* This inner class provides output data stream functionality
* for the OpenSSL native implementation. It is used to
* write data according to the encryption parameters given in SSL context.
*/
private class SSLOutputStream extends OutputStream {
/**
* OpenSSL only lets one thread write at a time, so this is used
* to make sure we serialize callers of SSL_write. Thread is
* already expected to have completed handshaking.
*/
private final Object writeLock = new Object();
SSLOutputStream() {
}
/**
* Method acts as described in spec for superclass.
* @see java.io.OutputStream#write(int)
*/
@Override
public void write(int oneByte) throws IOException {
byte[] buffer = new byte[1];
buffer[0] = (byte) (oneByte & 0xff);
write(buffer);
}
/**
* Method acts as described in spec for superclass.
* @see java.io.OutputStream#write(byte[],int,int)
*/
@Override
public void write(byte[] buf, int offset, int byteCount) throws IOException {
Platform.blockGuardOnNetwork();
checkOpen();
ArrayUtils.checkOffsetAndCount(buf.length, offset, byteCount);
if (byteCount == 0) {
return;
}
synchronized (writeLock) {
synchronized (ssl) {
if (state == STATE_CLOSED) {
throw new SocketException("socket is closed");
}
if (DBG_STATE) {
assertReadableOrWriteableState();
}
}
ssl.write(Platform.getFileDescriptor(socket), buf, offset, byteCount,
writeTimeoutMilliseconds);
synchronized (ssl) {
if (state == STATE_CLOSED) {
throw new SocketException("socket is closed");
}
}
}
}
void awaitPendingOps() {
if (DBG_STATE) {
synchronized (ssl) {
if (state != STATE_CLOSED) {
throw new AssertionError("State is: " + state);
}
}
}
synchronized (writeLock) {}
}
}
}
SSL发送_接收_路径 B
调用路径_[请求写入]_1_Step_B, org.conscrypt.NativeSsl.$write
调用路径_[响应获取]_2_Step_B, org.conscrypt.NativeSsl.$read
http://xrefandroid.com/android-10.0.0_r47/xref/external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/NativeSsl.java
/*
* Copyright (C) 2017 The Android Open Source Project
*/
package org.conscrypt;
/**
* A utility wrapper that abstracts operations on the underlying native SSL instance.
*/
final class NativeSsl {
// TODO(nathanmittler): Remove once after we switch to the engine socket.
int read(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
throws IOException {
lock.readLock().lock();
try {
if (isClosed() || fd == null || !fd.valid()) {
throw new SocketException("Socket is closed");
}
return NativeCrypto
.SSL_read(ssl, this, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
} finally {
lock.readLock().unlock();
}
}
// TODO(nathanmittler): Remove once after we switch to the engine socket.
void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
throws IOException {
lock.readLock().lock();
try {
if (isClosed() || fd == null || !fd.valid()) {
throw new SocketException("Socket is closed");
}
NativeCrypto
.SSL_write(ssl, this, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
} finally {
lock.readLock().unlock();
}
}
}
SSL发送_接收_路径 C
调用路径_[请求写入]_1_Step_C, org.conscrypt.NativeCrypto.$SSL_write
调用路径_[响应获取]_2_Step_C, org.conscrypt.NativeCrypto.$SSL_read
http://xrefandroid.com/android-10.0.0_r47/xref/external/conscrypt/common/src/main/java/org/conscrypt/NativeCrypto.java
/*
* Copyright (C) 2008 The Android Open Source Project
*/
package org.conscrypt;
@Internal
public final class NativeCrypto {
// --- OpenSSL library initialization --------------------------------------
/**
* Reads with the native SSL_read function from the encrypted data stream
* @return -1 if error or the end of the stream is reached.
*/
static native int SSL_read(long ssl, NativeSsl ssl_holder, FileDescriptor fd, SSLHandshakeCallbacks shc,
byte[] b, int off, int len, int readTimeoutMillis) throws IOException;
/**
* Writes with the native SSL_write function to the encrypted data stream.
*/
static native void SSL_write(long ssl, NativeSsl ssl_holder, FileDescriptor fd,
SSLHandshakeCallbacks shc, byte[] b, int off, int len, int writeTimeoutMillis)
throws IOException;
}
SSL发送路径_调用栈_验证
frida -D 10.10.10.200:5555 -f net.demo2.netd11_ssl02 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_SSL_Java_[SSL_write].js'
Java.perform(function () { // 使用Frida的Java.perform方法,确保代码在Java上下文中执行
var NativeCrypto = Java.use('com.android.org.conscrypt.NativeCrypto'); // 获取NativeCrypto类的引用,用于操作加密相关功能
var HexDump = Java.use("com.android.internal.util.HexDump"); // 获取HexDump类的引用,用于将字节数据转换为十六进制字符串
var ByteString = Java.use("com.android.okhttp.okio.ByteString"); // 获取ByteString类的引用,用于处理字节数据
NativeCrypto.SSL_write.implementation = function (ssl, ssl_holder, fd, shc, b, off, len, timeout) { // 重写NativeCrypto.SSL_write方法的实现
// ssl: 指向SSL对象的指针,表示当前的SSL连接
// ssl_holder: SSL对象的持有者,用于管理SSL状态
// fd: 文件描述符,表示底层的网络套接字
// shc: SSL握手上下文,用于管理SSL握手过程
// b: 字节数组,表示要写入的数据
// off: 数据在字节数组中的起始偏移量
// len: 要写入的数据长度
// timeout: 写入操作的超时时间
console.log(HexDump.dumpHexString(b, off, len), "\n") // 打印字节数组的十六进制表示(注释掉)
console.log(ByteString.of(b).utf8(), "\n"); // 将字节数组转换为UTF-8字符串并打印
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); // 打印当前堆栈信息(注释掉)
return this.SSL_write(ssl, ssl_holder, fd, shc, b, off, len, timeout); // 调用原始的SSL_write方法并返回结果
};
});
// (.venv) calleng@tuxedo-os:~/p9/Mikrom2.0/season5/day36 抓包-服务端证书$ frida -D 10.10.10.200:5555 -f net.demo2.netd11_ssl02 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_SSL_Java_[SSL_write].js'
// ____
// / _ | Frida 16.1.3 - A world-class dynamic instrumentation toolkit
// | (_| |
// > _ | Commands:
// /_/ |_| help -> Displays the help system
// . . . . object? -> Display information about 'object'
// . . . . exit/quit -> Exit
// . . . .
// . . . . More info at https://frida.re/docs/home/
// . . . .
// . . . . Connected to MI 8 (id=10.10.10.200:5555)
// Spawned `net.demo2.netd11_ssl02`. Resuming main thread!
// [MI 8::net.demo2.netd11_ssl02 ]->
// 0x00000000 47 45 54 20 2F 61 70 69 2F 76 31 2F 63 6F 75 72 GET./api/v1/cour
// 0x00000010 73 65 2F 61 63 74 75 61 6C 2F 3F 6C 69 6D 69 74 se/actual/?limit
// 0x00000020 3D 31 26 6F 66 66 73 65 74 3D 30 26 63 61 74 65 =1&offset=0&cate
// 0x00000030 67 6F 72 79 5F 69 64 3D 39 39 39 39 20 48 54 54 gory_id=9999.HTT
// 0x00000040 50 2F 31 2E 31 0D 0A 68 6F 73 74 3A 20 61 70 69 P/1.1..host:.api
// 0x00000050 2E 6C 75 66 66 79 63 69 74 79 2E 63 6F 6D 0D 0A .luffycity.com..
// 0x00000060 75 73 65 72 2D 41 67 65 6E 74 3A 20 74 65 73 74 user-Agent:.test
// 0x00000070 0D 0A 0D 0A ....
//
// GET /api/v1/course/actual/?limit=1&offset=0&category_id=9999 HTTP/1.1
// host: api.luffycity.com
// user-Agent: test
//
//
//
// java.lang.Throwable // 表示异常的根类,所有错误和异常的基类
// at com.android.org.conscrypt.NativeCrypto.SSL_write(Native Method) // 调用本地方法SSL_write,用于加密数据写入
// at com.android.org.conscrypt.NativeSsl.write(NativeSsl.java:426) // 调用NativeSsl的write方法,进一步处理加密数据写入
// at com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write(ConscryptFileDescriptorSocket.java:626) // 调用SSLOutputStream的write方法,将数据写入加密套接字
// at java.io.OutputStream.write(OutputStream.java:75) // 调用OutputStream的write方法,写入数据到输出流
// at net.demo2.netd11_ssl02.MainActivity$2.run(MainActivity.java:65) // 在MainActivity的第55行,线程执行时发生异常
SSL接收路径_调用栈_验证
frida -D 10.10.10.200:5555 -f net.demo2.netd11_ssl02 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_SSL_Java_[SSL_read].js'
Java.perform(function () { // 使用Frida的Java.perform方法,确保代码在Java上下文中执行
var NativeCrypto = Java.use('com.android.org.conscrypt.NativeCrypto'); // 获取NativeCrypto类的引用,用于操作加密相关功能
var HexDump = Java.use("com.android.internal.util.HexDump"); // 获取HexDump类的引用,用于将字节数据转换为十六进制字符串
var ByteString = Java.use("com.android.okhttp.okio.ByteString"); // 获取ByteString类的引用,用于处理字节数据
NativeCrypto.SSL_read.implementation = function (ssl, ssl_holder, fd, shc, b, off, len, timeout) { // 重写NativeCrypto.SSL_read方法的实现
// ssl: 指向SSL对象的指针,表示当前的SSL连接
// ssl_holder: SSL对象的持有者,用于管理SSL状态
// fd: 文件描述符,表示底层的网络套接字
// shc: SSL握手上下文,用于管理SSL握手过程
// b: 字节数组,表示存储读取数据的缓冲区
// off: 数据在字节数组中的起始偏移量
// len: 要读取的数据长度
// timeout: 读取操作的超时时间
var res = this.SSL_read(ssl, ssl_holder, fd, shc, b, off, len, timeout); // 调用原始的SSL_read方法并获取返回值(实际读取的字节数)
//console.log(HexDump.dumpHexString(b, off, len), "\n") // 打印字节数组的十六进制表示(注释掉)
console.log(ByteString.of(b).utf8(), "\n"); // 将字节数组转换为UTF-8字符串并打印
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); // 打印当前堆栈信息(注释掉)
return res; // 返回SSL_read方法的原始返回值
};
});
// frida -UF -l 4.hook.js // 使用Frida命令行工具附加到目标应用并加载脚本
// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); // 打印当前堆栈信息(注释掉)
// frida -D 10.10.10.200:5555 -f net.demo2.netd11_ssl02 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_SSL_Java_[SSL_read].js'
// ____
// / _ | Frida 16.1.3 - A world-class dynamic instrumentation toolkit
// | (_| |
// > _ | Commands:
// /_/ |_| help -> Displays the help system
// . . . . object? -> Display information about 'object'
// . . . . exit/quit -> Exit
// . . . .
// . . . . More info at https://frida.re/docs/home/
// . . . .
// . . . . Connected to MI 8 (id=10.10.10.200:5555)
// Spawned `net.demo2.netd11_ssl02`. Resuming main thread!
// [MI 8::net.demo2.netd11_ssl02 ]-> HTTP/1.1 200 OK
// Date: Thu, 23 Jan 2025 14:40:11 GMT
// Content-Type: application/json
// Content-Length: 69
// Connection: keep-alive
// Alt-Svc: h3=":443"; ma=86400
// allow: GET, HEAD, OPTIONS
// vary: Cookie, Origin
// x-frame-options: DENY
// x-content-type-options: nosniff
// referrer-policy: same-origin
// Server: nginx
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Methods: *
// Access-Control-Allow-Headers: *
// Access-Control-Expose-Headers: Content-Length, Content-Range
// Access-Control-Allow-Credentials: true
// Access-Control-Max-Age: 1728000
//
// {"code":0,"data":{"count":0,"next":null,"previous":null,"result":[]}}
//
// 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 net.demo2.netd11_ssl02.MainActivity$2.run(MainActivity.java:75)
so级别
上述的请求和写入,都是在java层面进行hook的获取,实际上真正的请求会调用so层的代码,最终将请求发送过去。
/*
* Copyright (C) 2008 The Android Open Source Project
*/
package org.conscrypt;
@Internal
public final class NativeCrypto {
// --- OpenSSL library initialization --------------------------------------
/**
* Reads with the native SSL_read function from the encrypted data stream
* @return -1 if error or the end of the stream is reached.
*/
static native int SSL_read(long ssl, NativeSsl ssl_holder, FileDescriptor fd, SSLHandshakeCallbacks shc,
byte[] b, int off, int len, int readTimeoutMillis) throws IOException;
/**
* Writes with the native SSL_write function to the encrypted data stream.
*/
static native void SSL_write(long ssl, NativeSsl ssl_holder, FileDescriptor fd,
SSLHandshakeCallbacks shc, byte[] b, int off, int len, int writeTimeoutMillis)
throws IOException;
}
libssl.so
一般情况下底层是基于openssl将https请求发送出去,而openssl基于的是libssl.so的文件。
`SSL_write`和`SSL_read`
Java.perform(function () { // 使用Frida的Java.perform方法,确保代码在Java上下文中执行
const apiResolver = new ApiResolver('module'); // 创建一个ApiResolver实例,用于解析模块中的符号(函数名)
// apiResolver: 用于查找模块中的导出函数(如libssl.so中的SSL_write和SSL_read)
// 包含:libttboringssl.so 或 libssl.so
// 'exports:*lib*ssl*!SSL_*'
// 匹配模式:查找所有导出函数名中包含"lib"、"ssl"和"SSL_"的函数
apiResolver.enumerateMatches('exports:*lib*ssl*!SSL_*').forEach(function (v) { // 遍历所有匹配的符号
// v: 包含符号信息的对象,包括函数名(v.name)和地址(v.address)
if (v.name.indexOf('SSL_write') > 0) { // 如果函数名包含"SSL_write"
// SSL_write = v.address; // 可以保存SSL_write函数的地址(注释掉)
console.log(v.name); // 打印函数名
} else if (v.name.indexOf('SSL_read') > 0) { // 如果函数名包含"SSL_read"
// SSL_read = v.address; // 可以保存SSL_read函数的地址(注释掉)
console.log(v.name); // 打印函数名
}
});
});
// (.venv) calleng@tuxedo-os:~/p9/Mikrom2.0/season5/day36 抓包-服务端证书$ frida -D 10.10.10.200:5555 -f net.demo2.netd11_ssl02 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_SSL_[libssl.so]_OpenSSL_寻找SSL_write和SSL_read的hook的脚本.js'
// ____
// / _ | Frida 16.1.3 - A world-class dynamic instrumentation toolkit
// | (_| |
// > _ | Commands:
// /_/ |_| help -> Displays the help system
// . . . . object? -> Display information about 'object'
// . . . . exit/quit -> Exit
// . . . .
// . . . . More info at https://frida.re/docs/home/
// . . . .
// . . . . Connected to MI 8 (id=10.10.10.200:5555)
// Spawned `net.demo2.netd11_ssl02`. Resuming main thread!
// [MI 8::net.demo2.netd11_ssl02 ]-> /apex/com.android.conscrypt/lib64/libssl.so!SSL_write
// /apex/com.android.conscrypt/lib64/libssl.so!SSL_read
// /apex/com.android.conscrypt/lib64/libssl.so!SSL_write
// /apex/com.android.conscrypt/lib64/libssl.so!SSL_read
SSL请求和响应的hook
frida -D 10.10.10.200:5555 -f net.demo2.netd11_ssl02 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_SSL_[libssl.so]_OpenSSL_得到请求和响应.js' // ____
Java.perform(function () { // 使用Frida的Java.perform方法,确保代码在Java上下文中执行
const SSL_write = Module.getExportByName('libssl.so', 'SSL_write'); // 获取libssl.so中SSL_write函数的地址
const SSL_read = Module.getExportByName('libssl.so', 'SSL_read'); // 获取libssl.so中SSL_read函数的地址
Interceptor.attach(SSL_write, { // Hook SSL_write函数
onEnter(args) { // 当SSL_write函数被调用时触发
console.log('write call'); // 打印日志,表示SSL_write被调用
//console.log(args[0]); // 打印第一个参数(ssl对象指针)
//console.log(args[1]); // 打印第二个参数(数据缓冲区的指针)
//console.log(args[2]); // 打印第三个参数(数据长度)
//console.log(hexdump(args[1], {length: args[2].toInt32()})); // 打印缓冲区的十六进制数据(注释掉)
console.log(Memory.readByteArray(args[1], parseInt(args[2]))); // 读取并打印缓冲区的字节数据
}
});
Interceptor.attach(SSL_read, { // Hook SSL_read函数
onEnter(args) { // 当SSL_read函数被调用时触发
console.log('read call'); // 打印日志,表示SSL_read被调用
this.buf = args[1]; // 保存第二个参数(数据缓冲区的指针)到this.buf
},
onLeave(retval) { // 当SSL_read函数返回时触发
retval |= 0; // 将返回值转换为32位整数
if (retval <= 0) { // 如果返回值小于等于0,表示读取失败或没有数据
return; // 直接返回
}
console.log(Memory.readByteArray(this.buf, retval)); // 读取并打印缓冲区的字节数据
}
});
});
// frida -D 10.10.10.200:5555 -f net.demo2.netd11_ssl02 -l '/home/calleng/p9/Mikrom2.0/JS_editor/OkHttp3_底层_SSL_[libssl.so]_OpenSSL_得到请求和响应.js'
// ____
// / _ | Frida 16.1.3 - A world-class dynamic instrumentation toolkit
// | (_| |
// > _ | Commands:
// /_/ |_| help -> Displays the help system
// . . . . object? -> Display information about 'object'
// . . . . exit/quit -> Exit
// . . . .
// . . . . More info at https://frida.re/docs/home/
// . . . .
// . . . . Connected to MI 8 (id=10.10.10.200:5555)
// Spawned `net.demo2.netd11_ssl02`. Resuming main thread!
// [MI 8::net.demo2.netd11_ssl02 ]-> write call
// 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
// 00000000 47 45 54 20 2f 61 70 69 2f 76 31 2f 63 6f 75 72 GET /api/v1/cour
// 00000010 73 65 2f 61 63 74 75 61 6c 2f 3f 6c 69 6d 69 74 se/actual/?limit
// 00000020 3d 31 26 6f 66 66 73 65 74 3d 30 26 63 61 74 65 =1&offset=0&cate
// 00000030 67 6f 72 79 5f 69 64 3d 39 39 39 39 20 48 54 54 gory_id=9999 HTT
// 00000040 50 2f 31 2e 31 0d 0a 68 6f 73 74 3a 20 61 70 69 P/1.1..host: api
// 00000050 2e 6c 75 66 66 79 63 69 74 79 2e 63 6f 6d 0d 0a .luffycity.com..
// 00000060 75 73 65 72 2d 41 67 65 6e 74 3a 20 74 65 73 74 user-Agent: test
// 00000070 0d 0a 0d 0a ....
// read call
// read call
// read call
// 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
// 00000000 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d HTTP/1.1 200 OK.
// 00000010 0a 44 61 74 65 3a 20 54 68 75 2c 20 32 33 20 4a .Date: Thu, 23 J
// 00000020 61 6e 20 32 30 32 35 20 31 34 3a 35 35 3a 34 34 an 2025 14:55:44
// 00000030 20 47 4d 54 0d 0a 43 6f 6e 74 65 6e 74 2d 54 79 GMT..Content-Ty
// 00000040 70 65 3a 20 61 70 70 6c 69 63 61 74 69 6f 6e 2f pe: application/
// 00000050 6a 73 6f 6e 0d 0a 43 6f 6e 74 65 6e 74 2d 4c 65 json..Content-Le
// 00000060 6e 67 74 68 3a 20 36 39 0d 0a 43 6f 6e 6e 65 63 ngth: 69..Connec
// 00000070 74 69 6f 6e 3a 20 6b 65 65 70 2d 61 6c 69 76 65 tion: keep-alive
// 00000080 0d 0a 41 6c 74 2d 53 76 63 3a 20 68 33 3d 22 3a ..Alt-Svc: h3=":
// 00000090 34 34 33 22 3b 20 6d 61 3d 38 36 34 30 30 0d 0a 443"; ma=86400..
// 000000a0 61 6c 6c 6f 77 3a 20 47 45 54 2c 20 48 45 41 44 allow: GET, HEAD
// 000000b0 2c 20 4f 50 54 49 4f 4e 53 0d 0a 76 61 72 79 3a , OPTIONS..vary:
// 000000c0 20 43 6f 6f 6b 69 65 2c 20 4f 72 69 67 69 6e 0d Cookie, Origin.
// 000000d0 0a 78 2d 66 72 61 6d 65 2d 6f 70 74 69 6f 6e 73 .x-frame-options
// 000000e0 3a 20 44 45 4e 59 0d 0a 78 2d 63 6f 6e 74 65 6e : DENY..x-conten
// 000000f0 74 2d 74 79 70 65 2d 6f 70 74 69 6f 6e 73 3a 20 t-type-options:
// 00000100 6e 6f 73 6e 69 66 66 0d 0a 72 65 66 65 72 72 65 nosniff..referre
// 00000110 72 2d 70 6f 6c 69 63 79 3a 20 73 61 6d 65 2d 6f r-policy: same-o
// 00000120 72 69 67 69 6e 0d 0a 53 65 72 76 65 72 3a 20 6e rigin..Server: n
// 00000130 67 69 6e 78 0d 0a 41 63 63 65 73 73 2d 43 6f 6e ginx..Access-Con
// 00000140 74 72 6f 6c 2d 41 6c 6c 6f 77 2d 4f 72 69 67 69 trol-Allow-Origi
// 00000150 6e 3a 20 2a 0d 0a 41 63 63 65 73 73 2d 43 6f 6e n: *..Access-Con
// 00000160 74 72 6f 6c 2d 41 6c 6c 6f 77 2d 4d 65 74 68 6f trol-Allow-Metho
// 00000170 64 73 3a 20 2a 0d 0a 41 63 63 65 73 73 2d 43 6f ds: *..Access-Co
// 00000180 6e 74 72 6f 6c 2d 41 6c 6c 6f 77 2d 48 65 61 64 ntrol-Allow-Head
// 00000190 65 72 73 3a 20 2a 0d 0a 41 63 63 65 73 73 2d 43 ers: *..Access-C
// 000001a0 6f 6e 74 72 6f 6c 2d 45 78 70 6f 73 65 2d 48 65 ontrol-Expose-He
// 000001b0 61 64 65 72 73 3a 20 43 6f 6e 74 65 6e 74 2d 4c aders: Content-L
// 000001c0 65 6e 67 74 68 2c 20 43 6f 6e 74 65 6e 74 2d 52 ength, Content-R
// 000001d0 61 6e 67 65 0d 0a 41 63 63 65 73 73 2d 43 6f 6e ange..Access-Con
// 000001e0 74 72 6f 6c 2d 41 6c 6c 6f 77 2d 43 72 65 64 65 trol-Allow-Crede
// 000001f0 6e 74 69 61 6c 73 3a 20 74 72 75 65 0d 0a 41 63 ntials: true..Ac
// 00000200 63 65 73 73 2d 43 6f 6e 74 72 6f 6c 2d 4d 61 78 cess-Control-Max
// 00000210 2d 41 67 65 3a 20 31 37 32 38 30 30 30 0d 0a 0d -Age: 1728000...
// 00000220 0a 7b 22 63 6f 64 65 22 3a 30 2c 22 64 61 74 61 .{"code":0,"data
// 00000230 22 3a 7b 22 63 6f 75 6e 74 22 3a 30 2c 22 6e 65 ":{"count":0,"ne
// 00000240 78 74 22 3a 6e 75 6c 6c 2c 22 70 72 65 76 69 6f xt":null,"previo
// 00000250 75 73 22 3a 6e 75 6c 6c 2c 22 72 65 73 75 6c 74 us":null,"result
// 00000260 22 3a 5b 5d 7d 7d ":[]}}
// read call
flutter的处理, 抓包
Quic 的抓包..
