This article is currently an experimental machine translation and may contain errors. If anything is unclear, please refer to the original Chinese version. I am continuously working to improve the translation.
Since the official SSL VPN provided by Nanjing University is notoriously hard to use—and thanks to my personal bout of OCD—I decided to reverse-engineer its protocol and reimplement it from scratch. (?)
Here’s a quick write-up of the analysis process.
Packet Capture Analysis
0x00 Wireshark Capture
After launching the VPN and logging in, I captured traffic on the physical network interface using Wireshark. I noticed two types of traffic: TLS 1.3 and TLS 1.1, both directed to port 443 of the VPN server.
wireshark capture
The TLS 1.3 traffic was likely HTTPS connections initiated by the Electron-based shell while loading web resources.
The TLS 1.1 traffic, however, looked suspicious—using an outdated cipher suite, and with a Session ID that followed a predictable pattern instead of being random. It didn’t resemble traffic from any standard library.
It was reasonable to assume this TLS connection carried the actual VPN traffic.
0x01 Attempting to Decrypt TLS Traffic
First, I checked log files in the program directory and tried exporting detailed logs via the official diagnostic tool, but found little useful information. So I moved on to attempting to decrypt the TLS traffic directly.
0x01.0 Exporting Keys via SSLKEYLOGFILE
On a whim, I set the SSLKEYLOGFILE environment variable, hoping to extract the session keys negotiated during the TLS handshake.
Unsurprisingly, it didn’t work for the target connection—but I did get the Electron app’s key log. Importing it into Wireshark successfully decrypted the TLS 1.3 traffic, confirming my suspicion: it was just HTTPS traffic from the web login interface.
0x01.1 Man-in-the-Middle Decryption with sslsplit
I usually use Fiddler for TLS decryption, but it only handles HTTPS proxy traffic, and the Nanjing University SSL VPN doesn’t support HTTP proxies.
After some searching, I found https://github.com/droe/sslsplit—a tool that can perform MITM decryption without analyzing binaries.
I set up SSLSplit on a Linux machine in my local network, redirected the gateway of the machine running the VPN through it, and installed the required CA certificate.
The targeted cipher suite had actually been removed from recent OpenSSL versions, so the initial build failed. After recompiling with an older OpenSSL version, it started without errors.
However, while regular TLS traffic decrypted fine, the Nanjing University VPN failed to connect once MITM was in place. I spent quite a while troubleshooting with no success. (At the time, I suspected certificate pinning—but as we’ll see later, that wasn’t the issue.)
0x01.2 Hooking SSL Functions with Frida
With no other options, I had to dive into the world of binary analysis—a realm I’m not particularly comfortable in.
The VPN spawns several processes. Using Huorong Sword (a system call monitor), I identified the one with the most socket activity and loaded it into IDA Pro.
IDA Pro
I initially considered DLL replacement for hooking, but realized the app was statically linked.
Eventually, thanks to the magic of Google, I found a ready-made hooking script on GitHub: https://github.com/he0xwhale/ssl_logger
Finally, I succeeded in decrypting the target TLS traffic.
Decrypted traffic
0x02 Traffic Analysis
Looking at the decrypted traffic, I was pleasantly surprised: the payload was literally just raw IP packets—no further parsing needed. This made sense given it’s an L3 (layer 3) VPN.
Traffic content
So, is the entire protocol just about sending IP packets over a TLS stream?
Well, obviously not that simple.
By temporarily disconnecting the network, starting the capture script, then reconnecting, I managed to capture the full handshake sequence.
After careful observation and testing, I discovered that the Nanjing University VPN requires exactly three connections:
- Handshake connection
- Send connection
- Receive connection
Each of these connections starts with a similar header:
1 | 00000000 00 00 00 XX 32 37 35 32 62 30 62 38 61 33 32 63 ....2752 b0b8a32c |
Here, XX is the request type (fixed per connection type). The last 4 bytes are FF FF FF FF for the handshake packet, and the client’s assigned IPv4 address for the send/receive connections.
The bytes from offset 0x04 to 0x33 represent a session token. After sending this initial packet, the server responds with something like:
1 | 00000000 0f 00 00 00 0f 00 00 00 00 76 04 53 f4 0d 0b c3 ........ .v.S.... |
The first byte indicates server status—00 or 01 usually means success. The rest are mostly constant values, which I didn’t dig too deep into. Notably, the handshake response includes the assigned IPv4 address (4 bytes).
Once all three connections are established, the handshake connection remains idle. Writing IP packets to the send connection results in corresponding response packets appearing on the receive connection (if any).
Did you think that was it? Nope, still not that simple.
Remember how MITM failed earlier, and how both the web interface and SSL VPN share port 443?
If you simply create a regular SSL connection to the VPN server, you end up talking to the web server instead.
At first, I thought the server used the actual TLS application data to route traffic—maybe sending a special payload would redirect you to the real backend. (Kind of like how certain ahem protocols work…)
But experiments showed that replaying requests only gave me 400 Bad Request. Eventually, I realized the server actually uses the Session ID field in the TLS Client Hello to route connections.
? My reaction: this protocol is kind of ridiculous.
This also explains why my MITM attempt failed—the proxy altered the handshake, breaking the routing.
Even more shocking: that session token I mentioned earlier? It actually contains the Session ID from the TLS Client Hello used during the initial HTTPS login.
I can’t even imagine how the developers came up with the idea of smuggling so much application-layer state inside the TLS handshake. ( ͡° ͜ʖ ͡°)
As for the actual login flow—standard HTTP and JavaScript analysis—and many other protocol details, I’ll skip them due to space constraints. Check out my GitHub repo for the full implementation.
0x03 Reimplementation
Given the heavy networking requirements, I decided to learn Go on the fly—famous for its powerful networking libraries.
Because the protocol required deep TLS manipulation, I used utls—a library originally designed for certain advanced networking use cases—to craft the TLS handshakes.
One interesting decision: I could’ve created a TUN device, assigned an IP, and let the OS handle IP packet construction. But cross-platform TUN setup is a pain. Instead, I integrated Google’s gvisor userspace network stack to turn a layer 3 interface into a socks5 proxy. This gave me cross-platform support without requiring admin privileges—a neat little trick.
0x04 Summary
From protocol analysis to full reimplementation, the whole process took about two days. It was a fun little project. While I haven’t done much binary protocol analysis before, and this one wasn’t particularly complex (nor did it require deep reverse engineering), it satisfied my long-standing curiosity about how Nanjing University’s SSL VPN actually works. Along the way, I got some hands-on Go experience and brushed up on computer networking concepts.
The project even gained some attention on GitHub: https://github.com/lyc8503/EasierConnect
After talking to the official software maintainers, I confirmed they’ve abandoned further development. So if anyone needs it, feel free to download, build, and use it—should be safe for personal use.
This article is licensed under the CC BY-NC-SA 4.0 license.
Author: lyc8503, Article link: https://blog.lyc8503.net/en/post/nju-ssl-vpn-protocol/
If this article was helpful or interesting to you, consider buy me a coffee¬_¬
Feel free to comment in English below o/