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
- 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:
- Proxy + Router: How mitmproxy works
- Socks Proxy: SocksV4 RFC - SocksV5 RFC
- Transparent proxy: SSLsplit
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
The problem is handler doesn’t know the context of the connection and it can only figure out when the first packet arrived.
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
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
For me, there are at least 2 possible ways to do it:
- Use MSGPEEK flag to peek the first packet.
- Hijack SSL/TLS handshake process.
I’ve done this experiment in Python 3. Hardly depends on the latest openssl wrapper - pyOpenSSL
Here is an two examples of SSL server with
- Wrap the
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
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
linux socket support
MSGPEEK flag for
recv function, which will return the data without removing that data from the buffer.
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
# ... # 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
ClientHelloand 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
- 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
readat any state. There are several examples around Internet to demostrate this supreme feature:
It’s a little bit hardcore to do the same in
pyOpenSSLwrapper, so …
- I chose to test the method with
tlslite-ng, a pure SSL/TLS implementation in
- Recv the first packet by normal
- If the packet is
MyTLSConnectionclass to that
socketwith that data
- The different is that we modify method
RecordSocketto use our data instead of re-read from
- Recv the first packet by normal
# ... # 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
- Also trust
certdirectory of the Github Repo.
- Run web server
- You can now access both http://web.weirdsocket.com:9999/ and https://web.weirdsocket.com:9999/ with the same app
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! 🖖