网络抓包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 打开。

SSL双向认证是一种安全机制,可以在客户端和服务器之间建立安全通信。在应用程序开发和爬虫抓包中,也可以使用SSL双向认证来加强安全性。以下是两种常见的方法: 1. 使用证书文件 在应用程序和爬虫中,可以使用证书文件来进行SSL双向认证。客户端和服务器之间交互时,需要客户端向服务器发送证书(通常是PEM格式的文件),证明自己的身份。服务器会验证证书的合法性,如果通过验证,就可以建立安全连接。在Python中,可以使用requests库来实现SSL双向认证。示例代码如下: ```python import requests # 客户端证书文件路径 client_cert = '/path/to/client/cert.pem' # 客户端私钥文件路径 client_key = '/path/to/client/key.pem' # 服务器证书文件路径 server_cert = '/path/to/server/cert.pem' # SSL双向认证 response = requests.get(url, cert=(client_cert, client_key), verify=server_cert) ``` 2. 使用OpenSSL工具 另一种常见的方法是使用OpenSSL工具进行SSL双向认证。在Linux和Mac系统中,可以使用openssl命令来生成证书和私钥文件。在Windows系统中,可以下载OpenSSL工具并安装。生成证书和私钥文件后,可以在应用程序和爬虫中使用。示例代码如下: ```python import subprocess # 生成客户端证书和私钥文件 subprocess.run(['openssl', 'req', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'client.key', '-x509', '-days', '365', '-out', 'client.crt', '-subj', '/CN=client']) # 生成服务器证书和私钥文件 subprocess.run(['openssl', 'req', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'server.key', '-x509', '-days', '365', '-out', 'server.crt', '-subj', '/CN=server']) ``` 在应用程序中,可以使用SSLContext对象来进行SSL双向认证。示例代码如下: ```python import ssl import socket # 客户端证书和私钥文件路径 client_cert = '/path/to/client/cert.pem' client_key = '/path/to/client/key.pem' # 服务器证书文件路径 server_cert = '/path/to/server/cert.pem' # SSLContext对象 context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) context.load_cert_chain(certfile=client_cert, keyfile=client_key) context.load_verify_locations(cafile=server_cert) # 建立SSL连接 with socket.create_connection((host, port)) as sock: with context.wrap_socket(sock, server_hostname=host) as ssock: ssock.sendall(b'Hello, world!') ``` 以上是两种常见的SSL双向认证方法,可以根据实际需求选择适合自己的方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值