Skip to content

Latest commit

 

History

History
102 lines (77 loc) · 10 KB

protocol-v3-zh.md

File metadata and controls

102 lines (77 loc) · 10 KB
title date updated author
ShadowTLS V3 协议设计
2023-02-06 03:00:00 -0800
2023-02-06 03:00:00 -0800
ihciah

版本演进

在 2022 年 8 月的时候我实现了第一版 ShadowTLS 协议。当时 V1 协议的目标非常简单,仅仅通过代理 TLS 握手来逃避中间人对流量的判别。V1 协议假定中间人只会观察握手流量,不会观察后续流量、不会做主动探测,也不会做流量劫持。

但这个假设并不成立。为了防御主动探测,在 V2 版本的协议中添加了通过 challenge-response 方式来验证客户端身份的机制;并新增了 Application Data 封装来更好地伪装流量。

V2 版本目前工作良好,在日常使用中我没有遇到被封锁等问题。在实现了对多 SNI 的支持后,它甚至可以作为一个 SNI Proxy 工作,看起来完全不像是一个偷渡数据用的代理。

但是 V2 协议仍假设中间人不会对流量做劫持(参考 issue)。流量劫持成本比较高,目前没有被广泛应用,目前中间人的手段仍以旁路观测和注入以及主动探测为主。但这并不意味这未来流量劫持不会被大规模使用,协议设计上能够抵御流量劫持一定是更好的方案。面临的最大的一个问题是,服务端很难隐蔽地表明身份。

这个 issue 提出的 restls 提供了一个极具创新性的思路。借鉴这个思路我们可以解决服务端身份鉴定问题。

除此之外,我在这篇博客里也提到了一些针对数据封装的可能的劫持攻击,这也是 V3 协议必须解决的问题。

V3 协议目标

  1. 能够防御流量特征检测、主动探测和流量劫持。
  2. 更易于正确实现。
  3. 尽可能地弱感知 TLS 协议本身,实现者无需 Hack TLS 库,更不需要自行实现 TLS 协议。
  4. 保持简单:仅作为 TCP 流代理,不重复造轮子。

关于对 TLS 1.2 的支持

V3 协议在严格模式下仅支持使用 TLS1.3 的握手服务器。你可以使用 openssl s_client -tls1_3 -connect example.com:443 来探测一个服务器是否支持 TLS1.3。

如果要支持 TLS1.2,需要感知更多 TLS 协议细节,实现起来会更加复杂;鉴于 TLS1.3 已经有较多厂商使用,我们决定在严格模式下仅支持 TLS1.3。

考虑到兼容性和部分对防御连接劫持需求较低的场景(如使用特定 SNI 绕过计费系统),在非严格模式下允许使用 TLS1.2。

握手流程

这部分协议设计借鉴 restls 但存在一定差别:弱化了对 TLS 细节的感知,更易于实现。

  1. 客户端的 TLS Client 构造 ClientHello,ClientHello 需要生成自定义的 SessionID。SessionID 长度需为 32,前 28 位是随机值,后 4 位是 ClientHello 帧(不含 TLS 帧的 5 字节头,SessionID 后 4 byte 填充 0)的 HMAC 签名数据。HMAC 实例仅为一次性使用,直接使用密码创建实例。同时需要一个 Read Wrapper 负责提取 ServerHello 中的 ServerRandom 并转发后续流。
  2. 服务端收到包后,会对 ClientHello 做鉴定,如果鉴定失败则直接持续性与握手服务器进行 TCP 中继。如果鉴定成功,也会将其转发至握手服务器,并持续劫持握手服务器的返回数据流。服务端会:
    1. 记录转发的 ServerHello 中的 ServerRandom。
    2. 对所有 ApplicationData 帧的内容部分做处理:
      1. 对数据做变换,将其 XOR SHA256(PreSharedKey + ServerRandom)。
      2. 添加 4 byte 前缀 HMAC_ServerRandom(处理后的帧数据),HMAC 实例需事先灌入 ServerRandom 作为初始值,对此后从握手服务器转发的 ApplicationData 需要复用这个 HMAC 实例。注意帧长度需要同时 + 4。
  3. 客户端的 ReadWrapper 需要解析 ApplicationData 帧,判定前 4 byte HMAC:
    1. 符合 HMAC_ServerRandom(帧数据),则证明服务端是可靠的。在握手完成后这类帧需要过滤掉。
    2. 符合 HMAC_ServerRandomS(帧数据),则证明数据已经完成切换。需要将内容部分转发至用户侧。
    3. 都不符合,此时可能流量已被劫持,需要继续握手(握手失败则作罢),并在握手成功后发送一个长度随机的 HTTP 请求(糊弄性请求),在读取完响应后正确关闭连接。

安全性验证

  1. 流量劫持时,Server 会返回没有做 XOR 的数据,Client 会直接进入糊弄流程。
  2. ClientHello 可能会被重放,但无法使用其正确握手(restls 的讨论),所以无法鉴别我们返回的带前缀的 XOR 数据是否可解密。
  3. 若 Client 假装数据解密成功,直接发送数据,由于存在数据帧校验,其也无法通过。

数据封装

V2 版本的数据封装协议事实上也无法抵御流量劫持,如中间人可能会在握手完成后对这部分数据做篡改,我们需要能够响应 Alert;中间人也可能会按照 V2 协议的样子将一个 ApplicationData 封装拆成两个,如果连接正常,则也可以用于识别协议。

