引言
前几篇文章介绍了 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 | // 数据的大小 |
方法比较简单,我们看下 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 | if (buf->len > ticket_len) |
buf->len
代表根据数据包计算出的数据大小,ticket_len
是发送方指定的数据大小。后者是肯定没有问题的,但是前者由于是根据数据包计算的,那么肯定会有几种情况使得 buf->len
大于等于甚至小于 ticket_len
。
粘包
TCP
是一种流协议,它只负责将数据源源不断地送往目的地,至于如何将数据进行拆分和组合那就是应用层需要做的事。
如果 buf->len
> ticket_len
说明产生了粘包,也就是本属于第 1 阶段的数据包也过来了。如图所示:
此时要做的就是将额外的数据也一并提取出来:
1 | // 注意,此时的 buf->len 是整个数据包的大小,hello_len 是数据包 + 混淆字段的总大小 |
接下来就需要对第 1 阶段的数据进行解析了:
1 | if (buf->len > ticket_len) |
以上是第 0 阶段跟第 1 阶段的数据包粘着在一起,如果是第 1 阶段跟第 1 阶段的包粘着在一起呢?
1 | else if (obfs->deobfs_stage == 1) |
从以上代码看出,deobfs_app_data
函数就自然而然地处理第 1 阶段的粘包了。
半包
有没有可能握手阶段的数据包被分成了多个包呢?一切皆有可能!
1 | if (buf->len > ticket_len) |
当 buf->len <= ticket_len
时,就将数据的偏移量保存到 ((frame_t *) obfs->extra)->idx
中,此时发生在握手包中。如果握手包被分成了两个包或者多个包,那么第一个包的数据已经解析完毕,然后保存此时数据的偏移量(负值)。当其余的包都是以第 1 阶段到来的(即使是握手包),那么就全权交由 deobfs_app_data
函数来处理。
📚 Tips
粘包与半包组合起来出现的情况比较多,比如完整包与完整包,完整包与非完整包、非完整包与完整包粘着的场景。
离成功只差一步之遥
有了以上的介绍,我们就来看一看 deobfs_app_data
函数是如何处理这些数据的,不过在开始之前,我们把可能的包组合列出来,看下函数是如何处理这些场景的。
握手情况下的数据包粘包(完整包与完整包)
握手包的半包(非完整包)
握手包与数据包(非完整包与完整包)
正常包
纯数据流下的粘包(完整包与完整包)
纯数据流下的粘包(非完整包与完整包)
纯数据流下的粘包(完整包与非完整包)
代码逻辑:
1 | static int deobfs_app_data(buffer_t *buf, size_t idx, obfs_t *obfs) |
以上就是对该函数的介绍,我们来总结一下列举的 7 中场景:
1、握手情况下的数据包粘包的处理(图 1)
图 1 是在握手阶段,下一阶段的数据包与握手包粘着在一起,此时 idx
传递的是握手包中数据的大小,此时 frame->idx = 0
, bofst = 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->len
,frame->len = 0
归零,但是 bidx < buf->len
,所以会继续循环处理下一个数据包。
6、纯数据流下的粘包(图 6)
frame->idx
、idx
、bofst
都为 0,但是 frame->len
不为 0,它等于当前包中有多少数据是属于上一个包的,所以会进入 left_len > frame->len
的逻辑,完成一个相同地址的相同内容的复制。frame->len
归零,然后开始解析下一个数据包。
7、纯数据流下的粘包(图 7)
当数据包解析完成之后,发现最后一个包中的数据在下一个包中了,此时会进入 left_len <= frame->len
的逻辑。将数据交由图 6 处理了。
尾声
至此,TLS
混淆的内容就全部结束了。本系列文章不仅介绍了如何混淆 TLS
流量,还介绍了应用层如何处理粘包、半包的问题(半包、粘包是应用层处理的,而不是 TCP 层)。也希望大家能有所收货,另外,如果有机会,再开一个关于代理协议实现的系列,敬请期待!