Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

VLESS/REALITY 协议探测原理:双 TLS 栈指纹差异分析

最近看到一个针对 VLESS/REALITY 协议的主动探测 PoC,技术思路挺有意思的。纯粹从技术层面拆解一下它的探测原理——核心问题是:为什么一个代理协议会被精准识别,以及识别的具体步骤是什么。

一、漏洞的根因:双 TLS 协议栈

VLESS/REALITY 的架构设计用了一套”偷梁换柱”的方案:服务端同时运行两套 TLS 协议栈。

  • 栈 A:VLESS 自身的 TLS 栈(xray-core 魔改的 TLS 实现),处理合法的代理流量
  • 栈 B:伪装目标网站的 TLS 栈,当客户端无法通过验证时,流量被回落到真实的目标网站

这个设计的初衷是好的——探测者连上来,发现是个正常网站,就走了。但问题在于:两套 TLS 栈对畸形数据的处理行为不可能完全一致。这就是指纹的来源。

打个比方,你找人代班,你们俩长得再像,遇到突发情况的反应也不可能一模一样。审查者要做的,就是制造”突发情况”,然后观察反应差异。

二、探测的完整流程

PoC 的探测分为六个步骤,每一步都有明确的目的:

步骤 1:流量捕获

通过 libpcap 监听网卡,粗过滤抓取 TLS 1.3 的 ClientHello 消息。

目的:缩小探测范围。TLS 1.3 + 特定 SNI 模式是 VLESS/REALITY 的典型特征,先从海量流量中筛出可疑目标。

实现细节:对 TCP 流进行重组(append_stream,处理乱序、重叠、间隙),在重组后的数据中查找 content_type=0x16, handshake_type=0x01 的 TLS 记录,提取 SNI、Session ID、ALPN 等字段。

步骤 2:A 组探测——重放原始 ClientHello

将捕获到的原始 ClientHello 原封不动地重放给服务器。服务器识别出 Session ID 已见过(TLS 1.3 的 anti-replay 机制),但 VLESS 的防重放实现有漏洞——它只检查时间戳,在 MaxDiffTime 窗口内不拒绝重放。于是这个请求落入了 VLESS 自身的 TLS 栈(栈 A)

收到 ServerHello 后,立即向服务器发送 7 组精心构造的畸形 TLS 探针,记录每组探针的响应状态。

目的:获取栈 A 的指纹。

步骤 3:C 组探测——随机化 Session ID 后重放

把原始 ClientHello 的 Session ID 替换为随机值,再次发送。由于 Session ID 变了,VLESS 无法将其关联到已有会话,请求被回落到伪装网站的 TLS 栈(栈 B)

等待 1 秒后(-d 参数控制),同样发送 7 组探针,记录响应状态。

目的:获取栈 B 的指纹。

步骤 4:指纹比对

将 A 组和 C 组的指纹逐一比较。如果所有探针的响应状态完全一致,说明只有一套 TLS 栈在响应,是正常服务器;如果不一致,说明存在两套 TLS 栈,判定为 VLESS/REALITY。

目的:通过差异确认双栈的存在。

步骤 5:报告过滤

这是 PoC 为了降低误报加的一层过滤。只有当 A 组指纹恰好匹配 xray-core 的预期特征 [TIMEOUT, ALERT, ALERT, ALERT, ALERT, ALERT, ALERT] 时,才输出检测报告。其他类型的 MISMATCH 会被抑制(suppressed)。

目的:减少假阳性。大型网站的负载均衡也可能导致不同后端的 TLS 栈不一致,但那种不一致是随机的、不稳定的,而 VLESS 的不一致是确定性的、可重复的。硬编码预期指纹就是只认 xray-core 的确定性特征。

步骤 6:用户溯源

如果探测成功,最初被抓取到 ClientHello 的源 IP 就是 VLESS 协议的使用者。

目的:从”发现可疑服务器”升级到”定位具体用户”。

三、7 组探针的设计逻辑

探针是整个方案的关键。每组探针都是一段畸形的 TLS 记录,目的是触发不同 TLS 实现在错误处理上的行为差异。

