0%

TLS 混淆(二)解 ClientHello

引言

【上一篇文章】 介绍了客户端发送请求到服务端进行 Hello Client 握手,本篇继续讲解服务端收到请求之后如何反解析具体的数据。

庐山真面目

Hello Client 握手包其实就只是一个用来欺骗防火墙的外壳,真正的数据也藏在其中。到达服务端之后,我们首先要做的就是去掉外壳,拿到真实的数据。

  1. 先判断当前是否启用混淆

    1
    if (obfs == NULL || obfs->deobfs_stage < 0) return 0;
  2. 如果当前混淆上下文中不存在 extra,实例化它,它的作用我们往后在介绍。

    1
    2
    3
    4
    5
    if (obfs->extra == NULL)
    {
    obfs->extra = ss_malloc(sizeof(frame_t));
    memset(obfs->extra, 0, sizeof(frame_t));
    }
  3. 为当前混淆上下文初始化缓存,这里分配了 32 个字节。具体的作用,我们稍后介绍。

    1
    2
    3
    4
    5
    6
    if (obfs->buf == NULL)
    {
    obfs->buf = (buffer_t *) ss_malloc(sizeof(buffer_t));
    balloc(obfs->buf, 32);
    obfs->buf->len = 32;
    }
  4. 由于当前的混淆阶段为 0(Hello Client 握手包,混淆阶段肯定为 0),所以开始解析 tls_client_hello

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
/*
* buf->len 为整个 Hello Client 数据包的长度,包括:固定的混淆模板 + 数据 + 混淆域名长度
*/
int len = buf->len;

len -= sizeof(struct tls_client_hello);

/*
* 减去 client hello 模板数据后为 0,说明此数据包有问题
*/
if (len <= 0) return OBFS_NEED_MORE;

/*
* 将 hello 指针指向数据起始地址,用于取值
*/
struct tls_client_hello *hello = (struct tls_client_hello *) buf->data;

/*
* 如果握手类型与 hello client 中的类型不一致,说明包出现了问题
*/
if (hello->content_type != tls_client_hello_template.content_type)
return OBFS_ERROR;

/*
* hello->len 是整个 Hello Client 内容的长度,加上 5 字节则代表整个 Hello Client 报文的大小(content_type、version、len 总共 5B),
* 理论上该字段与 buf->len 字段大小一致
*/
size_t hello_len = CT_NTOHS(hello->len) + 5;

/*
* 取 Hello Client 包中的 Session Id 字段,存与混淆上下文中的缓存里(步骤 3 中的缓存就是出于这个目的)
*/
memcpy(obfs->buf->data, hello->session_id, 32);
  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
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
78
79
80
/*
* 计算 session ticket 头大小
*/
len -= sizeof(struct tls_ext_session_ticket);

/*
* 如果小于 0,则说明数据包出现了问题
*/
if (len <= 0) return OBFS_NEED_MORE;

/*
* 计算指针偏移,获取 ticket 指针
*/
struct tls_ext_session_ticket *ticket =
(struct tls_ext_session_ticket *) (buf->data + sizeof(struct tls_client_hello));

/*
* 判断当前类型是否一致
*/
if (ticket->session_ticket_type != tls_ext_session_ticket_template.session_ticket_type)
return OBFS_ERROR;

/*
* 获取真实数据长度
*/
size_t ticket_len = CT_NTOHS(ticket->session_ticket_ext_len);

/*
* 如果去掉 hello client 头跟 ticket 头之后的长度 len 比数据内容还小,显然是有问题的
*/
if (len < ticket_len)
return OBFS_NEED_MORE;

/*
* 将真实数据存放到 buf->data 中,注意这里用到了 memmove 函数,是因为内存肯定有重叠,
* 此操作完成了以后,buf 中的数据只能通过 ticket_len 表示出有用的数据地址
*/
memmove(buf->data, (char *) ticket + sizeof(struct tls_ext_session_ticket), ticket_len);

/*
* 这是一个有意思的点,buf->len 是客户端传进来的,它等于 tls_len,
* 而 tls_len 等于 buf_len + hello_len + server_name_len + host_len + ticket_len + other_ext_len,
* 其中只有 buf_len 数据大小跟 host_len 混淆主机名长度是动态的,其他都是固定不变的。
* 我们再来看看 hello_len,它是数据报文的 len 字段加 5 字节之后的大小,也就是整个 Hello Client 报文的大小。
* 两者其实是等价的,但是为什么还会有这个判断逻辑呢?大家不妨可以先思考下
*/
if (buf->len > hello_len)
{
/*
* buf->data + hello_len 指向了包的结尾,此次操作是将数据包之外多余的数据追加到数据之后
*/
memmove(buf->data + ticket_len, buf->data + hello_len, buf->len - hello_len);
}

/*
* 将 buf->len 重新赋值为数据的真实长度
*/
buf->len = ticket_len + buf->len - hello_len;

/*
* 递增混淆阶段
*/
obfs->deobfs_stage++;

/*
* 如果有额外的数据
*/
if (buf->len > ticket_len)
{
/*
* 需要解混淆数据内容
*/
return deobfs_app_data(buf, ticket_len, obfs);
} else
{
/*
* 思考一下,buf->len 除了等于 ticket_len 之外,存在小于 ticket_len 也就是说 buf->len - hello_len 为负的情况吗?
*/
((frame_t *) obfs->extra)->idx = buf->len - ticket_len;
}

以上就是对 Hello Client 解混淆的介绍,其中有几个令人疑惑的点,deobfs_app_dataSession Id 的作用?什么情况会有额外的数据?buf->len 有可能比 ticket_len 小吗?不要着急,后续的文章会对这些内容逐一解释。

此外,我们还要记住的一点是,obfs 上下文对象是针对于当前连接存在的。只要当前连接没有被销毁,obfs 就是同一个对象。