客户端tcp通道防封禁笔记

最近做了一些关于防封禁和网络安全的一些事情,其中主要是关于tcp通道进行自定义协议收发的防封禁工作,在这段时间对防封禁这一块有更多一些的思考。同时通过跟我们公司中美的后台工程师的讨论,整个流程产生了很多有价值的想法和思考,在IT的各方面我感觉他们非常卓越,在防封禁方案讨论的过程中大部分的想法都是他们提出的建议,让我获益良多。在近期完成了客户端第一版防封禁功能的编码后来到美国出差,工作压力相对轻松,于是趁着这段时间尝试对近期工作内容产生的想法及方案的优缺点大概进行一个整理归纳。自感在这方面修炼还还差得很远,希望大家一起交流沟通。

背景

从事开发的很多技术人员都知道,网络稳定性和可用性对于每个应用来说都非常重要,应用中经常会遇到影响网络稳定性的很多不同条件,比如国家政策、运营商情况、目标恶意攻击等,可能也有很多人员在调查网络链接成功率下降或者面对用户反馈服务失败的时候彻夜不眠去定位问题,但是容易陷入到治标不治本的陷阱里面去。如果没有进行任何防封禁的工作,即使在理想情况下,HTTPS服务成功率也只有在98%~99%之间,如果是自定义的tcp协议通道成功率只会比这个数值要低,尤其是在一些有网络限制的国家或多运营商的国家下。

在该方案之前,进行过的防封禁策略就有很多,有关于HTTPS防封禁和tcp防封禁的,比如:
备用域名替换、http前置后置ip(http dns)、tcp/udp切换、端口切换、云服务转发等。这里只对HTTPS伪装方案进行了说明。

为了提升网络的稳定性与可用性,可以通过以下一些建议得到一些启发:

基于SSL/TLS的tcp通道伪装

首先,我们可以通过把自定义协议格式伪装起来,让他们看起来是正常的https协议,而不是私有协议格式。这是防封禁中最基础的一件事情,因为HTTPS是所有国家互联网中广泛应用。

关于SSL/TLS协议的更多定义,你们可以通过以下链接查阅:
RFC 5246
Traffic Analysis of an SSL/TLS Session

基本特性

伪装的主要目的是能够防止运营商通过一些应用交互协议行为特征对app进行封禁,假设你当前网络交互的消息是这样的(先不考虑安全性):
message = data length(4 bytes) + data
在这种情况下,它很容易被识别到有以下特征:

  1. 自定义协议
  2. 长度前面的bits大多都为0

在我们使用TLS ApplicationData的头对我们协议进行简单伪装之后,协议会变为这样:
message = TLS record(5 bytes) + counter(8bytes) + data length(4 bytes) + data
这时看起来有什么特别? 此时已经有一个TLS ApplicationData子协议的头及他的counter。但当你通过Wireshark观察这个协议,你仍然能通过协议的多个为0 的bits及明文的数据找到这条协议的特征。

因此下一步需要使该协议的数据部分进行随机化
TLS协议的初始作用是在TLS handshake交互中协商出能用于后续进行对称性加密的key,并在业务协议交互过程中使用该key对数据进行加密,因此,如果有通过正式TLS协商过程的协议会是这样
message = TLS record(5 bytes) + counter(8bytes) + encrypted(data length(4 bytes) + data)

这里有一个问题,有人会问到tls record 中后两个bytes是记录当前tls proto的length,那data length是否是可以不用?答案是:否,原因是由于tls一个message的长度只支持最长65535 bytes,在某些情况下自定义消息长度会大于该长度,若需要进行扩展则需要data length。另外一个case是在防封禁中,有时候需要把自定义协议打散为多条tls 协议发送(即使协议长度没到达65535),因此需要在客户端对数据进行拼接,所以data length字段在message的首个tls协议中必须是含有的。

到此为止,tcp消息进行tls伪装的基调已经能定下来了,其中两个要素

  1. 为TLS record 子协议类型的协议类型
  2. 业务协议的内容通过加密,其中的内容无法通过任何行为特征进行分析

使用TLS 1.1中TLS Resumption进行握手的特性

