Извлечение указания имени сервера (SNI) из клиента TLS hello

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

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

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

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


Я заметил, что домен всегда добавляется двумя нулевыми байтами и одним байтом длины. Возможно, это беззнаковое 24-битное целое число, но я не могу его проверить, так как мой DNS-сервер не позволит доменным именам превышать 77 символов.

на основании этого знания я придумал это (узел.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.


для всех, кто заинтересован, это предварительная версия кода C / C++. До сих пор это срабатывало. Функция возвращает позицию имени сервера в массиве байтов, содержащем клиент Hello и длину имени в