Reversing CheckPoint's SNX VPN protocol

My university uses SSL Network Extender (SNX) by Checkpoint as a VPN solution. Recently, I looked into how the VPN works. As part of my analysis, I looked at the protocol used to communicate with the VPN server.

Setup

The VPN client is a binary called snx, a 32-bit binary that is statically linked against OpenSSL. The client is able to connect to any VPN server running the Checkpoint server software. It allows the user to validate the server certificate. Therefore, we can intercept the traffic between the client and the server without worrying about a signed certificate.

The VPN client exchanges metadata via HTTPS and encapsulates regular traffic in TLS after the session has been established. Therefore, I could not get mitmproxy to work with the VPN client. Instead, I used a setup of nginx to intercept the traffic.

I created a local CA and signed a certificate for the use with nginx by using easy-rsa. I put the private key into /etc/nginx/server.key and the certificate into /etc/nginx/server.crt. I put the password for the private key into /etc/nginx/server.key.pass.

In its Client Hello, the VPN client lists its supported ciphersuites:

TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
TLS_RSA_WITH_AES_256_CBC_SHA (0x0035	)
TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x000a)
TLS_RSA_WITH_RC4_128_SHA (0x0005)
TLS_RSA_WITH_DES_CBC_SHA (0x0009)
TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)

Using openssl ciphers -convert TLS_RSA_WITH_AES_256_CBC_SHA, we can convert the standard ciphersuite name to the OpenSSL version AES256-SHA.

In nginx, we use the stream module to break the TLS without any application layer proxying:

worker_processes 1;

env LD_PRELOAD;
env SSLKEYLOGFILE;

events {
    worker_connections 1024;
}

stream {
    server {
        listen 443 ssl;

        ssl_certificate /etc/nginx/server.crt;
        ssl_certificate_key /etc/nginx/server.key;

        ssl_protocols TLSv1.2;
        ssl_prefer_server_ciphers off;
        ssl_password_file /etc/nginx/server.key.pass;

        ssl_ciphers "AES256-SHA";

        proxy_ssl_trusted_certificate /etc/nginx/server.crt;
        proxy_ssl on;
        proxy_ssl_verify off;
        proxy_ssl_session_reuse off;

        proxy_pass <host>;
    }
}

To log the TLS master secrets in nginx, I used libsslkeylog that I installed into /usr/local/lib/libsslkeylog.so. nginx needs to be started manually with the correct environment variables:

LD_PRELOAD=/usr/local/lib/libsslkeylog.so SSLKEYLOGFILE=/tmp/secrets.txt nginx

The LD_PRELOAD and SSLKEYLOGFILE environment variables are used to log the TLS session keys.

Analysis

There are two stages of a VPN connection: the authentication and the traffic exchange.

First of all, the client sends a HTTPS request that contains the user credentials:

POST /clients/ HTTP/1.0
Content-length: 262

(CCCclientRequest
	:RequestHeader (
		:id (2)
		:type (UserPass)
		:session_id ()
	)
	:RequestData (
            :client_type (TRAC)
            :endpoint_os (unix)
		:username (<username>)
		:password (<password>)
	)
)
HTTP/1.0 200 OK
Date: Sat, 19 Nov 2022 16:19:31 GMT
Server: Check Point SVN foundation
Content-Type: text/html
X-UA-Compatible: IE=EmulateIE7
Connection: close
X-Frame-Options: SAMEORIGIN
Content-Length: 445

(CCCserverResponse
	:ResponseHeader (
		:id (2)
		:type (UserPass)
		:session_id ()
		:return_code (600)
	)
	:ResponseData (
		:authn_status (done)
		:is_authenticated (true)
		:active_key (<active_key>)
		:server_fingerprint ()
		:server_cn ()
		:session_id (<session_id>)
		:active_key_timeout (43200)
	)
)

Analyzing the username and password passing, we see that we transmit one byte per character in the password. We reverse-engineer the username and password encoding scheme. The cipher looks like a regular stream cipher, except that the key stream is hardcoded. We implement the same functionality in Python:

#!/usr/bin/python3
table = b'-ODIFIED&W0ROPERTY3HEET7ITH/+4HE3HEET)$3?,$!0?!5?02/0%24)%3.5,,\x10&7?70?/\"*%#43'

def translate(i, c):
    if c == 0xff:
        c = 0
    c = c ^ table[i % 77]
    if c == 0:
        c = 0xff
    return c

def encode(s):
    return bytes([translate(i, c) for i,c in enumerate(s)])[::-1]

def decode(s):
    return encode(encode(s)[::-1])[::-1]

if __name__ == '__main__':
    print(encode(b'username'))

After the authentication stage is done, the client and server exchange some network information, like DNS suffixes and routes. Client -> Server:

........(client_hello
	:client_version (1)
	:protocol_version (1)
	:protocol_minor_version (1)
	:OM (
		:ipaddr (0.0.0.0)
		:keep_address (false)
	)
	:optional (
		:client_type (4)
	)
	:cookie (<cookie>)
)

Server -> Client:

.........(hello_reply
	:version (1)
	:protocol_version (1)
	:OM (
		:ipaddr (<client IPv4 address>)
		:dns_servers (
			: (<dns server 1>)
			: (<dns server 2>)
		)
		:wins_servers (
			: (<wins server 1>)
			: (<wins server 2>)
		)
		:dns_suffix ("<dns suffixes, comma-separated>")
	)
	:range (
		: (
			:from (<lower IPv4 address>)
			:to (<upper IPv4 address>)
		)
		[...]
	)
	:timeouts (
		:authentication (43193)
		:keepalive (60)
	)
	:optional (
		:subnet (255.255.255.0)
	)
)

Client -> Server:

.........(keepalive
	:id (0)
)

Summary

I showed how to intercept the Check Point VPN client traffic. Any real user would still have to confirm the changed CA certificate so my findings are not vulnerabilities. However, I think it is interesting to see the network protocol used. In total, this opens the door for further investigation of SNX.