如果实现了上述的基础特性,可能已经能解决协议特征行为识别的问题,运营商或者封禁技术人员已经无法识别你的协议是否是私有协议,他们只能认为你发送的消息与所有的https消息一样。
但在tcp正式发送tls伪装的消息前与tcp 三次握手链接成功之间的时间段,因使用tls则引入了一些能进行识别的因素。正常使用HTTPS需要先通过SSL/TLS握手验证,简单概括TLS正常握手过程为协商双方使用配置、验证身份、基于rsa算法协商并生成对称性加密key。 如果在不进行TLS握手的情况下直接使用TLS Record的ApplicationData子协议,运营商能可能会发现你们应用ip的流量上有异常,然后通过该路径进行识别进行封禁。

如果使用常规的TLS handshake流程,需要两个rtt进行交互。

  1. Client Hello
  2. Server Hello, Certificate, Server Hello Done
  3. Client Key Exchanged, Client Changed Cipher Spec, Finished
  4. Server Changed Cipher Spec, Finished

因此可以使用比较新的TLS 1.1的特性TLS Resumption handshake来模拟这一步交互,降低TLS 伪装交互所需的耗时,TLS Resumption的流程如下:

  1. Client Hello
  2. Server Hello, Change Cipher Spec, Finished
  3. Client Changed Cipher Spec, Finished
    在Client Finished之后,我们就能发送业务协议了。

到此,已经又解决了一个问题,已经较完整把tcp通道伪装成了HTTPS协议的交互模式,而且能在可以暂时不考虑安全的情况下,通过TLS一些较新的特性降低TLS伪装的耗时。

关注更多TLS handshake交互过程中的细节

通过回顾SSL/TLS的定义,我们能发现使用TLS resumption伪装的过程中仍有非常多的细节没有处理,主要信息在Client Hello与Server Hello中,若tls handshake的过程需要自己去实现,这时整个协议细节都可以进行调整,其中配置大致包含:

  1. TLS record header
  2. 32 bytes random bytes
  3. Session ID
  4. Cipher Suite
  5. Compression Suite
  6. Server name indicator
  7. Signature algo
    其中recorder header、session ID、cipher suite、compression suite、signature algo正常情况都是用于客户端与服务器协议的作用,这方面在防封禁上意义没那么大,其中最重要的是server name indicator,若你一直使用应用本身注册的domain,极容易出现针对性封禁(即某个国家或者运营商故意对该应用进行检测封禁,无论是https或者是tcp,都通过强硬的手段恶意封禁)。在这种情况下,我们的https伪装可以伪装得更好去解决这个问题。我们通过动态配置填写SNI字段,比如填写baidu.com,然后其他的配置也根据模拟目标域名的https配置进行下发替换。

OK,这时又解决一个难题,显然这个时候运营商已经较难识别到你app发送出来的数据是否是真的还是假的,他会认为这时你发出来的数据是百度、谷歌等app发送出来,极难通过网络数据行为分析针对你的app进行封禁(当然仍有其他方法进行封禁,但是封禁成本都已经非常高了)。

总结一下上面的方法就是为https伪装,让你自定义的协议看上去是一个HTTPS链接交互,但里面传输的内容却是自定义协议:

  1. 伪装SSL/TLS握手流程
  2. 伪装数据包为TLS ApplicationData数据包
    上面两步的每一步都是可以进行很多变化,这些都基于你的app实际需要使用的情况,但是思想离不开这两步。例如,如果也希望在tls handshake过程中验证服务器可靠性、进行密钥协商,均可以在伪装的过程中也利用TLS原有的特性。数据包格式也可以进行各种优化,如统一协议头、可变长度、协议压缩等。

Security

在安全性方面,我们这边的方案大概使用了0-RTT Pre Shared Key的方式进行密钥协商。涉及到安全相关,对于使用方案相关的策略不方便太多展开描述,大致进行几个原则说明。

  1. 使用0-RTT 的策略主要是考虑在TLS伪装之后,需要尽快进行业务协议交互,能直接使用AES方式进行对称性加密及业务数据传输,降低TLS伪装带来的额外损耗。
  2. 能覆盖使用TLS resumption策略从而没有对服务器进行身份校验的问题
  3. 在上述基础上需增加一些额外的安全策略

其他详细的扩展阅读可以看微信的文章,描述ECDH与PSK协商策略非常详细。
基于TLS1.3的微信安全通信协议mmtls介绍