简体中文 / [English]


Analysis and Implementation of Nanjing University's SSL VPN Protocol

 

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 capturewireshark 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 ProIDA 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 trafficDecrypted 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 contentTraffic 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
2
3
4
00000000  00 00 00 XX 32 37 35 32  62 30 62 38 61 33 32 63   ....2752 b0b8a32c
00000010 38 33 37 30 33 34 62 61 34 36 30 39 38 35 32 38 837034ba 46098528
00000020 34 30 63 00 39 38 35 30 30 38 64 62 33 37 34 64 40c.9850 08db374d
00000030 63 38 39 36 00 00 00 00 00 00 00 00 YY YY YY YY c896.... ........

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
2
3
00000000  0f 00 00 00 0f 00 00 00  00 76 04 53 f4 0d 0b c3   ........ .v.S....
00000010 50 ca 18 94 fd 7f 00 00 17 dc 84 66 b9 7f 00 00 P....... ...f....
00000020 a0 ca 18 94 ....

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/