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

时间:2013-07-24 11:24:18

标签: ssl language-agnostic protocols binary-data rfc

如何从TLS客户端Hello消息中提取服务器名称指示。我很难在TLS Extensions上理解这个非常神秘的RFC 3546,其中定义了SNI。

到目前为止我已经理解的事情:

  • 当你输入缓冲区时,主机是utf8编码和可读的。
  • 主机前一个字节,确定它的长度。

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

4 个答案:

答案 0 :(得分:29)

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

答案 1 :(得分:5)

使用WireShark并通过添加过滤器tcp port 443仅捕获TLS(SSL)包。然后找到一个"客户端Hello"信息。您可以在下面看到其原始数据。

展开Secure Socket Layer -> TLSv1.2 Record Layer: Handshake Protocol: Client Hello -> ...
您会看到Extension: server_name -> Server Name Indication extension。 Handshake包中的服务器名称未加密。

http://i.stack.imgur.com/qt0gu.png

答案 2 :(得分:1)

我注意到域总是前置两个零字节和一个长度字节。也许它是无符号的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字段嵌套在另一个字段中。

我愿意接受建议和改进! :)

我把它变成了一个节点模块,供所有关心的人使用:sni

答案 3 :(得分: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
}