0%

引言

本篇文章主要介绍一下 TLS 混淆的实现原理。当然,有关 TLS 相关的知识不是本篇的主题,所以不进行过多介绍。

从请求到响应



从图中可以看出,一个完整的请求与响应需要经历以下七个步骤:

  1. 客户端发起请求
  2. 客户端混淆插件对请求进行包装
  3. 服务端混淆插件解混淆获取真实数据
  4. 服务端通过原始请求拿到响应
  5. 服务端混淆插件混淆响应
  6. 客户端混淆插件解混淆
  7. 客户端获取响应数据

其中,客户端、服务端分别对应代理的客户端及服务端。

混淆上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#define OBFS_OK         0
#define OBFS_NEED_MORE -1
#define OBFS_ERROR -2

typedef struct obfs
{
int obfs_stage;
int deobfs_stage;
buffer_t *buf;
void *extra;
} obfs_t;

typedef struct obfs_para
{
const char *name;
const char *host;
const char *uri;
const char *method;
uint16_t port;
bool send_empty_response_upon_connection;

int (*const obfs_request)(buffer_t *, size_t, obfs_t *);

int (*const obfs_response)(buffer_t *, size_t, obfs_t *);

int (*const deobfs_request)(buffer_t *, size_t, obfs_t *);

int (*const deobfs_response)(buffer_t *, size_t, obfs_t *);

int (*const check_obfs)(buffer_t *);

void (*const disable)(obfs_t *);

int (*const is_enable)(obfs_t *);
} obfs_para_t;

结构体 obfs 封装混淆阶段及混淆期间需要传递的数据,结构体 obfs_para 则封装了混淆的具体实现方法。通过以上我们可以看出,可以根据自己的需要自定义混淆的实现。比如目前有名的混淆方案为:simple-obfs、v2ray-plugin。当然本文主要介绍 simple-obfs 实现,毕竟最终效果差不多,但性能相差较大。simple-obfs 包括 HTTP、TLS 两种实现,这里只介绍后者。

这里将混淆实现称为混淆插件,混淆插件也分为客户端及服务端。所以注意与代理客户端、服务端区分。为什么混淆要区分客户端跟服务端?其实很好理解,代理客户端发送请求之前,混淆客户端需要对请求数据进行封装。发送到服务端之前,混淆服务端需要对数据解析,然后将数据传给代理服务端处理。当代理服务端对数据进行响应之前,混淆服务端会对数据进行封装,到达代理客户端之前,混淆客户端需要对响应数据进行解析,再交给代理客户端。

一切均可混淆

在介绍代码之前,我们还需要补充一个概念,就是 TLS 握手。由于并非每次数据交互都需要握手,所以判断是否属于第一次通讯则交由结构体 obfs 中的 obfs_stageobfs_stage 字段。当握手之后,后续的数据交互就仅仅是简单的按照 TLS 格式进行包装就了。

对请求对象的封装方法为 static int obfs_tls_request(buffer_t *buf, size_t cap, obfs_t *obfs) ,其中 buf 为请求数据对象,cap 的容量大小为:2048obfs 为混淆上下文结构体。

  1. 如果未开启混淆或者混淆被禁用,直接返回,不做任何处理。

    1
    if (obfs == NULL || obfs->obfs_stage < 0) return 0;
  2. 创建临时空间,用于存储请求数据。因为需要对请求数据 buf 做处理,所以这里先声明一块内存。注意这里声明了 static 说明后续会重复使用。

    1
    static buffer_t tmp = {0, 0, 0, NULL};
  3. 握手处理
    接下来要做的就是把 TLS 相关的报文结构空间计算出来。主要包括:client_helloext_server_namehostext_session_ticketext_others 几个字段的长度,其中 host 为混淆的主机名。

1
2
3
4
5
6
7
8
size_t buf_len = buf->len;
size_t hello_len = sizeof(struct tls_client_hello);
size_t server_name_len = sizeof(struct tls_ext_server_name);
size_t host_len = strlen(obfs_tls->host);
size_t ticket_len = sizeof(struct tls_ext_session_ticket);
size_t other_ext_len = sizeof(struct tls_ext_others);
size_t tls_len = buf_len + hello_len + server_name_len
+ host_len + ticket_len + other_ext_len;

buf_len 为具体数据长度,hello_len 固定 138B,server_name_len 固定 9B,
host_len 取决于混淆的主机名,ticket_len 固定 4B,other_ext_len 固定 66B。

接下来为 tmpbuf 分配空间,同时将 buf 中的请求数据复制到 tmp 中。

1
2
3
4
brealloc(&tmp, buf_len, cap);
brealloc(buf, tls_len, cap);

memcpy(tmp.data, buf->data, buf_len);

brealloc 方法取 buf_lencap 的最大值为 tmp 分配内存。

