从TLS客户端提取服务器名称指示(SNI)hello

问题描述:

如何从TLS客户端问候语中提取服务器名称指示。我正努力在TLS扩展上努力了解这个非常隐秘RFC 3546,其中定义了SNI。从TLS客户端提取服务器名称指示(SNI)hello

事情我已经到目前为止明白:

  • 主机是UTF8编码,当你UTF8 enocde缓冲区读取。
  • Theres在主机之前一个字节,它决定了它的长度。

如果我能找出该长度字节的确切位置,提取SNI将非常简单。但是,我怎么才能到达那个字节呢?

+3

您试图采取的直接方法是错误的。您需要解析包含扩展名的请求,然后从相应的扩展名中获取数据。 –

+0

是的,我很确定,但我实际上不知道如何解析它。你了解TLS握手的工作原理吗? – buschtoens

+0

当然,我的确提供安全库作为我们的主要产品之一。您需要打开RFC(http://tools.ietf.org/html/rfc5246)并实施它。 –

我在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 
}; 
+0

很酷,感谢分享。 +1 – 2014-10-24 07:09:57

+0

这显然比我最初的一半答案更详细。打个勾。 :D – buschtoens

+0

太好了,我来到这里是因为我想要一个基于SNI的非解密简单TLS转发器。因此,已经完成的sniproxy。 – JanKanis

我注意到域总是由两个零字节和一个长度字节预先安排。也许它是无符号的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读取。稍后,可以RegEx数组并提取域。

作品非常好!不过,我注意到一些奇怪的东西

'�\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

+0

downvote的原因是什么? – buschtoens

+2

我不认为正则表达式是从二进制密码协议中提取数据的最佳方式。客户端Hello消息包含32个字节的随机数据,可能与您的正则表达式匹配。 – dlundquist

+0

我不知道它值得赞成,我的意思是他找到了解决办法。我遇到过类似dlundquist笔记,我不会依赖它是一致的或排除随机字节污染正则表达式匹配的可能性。它确实工作。 – 2014-10-24 07:09:34

使用Wireshark并添加滤镜tcp port 443只捕获TLS(SSL)封装。然后找到一个“客户问候”消息。你可以在下面看到它的原始数据。

展开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

+1

我们正在寻找编程方式来确定SNI。不过,这对一些人来说可能会很有趣,所以请不要删除它。 – buschtoens

任何有兴趣,这是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 
}