南京大学 SSL VPN 协议分析与实现

 

由于南大给的 SSL VPN 实在是过于难用加上个人的强迫症, 就心血来潮分析了一下它的协议并重新实现了一个.(?)

简单记录下相关的分析过程.

抓包分析

0x00 Wireshark 抓包

启动 VPN 并登录账号, 使用 Wireshark 抓取物理网卡上的流量. 观察到有 TLS 1.3TLS 1.1 两种流量, 都是发往 VPN 地址的 443 端口的.

抓包抓包

TLS 1.3 应该是套壳 Electron 加载网页资源时发起的 https 连接.

TLS 1.1 的流量非常可疑(使用了很旧的加密套件, 且 Session ID 为有规律的值而不是随机数, 不像是任何一种现有库发出的流量.)

合理推测这个 TLS 连接承载的就是真正通过 VPN 的流量.

0x01 尝试解密 TLS 流量

先是分析了一下程序文件夹下相关的 log 文件, 并尝试用官方的诊断工具导出详细 log, 但没有发现太多的有用信息, 于是直接开始试图解密流量.

0x01.0 SSLKEYLOGFILE 导出密钥

抱着试试看的心态, 在环境变量中设置了 SSLKEYLOGFILE, 看看能不能导出对应 TLS 连接协商出的密钥来解密流量.

结果毫无意外地失败了, 但却得到了 Electron 的密钥 log, 导入 Wireshark 成功解密了上述 TLS1.3 连接, 确实是套壳网页登陆时产生的 https 流量, 验证了我的猜想.

0x01.1 sslsplit 中间人攻击解密流量

解密 TLS 我一直用的是 Fiddler, 可它只能代理解密 https 流量, 然而南大的 SSL VPN 却不支持通过 http 代理连接.

搜索后找到了 https://github.com/droe/sslsplit 这个库, 可以在不分析二进制文件的情况下直接中间人攻击解密流量.

在局域网的一台 Linux 上设置好 SSLSplit, 将装有 VPN 的电脑的网关指向它并安装相关 CA.

这个 VPN 用的加密套件还在最新的 OpenSSL 里被移除了, 开始直接报错, 后来换低版本 OpenSSL 重新编译, 不报错了.

但发现普通的 TLS 流量能正常解密, 但一旦做了中间人之后南大 VPN 就连不上了, 折腾了半天没解决问题. (当时怀疑是做了证书固定, 实际上并不是, 见下文.)

0x01.2 Frida Hook SSL相关函数

没办法只能开始做我不熟悉的二进制分析了…

这个 VPN 启动后会有好几个进程, 直接用火绒剑分析系统调用找到使用 Socket 最多的进程, 拖进 IDA Pro.

IDAProIDAPro

本来还想着直接替换 DLL 进行 Hook, 结果发现程序是静态链接的.

最后还是求助万能的谷歌, 在 GitHub 上发现了 Hook 脚本. https://github.com/he0xwhale/ssl_logger

最后终于成功解密了目标 TLS 流量.

解密流量解密流量

0x02 分析流量

直接观察捕获到的流量, 惊喜地发现流量本体内容就是一个 IP 包(不用再自己分析了), 也符合其 L3 VPN 的功能定位.

流量内容流量内容

所以整体上只要得到 IP 包然后直接通过 TLS 流发送出去, 就完成了…吗?

显然并没有这么简单.

通过暂时断网, 先启动抓包脚本再恢复联网的小技巧能抓到完整的协议通讯包.

通过仔细观察+实验, 发现南大 VPN 连接必须的有三个连接.

分别称为握手连接, 发送连接和接收连接.

三个连接开头都会发送类似下面的一段:

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.... ........

其中 XX 为请求包类型, 对于每个连接是定值, 最后 4 个 byte 对于握手包是 FF FF FF FF, 对于发送和接收连接是当前 VPN 的 IPv4 地址.

中间 0x04-0x33 这些 byte 是本次会话的 token, 发完开头的请求后, 服务器会返回类似如下的内容:

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 ....

该响应首字节代表服务器的状态, 00/01 通常表示成功, 后面大部分是定值, 暂时没有深究. 握手包返回的报文中还包含的 4byte 的 IPv4 地址.

完成连接的创建后, 握手连接保持闲置, 向发送连接中写入 IP 包, 可以在接收连接中读取到响应的 IP 包.(如果有)

你以为就结束了吗, 还是没这么简单.

还记得上面提到 SSL 中间人失败 & 网页和 SSL VPN 协议共用 443 端口吗.

如果直接创建一个普通的 SSL 连接到对应的 VPN 连接地址, 会直接连接到 Web 服务器.

我开始还以为服务器是用 TLS 实际传输的 Application Data 分流的, 发送特定的内容就能被分流到真正的后端. (类似某不能说的协议实现…)

然而实验发现重放请求只会得到 400 Bad Request, 后来发现原来服务器用的是 TLS Client Hello 中的 Session ID 分流…

?我的评价是这协议有点离谱.

这也是我用中间人不能截获流量的根本原因.

然后更让我震惊的是, 上面提到的 token, 竟然包含了登录所用的 https 连接时所用的 TLS 连接的 Client Hello 中的那个 Session ID

不知道这些程序员是怎么想到在 TLS 里面夹带这么多应用层私货的.(

至于具体的登录流程就是普通的 http 和 js 分析, 还有协议的很多其他细节, 限于篇幅就不再赘述了, 具体可以看我 GitHub 上的实现代码.

0x03 重实现

考虑到和网络相关性强, 就现学现卖用了传说中网络库很强大的 golang 来实现这个协议.

因为涉及很多对 TLS 底层的魔改, 用了某本来用于 XX 的库 utls 进行请求的构造.

还值得一提的是本来可以使用创建 TUN 设备的方法, 直接创建虚拟网卡并分配 IP, 这样可以直接使用操作系统的网络栈来构造 IP 包. 但这样要做多个系统的适配非常麻烦, 于是后来尝试引入了 Google gvisor 里的 userspace network stack 直接将一个三层设备”转换”成了 socks5 代理, 做到了跨平台+无需特权, 也是一个比较有意思的玩法.

0x04 小结

这次从分析协议到重实现南大使用的 SSL VPN 协议一共花了满打满算两天时间, 还算是一次比较有趣的经历. 之前分析的二进制协议还不多, 这次分析的协议也还不算难, 也没涉及二进制可执行文件深入的分析, 但也算是满足了我一直以来关于”南大 SSL VPN 是怎么实现的”的好奇心, 顺便体验和学习了一下 golang, 还实践了一下计网相关的知识.

相关的项目还在 GitHub 上还引起了一些关注, 得到了不少的 star. https://github.com/lyc8503/EasierConnect

后来和 VPN 软件官方沟通了一下, 还是跑路不维护了, 有需要的话大家可以自行下载编译使用, 自用应该问题不大~

本文采用 CC BY-NC-SA 4.0 许可协议发布.

作者: lyc8503, 文章链接: https://blog.lyc8503.net/post/nju-ssl-vpn-protocol/
如果本文给你带来了帮助或让你觉得有趣, 可以考虑赞助我¬_¬