以下是对 TLS 中 Client Hello 包的封装

1
2
3
4
5
6
7
8
9
/* Client Hello Header */
struct tls_client_hello *hello = (struct tls_client_hello *) buf->data;
memcpy(hello, &tls_client_hello_template, hello_len);
hello->len = CT_HTONS(tls_len - 5);
hello->handshake_len_2 = CT_HTONS(tls_len - 9);
hello->random_unix_time = CT_HTONL((uint32_t) time(NULL));
rand_bytes(hello->random_bytes, 28);
rand_bytes(hello->session_id, 32);
hello->ext_len = CT_HTONS(server_name_len + host_len + ticket_len + buf_len + other_ext_len);

我们先对照下 Client Hello 结构体及实际的包头,首先看下结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct tls_client_hello {
char content_type;
short version;
short len;

char handshake_type;
char handshake_len_1;
short handshake_len_2;
short handshake_version;

int random_unix_time;
char random_bytes[28];
char session_id_len;
char session_id[32];
short cipher_suites_len;
char cipher_suites[56];
char comp_methods_len;
char comp_methods[1];
short ext_len;
} __attribute__((packed, aligned(1)));

结构体包含了报文类型、版本、长度、握手类型、随机时间、sessionId、协商的加密算法、压缩算法、扩展长度几个字段。这几个字段的赋值,我们先不着急介绍,先看下最终的报文生成:

红框里的部分就是 Client Hello 结构体的内容。我们通过代码一步一步进行介绍!

a. 由于我们已经为 buf->data 从新分配好了内存,所以接下来开始填充数据。首先要填充的就是 Client Hello 包,伪造 TLS 握手,通过 memcpy 函数实现复制。我们看下 hello_template 模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 static const struct tls_client_hello
tls_client_hello_template = {
.content_type = 0x16,
.version = CT_HTONS(0x0301),
.len = 0,

.handshake_type = 1,
.handshake_len_1 = 0,
.handshake_len_2 = 0,
.handshake_version = CT_HTONS(0x0303),

.random_unix_time = 0,
.random_bytes = {0},

.session_id_len = 32,
.session_id = {0},

.cipher_suites_len = CT_HTONS(56),
.cipher_suites = {
0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f,
0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a,
0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d,
0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff
},

.comp_methods_len = 1,
.comp_methods = {0},

.ext_len = 0,
};

我们通过代码跟报文图进行对比说明,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
2
3
4
5
6
/* Session Ticket */
struct tls_ext_session_ticket *ticket =
(struct tls_ext_session_ticket *) ((char *) hello + hello_len);
memcpy(ticket, &tls_ext_session_ticket_template, sizeof(struct tls_ext_session_ticket));
ticket->session_ticket_ext_len = CT_HTONS(buf_len);
memcpy((char *) ticket + ticket_len, tmp.data, buf_len);

我们再看下 Session Ticket 模板数据:

1
2
3
4
5
6
static 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_lenserver_name_typeserver_name_len 本身占用的 5 字节空间,以此类推。

1
2
3
4
5
6
7
8
/* SNI */
struct tls_ext_server_name *server_name =
(struct tls_ext_server_name *) ((char *) ticket + ticket_len + buf_len);
memcpy(server_name, &tls_ext_server_name_template, server_name_len);
server_name->ext_len = CT_HTONS(host_len + 3 + 2);
server_name->server_name_list_len = CT_HTONS(host_len + 3);
server_name->server_name_len = CT_HTONS(host_len);
memcpy((char *) server_name + server_name_len, obfs_tls->host, host_len);

以下红框里,混淆的主机名是 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  static const struct tls_ext_others
tls_ext_others_template = {
.ec_point_formats_ext_type = CT_HTONS(0x000B),
.ec_point_formats_ext_len = CT_HTONS(4),
.ec_point_formats_len = 3,
.ec_point_formats = {0x01, 0x00, 0x02},

.elliptic_curves_type = CT_HTONS(0x000a),
.elliptic_curves_ext_len = CT_HTONS(10),
.elliptic_curves_len = CT_HTONS(8),
.elliptic_curves = {0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18},

.sig_algos_type = CT_HTONS(0x000d),
.sig_algos_ext_len = CT_HTONS(32),
.sig_algos_len = CT_HTONS(30),
.sig_algos = {
0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02,
0x04, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03
},

.encrypt_then_mac_type = CT_HTONS(0x0016),
.encrypt_then_mac_ext_len = 0,

.extended_master_secret_type = CT_HTONS(0x0017),
.extended_master_secret_ext_len = 0,
};

数据的填充就比较简单了:

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
2
buf->len = tls_len;
obfs->obfs_stage++;

协议结构图

其中绿色部分为实际的数据,红色的部分为混淆的主机名,其他的为固定的模板数据。