0%

TLS 混淆(五)混淆数据传输

引言

前几篇文章介绍了 TLS 握手包的混淆过程,本篇文章将继续讲解数据传输的混淆过程。

混淆请求数据

TLS 握手之后,开始进行数据的传输(握手阶段已经开始数据传输了)。回到 obfs_tls_request 方法,握手之后会递增混淆阶段,当相同连接再有请求数据进来的时候,混淆阶段已经不为 0 了,说明接下来到了数据传输阶段了。

数据传输用的方法为 static int obfs_app_data(buffer_t *buf, size_t cap, obfs_t *obfs) 方法形参与 obfs_tls_request 方法一致。buf 代表请求数据,cap 代表自定义缓存的大小,obfs 则是混淆上下文。我们看下方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 数据的大小
size_t buf_len = buf->len;
// 将 buf 容量增加 5 个字节,用于存放混淆头
brealloc(buf, buf_len + 5, cap);
// 将数据向后移动 5 个字节
memmove(buf->data + 5, buf->data, buf_len);
// 将 TLS 头字段复制到起始地址
memcpy(buf->data, tls_data_header, 3);
// 存放数据大小
*(uint16_t *) (buf->data + 3) = CT_HTONS(buf_len);
// 总的数据包大小增加 5 个字节
buf->len = buf_len + 5;

return 0;

方法比较简单,我们看下 TLS 头:

1
const char tls_data_header[3] = {0x17, 0x03, 0x03}

0x17 代表此数据包的内容是 Application Data,接下来的两个字节组合为 0x0303 代表 TLS 协议版本为 1.2,紧接着 2 个字节代表数据的大小。我们看下实际的包信息:


解析数据

解析数据用的是 deobfs_app_data 函数,在前几篇介绍握手包的文章中也多次出现该函数。按理说这个函数应该只出现在混淆或者反混淆的第 1 阶段,但是为什么第 0 阶段也会出现呢?还记得第二篇文章介绍【解 Client Hello】 中提到的判断吗?

1
2
3
4
5
6
7
if (buf->len > ticket_len)
{
return deobfs_app_data(buf, ticket_len, obfs);
} else
{
((frame_t *) obfs->extra)->idx = buf->len - ticket_len;
}

buf->len 代表根据数据包计算出的数据大小,ticket_len 是发送方指定的数据大小。后者是肯定没有问题的,但是前者由于是根据数据包计算的,那么肯定会有几种情况使得 buf->len 大于等于甚至小于 ticket_len

粘包

TCP 是一种流协议,它只负责将数据源源不断地送往目的地,至于如何将数据进行拆分和组合那就是应用层需要做的事。

如果 buf->len > ticket_len 说明产生了粘包,也就是本属于第 1 阶段的数据包也过来了。如图所示:

此时要做的就是将额外的数据也一并提取出来:

1
2
3
4
5
6
7
8
// 注意,此时的 buf->len 是整个数据包的大小,hello_len 是数据包 + 混淆字段的总大小
if (buf->len > hello_len)
{
// 如果大于,说明产生了粘包,然后将额外的数据提取出来
memmove(buf->data + ticket_len, buf->data + hello_len, buf->len - hello_len);
}
// 此时的 buf->len 就是握手阶段的数据 + 第 1 阶段的数据
buf->len = ticket_len + buf->len - hello_len;

接下来就需要对第 1 阶段的数据进行解析了:

1
2
3
4
5
if (buf->len > ticket_len)
{
// ticket_len 代表第一阶段的数据长度,所以解析函数需要从第 0 阶段数据的尾部开始解析
return deobfs_app_data(buf, ticket_len, obfs);
}

以上是第 0 阶段跟第 1 阶段的数据包粘着在一起,如果是第 1 阶段跟第 1 阶段的包粘着在一起呢?

1
2
3
4
5
6
else if (obfs->deobfs_stage == 1)
{

return deobfs_app_data(buf, 0, obfs);

}

从以上代码看出,deobfs_app_data 函数就自然而然地处理第 1 阶段的粘包了。

半包

有没有可能握手阶段的数据包被分成了多个包呢?一切皆有可能!

1
2
3
4
5
6
7
8
if (buf->len > ticket_len)
{
return deobfs_app_data(buf, ticket_len, obfs);
} else
{
// buf->len - ticket_len 为 0 或者负值
((frame_t *) obfs->extra)->idx = buf->len - ticket_len;
}

buf->len <= ticket_len 时,就将数据的偏移量保存到 ((frame_t *) obfs->extra)->idx 中,此时发生在握手包中。如果握手包被分成了两个包或者多个包,那么第一个包的数据已经解析完毕,然后保存此时数据的偏移量(负值)。当其余的包都是以第 1 阶段到来的(即使是握手包),那么就全权交由 deobfs_app_data 函数来处理。

📚 Tips

粘包与半包组合起来出现的情况比较多,比如完整包与完整包,完整包与非完整包、非完整包与完整包粘着的场景。

离成功只差一步之遥