要应对流量劫持,除了要优化握手流程,数据封装部分也要重新设计。我们需要能够对数据流做验证,并且抵御重放、数据篡改、数据切分、数据乱序等攻击。

数据除了最外层继续使用 ApplicationData 封装外,内层添加了 4 byte 的 HMAC 计算值。我们在使用 preshared key 创建 HMAC 实例后,会灌入 ServerRandom+"C"ServerRandom+"S" 作为初始值,前者对应 Client 的发送数据流,后者对应 Server 的发送数据流(目的是防止中间人将我们发送的数据发回来,或者将不同连接的数据重放)。在转发过程中,首先将纯数据灌入 HMAC 实例,之后计算 4 byte 值后放于纯数据最前面。封装出的数据帧格式:(5B tls frame header)(4B HMAC)(data)。封装结束后将 4 byte 数据输入 HMAC 实例(避免中间人剪切拼接请求)。

当数据校验失败时,我们需要立刻发送 TLS Alert 正确关闭连接。在连接断开时也需要能够正确关闭。

安全性验证

  1. 对于中间人的数据篡改,HMAC 会直接验证出来,会响应 Alert。
  2. 对于中间人乱序攻击,HMAC 会直接验证出来,会响应 Alert。
  3. 对于剪切拼接攻击(合并两个 AppData),虽然 HMAC 处理的是数据流,但是由于我们在处理完成后又额外 update 进去一个 4 byte 的值,所以可以打断两个连续的流,防御这种攻击。

实现指南

客户端

客户端负责 TLS 握手、切换并在切换后做数据封装和解封装。

客户端需要内置一个 TLS Client,并在其对网络流读时加一层 Read Wrapper:TLSClient <- ReadWrapper <- TCPStream;同样,在写数据链路上也需要附加一个 Write Wrapper:TLSClient -> WriteWrapper -> TCPStream。

Stage1: TLS 握手

  1. 通过 TLS 库构造自定义 SessionID 并签名。
  2. ReadWrapper:
    1. 提取 ServerHello 中的 ServerRandom;创建 HMAC_ServerRandom
    2. 对 ApplicationData 使用 HMAC_ServerRandom 判定帧内容(不含 4byte HMAC)的 HMAC 与前 4 byte 是否匹配。若匹配则重写数据帧内容为其 XOR SHA256(PreSharedKey + ServerRandom),并去掉前 4 byte HMAC 值。若不匹配则不做修改,并标记该连接被劫持,握手成功后发送糊弄性请求;握手失败则不做处理。

Stage2: 数据转发(该过程不依赖 TLS 库)

  1. 创建 HMAC_ServerRandomCHMAC_ServerRandomS
  2. 读连接时 Parse Application Data 封装,并利用 HMAC_ServerRandomSHMAC_ServerRandom 验证数据前 4 byte。
    1. HMAC_ServerRandom 通过验证则表示这个是握手残留数据,直接忽略。
    2. HMAC_ServerRandomS 通过验证则表示这个是我们自己的数据封装,此后禁用 HMAC_ServerRandom 分支判定,并将数据转发至用户(不含 HMAC)
    3. 均未通过,则按照 Alert bad_record_mac 处理。
    4. 说明:这里允许 Server 在 Client 完成切换后向 Client 发送残留的 TLS 帧,客户端需要负责过滤掉。因为 Server 并不强感知 TLS 握手结束,其仅感知 Client 发送切换后的数据。
  3. 写连接时添加 Application Data 与 HMAC,HMAC 通过 HMAC_ServerRandomC 计算得到。

服务端

服务端负责转发 TLS 握手、判定切换时机并在切换后做数据封装和解封装,它不依赖 TLS 库。

Stage1: 转发 TLS 握手

  1. 读 ClientHello: 提取并鉴定 ClientHello 中的 SessionID,若未通过则标记为主动探测流量,后续直接启动 TCP 转发(实现多 SNI 的话还需要解析 SNI 并做对应分流转发);若通过也会转发该数据帧,并继续第二步。
  2. 从另一侧读 ServerHello: 提取 ServerRandom。
  3. 启动双向转发(with Handshake Server):
    1. 创建 HMAC_ServerRandomCHMAC_ServerRandom
    2. ShadowTLS Client -> Handshake Server: 直接转发,直到遇到符合 ApplicationData 前 4 byte 符合 HMAC_ServerRandomC 签名结果的数据帧,此时停止双向转发(但需要保证残留正在发送中的帧的完整性)。
    3. Handshake Server -> ShadowTLS Client: 对 Application Data 帧做修改,对数据做 XOR SHA256(PreSharedKey + ServerRandom) 之后在头部添加 4 byte HMAC(由 HMAC_ServerRandom 计算)。

Stage2: 数据转发(with Data Server)

  1. 创建 HMAC_ServerRandomS
  2. ShadowTLS Client -> Data Server: Parse Application Data 封装,并利用 HMAC_ServerRandomC 验证数据前 4 byte。若未通过,则按照 Alert bad_record_mac 处理。验证完成后,将该 4 字节数据也输入到 HMAC_ServerRandomC 实例中。
  3. Data Server -> ShadowTLS Client: 添加 Application Data 与 HMAC,HMAC 通过 HMAC_ServerRandomS 计算得到。之后将该 4 字节数据也输入到 HMAC_ServerRandomS 实例中。