如何检测传入的 SSL (https) 握手(SSL 有线格式)?

Posted

技术标签:

【中文标题】如何检测传入的 SSL (https) 握手(SSL 有线格式)?【英文标题】:How to detect an incoming SSL (https) handshake (SSL wire format)? 【发布时间】:2011-04-23 07:26:51 【问题描述】:

我正在编写一个接受传入 TCP 连接的服务器。假设服务器已经接受了一个 TCP 连接,并且已经从客户端收到了 16 个(左右)字节。知道这 16 个字节,服务器如何检测客户端是否要发起 SSL 握手?

我做了一个实验,结果表明在我的 Linux 系统上通过 SSL 连接到 localhost(127.0.0.1 或 AF_UNIX)会使客户端发送以下握手(hexdump),然后是 16 个看似随机的字节:

8064010301004b0000001000003900003800003500001600001300000a07
00c000003300003200002f03008000000500000401008000001500001200
0009060040000014000011000008000006040080000003020080

服务器应该如何探测这些前几个字节,以便能够确定客户端是否正在发送 SSL 握手?对于所有有效的 SSL 握手,探测必须返回 true,并且对于客户端发送的不是 SSL 握手的消息,它必须返回 false 的概率很高。不允许使用任何库(如 OpenSSL)进行探测。探测必须是简单的代码(如 C 或 Python 中的几十行代码)。

【问题讨论】:

【参考方案1】:

客户端总是先发送所谓的 HelloClient 消息。它可以是 SSL 2 格式或 SSL 3.0 格式(与 TLS 1.0、1.1 和 1.2 中的格式相同)。

SSL 3.0/TLS 1.0/1.1/1.2 客户端也有可能使用旧格式 (SSL 2) 发送 HelloClient,只是数据中的版本号更高。因此,新客户端也需要检测 SSL 2 HelloClient。 (例如 Java SSL 实现就是这样做的)

假设'b'是你的缓冲区。我试图绘制消息格式。

SSL 2

+-----------------+------+-------
| 2 byte header   | 0x01 | etc.
+-----------------|------+-------

b[0] & 0x80 == 0x80(这意味着 b[0] 的最高有效位是 '1')

((b[0] & 0x7f) 9 (这意味着 b[0] 的低 7 位与 b[1] 一起是数据长度。你可以少在您的缓冲区中,因此您无法检查它们。但是从消息格式中我们知道有 2 个字节的 3 个字段(长度字段),以及密码列表字段中的至少一项(大小为 3)。所以至少应该有9 个字节(数据长度 >= 9)。

b[2] 必须为 0x01(消息类型“ClientHello”)

SSL 3.0 或 TLS 1.0、1.1 和 1.2

+-------+------------------+------------------+--------+------
| 0x16  | 2 bytes version  |  2 bytes length  |  0x01  |  etc.
+-------+------------------+------------------+--------+------

b[0] == 0x16(消息类型“SSL 握手”)

b[1] 应该是 0x03(目前是最新的主要版本,但谁知道以后呢?)

b[5] 必须为 0x01(握手协议消息“HelloClient”)

参考可以看http://www.mozilla.org/projects/security/pki/nss/ssl/draft02.html和https://www.rfc-editor.org/rfc/rfc4346

【讨论】:

为方便起见:SSL/TLS 版本按以下顺序排列:SSL 2 详细信息:它是 ClientHello 而不是 HelloClient。作为旁注,最近在this question 中讨论了使用 SSLv2 格式的 SSLv3。 @VinnieFalco b[2] 是版本,目前最新的是 01【参考方案2】:

我可以根据ClientHello.parse 方法的实现来解决这个问题 http://tlslite.cvs.sourceforge.net/viewvc/tlslite/tlslite/tlslite/messages.py?view=markup

我在这里用 Python 给出两个解决方案。 IsSSlClientHandshakeSimple 是一个简单的正则表达式,很容易产生一些误报; IsSslClientHandshake 更复杂:它检查长度的一致性,以及其他一些数字的范围。

import re

def IsSslClientHandshakeSimple(buf):
  return bool(re.match(r'(?s)\A(?:\x80[\x0f-\xff]\x01[\x00-\x09][\x00-\x1f]'
                       r'[\x00-\x05].\x00.\x00.|'
                       r'\x16[\x2c-\xff]\x01\x00[\x00-\x05].'
                       r'[\x00-\x09][\x00-\x1f])', buf))

def IsSslClientHandshake(buf):
  if len(buf) < 2:  # Missing record header.
    return False
  if len(buf) < 2 + ord(buf[1]):  # Incomplete record body.
    return False
  # TODO(pts): Support two-byte lengths in buf[1].
  if ord(buf[0]) == 0x80:  # SSL v2.
    if ord(buf[1]) < 9:  # Message body too short.
      return False
    if ord(buf[2]) != 0x01:  # Not client_hello.
      return False
    if ord(buf[3]) > 9:  # Client major version too large. (Good: 0x03)
      return False
    if ord(buf[4]) > 31:  # Client minor version too large. (Good: 0x01)
      return False
    cipher_specs_size = ord(buf[5]) << 8 | ord(buf[6])
    session_id_size = ord(buf[7]) << 8 | ord(buf[8])
    random_size = ord(buf[9]) << 8 | ord(buf[10])
    if ord(buf[1]) < 9 + cipher_specs_size + session_id_size + random_size:
      return False
    if cipher_specs_size % 3 != 0:  # Cipher specs not a multiple of 3 bytes.
      return False
  elif ord(buf[0]) == 0x16:  # SSL v1.
    # TODO(pts): Test this.
    if ord(buf[1]) < 39:  # Message body too short.
      return False
    if ord(buf[2]) != 0x01:  # Not client_hello.
      return False
    head_size = ord(buf[3]) << 16 | ord(buf[4]) << 8 | ord(buf[5])
    if ord(buf[1]) < head_size + 4:  # Head doesn't fit in message body.
      return False
    if ord(buf[6]) > 9:  # Client major version too large. (Good: 0x03)
      return False
    if ord(buf[7]) > 31:  # Client minor version too large. (Good: 0x01)
      return False
    # random is at buf[8 : 40]
    session_id_size = ord(buf[40])
    i = 41 + session_id_size
    if ord(buf[1]) < i + 2:  # session_id + cipher_suites_size doesn't fit.
      return False
    cipher_specs_size = ord(buf[i]) << 8 | ord(buf[i + 1])
    if cipher_specs_size % 2 != 0:
      return False
    i += 2 + cipher_specs_size
    if ord(buf[1]) < i + 1: # cipher_specs + c..._methods_size doesn't fit.
      return False
    if ord(buf[1]) < i + 1 + ord(buf[i]): # compression_methods doesn't fit.
      return False
  else:  # Not SSL v1 or SSL v2.
    return False
return True

【讨论】:

但是故意制作数据包来欺骗您的检测方法是微不足道的。也许这对你来说无关紧要。 @GregS:你是对的(这是微不足道的,没关系)。我要避免的是 1. 一些其他重要(非 SSL)协议的野包被检测为 SSL; 2. SSL 数据包没有被检测为 SSL。

以上是关于如何检测传入的 SSL (https) 握手(SSL 有线格式)?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Netty 中的 ssl 握手之前优雅地关闭频道?

Qt SSL:从 QML 发出的请求握手失败

SSL 握手问题

SSL握手成功后如何获取peer的QSslCertificate

HTTPS协议的SSL握手过程

SSL/TLS 握手优化详解