class SSLv2ClientHello(_SSLv2Handshake): """ SSLv2 ClientHello. """ name = "SSLv2 Handshake - Client Hello" fields_desc = [ByteEnumField("msgtype", 1, _sslv2_handshake_type), _TLSVersionField("version", 0x0002, _tls_version), FieldLenField("cipherslen", None, fmt="!H", length_of="ciphers"), FieldLenField("sidlen", None, fmt="!H", length_of="sid"), FieldLenField("challengelen", None, fmt="!H", length_of="challenge"), XStrLenField("sid", b"", length_from=lambda pkt:pkt.sidlen), _SSLv2CipherSuitesField("ciphers", [SSL_CK_DES_192_EDE3_CBC_WITH_MD5], _tls_cipher_suites, length_from=lambda pkt: pkt.cipherslen), XStrLenField("challenge", b"", length_from=lambda pkt:pkt.challengelen)] def tls_session_update(self, msg_str): super(SSLv2ClientHello, self).tls_session_update(msg_str) self.tls_session.advertised_tls_version = self.version self.tls_session.sslv2_common_cs = self.ciphers self.tls_session.sslv2_challenge = self.challenge
class TLSHelloRetryRequest(_TLSHandshake): name = "TLS 1.3 Handshake - Hello Retry Request" fields_desc = [ ByteEnumField("msgtype", 6, _tls_handshake_type), ThreeBytesField("msglen", None), _TLSVersionField("version", None, _tls_version), _ExtensionsLenField("extlen", None, length_of="ext"), _ExtensionsField("ext", None, length_from=lambda pkt: pkt.msglen - 4) ]
class TLS13ServerHello(TLSClientHello): """ TLS 1.3 ServerHello """ name = "TLS 1.3 Handshake - Server Hello" fields_desc = [ ByteEnumField("msgtype", 2, _tls_handshake_type), ThreeBytesField("msglen", None), _TLSVersionField("version", None, _tls_version), _TLSRandomBytesField("random_bytes", None, 32), EnumField("cipher", None, _tls_cipher_suites), _ExtensionsLenField("extlen", None, length_of="ext"), _ExtensionsField("ext", None, length_from=lambda pkt: (pkt.msglen - 38)) ] def tls_session_update(self, msg_str): """ Either for parsing or building, we store the server_random along with the raw string representing this handshake message. We also store the cipher suite (if recognized), and finally we instantiate the write and read connection states. """ super(TLSClientHello, self).tls_session_update(msg_str) s = self.tls_session s.tls_version = self.version s.server_random = self.random_bytes cs_cls = None if self.cipher: cs_val = self.cipher if cs_val not in _tls_cipher_suites_cls: warning("Unknown cipher suite %d from ServerHello" % cs_val) # we do not try to set a default nor stop the execution else: cs_cls = _tls_cipher_suites_cls[cs_val] connection_end = s.connection_end s.pwcs = writeConnState(ciphersuite=cs_cls, connection_end=connection_end, tls_version=self.version) s.triggered_pwcs_commit = True s.prcs = readConnState(ciphersuite=cs_cls, connection_end=connection_end, tls_version=self.version) s.triggered_prcs_commit = True if self.tls_session.tls13_early_secret is None: # In case the connState was not pre-initialized, we could not # compute the early secrets at the ClientHello, so we do it here. self.tls_session.compute_tls13_early_secrets() s.compute_tls13_handshake_secrets()
class SSLv2ServerHello(_SSLv2Handshake): """ SSLv2 ServerHello. """ name = "SSLv2 Handshake - Server Hello" fields_desc = [ ByteEnumField("msgtype", 4, _sslv2_handshake_type), ByteField("sid_hit", 0), ByteEnumField("certtype", 1, {1: "x509_cert"}), _TLSVersionField("version", 0x0002, _tls_version), FieldLenField("certlen", None, fmt="!H", length_of="cert"), FieldLenField("cipherslen", None, fmt="!H", length_of="ciphers"), FieldLenField("connection_idlen", None, fmt="!H", length_of="connection_id"), _SSLv2CertDataField("cert", b"", length_from=lambda pkt: pkt.certlen), _SSLv2CipherSuitesField( "ciphers", [], _tls_cipher_suites, length_from=lambda pkt: pkt.cipherslen), # noqa: E501 XStrLenField("connection_id", b"", length_from=lambda pkt: pkt.connection_idlen) ] def tls_session_update(self, msg_str): """ XXX Something should be done about the session ID here. """ super(SSLv2ServerHello, self).tls_session_update(msg_str) s = self.tls_session client_cs = s.sslv2_common_cs css = [cs for cs in client_cs if cs in self.ciphers] s.sslv2_common_cs = css s.sslv2_connection_id = self.connection_id s.tls_version = self.version if self.cert is not None: s.server_certs = [self.cert]
class TLS(_GenericTLSSessionInheritance): """ The generic TLS Record message, based on section 6.2 of RFC 5246. When reading a TLS message, we try to parse as much as we can. In .pre_dissect(), according to the type of the current cipher algorithm (self.tls_session.rcs.cipher.type), we extract the 'iv', 'mac', 'pad' and 'padlen'. Some of these fields may remain blank: for instance, when using a stream cipher, there is no IV nor any padding. The 'len' should always hold the length of the ciphered message; for the plaintext version, you should rely on the additional 'deciphered_len' attribute. XXX Fix 'deciphered_len' which should not be defined when failing with AEAD decryption. This is related to the 'decryption_success' below. Also, follow this behaviour in record_sslv2.py and record_tls13.py Once we have isolated the ciphered message aggregate (which should be one or several TLS messages of the same type), we try to decipher it. Either we succeed and store the clear data in 'msg', or we graciously fail with a CipherError and store the ciphered data in 'msg'. Unless the user manually provides the session secrets through the passing of a 'tls_session', obviously the ciphered messages will not be deciphered. Indeed, the need for a proper context may also present itself when trying to parse clear handshake messages. For instance, suppose you sniffed the beginning of a DHE-RSA negotiation:: t1 = TLS(<client_hello>) t2 = TLS(<server_hello | certificate | server_key_exchange>) t3 = TLS(<server_hello | certificate | server_key_exchange>, tls_session=t1.tls_session) (Note that to do things properly, here 't1.tls_session' should actually be 't1.tls_session.mirror()'. See session.py for explanations.) As no context was passed to t2, neither was any client_random. Hence Scapy will not be able to verify the signature of the server_key_exchange inside t2. However, it should be able to do so for t3, thanks to the tls_session. The consequence of not having a complete TLS context is even more obvious when trying to parse ciphered content, as we described before. Thus, in order to parse TLS-protected communications with Scapy: _either Scapy reads every message from one side of the TLS connection and builds every message from the other side (as such, it should know the secrets needed for the generation of the pre_master_secret), while passing the same tls_session context (this is how our automaton.py mostly works); _or, if Scapy did not build any TLS message, it has to create a TLS context and feed it with secrets retrieved by whatever technique. Note that the knowing the private key of the server certificate will not be sufficient if a PFS ciphersuite was used. However, if you got a master_secret somehow, use it with tls_session.(w|r)cs.derive_keys() and leave the rest to Scapy. When building a TLS message with raw_stateful, we expect the tls_session to have the right parameters for ciphering. Else, .post_build() might fail. """ __slots__ = ["deciphered_len"] name = "TLS" fields_desc = [ ByteEnumField("type", None, _tls_type), _TLSVersionField("version", None, _tls_version), _TLSLengthField("len", None), _TLSIVField("iv", None), _TLSMsgListField("msg", []), _TLSMACField("mac", None), _TLSPadField("pad", None), _TLSPadLenField("padlen", None) ] def __init__(self, *args, **kargs): self.deciphered_len = kargs.get("deciphered_len", None) super(TLS, self).__init__(*args, **kargs) @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): """ If the TLS class was called on raw SSLv2 data, we want to return an SSLv2 record instance. We acknowledge the risk of SSLv2 packets with a msglen of 0x1403, 0x1503, 0x1603 or 0x1703 which will never be casted as SSLv2 records but TLS ones instead, but hey, we can't be held responsible for low-minded extensibility choices. """ if _pkt is not None: plen = len(_pkt) if plen >= 2: byte0, byte1 = struct.unpack("BB", _pkt[:2]) s = kargs.get("tls_session", None) if byte0 not in _tls_type or byte1 != 3: # Unknown type # Check SSLv2: either the session is already SSLv2, # either the packet looks like one. As said above, this # isn't 100% reliable, but Wireshark does the same if s and (s.tls_version == 0x0002 or s.advertised_tls_version == 0x0002) or \ (_ssl_looks_like_sslv2(_pkt) and (not s or s.tls_version is None)): from scapy.layers.tls.record_sslv2 import SSLv2 return SSLv2 # Not SSLv2: continuation return _TLSEncryptedContent # Check TLS 1.3 if s and _tls_version_check(s.tls_version, 0x0304): if (s.rcs and not isinstance(s.rcs.cipher, Cipher_NULL) and byte0 == 0x17): from scapy.layers.tls.record_tls13 import TLS13 return TLS13 if plen < 5: # Layer detected as TLS but too small to be a # parsed. Scapy should not try to decode them return _TLSEncryptedContent return TLS # Parsing methods def _tls_auth_decrypt(self, hdr, s): """ Provided with the record header and AEAD-ciphered data, return the sliced and clear tuple (nonce, TLSCompressed.fragment, mac). Note that we still return the slicing of the original input in case of decryption failure. Also, if the integrity check fails, a warning will be issued, but we still return the sliced (unauthenticated) plaintext. """ try: read_seq_num = struct.pack("!Q", self.tls_session.rcs.seq_num) self.tls_session.rcs.seq_num += 1 # self.type and self.version have not been parsed yet, # this is why we need to look into the provided hdr. add_data = read_seq_num + hdr[:3] # Last two bytes of add_data are appended by the return function return self.tls_session.rcs.cipher.auth_decrypt( add_data, s, read_seq_num) except CipherError as e: return e.args except AEADTagError as e: pkt_info = self.firstlayer().summary() log_runtime.info("TLS: record integrity check failed [%s]", pkt_info) # noqa: E501 return e.args def _tls_decrypt(self, s): """ Provided with stream- or block-ciphered data, return the clear version. The cipher should have been updated with the right IV early on, which should not be at the beginning of the input. In case of decryption failure, a CipherError will be raised with the slicing of the original input as first argument. """ return self.tls_session.rcs.cipher.decrypt(s) def _tls_hmac_verify(self, hdr, msg, mac): """ Provided with the record header, the TLSCompressed.fragment and the HMAC, return True if the HMAC is correct. If we could not compute the HMAC because the key was missing, there is no sense in verifying anything, thus we also return True. Meant to be used with a block cipher or a stream cipher. It would fail with an AEAD cipher, because rcs.hmac would be None. See RFC 5246, section 6.2.3. """ read_seq_num = struct.pack("!Q", self.tls_session.rcs.seq_num) self.tls_session.rcs.seq_num += 1 mac_len = self.tls_session.rcs.mac_len if mac_len == 0: # should be TLS_NULL_WITH_NULL_NULL return True if len(mac) != mac_len: return False alg = self.tls_session.rcs.hmac version = struct.unpack("!H", hdr[1:3])[0] try: if version > 0x300: h = alg.digest(read_seq_num + hdr + msg) elif version == 0x300: h = alg.digest_sslv3(read_seq_num + hdr[:1] + hdr[3:5] + msg) else: raise Exception("Unrecognized version.") except HMACError: h = mac return h == mac def _tls_decompress(self, s): """ Provided with the TLSCompressed.fragment, return the TLSPlaintext.fragment. """ alg = self.tls_session.rcs.compression return alg.decompress(s) def pre_dissect(self, s): """ Decrypt, verify and decompress the message, i.e. apply the previous methods according to the reading cipher type. If the decryption was successful, 'len' will be the length of the TLSPlaintext.fragment. Else, it should be the length of the _TLSEncryptedContent. """ if len(s) < 5: raise Exception("Invalid record: header is too short.") msglen = struct.unpack('!H', s[3:5])[0] hdr, efrag, r = s[:5], s[5:5 + msglen], s[msglen + 5:] iv = mac = pad = b"" self.padlen = None decryption_success = False cipher_type = self.tls_session.rcs.cipher.type def extract_mac(data): """Extract MAC.""" tmp_len = self.tls_session.rcs.mac_len if tmp_len != 0: frag, mac = data[:-tmp_len], data[-tmp_len:] else: frag, mac = data, b"" return frag, mac def verify_mac(hdr, cfrag, mac): """Verify integrity.""" chdr = hdr[:3] + struct.pack('!H', len(cfrag)) is_mac_ok = self._tls_hmac_verify(chdr, cfrag, mac) if not is_mac_ok: pkt_info = self.firstlayer().summary() log_runtime.info( "TLS: record integrity check failed [%s]", pkt_info, ) if cipher_type == 'block': version = struct.unpack("!H", s[1:3])[0] if self.tls_session.encrypt_then_mac: efrag, mac = extract_mac(efrag) verify_mac(hdr, efrag, mac) # Decrypt try: if version >= 0x0302: # Explicit IV for TLS 1.1 and 1.2 block_size = self.tls_session.rcs.cipher.block_size iv, efrag = efrag[:block_size], efrag[block_size:] self.tls_session.rcs.cipher.iv = iv pfrag = self._tls_decrypt(efrag) else: # Implicit IV for SSLv3 and TLS 1.0 pfrag = self._tls_decrypt(efrag) except CipherError as e: # This will end up dissected as _TLSEncryptedContent. cfrag = e.args[0] else: decryption_success = True # Excerpt below better corresponds to TLS 1.1 IV definition, # but the result is the same as with TLS 1.2 anyway. # This leading *IV* has been decrypted by _tls_decrypt with a # random IV, hence it does not correspond to anything. # What actually matters is that we got the first encrypted block # noqa: E501 # in order to decrypt the second block (first data block). # if version >= 0x0302: # block_size = self.tls_session.rcs.cipher.block_size # iv, pfrag = pfrag[:block_size], pfrag[block_size:] # l = struct.unpack('!H', hdr[3:5])[0] # hdr = hdr[:3] + struct.pack('!H', l-block_size) # Extract padding ('pad' actually includes the trailing padlen) padlen = orb(pfrag[-1]) + 1 mfrag, pad = pfrag[:-padlen], pfrag[-padlen:] self.padlen = padlen if self.tls_session.encrypt_then_mac: cfrag = mfrag else: cfrag, mac = extract_mac(mfrag) verify_mac(hdr, cfrag, mac) elif cipher_type == 'stream': # Decrypt try: pfrag = self._tls_decrypt(efrag) except CipherError as e: # This will end up dissected as _TLSEncryptedContent. cfrag = e.args[0] else: decryption_success = True cfrag, mac = extract_mac(pfrag) verify_mac(hdr, cfrag, mac) elif cipher_type == 'aead': # Authenticated encryption # crypto/cipher_aead.py prints a warning for integrity failure if (conf.crypto_valid_advanced and isinstance(self.tls_session.rcs.cipher, Cipher_CHACHA20_POLY1305)): # noqa: E501 iv = b"" cfrag, mac = self._tls_auth_decrypt(hdr, efrag) else: iv, cfrag, mac = self._tls_auth_decrypt(hdr, efrag) decryption_success = True # see XXX above frag = self._tls_decompress(cfrag) if decryption_success: self.deciphered_len = len(frag) else: self.deciphered_len = None reconstructed_body = iv + frag + mac + pad return hdr + reconstructed_body + r def post_dissect(self, s): """ Commit the pending r/w state if it has been triggered (e.g. by an underlying TLSChangeCipherSpec or a SSLv2ClientMasterKey). We update nothing if the prcs was not set, as this probably means that we're working out-of-context (and we need to keep the default rcs). """ if (self.tls_session.tls_version and self.tls_session.tls_version <= 0x0303): if self.tls_session.triggered_prcs_commit: if self.tls_session.prcs is not None: self.tls_session.rcs = self.tls_session.prcs self.tls_session.prcs = None self.tls_session.triggered_prcs_commit = False if self.tls_session.triggered_pwcs_commit: if self.tls_session.pwcs is not None: self.tls_session.wcs = self.tls_session.pwcs self.tls_session.pwcs = None self.tls_session.triggered_pwcs_commit = False return s def do_dissect_payload(self, s): """ Try to dissect the following data as a TLS message. Note that overloading .guess_payload_class() would not be enough, as the TLS session to be used would get lost. """ if s: try: p = TLS(s, _internal=1, _underlayer=self, tls_session=self.tls_session) except KeyboardInterrupt: raise except Exception: p = conf.raw_layer(s, _internal=1, _underlayer=self) self.add_payload(p) # Building methods def _tls_compress(self, s): """ Provided with the TLSPlaintext.fragment, return the TLSCompressed.fragment. """ alg = self.tls_session.wcs.compression return alg.compress(s) def _tls_auth_encrypt(self, s): """ Return the TLSCiphertext.fragment for AEAD ciphers, i.e. the whole GenericAEADCipher. Also, the additional data is computed right here. """ write_seq_num = struct.pack("!Q", self.tls_session.wcs.seq_num) self.tls_session.wcs.seq_num += 1 add_data = (write_seq_num + pkcs_i2osp(self.type, 1) + pkcs_i2osp(self.version, 2) + pkcs_i2osp(len(s), 2)) return self.tls_session.wcs.cipher.auth_encrypt( s, add_data, write_seq_num) def _tls_hmac_add(self, hdr, msg): """ Provided with the record header (concatenation of the TLSCompressed type, version and length fields) and the TLSCompressed.fragment, return the concatenation of the TLSCompressed.fragment and the HMAC. Meant to be used with a block cipher or a stream cipher. It would fail with an AEAD cipher, because wcs.hmac would be None. See RFC 5246, section 6.2.3. """ write_seq_num = struct.pack("!Q", self.tls_session.wcs.seq_num) self.tls_session.wcs.seq_num += 1 alg = self.tls_session.wcs.hmac version = struct.unpack("!H", hdr[1:3])[0] if version > 0x300: h = alg.digest(write_seq_num + hdr + msg) elif version == 0x300: h = alg.digest_sslv3(write_seq_num + hdr[:1] + hdr[3:5] + msg) else: raise Exception("Unrecognized version.") return msg + h def _tls_pad(self, s): """ Provided with the concatenation of the TLSCompressed.fragment and the HMAC, append the right padding and return it as a whole. This is the TLS-style padding: while SSL allowed for random padding, TLS (misguidedly) specifies the repetition of the same byte all over, and this byte must be equal to len(<entire padding>) - 1. Meant to be used with a block cipher only. """ padding = b"" block_size = self.tls_session.wcs.cipher.block_size padlen = block_size - ((len(s) + 1) % block_size) if padlen == block_size: padlen = 0 pad_pattern = chb(padlen) padding = pad_pattern * (padlen + 1) return s + padding def _tls_encrypt(self, s): """ Return the stream- or block-ciphered version of the concatenated input. In case of GenericBlockCipher, no IV has been specifically prepended to the output, so this might not be the whole TLSCiphertext.fragment yet. """ return self.tls_session.wcs.cipher.encrypt(s) def post_build(self, pkt, pay): """ Apply the previous methods according to the writing cipher type. """ # Compute the length of TLSPlaintext fragment hdr, frag = pkt[:5], pkt[5:] tmp_len = len(frag) hdr = hdr[:3] + struct.pack("!H", tmp_len) # Compression cfrag = self._tls_compress(frag) tmp_len = len(cfrag) # Update the length as a result of compression hdr = hdr[:3] + struct.pack("!H", tmp_len) cipher_type = self.tls_session.wcs.cipher.type if cipher_type == 'block': # Integrity if not self.tls_session.encrypt_then_mac: cfrag = self._tls_hmac_add(hdr, cfrag) # Excerpt below better corresponds to TLS 1.1 IV definition, # but the result is the same as with TLS 1.2 anyway. # if self.version >= 0x0302: # l = self.tls_session.wcs.cipher.block_size # iv = randstring(l) # mfrag = iv + mfrag # Add padding pfrag = self._tls_pad(cfrag) # Encryption if self.version >= 0x0302: # Explicit IV for TLS 1.1 and 1.2 tmp_len = self.tls_session.wcs.cipher.block_size iv = randstring(tmp_len) self.tls_session.wcs.cipher.iv = iv efrag = self._tls_encrypt(pfrag) efrag = iv + efrag else: # Implicit IV for SSLv3 and TLS 1.0 efrag = self._tls_encrypt(pfrag) if self.tls_session.encrypt_then_mac: efrag = self._tls_hmac_add(hdr, efrag) elif cipher_type == "stream": # Integrity mfrag = self._tls_hmac_add(hdr, cfrag) # Encryption efrag = self._tls_encrypt(mfrag) elif cipher_type == "aead": # Authenticated encryption (with nonce_explicit as header) efrag = self._tls_auth_encrypt(cfrag) if self.len is not None: # The user gave us a 'len', let's respect this ultimately hdr = hdr[:3] + struct.pack("!H", self.len) else: # Update header with the length of TLSCiphertext.fragment hdr = hdr[:3] + struct.pack("!H", len(efrag)) # Now we commit the pending write state if it has been triggered (e.g. # by an underlying TLSChangeCipherSpec or a SSLv2ClientMasterKey). We # update nothing if the pwcs was not set. This probably means that # we're working out-of-context (and we need to keep the default wcs). if self.tls_session.triggered_pwcs_commit: if self.tls_session.pwcs is not None: self.tls_session.wcs = self.tls_session.pwcs self.tls_session.pwcs = None self.tls_session.triggered_pwcs_commit = False return hdr + efrag + pay def mysummary(self): s = super(TLS, self).mysummary() if self.msg: s += " / " s += " / ".join(getattr(x, "_name", x.name) for x in self.msg) return s
class TLSServerHello(TLSClientHello): """ TLS ServerHello, with abilities to handle extensions. The Random structure follows the RFC 5246: while it is 32-byte long, many implementations use the first 4 bytes as a gmt_unix_time, and then the remaining 28 byts should be completely random. This was designed in order to (sort of) mitigate broken RNGs. If you prefer to show the full 32 random bytes without any GMT time, just comment in/out the lines below. """ name = "TLS Handshake - Server Hello" fields_desc = [ ByteEnumField("msgtype", 2, _tls_handshake_type), ThreeBytesField("msglen", None), _TLSVersionField("version", None, _tls_version), #_TLSRandomBytesField("random_bytes", None, 32), _GMTUnixTimeField("gmt_unix_time", None), _TLSRandomBytesField("random_bytes", None, 28), FieldLenField("sidlen", None, length_of="sid", fmt="B"), _SessionIDField("sid", "", length_from=lambda pkt: pkt.sidlen), EnumField("cipher", None, _tls_cipher_suites), _CompressionMethodsField("comp", [0], _tls_compression_algs, itemfmt="B", length_from=lambda pkt: 1), _ExtensionsLenField("extlen", None, length_of="ext"), _ExtensionsField("ext", None, length_from=lambda pkt: (pkt.msglen - (pkt.sidlen or 0) - 38)) ] #40)) ] @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): if _pkt and len(_pkt) >= 6: version = struct.unpack("!H", _pkt[4:6])[0] if version == 0x0304 or version > 0x7f00: return TLS13ServerHello return TLSServerHello def tls_session_update(self, msg_str): """ Either for parsing or building, we store the server_random along with the raw string representing this handshake message. We also store the session_id, the cipher suite (if recognized), the compression method, and finally we instantiate the pending write and read connection states. Usually they get updated later on in the negotiation when we learn the session keys, and eventually they are committed once a ChangeCipherSpec has been sent/received. """ super(TLSClientHello, self).tls_session_update(msg_str) self.tls_session.tls_version = self.version self.random_bytes = msg_str[10:38] self.tls_session.server_random = ( struct.pack('!I', self.gmt_unix_time) + self.random_bytes) self.tls_session.sid = self.sid cs_cls = None if self.cipher: cs_val = self.cipher if cs_val not in _tls_cipher_suites_cls: warning("Unknown cipher suite %d from ServerHello" % cs_val) # we do not try to set a default nor stop the execution else: cs_cls = _tls_cipher_suites_cls[cs_val] comp_cls = Comp_NULL if self.comp: comp_val = self.comp[0] if comp_val not in _tls_compression_algs_cls: err = "Unknown compression alg %d from ServerHello" % comp_val warning(err) comp_val = 0 comp_cls = _tls_compression_algs_cls[comp_val] connection_end = self.tls_session.connection_end self.tls_session.pwcs = writeConnState(ciphersuite=cs_cls, compression_alg=comp_cls, connection_end=connection_end, tls_version=self.version) self.tls_session.prcs = readConnState(ciphersuite=cs_cls, compression_alg=comp_cls, connection_end=connection_end, tls_version=self.version)
class TLS13(_GenericTLSSessionInheritance): __slots__ = ["deciphered_len"] name = "TLS 1.3" fields_desc = [ ByteEnumField("type", 0x17, _tls_type), _TLSVersionField("version", 0x0301, _tls_version), _TLSLengthField("len", None), _TLSInnerPlaintextField("inner", TLSInnerPlaintext()), _TLSMACField("auth_tag", None) ] def __init__(self, *args, **kargs): self.deciphered_len = kargs.get("deciphered_len", None) super(TLS13, self).__init__(*args, **kargs) # Parsing methods def _tls_auth_decrypt(self, s): """ Provided with the record header and AEAD-ciphered data, return the sliced and clear tuple (TLSInnerPlaintext, tag). Note that we still return the slicing of the original input in case of decryption failure. Also, if the integrity check fails, a warning will be issued, but we still return the sliced (unauthenticated) plaintext. """ rcs = self.tls_session.rcs read_seq_num = struct.pack("!Q", rcs.seq_num) rcs.seq_num += 1 try: return rcs.cipher.auth_decrypt(b"", s, read_seq_num) except CipherError as e: return e.args except AEADTagError as e: pkt_info = self.firstlayer().summary() log_runtime.info("TLS: record integrity check failed [%s]", pkt_info) # noqa: E501 return e.args def pre_dissect(self, s): """ Decrypt, verify and decompress the message. """ if len(s) < 5: raise Exception("Invalid record: header is too short.") if isinstance(self.tls_session.rcs.cipher, Cipher_NULL): self.deciphered_len = None return s else: msglen = struct.unpack('!H', s[3:5])[0] hdr, efrag, r = s[:5], s[5:5 + msglen], s[msglen + 5:] frag, auth_tag = self._tls_auth_decrypt(efrag) self.deciphered_len = len(frag) return hdr + frag + auth_tag + r def post_dissect(self, s): """ Commit the pending read state if it has been triggered. We update nothing if the prcs was not set, as this probably means that we're working out-of-context (and we need to keep the default rcs). """ if self.tls_session.triggered_prcs_commit: if self.tls_session.prcs is not None: self.tls_session.rcs = self.tls_session.prcs self.tls_session.prcs = None self.tls_session.triggered_prcs_commit = False return s def do_dissect_payload(self, s): """ Try to dissect the following data as a TLS message. Note that overloading .guess_payload_class() would not be enough, as the TLS session to be used would get lost. """ if s: try: p = TLS(s, _internal=1, _underlayer=self, tls_session=self.tls_session) except KeyboardInterrupt: raise except Exception: p = conf.raw_layer(s, _internal=1, _underlayer=self) self.add_payload(p) # Building methods def _tls_auth_encrypt(self, s): """ Return the TLSCiphertext.encrypted_record for AEAD ciphers. """ wcs = self.tls_session.wcs write_seq_num = struct.pack("!Q", wcs.seq_num) wcs.seq_num += 1 return wcs.cipher.auth_encrypt(s, b"", write_seq_num) def post_build(self, pkt, pay): """ Apply the previous methods according to the writing cipher type. """ # Compute the length of TLSPlaintext fragment hdr, frag = pkt[:5], pkt[5:] if not isinstance(self.tls_session.rcs.cipher, Cipher_NULL): frag = self._tls_auth_encrypt(frag) if self.len is not None: # The user gave us a 'len', let's respect this ultimately hdr = hdr[:3] + struct.pack("!H", self.len) else: # Update header with the length of TLSCiphertext.inner hdr = hdr[:3] + struct.pack("!H", len(frag)) # Now we commit the pending write state if it has been triggered. We # update nothing if the pwcs was not set. This probably means that # we're working out-of-context (and we need to keep the default wcs). if self.tls_session.triggered_pwcs_commit: if self.tls_session.pwcs is not None: self.tls_session.wcs = self.tls_session.pwcs self.tls_session.pwcs = None self.tls_session.triggered_pwcs_commit = False return hdr + frag + pay
class TLS(_GenericTLSSessionInheritance): """ The generic TLS Record message, based on section 6.2 of RFC 5246. When reading a TLS message, we try to parse as much as we can. In .pre_dissect(), according to the type of the current cipher algorithm (self.tls_session.rcs.cipher.type), we extract the 'iv', 'mac', 'pad' and 'padlen'. Some of these fields may remain blank: for instance, when using a stream cipher, there is no IV nor any padding. Once we have isolated the ciphered message aggregate (which should be one or several TLS messages of the same type), we try to decipher it. Either we succeed and store the clear data in 'msg', or we graciously fail with a CipherError and store the ciphered data in 'msg'. Unless the user manually provides the session secrets through the passing of a 'tls_session', obviously the ciphered messages will not be deciphered. Indeed, the need for a proper context may also present itself when trying to parse clear handshake messages. For instance, suppose you sniffed the beginning of a DHE-RSA negotiation: t1 = TLS(<client_hello>) t2 = TLS(<server_hello | certificate | server_key_exchange>) t3 = TLS(<server_hello | certificate | server_key_exchange>, tls_session=t1.tls_session) As no context was passed to t2, neither was any client_random. Hence scapy will not be able to verify the signature of the server_key_exchange inside t2. However, it should be able to do so for t3, thanks to the tls_session. The consequence of not having a complete TLS context is even more obvious when trying to parse ciphered content, as we decribed before. Thus, in order to parse TLS-protected communications with scapy: _either scapy reads every message from one side of the TLS connection and builds every message from the other side (as such, it should know the secrets needed for the generation of the pre_master_secret), while passing the same tls_session context (this is how our automaton.py mostly works); _or, if scapy did not build any TLS message, it has to create a TLS context and feed it with secrets retrieved by whatever technique. Note that the knowing the private key of the server certificate will not be sufficient if a PFS ciphersuite was used. However, if you got a master_secret somehow, use it with tls_session.(w|r)cs.derive_keys() and leave the rest to scapy. When building a TLS message, we expect the tls_session to have the right parameters for ciphering. Else, .post_build() might fail. """ __slots__ = ["decipherable"] name = "TLS" fields_desc = [ ByteEnumField("type", None, _tls_type), _TLSVersionField("version", None, _tls_version), _TLSLengthField("len", None), _TLSIVField("iv", None), _TLSMsgListField("msg", None, length_from=lambda pkt: pkt.len), _TLSMACField("mac", None), _TLSPadField("pad", None), _TLSPadLenField("padlen", None) ] def __init__(self, *args, **kargs): """ As long as 'decipherable' is True, _TLSMsgListField will try to decipher the content of the TLS message. Else, it will simply store/deliver the ciphered version. """ self.decipherable = True _GenericTLSSessionInheritance.__init__(self, *args, **kargs) ### Parsing methods def _tls_auth_decrypt(self, hdr, s): """ Provided with the record header and AEAD-ciphered data, return the sliced and clear tuple (nonce, TLSCompressed.fragment, mac). Note that we still return the slicing of the original input in case of decryption failure. Also, if the integrity check fails, a warning will be issued, but we still return the sliced (unauthenticated) plaintext. """ try: read_seq_num = struct.pack("!Q", self.tls_session.rcs.seq_num) self.tls_session.rcs.seq_num += 1 # self.type and self.version have not been parsed yet, # this is why we need to look into the provided hdr. add_data = read_seq_num + hdr[0] + hdr[1:3] # Last two bytes of add_data are appended by the return function return self.tls_session.rcs.cipher.auth_decrypt(add_data, s) except CipherError as e: self.decipherable = False return e.args except AEADTagError as e: print("INTEGRITY CHECK FAILED") return e.args def _tls_decrypt(self, s): """ Provided with stream- or block-ciphered data, return the clear version. The cipher should have been updated with the right IV early on, which should not be at the beginning of the input. Note that we still return the slicing of the original input in case of decryption failure. """ try: return self.tls_session.rcs.cipher.decrypt(s) except CipherError as e: self.decipherable = False return e.args def _tls_hmac_verify(self, hdr, msg, mac): """ Provided with the record header, the TLSCompressed.fragment and the HMAC, return True if the HMAC is correct. If we could not compute the HMAC because the key was missing, there is no sense in verifying anything, thus we also return True. Meant to be used with a block cipher or a stream cipher. It would fail with an AEAD cipher, because rcs.hmac would be None. See RFC 5246, section 6.2.3. """ mac_len = self.tls_session.rcs.mac_len if mac_len == 0: # should be TLS_NULL_WITH_NULL_NULL return True if len(mac) != mac_len: return False read_seq_num = struct.pack("!Q", self.tls_session.rcs.seq_num) self.tls_session.rcs.seq_num += 1 alg = self.tls_session.rcs.hmac version = struct.unpack("!H", hdr[1:3])[0] try: if version > 0x300: h = alg.digest(read_seq_num + hdr + msg) elif version == 0x300: h = alg.digest_sslv3(read_seq_num + hdr[0] + hdr[3:5] + msg) else: raise Exception("Unrecognized version.") except HMACError: h = mac return h == mac def _tls_decompress(self, s): """ Provided with the TLSCompressed.fragment, return the TLSPlaintext.fragment. """ alg = self.tls_session.rcs.compression return alg.decompress(s) def pre_dissect(self, s): """ Decrypt, verify and decompress the message, i.e. apply the previous methods according to the reading cipher type. If the decryption was successful, 'len' will be the length of the TLSPlaintext.fragment. Else, it should be the length of the _TLSEncryptedContent. """ if len(s) < 5: raise Exception("Invalid record: header is too short.") msglen = struct.unpack('!H', s[3:5])[0] hdr, efrag, r = s[:5], s[5:5+msglen], s[msglen+5:] iv = mac = pad = "" cipher_type = self.tls_session.rcs.cipher.type if cipher_type == 'block': version = struct.unpack("!H", s[1:3])[0] # Decrypt if version >= 0x0302: # Explicit IV for TLS 1.1 and 1.2 block_size = self.tls_session.rcs.cipher.block_size iv, efrag = efrag[:block_size], efrag[block_size:] self.tls_session.rcs.cipher.iv = iv pfrag = self._tls_decrypt(efrag) hdr = hdr[:3] + struct.pack("!H", len(pfrag)) else: # Implicit IV for SSLv3 and TLS 1.0 pfrag = self._tls_decrypt(efrag) # Excerpt below better corresponds to TLS 1.1 IV definition, # but the result is the same as with TLS 1.2 anyway. # This leading *IV* has been decrypted by _tls_decrypt with a # random IV, hence it does not correspond to anything. # What actually matters is that we got the first encrypted block # in order to decrypt the second block (first data block). #if version >= 0x0302: # block_size = self.tls_session.rcs.cipher.block_size # iv, pfrag = pfrag[:block_size], pfrag[block_size:] # l = struct.unpack('!H', hdr[3:5])[0] # hdr = hdr[:3] + struct.pack('!H', l-block_size) # Extract padding ('pad' actually includes the trailing padlen) padlen = ord(pfrag[-1]) + 1 mfrag, pad = pfrag[:-padlen], pfrag[-padlen:] # Extract MAC l = self.tls_session.rcs.mac_len if l != 0: cfrag, mac = mfrag[:-l], mfrag[-l:] else: cfrag, mac = mfrag, "" # Verify integrity hdr = hdr[:3] + struct.pack('!H', len(cfrag)) is_mac_ok = self._tls_hmac_verify(hdr, cfrag, mac) if not is_mac_ok: print("INTEGRITY CHECK FAILED") elif cipher_type == 'stream': # Decrypt pfrag = self._tls_decrypt(efrag) mfrag = pfrag # Extract MAC l = self.tls_session.rcs.mac_len if l != 0: cfrag, mac = mfrag[:-l], mfrag[-l:] else: cfrag, mac = mfrag, "" # Verify integrity hdr = hdr[:3] + struct.pack('!H', len(cfrag)) is_mac_ok = self._tls_hmac_verify(hdr, cfrag, mac) if not is_mac_ok: print("INTEGRITY CHECK FAILED") elif cipher_type == 'aead': # Authenticated encryption # crypto/cipher_aead.py prints a warning for integrity failure iv, cfrag, mac = self._tls_auth_decrypt(hdr, efrag) if self.decipherable: frag = self._tls_decompress(cfrag) else: frag = cfrag reconstructed_body = iv + frag + mac + pad l = len(frag) # note that we do not include the MAC, only the content hdr = hdr[:3] + struct.pack("!H", l) return hdr + reconstructed_body + r def post_dissect(self, s): """ Commit the pending read state if it has been triggered. We update nothing if the prcs was not set, as this probably means that we're working out-of-context (and we need to keep the default rcs). """ if self.tls_session.triggered_prcs_commit: if self.tls_session.prcs is not None: self.tls_session.rcs = self.tls_session.prcs self.tls_session.prcs = None self.tls_session.triggered_prcs_commit = False return s def do_dissect_payload(self, s): """ Try to dissect the following data as a TLS message. Note that overloading .guess_payload_class() would not be enough, as the TLS session to be used would get lost. """ if s: try: p = TLS(s, _internal=1, _underlayer=self, tls_session = self.tls_session) except KeyboardInterrupt: raise except: p = conf.raw_layer(s, _internal=1, _underlayer=self) self.add_payload(p) ### Building methods def _tls_compress(self, s): """ Provided with the TLSPlaintext.fragment, return the TLSCompressed.fragment. """ alg = self.tls_session.wcs.compression return alg.compress(s) def _tls_auth_encrypt(self, s): """ Return the TLSCiphertext.fragment for AEAD ciphers, i.e. the whole GenericAEADCipher. Also, the additional data is computed right here. """ write_seq_num = struct.pack("!Q", self.tls_session.wcs.seq_num) self.tls_session.wcs.seq_num += 1 add_data = (write_seq_num + pkcs_i2osp(self.type, 1) + pkcs_i2osp(self.version, 2) + pkcs_i2osp(len(s), 2)) return self.tls_session.wcs.cipher.auth_encrypt(s, add_data) def _tls_hmac_add(self, hdr, msg): """ Provided with the record header (concatenation of the TLSCompressed type, version and length fields) and the TLSCompressed.fragment, return the concatenation of the TLSCompressed.fragment and the HMAC. Meant to be used with a block cipher or a stream cipher. It would fail with an AEAD cipher, because wcs.hmac would be None. See RFC 5246, section 6.2.3. """ write_seq_num = struct.pack("!Q", self.tls_session.wcs.seq_num) self.tls_session.wcs.seq_num += 1 alg = self.tls_session.wcs.hmac version = struct.unpack("!H", hdr[1:3])[0] if version > 0x300: h = alg.digest(write_seq_num + hdr + msg) elif version == 0x300: h = alg.digest_sslv3(write_seq_num + hdr[0] + hdr[3:5] + msg) else: raise Exception("Unrecognized version.") return msg + h def _tls_pad(self, s): """ Provided with the concatenation of the TLSCompressed.fragment and the HMAC, append the right padding and return it as a whole. This is the TLS-style padding: while SSL allowed for random padding, TLS (misguidedly) specifies the repetition of the same byte all over, and this byte must be equal to len(<entire padding>) - 1. Meant to be used with a block cipher only. """ padding = "" block_size = self.tls_session.wcs.cipher.block_size padlen = block_size - ((len(s) + 1) % block_size) if padlen == block_size: padlen = 0 pad_pattern = chr(padlen) padding = pad_pattern * (padlen + 1) return s + padding def _tls_encrypt(self, s): """ Return the stream- or block-ciphered version of the concatenated input. In case of GenericBlockCipher, no IV has been specifically prepended to the output, so this might not be the whole TLSCiphertext.fragment yet. """ return self.tls_session.wcs.cipher.encrypt(s) def post_build(self, pkt, pay): """ Apply the previous methods according to the writing cipher type. """ # Compute the length of TLSPlaintext fragment hdr, frag = pkt[:5], pkt[5:] l = len(frag) hdr = hdr[:3] + struct.pack("!H", l) # Compression cfrag = self._tls_compress(frag) l = len(cfrag) # Update the length as a result of compression hdr = hdr[:3] + struct.pack("!H", l) cipher_type = self.tls_session.wcs.cipher.type if cipher_type == 'block': # Integrity mfrag = self._tls_hmac_add(hdr, cfrag) # Excerpt below better corresponds to TLS 1.1 IV definition, # but the result is the same as with TLS 1.2 anyway. #if self.version >= 0x0302: # l = self.tls_session.wcs.cipher.block_size # iv = randstring(l) # mfrag = iv + mfrag # Add padding pfrag = self._tls_pad(mfrag) # Encryption if self.version >= 0x0302: # Explicit IV for TLS 1.1 and 1.2 l = self.tls_session.wcs.cipher.block_size iv = randstring(l) self.tls_session.wcs.cipher.iv = iv efrag = self._tls_encrypt(pfrag) efrag = iv + efrag else: # Implicit IV for SSLv3 and TLS 1.0 efrag = self._tls_encrypt(pfrag) elif cipher_type == "stream": # Integrity mfrag = self._tls_hmac_add(hdr, cfrag) # Encryption efrag = self._tls_encrypt(mfrag) elif cipher_type == "aead": # Authenticated encryption (with nonce_explicit as header) efrag = self._tls_auth_encrypt(cfrag) # Now, we can commit pending write state if needed # We update nothing if the pwcs was not set. This probably means that # we're working out-of-context (and we need to keep the default wcs). if self.tls_session.triggered_pwcs_commit: if self.tls_session.pwcs is not None: self.tls_session.wcs = self.tls_session.pwcs self.tls_session.pwcs = None self.tls_session.triggered_pwcs_commit = False if self.len is not None: # The user gave us a 'len', let's respect this ultimately hdr = hdr[:3] + struct.pack("!H", self.len) else: # Update header with the length of TLSCiphertext.fragment hdr = hdr[:3] + struct.pack("!H", len(efrag)) return hdr + efrag + pay