👊👊👊 Brilliant source code: WeirdSocket

While working in a network project, I face a task where we need to sniff/process traffic in plain data and in secure channel. With some experiences in the several solutions in the past, also thanks for many years of using BurpSuite and mitmproxy, I understand the process model (A) to solve it is quite simple

/assets/images/weirdsocket/man-in-the-middle-attack.svg

  • A1: a man-in-the-middle handler (rogue router, http/https/socks proxy, transparent endpoint, …) will somehow takeover the connection from client
  • A2: (optional) it will process the data (packet/request) based on predefined protocol
  • A3: after its decision to do with the protocol, the processed packet will be forwarded to destination node.

It is the general flow model for mitm traffic - active mode (for passive mode, you aren’t gonna find out in this article 😅). In each scenarios, where handler is diffrent type of proxy or rogue router, the implementation will follow specific protocol to do its job.

I’m not gonna reinvent the wheel for anything, or explain how fancy they are in technical detail. Here are some articles/references for them:

The traffic sniffing can be done at A2 step. At this phase, there are 2 possible type of traffic:

  • Plain data
  • SSL/TLS data in secure channel - lets call them SSL in general

We only have to deal with SSL data, which is ofc, already solved in another model (B) too:

  • B1: on the first initial packet/request of client, handler will detect if it is SSL traffic.
  • B2: handler generates a fake certificate for that connection with our trusted rootCA.
  • B3a: The socket context will be switch to SSL with our generated certificate.
  • B3b: Meanwhile handler also does SSL connection to destination node.
  • B4: We have 2 successed connection now, packet will be forwarded as normal.

Assume client trusts our rootCA, and doesn’t use any certificate pinning mechanism !

So basically, the connection between client and handler is dynamic, it can be converted into SSL channel when needed, and supports both type of connection. I call that socket as WeirdSocket.

The problem is handler doesn’t know the context of the connection and it can only figure out when the first packet arrived.

/assets/images/weirdsocket/Port80clienthello.png

If it is plain data, nothing to worry about. But if it is ClientHello packet, it means client wants to use SSL context, and in that case, we should be in the middle of SSL handshake process now.

Here is the whole handshake process of TLS 1.2

/assets/images/weirdsocket/SSL_Handshake_10-Steps-1.png

Awesome illustrated for this process 👉 https://tls.ulfheim.net/

As the server role, what we should do now is resuming the SSL handshake process - starting from (2) sending ServerHello, …

For me, there are at least 2 possible ways to do it:

  1. Use MSGPEEK flag to peek the first packet.
  2. Hijack SSL/TLS handshake process.

I’ve done this experiment in Python 3. Hardly depends on the latest openssl wrapper - pyOpenSSL

MSGPEEK

Here is an two examples of SSL server with pyOpenSSL

  • Wrap the listener socket
import socket
from pyOpenSSL import SSL

ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.use_privatekey_file ("cert/my.key")
ctx.use_certificate_file("cert/my.crt")

server = SSL.Connection(ctx, socket.socket(socket.AF_INET, socket.SOCK_STREAM))
server.bind(("localhost", 9999))
server.listen(3)

client, addr = server.accept() # Handshake already
client.send("Hello from SSL channel")
  • Wrap the client socket
import socket
from pyOpenSSL import SSL

ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.use_privatekey_file ("cert/my.key")
ctx.use_certificate_file("cert/my.crt")

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("localhost", 9999))
server.listen(3)

client_raw, addr = server.accept()
client = SSL.Connection(ctx, client_raw)
client.set_accept_state() # Act as server
client.do_handshake() # Start the handshake

client.send("Hello from SSL channel")

When a SSL socket server does handshake, it would wait to receive ClientHello packet from client, and then do its work.

If we already read the first packet, detect ClientHello request (by bytes header 16 03), and then do handshake as normal, OpenSSL context variable doesn’t know about it, it will wait to receive another ClientHello from socket.

