从 TLS 客户端 hello 中提取服务器名称指示 (SNI)
Posted
技术标签:
【中文标题】从 TLS 客户端 hello 中提取服务器名称指示 (SNI)【英文标题】:Extract Server Name Indication (SNI) from TLS client hello 【发布时间】:2013-07-23 20:17:07 【问题描述】:您将如何从 TLS 客户端 Hello 消息中提取服务器名称指示。我目前正在努力理解 TLS 扩展上的这个非常神秘的RFC 3546,其中定义了 SNI。
到目前为止我所理解的事情:
当您对缓冲区进行 utf8 编码时,主机是 utf8 编码和可读的。 主机前有一个字节,它决定了它的长度。如果我能找出那个长度字节的确切位置,那么提取 SNI 将非常简单。但是我首先要如何获得那个字节呢?
【问题讨论】:
您尝试采用的直接方法是错误的。您需要解析请求及其扩展,然后从相应的扩展中获取数据。 是的,我很确定,但我实际上不知道如何解析它。你了解 TLS 握手的工作原理吗? 当然,我们提供安全库作为我们的主要产品之一。您需要打开 RFC (tools.ietf.org/html/rfc5246) 并实施它。 哈哈,谢谢,这就像 100 页的纯技术。我想page 41 上的事情开始变得有趣了。这是提到扩展的地方,作为回报,RFC 3546 中对此进行了描述。哦,我的,哦,我的。 :D 嘿看那个,另一个答案是 Eugene 正在销售 2 万美元的产品作为答案。我想通过将人们指向一个巨大的 RFC 并让任务感到非常艰巨是一种销售策略? 【参考方案1】:对于任何感兴趣的人,这是 C/C++ 代码的暂定版本。到目前为止它已经奏效了。该函数返回服务器名称在包含 Client Hello 的字节数组中的位置以及 len
参数中名称的长度。
char *get_TLS_SNI(unsigned char *bytes, int* len)
unsigned char *curr;
unsigned char sidlen = bytes[43];
curr = bytes + 1 + 43 + sidlen;
unsigned short cslen = ntohs(*(unsigned short*)curr);
curr += 2 + cslen;
unsigned char cmplen = *curr;
curr += 1 + cmplen;
unsigned char *maxchar = curr + 2 + ntohs(*(unsigned short*)curr);
curr += 2;
unsigned short ext_type = 1;
unsigned short ext_len;
while(curr < maxchar && ext_type != 0)
ext_type = ntohs(*(unsigned short*)curr);
curr += 2;
ext_len = ntohs(*(unsigned short*)curr);
curr += 2;
if(ext_type == 0)
curr += 3;
unsigned short namelen = ntohs(*(unsigned short*)curr);
curr += 2;
*len = namelen;
return (char*)curr;
else curr += ext_len;
if (curr != maxchar) throw std::exception("incomplete SSL Client Hello");
return NULL; //SNI was not present
【讨论】:
【参考方案2】:使用 WireShark 并通过添加过滤器 tcp port 443
仅捕获 TLS (SSL) 包。然后找到“Client Hello”消息。你可以在下面看到它的原始数据。
展开Secure Socket Layer
->
TLSv1.2 Record Layer: Handshake Protocol: Client Hello
->
...
你会看到Extension: server_name
->
Server Name Indication extension
。握手包中的服务器名称未加密。
http://i.stack.imgur.com/qt0gu.png
【讨论】:
我们正在寻找一种编程方式来确定 SNI。但是,这可能对某些人来说很有趣,所以请不要删除它。【参考方案3】:我在sniproxy 中进行了此操作,在 Wireshark 中检查 TLS 客户端 hello 数据包,同时阅读 RFC 是一个很好的方法。这并不难,只是你必须跳过许多可变长度字段并检查是否有正确的元素类型。
我现在正在做我的测试,并且有这个带注释的示例数据包可能会有所帮助:
const unsigned char good_data_2[] =
// TLS record
0x16, // Content Type: Handshake
0x03, 0x01, // Version: TLS 1.0
0x00, 0x6c, // Length (use for bounds checking)
// Handshake
0x01, // Handshake Type: Client Hello
0x00, 0x00, 0x68, // Length (use for bounds checking)
0x03, 0x03, // Version: TLS 1.2
// Random (32 bytes fixed length)
0xb6, 0xb2, 0x6a, 0xfb, 0x55, 0x5e, 0x03, 0xd5,
0x65, 0xa3, 0x6a, 0xf0, 0x5e, 0xa5, 0x43, 0x02,
0x93, 0xb9, 0x59, 0xa7, 0x54, 0xc3, 0xdd, 0x78,
0x57, 0x58, 0x34, 0xc5, 0x82, 0xfd, 0x53, 0xd1,
0x00, // Session ID Length (skip past this much)
0x00, 0x04, // Cipher Suites Length (skip past this much)
0x00, 0x01, // NULL-MD5
0x00, 0xff, // RENEGOTIATION INFO SCSV
0x01, // Compression Methods Length (skip past this much)
0x00, // NULL
0x00, 0x3b, // Extensions Length (use for bounds checking)
// Extension
0x00, 0x00, // Extension Type: Server Name (check extension type)
0x00, 0x0e, // Length (use for bounds checking)
0x00, 0x0c, // Server Name Indication Length
0x00, // Server Name Type: host_name (check server name type)
0x00, 0x09, // Length (length of your data)
// "localhost" (data your after)
0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74,
// Extension
0x00, 0x0d, // Extension Type: Signature Algorithms (check extension type)
0x00, 0x20, // Length (skip past since this is the wrong extension)
// Data
0x00, 0x1e, 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,
// Extension
0x00, 0x0f, // Extension Type: Heart Beat (check extension type)
0x00, 0x01, // Length (skip past since this is the wrong extension)
0x01 // Mode: Peer allows to send requests
;
【讨论】:
这显然比我最初的半途而废的答案更详尽。打勾。 :D 太好了,我来这里是因为我想拥有一个基于 SNI 的非解密简单 TLS 转发器。所以使用已经完成的 sniproxy。【参考方案4】:我注意到域总是以两个零字节和一个长度字节为前缀。也许它是无符号的 24 位整数,但我无法测试它,因为我的 DNS 服务器不允许超过 77 个字符的域名。
根据这些知识,我想出了这个 (Node.js) 代码。
function getSNI(buf)
var sni = null
, regex = /^(?:[a-z0-9-]+\.)+[a-z]+$/i;
for(var b = 0, prev, start, end, str; b < buf.length; b++)
if(prev === 0 && buf[b] === 0)
start = b + 2;
end = start + buf[b + 1];
if(start < end && end < buf.length)
str = buf.toString("utf8", start, end);
if(regex.test(str))
sni = str;
continue;
prev = buf[b];
return sni;
此代码查找两个零字节的序列。如果它找到一个,它假定下面的字节是一个长度参数。它检查长度是否仍在缓冲区的边界内,如果是,则将字节序列读取为 UTF-8。稍后,可以对数组进行正则表达式并提取域。
效果非常好!不过,我还是注意到了一些奇怪的事情。
'�\n�\u0014\u0000�\u0000�\u00009\u00008�\u000f�\u0005\u0000�\u00005�\u0007�\t�\u0011�\u0013\u0000E\u0000D\u0000f\u00003\u00002�\f�\u000e�\u0002�\u0004\u0000�\u0000A\u0000\u0005\u0000\u0004\u0000/�\b�\u0012\u0000\u0016\u0000\u0013�\r�\u0003��\u0000\n'
'\u0000\u0015\u0000\u0000\u0012test.cubixcraft.de'
'test.cubixcraft.de'
'\u0000\b\u0000\u0006\u0000\u0017\u0000\u0018\u0000\u0019'
'\u0000\u0005\u0001\u0000\u0000'
始终,无论我选择哪个子域,该域都会被定位两次。 SNI 字段似乎嵌套在另一个字段中。
我愿意接受建议和改进! :)
我把它变成了一个 Node 模块,供所有关心的人使用:sni。
【讨论】:
我不认为正则表达式是从二进制加密协议中提取数据的最佳方式。 Client Hello 消息包含可能与您的正则表达式匹配的 32 个字节的随机数据。 我不知道它值得否决,我的意思是他找到了解决方案。我也遇到过同样的情况,但正如 dlundquist 所说,我不会依赖它保持一致或排除随机字节污染正则表达式匹配的可能性。但是它确实有效。以上是关于从 TLS 客户端 hello 中提取服务器名称指示 (SNI)的主要内容,如果未能解决你的问题,请参考以下文章