Извлечь индикацию имени сервера (SNI) из клиента TLS привет

Как бы вы извлекли индикацию имени сервера из сообщения приветствия клиента TLS. Я сейчас изо всех сил пытаюсь понять этот очень загадочный RFC 3546 на расширениях TLS, в котором определен SNI.

Вещи, которые я понял до сих пор:

  • Хост кодируется в формате utf8 и доступен для чтения при включении буфера в utf8.
  • Существует один байт перед хостом, который определяет его длину.

Если бы я мог узнать точное положение этого байта длины, извлечь SNI было бы довольно просто. Но как мне добраться до этого байта в первую очередь?

4 ответа

Решение

Я сделал это в sniproxy, изучая приветственный пакет клиента TLS в Wireshark, читая, что 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
};

Используйте WireShark и захватывайте только пакеты TLS (SSL), добавляя фильтр tcp port 443, Затем найдите сообщение "Привет клиент". Вы можете увидеть его необработанные данные ниже.

расширять Secure Socket Layer->TLSv1.2 Record Layer: Handshake Protocol: Client Hello->...
и ты увидишь Extension: server_name->Server Name Indication extension, Имя сервера в пакете Handshake не зашифровано.

Для всех, кто интересуется, это предварительная версия кода C/C++. Это сработало до сих пор. Функция возвращает позицию имени сервера в байтовом массиве, содержащем 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
}

Я заметил, что домен всегда предваряется двумя нулевыми байтами и одним байтом длины. Может быть, это 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.

Другие вопросы по тегам