编号 内容 构造方式 测试目的
1 14 03 03 00 01 01 ×3 3 个连续的 TLS 1.2 Close Notify 正常栈会立即关闭连接,魔改栈可能容忍
2 17 99 99 00 10 Application Data,版本号 0x9999 版本号非法,不同栈的处理策略不同
3 17 03 03 42 00 Application Data,长度字段 0x4200 超大长度声明,不同栈的缓冲处理不同
4 14 03 03 00 02 01 01 Alert,负载长度 2 字节 非标准 Alert 负载,解析逻辑可能不同
5 14 03 03 00 01 02 Alert,描述码为 2(fatal) 非常见警告码,错误映射可能不同
6 14 03 01 00 01 01 TLS 1.0 版本的 Alert 协议版本降级,兼容性处理不同
7 14 03 03 00 05 01 01 01 01 01 Alert,5 字节超长负载 负载长度异常,解析边界处理不同

核心思路:TLS 规范定义了正确行为,但对错误输入的处理是实现定义的(implementation-defined)。不同 TLS 库(Go 标准库 vs xray-core 魔改版)在遇到这些畸形记录时,有的返回 Alert,有的直接断开,有的超时无响应——这些差异就构成了指纹。

四、响应分类机制

探针发出后,PoC 将服务器的响应分为四类:

状态 含义 判定条件
ALERT 收到 TLS Alert 缓冲区中出现 type=0x15 的记录,或加密 Alert(type=0x17, len=19
FIN TCP 正常关闭 recv() 返回 0
RST TCP 异常断开 recv() 返回 ECONNRESET
TIMEOUT 4 秒内无响应 超时

每组探针产生一个状态值,7 组探针产生一个长度为 7 的指纹序列。A 组和 C 组各一个序列,直接逐位比较。

五、为什么这个漏洞难以彻底修复

漏洞报告里提到了两种修补思路,但都认为无法根治:

思路一:行为模拟,抹平差异

让 VLESS 的 TLS 栈在处理畸形数据时,完全模拟目标网站的行为。但问题是:目标网站用的是哪个 TLS 库?什么版本?什么配置?这些你控制不了。而且探针可以无限构造——7 组不够就 70 组,还可以结合时序分析。这是打地鼠,永远打不完。

思路二:修复防重放漏洞

让 VLESS 在 MaxDiffTime 窗口内也拒绝重放的 ClientHello。这样 A 组探测就无法让请求落入 VLESS 自身的 TLS 栈。但审查者可以换策略:在原始 TCP 流中伪造帧插入探针,或者拦截真实客户端的连接来探测。防重放只能防住当前 PoC 的这一种触发方式,防不住变种。

根本问题:双 TLS 栈的架构设计本身就决定了差异的存在。只要两套栈不是同一个实现,就一定有行为差异。而 VLESS 不可能自己实现一个完整的 TLS 栈来替代伪装网站的栈——那样的话它就不是”伪装”了,它就是那个网站。

六、对 sing-box 的影响

当前 PoC 的报告过滤是硬编码 xray-core 的指纹 [TIMEOUT, ALERT×6]。sing-box 的 VLESS/REALITY 实现使用 Go 标准库的 crypto/tls,对同样 7 组探针的响应模式大概率与 xray-core 不同,所以当前 PoC 不会对 sing-box 触发报告。

但这不意味着 sing-box 安全。漏洞的根因是双 TLS 栈架构,sing-box 同样存在。只要去掉报告过滤(所有 MISMATCH 都报告),或者针对 Go crypto/tls 的行为特征调整预期指纹,同样可以探测 sing-box。当前 PoC 只是在最后一道门上只认 xray-core 的脸,换把锁就能认别的脸。

七、技术启示

  1. 不要自己魔改标准协议。TLS 是经过几十年审查的成熟协议,任何非标准修改都可能引入不可预见的行为差异。这些差异在正常使用中看不出来,但在主动探测下会暴露无遗。

  2. 复杂度是安全的敌人。VLESS 为了追求”完美伪装”堆砌了大量设计——REALITY、XTLS、Vision,每一层都增加了攻击面。相比之下,Trojan 这种”TLS in TLS”的简单方案,反而没有这种确定性的指纹。

  3. 防重放不能只靠时间戳。TLS 1.3 规范要求服务端记录已使用的 ClientHello 哈希值来防重放,VLESS 只检查了时间窗口,这是一个低级错误。