有了以上的介绍,我们就来看一看 deobfs_app_data 函数是如何处理这些数据的,不过在开始之前,我们把可能的包组合列出来,看下函数是如何处理这些场景的。

  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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
static int deobfs_app_data(buffer_t *buf, size_t idx, obfs_t *obfs)
{
// bidex、idx、bofst 指向需要解析数据的起始地址(idx 的取值可能为:0、应用数据的长度、)
int bidx = idx, bofst = idx;

frame_t *frame = (frame_t *) obfs->extra;
// buf->len 代表要处理包的总大小
while (bidx < buf->len)
{
// 如果数据字段还没计算出来,则进行处理
if (frame->len == 0)
{
/*
* 先判断前 3 个字节也就是 content-type、version 是否是模板预期的
* 注意这里取的是 frame->idx 而不是直接用的 idx。
*/
if (frame->idx >= 0 && frame->idx < 3
&& buf->data[bidx] != tls_data_header[frame->idx])
{
return OBFS_ERROR;
} else if (frame->idx >= 3 && frame->idx < 5)
{
// 复制 length 字段(两个字节)到 frame->buf 中,用于计算 length
memcpy(frame->buf + frame->idx - 3, buf->data + bidx, 1);
} else if (frame->idx < 0)
{
// 算出有多少数据是属于握手包的(只有存在握手包时,frame->idx 才会为负值)
bofst++;
}
frame->idx++;
bidx++;
// 等于 5 时,说明 length 字段已经全部取出来了,可以计算 length 值了
if (frame->idx == 5)
{
// 算出数据大小
frame->len = CT_NTOHS(*(uint16_t *) (frame->buf));
// 指针归零
frame->idx = 0;
}
continue;
}
// 数据的大小不能超过 16384
if (frame->len > 16384)
return OBFS_ERROR;

// bidx 指向了数据的起始地址,这里算出还剩多少数据
int left_len = buf->len - bidx;
// 如果 left_len > frame->len 说明有粘包数据存在
if (left_len > frame->len)
{
/* 发生粘包,这里 bofst 代表上一数据包剩余数据的大小,所以需要将当前数复制到上一数据尾部
* 对应
*/
memmove(buf->data + bofst, buf->data + bidx, frame->len);
// bidx 指向下一个要解析的数据包
bidx += frame->len;
// 计算当前解析好的应用数据大小
bofst += frame->len;
// 归零,准备继续解析下一个数据包
frame->len = 0;
} else
{
// 到这里说明当前数据包中是完整的数据或者一部分数据(另一部分在下一个包中)
memmove(buf->data + bofst, buf->data + bidx, left_len);
// bidx 指向结尾
bidx = buf->len;
// 算出当前解析好的数据大小
bofst += left_len;
// 有多少数据出现在下一个包中
frame->len -= left_len;
}
}
// 真正的数据长度了
buf->len = bofst;

return OBFS_OK;
}

以上就是对该函数的介绍,我们来总结一下列举的 7 中场景:

1、握手情况下的数据包粘包的处理(图 1)

图 1 是在握手阶段,下一阶段的数据包与握手包粘着在一起,此时 idx 传递的是握手包中数据的大小,此时 frame->idx = 0bofst = idx 且不会发生变化,程序只需要从 idx 开始解析数据并且将数据移动到握手包数据末尾即可。

2、握手包半包(图 2)

此时 frame->idx 为负的剩余数据大小idx = 0。此时会一直递增 bofst,直到循环退出,然后更新 buf->len 即可。

3、握手包与数据包(图 3)

此时 frame->idx 为负的剩余数据大小idx=0。此时先递增 bofst 算出上一数据包的数据大小,然后再解析当前数据包内容。然后进 left_len > frame->len 的逻辑,复制数据。

4、正常包(图 4)

正常包直接参与解析,然后进入 left_len <= frame->len 的逻辑,完成数据复制,只不过 frame->len -= left_len 之后等于 0。

5、纯数据流下的粘包(图 5)

解析完第一个数据包之后,由于 left_len > frame->lenframe->len = 0 归零,但是 bidx < buf->len,所以会继续循环处理下一个数据包。

6、纯数据流下的粘包(图 6)

frame->idxidxbofst 都为 0,但是 frame->len 不为 0,它等于当前包中有多少数据是属于上一个包的,所以会进入 left_len > frame->len 的逻辑,完成一个相同地址的相同内容的复制。frame->len 归零,然后开始解析下一个数据包。

7、纯数据流下的粘包(图 7)

当数据包解析完成之后,发现最后一个包中的数据在下一个包中了,此时会进入 left_len <= frame->len 的逻辑。将数据交由图 6 处理了。

尾声

至此,TLS 混淆的内容就全部结束了。本系列文章不仅介绍了如何混淆 TLS 流量,还介绍了应用层如何处理粘包、半包的问题(半包、粘包是应用层处理的,而不是 TCP 层)。也希望大家能有所收货,另外,如果有机会,再开一个关于代理协议实现的系列,敬请期待!