We wish to put back read packet data into socket buffer again. It’s difficult task, and from my searching there is no possible way to do it for most type of modern socket.

Luckily, we still have an alternative solution for that. Both winsock and linux socket support MSGPEEK flag for recv function, which will return the data without removing that data from the buffer.

Ref

So we don’t have to mess with OpenSSL or any SSL/TLS library to do the context converting, and this is easy to implement in any programming language not just Python

# ...

# recv first packet in peek mode
data = conn.recv(BUFFER_SIZE, socket.MSG_PEEK)

# check if ssl context
if data.startswith(b"\x16\x03"):
    print(">> Detect ssl request")

    # ssl
    ctx = SSL.Context(SSL.SSLv23_METHOD)
    ctx.use_privatekey_file(KEY)
    ctx.use_certificate_file(CERT)
    sock = SSL.Connection(ctx, conn)

    sock.set_accept_state()
    try:
        sock.do_handshake()
    except:
        print(">> Failed to switch ssl")
    else:
        print(">> Upgraded")
else:
    pass

# ...

You can check out other examples in Github Repo above.

I also believe that mitmproxy is currently using this method to do the same task.

Hijack TLS handshake

As described in the above session, we wish to create a SSL context which we could put back our received data in, and resume in the middle of that.

How to do ?

  • If you are good and confident enough, you can parse the data from ClientHello and simulate your own TLS handshake process
    • Generate server TLS session
    • Do key exchange
    • Do decryption/encryption
    • Handle error
    • Create a socket wrapper layer for send/recv data
    • Support multiple TLS version?

    It would be a fun exercise to understand SSL/TLS. I suggest you check out these packages in Python

  • A less brave method, OpenSSL supports us a context from BIO_memory. It’s like a virtual file descriptor (which socket is a kind of, in linux), where we could write / read at any state. There are several examples around Internet to demostrate this supreme feature:

    It’s a little bit hardcore to do the same in pyOpenSSL wrapper, so …

  • I chose to test the method with tlslite-ng, a pure SSL/TLS implementation in Python, supports SSLv3, TLSv1.0, TLSv1.1, TLSv1.2 and TLSv1.3
    • Recv the first packet by normal socket
    • If the packet is ClientHello, wrap MyTLSConnection class to that socket with that data
    • Do handshakeServer as normal
    • The different is that we modify method recv of RecordSocket to use our data instead of re-read from socket
# ...

# TLSConnection -> _recordLayer (RecordLayer) -> _recordSocket (RecordSocket)
class MyRecordSocket(RecordSocket):
    def __init__(self, sock, data):
        super().__init__(sock)
        self._amazingData = data
        self._startOfSomethingAmazing = True # first packet

    def recv(self):
        # check if it is ClientHello
        if self._startOfSomethingAmazing:
            self._startOfSomethingAmazing = False
            # assume it is SSL3 header, who cares about SSL2 ?
            record = RecordHeader3().parse(Parser(self._amazingData))
            return (record, self._amazingData[5:]), 
        else:
            return super().recv()


class MyRecordLayer(RecordLayer):
    def __init__(self, sock, data):
        super().__init__(sock)
        self._recordSocket = MyRecordSocket(sock, data)


# Wrapper with our recv data
class MyTLSConnection(TLSConnection):
    def __init__(self, sock, data):
        super().__init__(sock)
        self._recordLayer = MyRecordLayer(sock, data)

# ...

Please check out example server_tlslite_once.py in Github Repo


Fun🙃demo

  • Add web.weirdsocket.com as 127.0.0.1 in your hosts file.

/assets/images/weirdsocket/Capturex.png

/assets/images/weirdsocket/Capturex2.png


Conclusion

Just a little experiment seems very simple, but not really easy to find out the answer for who has zero knowledge about it like me 😬. Hope you guys enjoy!

Feel free to fix me below. Thanks! 🖖