网络抓包04 - SSLSocket
以 Android10 - OkHttp 的实现为例。
因为 OkHttp 的实现与Android的版本有关。
在Android 10 及以上,SSLSocket 的实现类是 Java8EngineSocket,这个打个断点就能看出来。
SSLSocket 的使用
SSLSocket 只是在 Socket 的基础上套了一层加密。所以,它的用法与 Socket 也差不多,看一个例子:
public static void sslSocket2() throws Exception {
SSLContext context = SSLContext.getInstance("SSL");
// 初始化
context.init(null,
new TrustManager[] { new Test2.MyX509TrustManager() },
new SecureRandom());
SSLSocketFactory factory = context.getSocketFactory();
SSLSocket s = (SSLSocket) factory.createSocket("localhost", 10002);
System.out.println("ok");
OutputStream output = s.getOutputStream();
InputStream input = s.getInputStream();
output.write("alert".getBytes());
System.out.println("sent: alert");
output.flush();
byte[] buf = new byte[1024];
int len = input.read(buf);
System.out.println("received:" + new String(buf, 0, len));
}
这里,我们使用 SSLSocket
发送了一个 alert
字符串。
server 端接收如下:
public static void sslSocketServer() throws Exception {
...
// 监听和接收客户端连接
SSLServerSocketFactory factory = context.getServerSocketFactory();
SSLServerSocket server = (SSLServerSocket) factory
.createServerSocket(10002);
System.out.println("ok");
Socket client = server.accept();
System.out.println(client.getRemoteSocketAddress());
// 向客户端发送接收到的字节序列
OutputStream output = client.getOutputStream();
// 当一个普通 socket 连接上来, 这里会抛出异常
// Exception in thread "main" javax.net.ssl.SSLException: Unrecognized
// SSL message, plaintext connection?
InputStream input = client.getInputStream();
byte[] buf = new byte[1024];
int len = input.read(buf);
System.out.println("received: " + new String(buf, 0, len));
output.write(buf, 0, len);
output.flush();
output.close();
input.close();
// 关闭socket连接
client.close();
server.close();
}
总的来说,还是通过流来传递数据。
我们需要注意的是,SSLSocket 往流里面写入或者读取数据的时候,它是明文。如果我们hook这个写入与读取的方法,是不是就可以拿到请求与相应的明文数据呢?
OKHTTP 的请求写入逻辑
OkHttp 使用 RealConnection
这个类来描述一个链接。
先创建一个 socket 对象,并且保存其输入输出流:
okhttp3.internal.connection.RealConnection#connectSocket
@Throws(IOException::class)
private fun connectSocket(
connectTimeout: Int,
readTimeout: Int,
call: Call,
eventListener: EventListener
) {
...
this.rawSocket = rawSocket
...
// The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
// More details:
// https://github.com/square/okhttp/issues/3245
// https://android-review.googlesource.com/#/c/271775/
try {
source = rawSocket.source().buffer()
sink = rawSocket.sink().buffer()
} catch (npe: NullPointerException) {
...
}
}
source
与 sink
是 OKIO
中的 api
:
@Throws(IOException::class)
fun Socket.source(): Source {
val timeout = SocketAsyncTimeout(this)
val source = InputStreamSource(getInputStream(), timeout)
return timeout.source(source)
}
@Throws(IOException::class)
fun Socket.sink(): Sink {
val timeout = SocketAsyncTimeout(this)
val sink = OutputStreamSink(getOutputStream(), timeout)
return timeout.sink(sink)
}
可以看到,source
是对 getInputStream
的包装,sink
是对 getOutputStream
的包装。
接下来链接 tls:
okhttp3.internal.connection.RealConnection#connectTls
@Throws(IOException::class)
private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) {
...
try {
// Create the wrapper over the connected socket.
sslSocket = sslSocketFactory!!.createSocket(
rawSocket, address.url.host, address.url.port, true /* autoClose */) as SSLSocket
...
// Force handshake. This can throw!
sslSocket.startHandshake()
// block for session establishment
val sslSocketSession = sslSocket.session
val unverifiedHandshake = sslSocketSession.handshake()
...
source = sslSocket.source().buffer()
sink = sslSocket.sink().buffer()
...
} finally {
...
}
}
这里面还会校验握手拿到的证书等逻辑,具体就不展开了。
我们看到 source
与 sink
被重新赋值了,sslSocket
就是对 rawSockt
的包装:
Java8EngineSocket(Socket socket, String hostname, int port, boolean autoClose,
SSLParametersImpl sslParameters) throws IOException {
super(socket, hostname, port, autoClose, sslParameters);
}
具体可看:
external/conscrypt/repackaged/common/src/main/java/com/android/org/conscrypt/AbstractConscryptSocket.java
SSLSocket数据加密逻辑分析
我们继续分析请求逻辑,看看数据是如何写出去的,以及数据是什么时候加密的。
请求发出去的逻辑类为 CallServerInterceptor
:
exchange.writeRequestHeaders(request)
codec.writeRequestHeaders(request)
writeRequest(request.headers, requestLine)
fun writeRequest(headers: Headers, requestLine: String) {
check(state == STATE_IDLE) { "state: $state" }
sink.writeUtf8(requestLine).writeUtf8("\r\n")
for (i in 0 until headers.size) {
sink.writeUtf8(headers.name(i))
.writeUtf8(": ")
.writeUtf8(headers.value(i))
.writeUtf8("\r\n")
}
sink.writeUtf8("\r\n")
state = STATE_OPEN_REQUEST_BODY
}
最终还是调用了 sink 的write方法,而这个 sink 就是 RealConnection 的 sink,具体传递链就不跟了。
我们跟一下,SSLSocket 的输出流的 write 方法。
external/conscrypt/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java
private final class SSLOutputStream extends OutputStream {
@Override
public void write(byte[] b, int off, int len) throws IOException {
waitForHandshake();
synchronized (writeLock) {
writeInternal(ByteBuffer.wrap(b, off, len));
}
}
}
writeInternal()
// buffer 是明文
engineResult = engine.wrap(buffer, target);
writeToSocket();
wrap 方法看注释:
Attempts to encode plaintext bytes from a subsequence of data buffers into SSL/TLS network data.
所以,writeToSocket 写的就是加密后的数据了。
继续跟一下 wrap 真正是实现会发现这样的一个方法:
public SSLEngineResult wrap(ByteBuffer[] srcs, int srcsOffset, int srcsLength, ByteBuffer dst)
throws SSLException {
// Write plaintext application data to the SSL engine
int result = writePlaintextData(outputBuffer, min(SSL3_RT_MAX_PLAIN_LENGTH, outputBuffer.remaining()));
}
最终会来到:
int writeDirectByteBuffer(long sourceAddress, int sourceLength) throws IOException {
lock.readLock().lock();
try {
return NativeCrypto.ENGINE_SSL_write_direct(
ssl, this, sourceAddress, sourceLength, handshakeCallbacks);
} finally {
lock.readLock().unlock();
}
}
这里有一个JNI调用,会调用到:
static int NativeCrypto_ENGINE_SSL_write_direct(JNIEnv* env, jclass, jlong ssl_address,
CONSCRYPT_UNUSED jobject ssl_holder, jlong address,
jint len, jobject shc) {}
而这个方法又会调用到:
int SSL_write(SSL *ssl, const void *buf, int num) {}
注意这里还是明文,而这个方法就是一些通杀库HOOK的点了。
数据处理
当我们Hook到了数据之后,由于它是一个二进制的流,并不方便我们直观的查看。
我们可以看一下别人是如何处理的:
https://github.com/r0ysue/r0capture
将Hook到的数据储存为 pcap 格式,然后使用 wireshark 打开。