引言
本篇文章主要介绍一下 TLS 混淆的实现原理。当然,有关 TLS 相关的知识不是本篇的主题,所以不进行过多介绍。
从请求到响应
从图中可以看出,一个完整的请求与响应需要经历以下七个步骤:
- 客户端发起请求
- 客户端混淆插件对请求进行包装
- 服务端混淆插件解混淆获取真实数据
- 服务端通过原始请求拿到响应
- 服务端混淆插件混淆响应
- 客户端混淆插件解混淆
- 客户端获取响应数据
其中,客户端、服务端分别对应代理的客户端及服务端。
混淆上下文
1 |
|
结构体 obfs
封装混淆阶段及混淆期间需要传递的数据,结构体 obfs_para
则封装了混淆的具体实现方法。通过以上我们可以看出,可以根据自己的需要自定义混淆的实现。比如目前有名的混淆方案为:simple-obfs、v2ray-plugin。当然本文主要介绍 simple-obfs 实现,毕竟最终效果差不多,但性能相差较大。simple-obfs 包括 HTTP、TLS 两种实现,这里只介绍后者。
这里将混淆实现称为混淆插件,混淆插件也分为客户端及服务端。所以注意与代理客户端、服务端区分。为什么混淆要区分客户端跟服务端?其实很好理解,代理客户端发送请求之前,混淆客户端需要对请求数据进行封装。发送到服务端之前,混淆服务端需要对数据解析,然后将数据传给代理服务端处理。当代理服务端对数据进行响应之前,混淆服务端会对数据进行封装,到达代理客户端之前,混淆客户端需要对响应数据进行解析,再交给代理客户端。
一切均可混淆
在介绍代码之前,我们还需要补充一个概念,就是 TLS 握手。由于并非每次数据交互都需要握手,所以判断是否属于第一次通讯则交由结构体 obfs
中的 obfs_stage
及 obfs_stage
字段。当握手之后,后续的数据交互就仅仅是简单的按照 TLS 格式进行包装就了。
对请求对象的封装方法为 static int obfs_tls_request(buffer_t *buf, size_t cap, obfs_t *obfs)
,其中 buf
为请求数据对象,cap
的容量大小为:2048,obfs
为混淆上下文结构体。
如果未开启混淆或者混淆被禁用,直接返回,不做任何处理。
1
if (obfs == NULL || obfs->obfs_stage < 0) return 0;
创建临时空间,用于存储请求数据。因为需要对请求数据 buf 做处理,所以这里先声明一块内存。注意这里声明了
static
说明后续会重复使用。1
static buffer_t tmp = {0, 0, 0, NULL};
握手处理
接下来要做的就是把 TLS 相关的报文结构空间计算出来。主要包括:client_hello
、ext_server_name
、host
、ext_session_ticket
、ext_others
几个字段的长度,其中host
为混淆的主机名。
1 | size_t buf_len = buf->len; |
buf_len
为具体数据长度,hello_len
固定 138B,server_name_len
固定 9B,host_len
取决于混淆的主机名,ticket_len
固定 4B,other_ext_len
固定 66B。
接下来为 tmp
及 buf
分配空间,同时将 buf
中的请求数据复制到 tmp
中。
1 | brealloc(&tmp, buf_len, cap); |
brealloc
方法取 buf_len
及 cap
的最大值为 tmp
分配内存。
以下是对 TLS 中 Client Hello 包的封装
1 | /* Client Hello Header */ |
我们先对照下 Client Hello 结构体及实际的包头,首先看下结构体:
1 | struct tls_client_hello { |
结构体包含了报文类型、版本、长度、握手类型、随机时间、sessionId、协商的加密算法、压缩算法、扩展长度几个字段。这几个字段的赋值,我们先不着急介绍,先看下最终的报文生成:
红框里的部分就是 Client Hello
结构体的内容。我们通过代码一步一步进行介绍!
a. 由于我们已经为 buf->data
从新分配好了内存,所以接下来开始填充数据。首先要填充的就是 Client Hello
包,伪造 TLS 握手,通过 memcpy
函数实现复制。我们看下 hello_template
模板:
1 | static const struct tls_client_hello |
我们通过代码跟报文图进行对比说明,content_type
= 0x16 代表握手协议,协议版本 0x0301 代表 TLS 1.0。握手类型 1 代表此次握手为 Client Hello
握手,加密算法长度 56B 也就是包括 28 种加密算法组合。其他的主要是占位符,需要动态计算。而比较重要的一个字段就是 session_id
,需要服务端回传。
b. 接下来就是 Session Ticket
的填充,我们先看下结构体:
1
2
3
4
5 struct tls_ext_session_ticket {
short session_ticket_type;
short session_ticket_ext_len;
// char session_ticket[session_ticket_ext_len];
} __attribute__((packed, aligned(1)));
结构体比较简单,包括了类型、长度,而通过以下代码我们可以看到,真实数据就填充到了该段中,session_ticket_ext_len
代表了真实数据的长度,也就是下图红框中的内容。
1 | /* Session Ticket */ |
我们再看下 Session Ticket
模板数据:
1
2
3
4
5
6static const struct tls_ext_session_ticket
tls_ext_session_ticket_template = {
.session_ticket_type = CT_HTONS(0x0023),
.session_ticket_ext_len = 0,
// char session_ticket[session_ticket_ext_len];
};ticket_type
值为 0x0023 代表 session_ticket
。
c. 接下来就是对 SNI
的封装了,我们看下结构体:
1
2
3
4
5
6
7
8 struct tls_ext_server_name {
short ext_type;
short ext_len;
short server_name_list_len;
char server_name_type;
short server_name_len;
// char server_name[server_name_len];
} __attribute__((packed, aligned(1)));
比较简单,主要是强调扩展字段的类型、长度、server_name
的长度、类型等。以下代码我们要注意,计算扩展长度的时候,需要加上各个字段本身占用的空间大小,如:ext_len
需要记录主机名长度的同时,还需要记录 server_name_list_len
、server_name_type
、server_name_len
本身占用的 5 字节空间,以此类推。
1 | /* SNI */ |
以下红框里,混淆的主机名是 itunes.apple.com
:
d. 最后就是其他的扩展字段,如以下结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 struct tls_ext_others {
short ec_point_formats_ext_type;
short ec_point_formats_ext_len;
char ec_point_formats_len;
char ec_point_formats[3];
short elliptic_curves_type;
short elliptic_curves_ext_len;
short elliptic_curves_len;
char elliptic_curves[8];
short sig_algos_type;
short sig_algos_ext_len;
short sig_algos_len;
char sig_algos[30];
short encrypt_then_mac_type;
short encrypt_then_mac_ext_len;
short extended_master_secret_type;
short extended_master_secret_ext_len;
} __attribute__((packed, aligned(1)));
主要包括五块内容,就不再过多介绍了。以下就是模板数据:
1 | static const struct tls_ext_others |
数据的填充就比较简单了:
1
2
3/* Other Extensions */
memcpy((char *) server_name + server_name_len + host_len, &tls_ext_others_template,
other_ext_len);
扩展字段实际展示:
e. 最后将数据的总长度赋值给 buf->len
,同时递增混淆阶段。
1 | buf->len = tls_len; |
协议结构图
其中绿色部分为实际的数据,红色的部分为混淆的主机名,其他的为固定的模板数据。