web-dev-qa-db-ja.com

TLSクライアントからサーバー名表示(SNI)を抽出するhello

TLSクライアントHelloメッセージからサーバー名表示をどのように抽出しますか。私は現在、SNIが定義されているTLS拡張でこれverycryptic RFC 3546 を理解するのに苦労しています。

私がこれまでに理解したこと:

  • ホストはutf8でエンコードされており、utf8がバッファーを暗号化すると読み取り可能になります。
  • ホストの前に1バイトあり、ホストの長さを決定します。

その長さのバイトの正確な位置を見つけることができれば、SNIの抽出は非常に簡単です。しかし、そもそもどうすればそのバイトに到達できますか?

13
buschtoens

私はこれを sniproxy で行い、RFCがかなり良い方法であることを読みながら、WiresharkでTLSクライアントのhelloパケットを調べました。それほど難しいことではありません。多くの可変長フィールドをスキップして、正しい要素タイプがあるかどうかを確認する必要があります。

私は現在テストに取り組んでおり、次のような注釈付きのサンプルパケットがあります。

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
};
34
dlundquist

WireSharkを使用し、フィルターtcp port 443を追加してTLS(SSL)パッケージのみをキャプチャします。次に、「ClientHello」メッセージを見つけます。以下にその生データを見ることができます。

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

6
LYF

ドメインには常に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;
}

このコードは、2つのゼロバイトのシーケンスを探します。見つかった場合は、次のバイトが長さパラメーターであると見なします。長さがまだバッファの境界にあるかどうかをチェックし、ある場合はバイトシーケンスを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'

常に、どのサブドメインを選択しても、ドメインは2回ターゲットにされます。 SNIフィールドが別のフィールド内にネストされているようです。

私は提案や改善を受け入れています! :)

私はこれをNodeモジュールに変えました、気にするすべての人のために: sni

2
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
}
2
Æðelstan