Java层SSL通信抓包与溯源
http/https都是建立在tcp/udp之上应用层的协议,SSL/TLS又是HTTPS工作的一个基础,所以对socket进行字段溯源的时候绕不开的一个就是HTTPS协议。但是HTTPS是加密的,另外它中间还有证书验证,所以通过抓包工具无法直接抓到数据包。
在这篇文章中我们先尝试通过源码分析HTTPS的加密流程,获取hook点。通过FridaHook的方式抓到数据包。
这里同样写一个使用okhttp去访问https://www.baidu.com 的Demo App
环境:
环境跟前两篇文章用的不同,因为pixel XL的手机电池鼓包game over了,可能电脑连接的USB接口上电流不稳定+时间长没拔发热严重的原因。换了一台pixel 5来测试,虚拟机换了kali,因为安装的在Ubuntu上安装了新版Charles界面显示有点问题,还是新版kali更好看点
Android版本:11.0
虚拟机:kali 2023.4
那环境配置不再啰嗦,直接开始调试吧
因为这里都是多线程,所以没法一步一步的单步调试,只能下断点F9执行然后往断点上撞了:因为HTTP/HTTPS无论如何都是要经过socket执行的所以我们这里在connectSocket
都下了断点
分析这段代码可知
try { Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout); } catch (ConnectException e) { throw new ConnectException("Failed to connect to " + route.socketAddress()); }
- 使用前面创建的套接字来尝试连接目标地址
创建缓冲的输入和输出流,方便后续的读写操作
source = Okio.buffer(Okio.source(rawSocket)); // 创建一个带缓冲的输入流 sink = Okio.buffer(Okio.sink(rawSocket)); // 创建一个带缓冲的输出流
- 使用Okio库为套接字创建输入和输出流缓冲区,我们抓包的目的就是通过hook拿到输入(接收数据)和输出(发送数据),跟进Okio.java分析
分析Okio.java
中对输出流缓冲区的操作
- 这里进行了一些检查判断数据是否为空,不为空就调用
write
方法循环将数据从buffer写到OutputStream
中 - 之后调用
OutputStream
的flush
方法,确保所有数据都已写入底层流 - 所以我们等下直接去Android 11的源码中去找
java.net.SocketinputStream->write()
看怎么hook能获取到输出流
分析Okio.java
中对输入流缓冲区的操作
- 这里同样的使用
read
方法从InputStream
读取数据 - 同样去Android源码中找
java.net.SocketinputStream->read()
源码分析
不确定在哪个模块中,直接在Android11的源码中全局搜索
java.net.SocketinputStream->write()
- 进来之后直接找到了它最终调用的JNI函数
socketWrite0()
等下直接hook这个函数
java.net.SocketinputStream->read()
基本跟刚刚是一样的流程(代码太长了不好截图,直接贴了下来)
97 private native int socketRead0(FileDescriptor fd,
98 byte b[], int off, int len,
99 int timeout)
100 throws IOException;
101
。。。。。。
115 private int socketRead(FileDescriptor fd,
116 byte b[], int off, int len,
117 int timeout)
118 throws IOException {
119 return socketRead0(fd, b, off, len, timeout);
120 }
。。。。。。
147 int read(byte b[], int off, int length, int timeout) throws IOException {
148 int n;
149
150 // EOF already encountered
151 if (eof) {
152 return -1;
153 }
154
155 // connection reset
156 if (impl.isConnectionReset()) {
157 throw new SocketException("Connection reset");
158 }
159
160 // bounds check
161 if (length <= 0 || off < 0 || length > b.length - off) {
162 if (length == 0) {
163 return 0;
164 }
165 throw new ArrayIndexOutOfBoundsException("length == " + length
166 + " off == " + off + " buffer length == " + b.length);
167 }
168
169 boolean gotReset = false;
170
171 // acquire file descriptor and do the read
172 FileDescriptor fd = impl.acquireFD();
173 try {
174 // Android-added: Check BlockGuard policy in read().
175 BlockGuard.getThreadPolicy().onNetwork();
176 n = socketRead(fd, b, off, length, timeout); // 标记
177 if (n > 0) {
178 return n;
179 }
180 } catch (ConnectionResetException rstExc) {
181 gotReset = true;
182 } finally {
183 impl.releaseFD();
184 }
185
186 /*
187 * We receive a "connection reset" but there may be bytes still
188 * buffered on the socket
189 */
190 if (gotReset) {
191 impl.setConnectionResetPending();
192 impl.acquireFD();
193 try {
194 n = socketRead(fd, b, off, length, timeout); // 标记
195 if (n > 0) {
196 return n;
197 }
198 } catch (ConnectionResetException rstExc) {
199 } finally {
200 impl.releaseFD();
201 }
202 }
203
204 /*
205 * If we get here we are at EOF, the socket has been closed,
206 * or the connection has been reset.
207 */
208 if (impl.isClosedOrPending()) {
209 throw new SocketException("Socket closed");
210 }
211 if (impl.isConnectionResetPending()) {
212 impl.setConnectionReset();
213 }
214 if (impl.isConnectionReset()) {
215 throw new SocketException("Connection reset");
216 }
217 eof = true;
218 return -1;
219 }
FridaHook
hook点
// 发送数据
// java.net.SocketinputStream->socketRead0()
private native int socketRead0(FileDescriptor fd,
byte b[], int off, int len,
int timeout)
// 接收数据
// java.net.SocketinputStream->socketRead0()
private native void socketWrite0(FileDescriptor fd, byte[] b, int off,
int len) throws IOException;
- 最后发现这两个hook点跟我们之前Java层socket的TCP hook点是一样的
编写脚本
直接使用原来的脚本稍微修改一下就ok了
function LogPrint(log) {
var theDate = new Date();
var hour = theDate.getHours();
var minute = theDate.getMinutes();
var second = theDate.getSeconds();
var mSecond = theDate.getMilliseconds();
hour < 10 ? hour = "0" + hour : hour;
minute < 10 ? minute = "0" + minute : minute;
second < 10 ? second = "0" + second : second;
mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;
var time = hour + ":" + minute + ":" + second + ":" + mSecond;
var threadid = Process.getCurrentThreadId();
console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);
}
function printJavaStack(name) {
Java.perform(function () {
var Exception = Java.use("java.lang.Exception");
var ins = Exception.$new("Exception");
var straces = ins.getStackTrace();
if (straces != undefined && straces != null) {
var strace = straces.toString();
var replaceStr = strace.replace(/,/g, " \n ");
LogPrint("=============================" + name + " Stack strat=======================");
LogPrint(replaceStr);
LogPrint("=============================" + name + " Stack end======================= \n ");
Exception.$dispose();
}
});
}
// 对可打印字符区间进行一个判断只打印可打印字符
function isprintable(value) {
if (value >= 32 && value <= 126) {
return true
}
return false
}
function hookssl() {
Java.perform(function () {
var SocketInputStreamClass = Java.use('java.net.SocketInputStream')
// hook socketRead0()
SocketInputStreamClass.socketRead0.implementation = function (arg0, arg1, arg2, arg3, arg4) {
var size = this.socketRead0(arg0, arg1, arg2, arg3, arg4)
console.log("[" + Process.getCurrentThreadId() + "]socketRead0 > size: " + size + ",content: " + JSON.stringify(arg1))
var byteArray = Java.array("byte", arg1)
var content = '';
for (var i = 0; i < size; i++) {
if (isprintable(byteArray[i])) {
content = content + String.fromCharCode(byteArray[i])
}
}
var socketimpl = this.impl.value
var address = socketimpl.address.value
var port = socketimpl.port.value
console.log("\naddress:" + address + ",port: " + port + "\n" + JSON.stringify(this.socket.value) + "\n[" + Process.getCurrentThreadId() + "]receive:" + content);
printJavaStack('socketRead0()...')
return size;
}
var SocketOutputStreamClass = Java.use('java.net.SocketOutputStream')
// hook socketWrite0()
SocketOutputStreamClass.socketWrite0.implementation = function (arg0, arg1, arg2, arg3) {
var result = this.socketWrite0(arg0, arg1, arg2, arg3)
console.log("[" + Process.getCurrentThreadId() + "]socketWrite0 > result: " + arg3 + "--content: " + JSON.stringify(arg1))
var byteArray = Java.array("byte", arg1)
var content = '';
for (var i = 0; i < arg3; i++) {
if (isprintable(byteArray[i])) {
content = content + String.fromCharCode(byteArray[i])
}
}
var socketimpl = this.impl.value
var address = socketimpl.address.value
var port = socketimpl.port.value
// console.log("[" + Process.getCurrentThreadId() + "]send: " + content)
console.log("send address:" + address + ",port: " + port + "[" + Process.getCurrentThreadId() + "]send:" + content);
console.log("\n" + JSON.stringify(this.socket.value));
printJavaStack('socketWrite0()...')
return result;
}
})
}
function main() {
hookssl()
}
setImmediate(main)
执行hook
frida -UF -l hookSSL.js -o OkhttpDemo.log
- 这里成功的抓到了数据包,也把调用的堆栈打印了出来
对其他App进行抓包
滴答清单
Frida也支持对多进程App进行抓包:UC浏览器
frida -U -p 进程编号 -l hookSSL.js
后记:
分析后对照源码发现8.0的SSL流程和8.0以后的SSL流程完全不一样,8.0最后的JNI函数分别是
com.android.org.conscrypt.NativeCrypto.SSL_read
com.android.org.conscrypt.NativeCrypto.SSL_write
附件:
- 标题: Java层SSL通信抓包与溯源
- 作者: xiaoeryu
- 创建于 : 2024-06-06 22:06:18
- 更新于 : 2024-06-09 17:29:18
- 链接: https://github.com/xiaoeryu/2024/06/06/Java层SSL通信抓包与溯源/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。