256 lines
10 KiB
Python
256 lines
10 KiB
Python
|
# This file is part of Scapy
|
||
|
# Copyright (C) 2007, 2008, 2009 Arnaud Ebalard
|
||
|
# 2015, 2016, 2017 Maxence Tury
|
||
|
# This program is published under a GPLv2 license
|
||
|
|
||
|
"""
|
||
|
The _TLSAutomaton class provides methods common to both TLS client and server.
|
||
|
"""
|
||
|
|
||
|
import struct
|
||
|
|
||
|
from scapy.automaton import Automaton
|
||
|
from scapy.config import conf
|
||
|
from scapy.error import log_interactive
|
||
|
from scapy.packet import Raw
|
||
|
from scapy.layers.tls.basefields import _tls_type
|
||
|
from scapy.layers.tls.cert import Cert, PrivKey
|
||
|
from scapy.layers.tls.record import TLS
|
||
|
from scapy.layers.tls.record_sslv2 import SSLv2
|
||
|
from scapy.layers.tls.record_tls13 import TLS13
|
||
|
|
||
|
|
||
|
class _TLSAutomaton(Automaton):
|
||
|
"""
|
||
|
SSLv3 and TLS 1.0-1.2 typically need a 2-RTT handshake:
|
||
|
|
||
|
Client Server
|
||
|
| --------->>> | C1 - ClientHello
|
||
|
| <<<--------- | S1 - ServerHello
|
||
|
| <<<--------- | S1 - Certificate
|
||
|
| <<<--------- | S1 - ServerKeyExchange
|
||
|
| <<<--------- | S1 - ServerHelloDone
|
||
|
| --------->>> | C2 - ClientKeyExchange
|
||
|
| --------->>> | C2 - ChangeCipherSpec
|
||
|
| --------->>> | C2 - Finished [encrypted]
|
||
|
| <<<--------- | S2 - ChangeCipherSpec
|
||
|
| <<<--------- | S2 - Finished [encrypted]
|
||
|
|
||
|
We call these successive groups of messages:
|
||
|
ClientFlight1, ServerFlight1, ClientFlight2 and ServerFlight2.
|
||
|
|
||
|
With TLS 1.3, the handshake require only 1-RTT:
|
||
|
|
||
|
Client Server
|
||
|
| --------->>> | C1 - ClientHello
|
||
|
| <<<--------- | S1 - ServerHello
|
||
|
| <<<--------- | S1 - Certificate [encrypted]
|
||
|
| <<<--------- | S1 - CertificateVerify [encrypted]
|
||
|
| <<<--------- | S1 - Finished [encrypted]
|
||
|
| --------->>> | C2 - Finished [encrypted]
|
||
|
|
||
|
We want to send our messages from the same flight all at once through the
|
||
|
socket. This is achieved by managing a list of records in 'buffer_out'.
|
||
|
We may put several messages (i.e. what RFC 5246 calls the record fragments)
|
||
|
in the same record when possible, but we may need several records for the
|
||
|
same flight, as with ClientFlight2.
|
||
|
|
||
|
However, note that the flights from the opposite side may be spread wildly
|
||
|
across TLS records and TCP packets. This is why we use a 'get_next_msg'
|
||
|
method for feeding a list of received messages, 'buffer_in'. Raw data
|
||
|
which has not yet been interpreted as a TLS record is kept in 'remain_in'.
|
||
|
"""
|
||
|
|
||
|
def parse_args(self, mycert=None, mykey=None, **kargs):
|
||
|
|
||
|
super(_TLSAutomaton, self).parse_args(**kargs)
|
||
|
|
||
|
self.socket = None
|
||
|
self.remain_in = b""
|
||
|
self.buffer_in = [] # these are 'fragments' inside records
|
||
|
self.buffer_out = [] # these are records
|
||
|
|
||
|
self.cur_session = None
|
||
|
self.cur_pkt = None # this is usually the latest parsed packet
|
||
|
|
||
|
if mycert:
|
||
|
self.mycert = Cert(mycert)
|
||
|
else:
|
||
|
self.mycert = None
|
||
|
|
||
|
if mykey:
|
||
|
self.mykey = PrivKey(mykey)
|
||
|
else:
|
||
|
self.mykey = None
|
||
|
|
||
|
self.verbose = kargs.get("verbose", True)
|
||
|
|
||
|
def get_next_msg(self, socket_timeout=2, retry=2):
|
||
|
"""
|
||
|
The purpose of the function is to make next message(s) available in
|
||
|
self.buffer_in. If the list is not empty, nothing is done. If not, in
|
||
|
order to fill it, the function uses the data already available in
|
||
|
self.remain_in from a previous call and waits till there are enough to
|
||
|
dissect a TLS packet. Once dissected, the content of the TLS packet
|
||
|
(carried messages, or 'fragments') is appended to self.buffer_in.
|
||
|
|
||
|
We have to grab enough data to dissect a TLS packet. We start by
|
||
|
reading the first 2 bytes. Unless we get anything different from
|
||
|
\\x14\\x03, \\x15\\x03, \\x16\\x03 or \\x17\\x03 (which might indicate
|
||
|
an SSLv2 record, whose first 2 bytes encode the length), we retrieve
|
||
|
3 more bytes in order to get the length of the TLS record, and
|
||
|
finally we can retrieve the remaining of the record.
|
||
|
"""
|
||
|
if self.buffer_in:
|
||
|
# A message is already available.
|
||
|
return
|
||
|
|
||
|
self.socket.settimeout(socket_timeout)
|
||
|
is_sslv2_msg = False
|
||
|
still_getting_len = True
|
||
|
grablen = 2
|
||
|
while retry and (still_getting_len or len(self.remain_in) < grablen):
|
||
|
if not is_sslv2_msg and grablen == 5 and len(self.remain_in) >= 5:
|
||
|
grablen = struct.unpack('!H', self.remain_in[3:5])[0] + 5
|
||
|
still_getting_len = False
|
||
|
elif grablen == 2 and len(self.remain_in) >= 2:
|
||
|
byte0, byte1 = struct.unpack("BB", self.remain_in[:2])
|
||
|
if (byte0 in _tls_type) and (byte1 == 3):
|
||
|
# Retry following TLS scheme. This will cause failure
|
||
|
# for SSLv2 packets with length 0x1{4-7}03.
|
||
|
grablen = 5
|
||
|
else:
|
||
|
# Extract the SSLv2 length.
|
||
|
is_sslv2_msg = True
|
||
|
still_getting_len = False
|
||
|
if byte0 & 0x80:
|
||
|
grablen = 2 + 0 + ((byte0 & 0x7f) << 8) + byte1
|
||
|
else:
|
||
|
grablen = 2 + 1 + ((byte0 & 0x3f) << 8) + byte1
|
||
|
elif not is_sslv2_msg and grablen == 5 and len(self.remain_in) >= 5: # noqa: E501
|
||
|
grablen = struct.unpack('!H', self.remain_in[3:5])[0] + 5
|
||
|
|
||
|
if grablen == len(self.remain_in):
|
||
|
break
|
||
|
|
||
|
try:
|
||
|
tmp = self.socket.recv(grablen - len(self.remain_in))
|
||
|
if not tmp:
|
||
|
retry -= 1
|
||
|
else:
|
||
|
self.remain_in += tmp
|
||
|
except Exception:
|
||
|
self.vprint("Could not join host ! Retrying...")
|
||
|
retry -= 1
|
||
|
|
||
|
if len(self.remain_in) < 2 or len(self.remain_in) != grablen:
|
||
|
# Remote peer is not willing to respond
|
||
|
return
|
||
|
|
||
|
if (byte0 == 0x17 and
|
||
|
(self.cur_session.advertised_tls_version >= 0x0304 or
|
||
|
self.cur_session.tls_version >= 0x0304)):
|
||
|
p = TLS13(self.remain_in, tls_session=self.cur_session)
|
||
|
self.remain_in = b""
|
||
|
self.buffer_in += p.inner.msg
|
||
|
else:
|
||
|
p = TLS(self.remain_in, tls_session=self.cur_session)
|
||
|
self.cur_session = p.tls_session
|
||
|
self.remain_in = b""
|
||
|
if isinstance(p, SSLv2) and not p.msg:
|
||
|
p.msg = Raw("")
|
||
|
if self.cur_session.tls_version is None or \
|
||
|
self.cur_session.tls_version < 0x0304:
|
||
|
self.buffer_in += p.msg
|
||
|
else:
|
||
|
if isinstance(p, TLS13):
|
||
|
self.buffer_in += p.inner.msg
|
||
|
else:
|
||
|
# should be TLS13ServerHello only
|
||
|
self.buffer_in += p.msg
|
||
|
|
||
|
while p.payload:
|
||
|
if isinstance(p.payload, Raw):
|
||
|
self.remain_in += p.payload.load
|
||
|
p = p.payload
|
||
|
elif isinstance(p.payload, TLS):
|
||
|
p = p.payload
|
||
|
if self.cur_session.tls_version is None or \
|
||
|
self.cur_session.tls_version < 0x0304:
|
||
|
self.buffer_in += p.msg
|
||
|
else:
|
||
|
self.buffer_in += p.inner.msg
|
||
|
else:
|
||
|
p = p.payload
|
||
|
|
||
|
def raise_on_packet(self, pkt_cls, state, get_next_msg=True):
|
||
|
"""
|
||
|
If the next message to be processed has type 'pkt_cls', raise 'state'.
|
||
|
If there is no message waiting to be processed, we try to get one with
|
||
|
the default 'get_next_msg' parameters.
|
||
|
"""
|
||
|
# Maybe we already parsed the expected packet, maybe not.
|
||
|
if get_next_msg:
|
||
|
self.get_next_msg()
|
||
|
from scapy.layers.tls.handshake import TLSClientHello
|
||
|
if (not self.buffer_in or
|
||
|
(not isinstance(self.buffer_in[0], pkt_cls) and
|
||
|
not (isinstance(self.buffer_in[0], TLSClientHello) and
|
||
|
self.cur_session.advertised_tls_version == 0x0304))):
|
||
|
return
|
||
|
self.cur_pkt = self.buffer_in[0]
|
||
|
self.buffer_in = self.buffer_in[1:]
|
||
|
raise state()
|
||
|
|
||
|
def add_record(self, is_sslv2=None, is_tls13=None, is_tls12=None):
|
||
|
"""
|
||
|
Add a new TLS or SSLv2 or TLS 1.3 record to the packets buffered out.
|
||
|
"""
|
||
|
if is_sslv2 is None and is_tls13 is None and is_tls12 is None:
|
||
|
v = (self.cur_session.tls_version or
|
||
|
self.cur_session.advertised_tls_version)
|
||
|
if v in [0x0200, 0x0002]:
|
||
|
is_sslv2 = True
|
||
|
elif v >= 0x0304:
|
||
|
is_tls13 = True
|
||
|
if is_sslv2:
|
||
|
self.buffer_out.append(SSLv2(tls_session=self.cur_session))
|
||
|
elif is_tls13:
|
||
|
self.buffer_out.append(TLS13(tls_session=self.cur_session))
|
||
|
# For TLS 1.3 middlebox compatibility, TLS record version must
|
||
|
# be 0x0303
|
||
|
elif is_tls12:
|
||
|
self.buffer_out.append(TLS(version="TLS 1.2",
|
||
|
tls_session=self.cur_session))
|
||
|
else:
|
||
|
self.buffer_out.append(TLS(tls_session=self.cur_session))
|
||
|
|
||
|
def add_msg(self, pkt):
|
||
|
"""
|
||
|
Add a TLS message (e.g. TLSClientHello or TLSApplicationData)
|
||
|
inside the latest record to be sent through the socket.
|
||
|
We believe a good automaton should not use the first test.
|
||
|
"""
|
||
|
if not self.buffer_out:
|
||
|
self.add_record()
|
||
|
r = self.buffer_out[-1]
|
||
|
if isinstance(r, TLS13):
|
||
|
self.buffer_out[-1].inner.msg.append(pkt)
|
||
|
else:
|
||
|
self.buffer_out[-1].msg.append(pkt)
|
||
|
|
||
|
def flush_records(self):
|
||
|
"""
|
||
|
Send all buffered records and update the session accordingly.
|
||
|
"""
|
||
|
s = b"".join(p.raw_stateful() for p in self.buffer_out)
|
||
|
self.socket.send(s)
|
||
|
self.buffer_out = []
|
||
|
|
||
|
def vprint(self, s=""):
|
||
|
if self.verbose:
|
||
|
if conf.interactive:
|
||
|
log_interactive.info("> %s", s)
|
||
|
else:
|
||
|
print("> %s" % s)
|