Skip to content

Commit bf0725f

Browse files
authored
Minor TLS fixes (#2767)
* Improve doc * TLS TCP decompression & small bugs * Add TLS tests * Fix issue #2767 2527
1 parent cbd48fa commit bf0725f

11 files changed

Lines changed: 167 additions & 38 deletions

File tree

doc/scapy/usage.rst

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -758,10 +758,12 @@ Advanced Sniffing - Sniffing Sessions
758758
Scapy includes some basic Sessions, but it is possible to implement your own.
759759
Available by default:
760760

761-
- ``IPSession`` -> *defragment IP packets* on-the-flow, to make a stream usable by ``prn``.
762-
- ``TCPSession`` -> *defragment certain TCP protocols**. Only **HTTP 1.0** currently uses this functionality.
763-
- ``TLSSession`` -> *matches TLS sessions* on the flow.
764-
- ``NetflowSession`` -> *resolve Netflow V9 packets* from their NetflowFlowset information objects
761+
- :py:class:`~scapy.sessions.IPSession` -> *defragment IP packets* on-the-flow, to make a stream usable by ``prn``.
762+
- :py:class:`~scapy.sessions.TCPSession` -> *defragment certain TCP protocols*. Currently supports:
763+
- HTTP 1.0
764+
- TLS
765+
- :py:class:`~scapy.sessions.TLSSession` -> *matches TLS sessions* on the flow.
766+
- :py:class:`~scapy.sessions.NetflowSession` -> *resolve Netflow V9 packets* from their NetflowFlowset information objects
765767

766768
Those sessions can be used using the ``session=`` parameter of ``sniff()``. Examples::
767769

@@ -771,7 +773,34 @@ Those sessions can be used using the ``session=`` parameter of ``sniff()``. Exam
771773

772774
.. note::
773775
To implement your own Session class, in order to support another flow-based protocol, start by copying a sample from `scapy/sessions.py <https://github.com/secdev/scapy/blob/master/scapy/sessions.py>`_
774-
Your custom ``Session`` class only needs to extend the ``DefaultSession`` class, and implement a ``on_packet_received`` function, such as in the example.
776+
Your custom ``Session`` class only needs to extend the :py:class:`~scapy.sessions.DefaultSession` class, and implement a ``on_packet_received`` function, such as in the example.
777+
778+
.. note:: Would you need it, you can use: ``class TLS_over_TCP(TLSSession, TCPSession): pass`` to sniff TLS packets that are defragmented.
779+
780+
How to use TCPSession to defragment TCP packets
781+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
782+
783+
The layer on which the decompression is applied must be immediately following the TCP layer. You need to implement a class function called ``tcp_reassemble`` that accepts the binary data and a metada dictionary as argument and returns, when full, a packet. Let's study the (pseudo) example of TLS:
784+
785+
.. code::
786+
787+
class TLS(Packet):
788+
[...]
789+
790+
@classmethod
791+
def tcp_reassemble(cls, data, metadata):
792+
length = struct.unpack("!H", data[3:5])[0] + 5
793+
if len(data) == length:
794+
return TLS(data)
795+
796+
797+
In this example, we first get the total length of the TLS payload announced by the TLS header, and we compare it to the length of the data. When the data reaches this length, the packet is complete and can be returned. When implementing ``tcp_reassemble``, it's usually a matter of detecting when a packet isn't missing anything else.
798+
799+
The ``data`` argument is bytes and the ``metadata`` argument is a dictionary which keys are as follow:
800+
801+
- ``metadata["pay_class"]``: the TCP payload class (here TLS)
802+
- ``metadata.get("tcp_psh", False)``: will be present if the PUSH flag is set
803+
- ``metadata.get("tcp_end", False)``: will be present if the END or RESET flag is set
775804

776805
Filters
777806
-------

scapy/layers/tls/automaton_cli.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,21 @@ class TLSClientAutomaton(_TLSAutomaton):
8484
Rather than with an interruption, the best way to stop this client is by
8585
typing 'quit'. This won't be a message sent to the server.
8686
87-
_'mycert' and 'mykey' may be provided as filenames. They will be used in
88-
the handshake, should the server ask for client authentication.
89-
_'server_name' is the SNI. It does not need to be set.
90-
_'client_hello' may hold a TLSClientHello or SSLv2ClientHello to be sent
91-
to the server. This is particularly useful for extensions tweaking.
92-
_'version' is a quicker way to advertise a protocol version ("sslv2",
93-
"tls1", "tls12", etc.) It may be overridden by the previous 'client_hello'.
94-
_'data' is a list of raw data to be sent to the server once the handshake
95-
has been completed. Both 'stop_server' and 'quit' will work this way.
87+
:param server: the server IP or hostname. defaults to 127.0.0.1
88+
:param dport: the server port. defaults to 4433
89+
:param server_name: the SNI to use. It does not need to be set
90+
:param mycert:
91+
:param mykey: may be provided as filenames. They will be used in
92+
the handshake, should the server ask for client authentication.
93+
:param client_hello: may hold a TLSClientHello or SSLv2ClientHello to be
94+
sent to the server. This is particularly useful for extensions
95+
tweaking.
96+
:param version: is a quicker way to advertise a protocol version ("sslv2",
97+
"tls1", "tls12", etc.) It may be overridden by the previous
98+
'client_hello'.
99+
:param data: is a list of raw data to be sent to the server once the
100+
handshake has been completed. Both 'stop_server' and 'quit' will
101+
work this way.
96102
"""
97103

98104
def parse_args(self, server="127.0.0.1", dport=4433, server_name=None,

scapy/layers/tls/extensions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,7 @@ def addfield(self, pkt, s, i):
761761
i = self.adjust(pkt, f)
762762
if i == 0: # for correct build if no ext and not explicitly 0
763763
v = pkt.tls_session.tls_version
764-
# Xith TLS 1.3, zero lengths are always explicit.
764+
# With TLS 1.3, zero lengths are always explicit.
765765
if v is None or v < 0x0304:
766766
return s
767767
else:
@@ -779,8 +779,8 @@ def i2len(self, pkt, i):
779779
return len(self.i2m(pkt, i))
780780

781781
def getfield(self, pkt, s):
782-
tmp_len = self.length_from(pkt)
783-
if tmp_len is None:
782+
tmp_len = self.length_from(pkt) or 0
783+
if tmp_len <= 0:
784784
return s, []
785785
return s[tmp_len:], self.m2i(pkt, s[:tmp_len])
786786

scapy/layers/tls/handshake.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,21 @@
1717
import struct
1818

1919
from scapy.error import log_runtime, warning
20-
from scapy.fields import ByteEnumField, ByteField, EnumField, Field, \
21-
FieldLenField, IntField, PacketField, PacketListField, ShortField, \
22-
StrFixedLenField, StrLenField, ThreeBytesField, UTCTimeField
20+
from scapy.fields import (
21+
ByteEnumField,
22+
ByteField,
23+
Field,
24+
FieldLenField,
25+
IntField,
26+
PacketField,
27+
PacketListField,
28+
ShortEnumField,
29+
ShortField,
30+
StrFixedLenField,
31+
StrLenField,
32+
ThreeBytesField,
33+
UTCTimeField,
34+
)
2335

2436
from scapy.compat import hex_bytes, orb, raw
2537
from scapy.config import conf
@@ -481,18 +493,17 @@ class TLSServerHello(_TLSHandshake):
481493
_SessionIDField("sid", "",
482494
length_from=lambda pkt: pkt.sidlen),
483495

484-
EnumField("cipher", None, _tls_cipher_suites),
496+
ShortEnumField("cipher", None, _tls_cipher_suites),
485497
_CompressionMethodsField("comp", [0],
486498
_tls_compression_algs,
487499
itemfmt="B",
488500
length_from=lambda pkt: 1),
489501

490502
_ExtensionsLenField("extlen", None, length_of="ext"),
491503
_ExtensionsField("ext", None,
492-
length_from=lambda pkt: (pkt.msglen -
493-
(pkt.sidlen or 0) - # noqa: E501
494-
38))]
495-
# 40)) ]
504+
length_from=lambda pkt: (
505+
pkt.msglen - (pkt.sidlen or 0) - 40
506+
))]
496507

497508
@classmethod
498509
def dispatch_hook(cls, _pkt=None, *args, **kargs):
@@ -563,7 +574,7 @@ def tls_session_update(self, msg_str):
563574
FieldLenField("sidlen", None, length_of="sid", fmt="B"),
564575
_SessionIDField("sid", "",
565576
length_from=lambda pkt: pkt.sidlen),
566-
EnumField("cipher", None, _tls_cipher_suites),
577+
ShortEnumField("cipher", None, _tls_cipher_suites),
567578
_CompressionMethodsField("comp", [0],
568579
_tls_compression_algs,
569580
itemfmt="B",
@@ -1020,7 +1031,7 @@ def build(self, *args, **kargs):
10201031
fval = self.getfieldval("sig")
10211032
if fval is None:
10221033
s = self.tls_session
1023-
if s.pwcs:
1034+
if s.pwcs and s.client_random:
10241035
if not s.pwcs.key_exchange.anonymous:
10251036
p = self.params
10261037
if p is None:
@@ -1126,7 +1137,7 @@ class TLSCertificateRequest(_TLSHandshake):
11261137
SigAndHashAlgsLenField("sig_algs_len", None,
11271138
length_of="sig_algs"),
11281139
SigAndHashAlgsField("sig_algs", [0x0403, 0x0401, 0x0201],
1129-
EnumField("hash_sig", None, _tls_hash_sig), # noqa: E501
1140+
ShortEnumField("hash_sig", None, _tls_hash_sig), # noqa: E501
11301141
length_from=lambda pkt: pkt.sig_algs_len), # noqa: E501
11311142
FieldLenField("certauthlen", None, fmt="!H",
11321143
length_of="certauth"),

scapy/layers/tls/keyexchange.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def m2i(self, pkt, m):
272272
if s.prcs:
273273
cls = s.prcs.key_exchange.server_kx_msg_cls(m)
274274
if cls is None:
275-
return None, Raw(m[:tmp_len]) / Padding(m[tmp_len:])
275+
return Raw(m[:tmp_len]) / Padding(m[tmp_len:])
276276
return cls(m, tls_session=s)
277277
else:
278278
try:
@@ -284,7 +284,7 @@ def m2i(self, pkt, m):
284284
cls = _tls_server_ecdh_cls_guess(m)
285285
p = cls(m, tls_session=s)
286286
if pkcs_os2ip(p.load[:2]) not in _tls_hash_sig:
287-
return None, Raw(m[:tmp_len]) / Padding(m[tmp_len:])
287+
return Raw(m[:tmp_len]) / Padding(m[tmp_len:])
288288
return p
289289

290290

scapy/layers/tls/session.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,25 @@ def mysummary(self):
10021002
return "TLS %s / %s" % (repr(self.tls_session),
10031003
getattr(self, "_name", self.name))
10041004

1005+
@classmethod
1006+
def tcp_reassemble(cls, data, metadata):
1007+
# Used with TLSSession
1008+
from scapy.layers.tls.record import TLS
1009+
from scapy.layers.tls.record_tls13 import TLS13
1010+
if cls in (TLS, TLS13):
1011+
length = struct.unpack("!H", data[3:5])[0] + 5
1012+
if len(data) == length:
1013+
return cls(data)
1014+
elif len(data) > length:
1015+
pkt = cls(data)
1016+
if hasattr(pkt.payload, "tcp_reassemble"):
1017+
if pkt.payload.tcp_reassemble(data[length:], metadata):
1018+
return pkt
1019+
else:
1020+
return pkt
1021+
else:
1022+
return cls(data)
1023+
10051024

10061025
###############################################################################
10071026
# Multiple TLS sessions #

scapy/sendrecv.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,8 @@ class AsyncSniffer(object):
752752
is displayed.
753753
--Ex: prn = lambda x: x.summary()
754754
session: a session = a flow decoder used to handle stream of packets.
755-
e.g: IPSession (to defragment on-the-flow) or NetflowSession
755+
--Ex: session=TCPSession
756+
See below for more details.
756757
filter: BPF filter to apply.
757758
lfilter: Python function applied to each packet to determine if
758759
further action may be done.
@@ -778,6 +779,9 @@ class AsyncSniffer(object):
778779
element, a list of elements, or a dict object mapping an element to a
779780
label (see examples below).
780781
782+
For more information about the session argument, see
783+
https://scapy.rtfd.io/en/latest/usage.html#advanced-sniffing-sniffing-sessions
784+
781785
Examples: synchronous
782786
>>> sniff(filter="arp")
783787
>>> sniff(filter="tcp",

scapy/sessions.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,12 @@ def tcp_reassemble(cls, data, metadata):
194194
# as you need additional data.
195195
return None
196196
197-
A (hard to understand) example can be found in scapy/layers/http.py
197+
For more details and a real example, see:
198+
https://scapy.readthedocs.io/en/latest/usage.html#how-to-use-tcpsession-to-defragment-tcp-packets
198199
199200
:param app: Whether the socket is on application layer = has no TCP
200-
layer. Default to False
201+
layer. This is used for instance if you are using a native
202+
TCP socket. Default to False
201203
"""
202204

203205
fmt = ('TCP {IP:%IP.src%}{IPv6:%IPv6.src%}:%r,TCP.sport% > ' +
@@ -224,7 +226,8 @@ def _process_packet(self, pkt):
224226
# Special mode: Application layer. Use on top of TCP
225227
pay_class = pkt.__class__
226228
if not hasattr(pay_class, "tcp_reassemble"):
227-
# Cannot tcp-reassemble
229+
# Being on top of TCP, we have no way of knowing
230+
# when a packet ends.
228231
return pkt
229232
self.data += bytes(pkt)
230233
pkt = pay_class.tcp_reassemble(self.data, self.metadata)
@@ -248,12 +251,16 @@ def _process_packet(self, pkt):
248251
# Let's guess which class is going to be used
249252
if "pay_class" not in metadata:
250253
pay_class = pay.__class__
251-
if not hasattr(pay_class, "tcp_reassemble"):
252-
# Cannot tcp-reassemble
254+
if hasattr(pay_class, "tcp_reassemble"):
255+
tcp_reassemble = pay_class.tcp_reassemble
256+
else:
257+
# We can't know for sure when a packet ends.
258+
# Ignore.
253259
return pkt
254260
metadata["pay_class"] = pay_class
261+
metadata["tcp_reassemble"] = tcp_reassemble
255262
else:
256-
pay_class = metadata["pay_class"]
263+
tcp_reassemble = metadata["tcp_reassemble"]
257264
# Get a relative sequence number for a storage purpose
258265
relative_seq = metadata.get("relative_seq", None)
259266
if relative_seq is None:
@@ -275,10 +282,11 @@ def _process_packet(self, pkt):
275282
packet = None
276283
if data.full():
277284
# Reassemble using all previous packets
278-
packet = pay_class.tcp_reassemble(bytes(data), metadata)
285+
packet = tcp_reassemble(bytes(data), metadata)
279286
# Stack the result on top of the previous frames
280287
if packet:
281288
data.clear()
289+
metadata.clear()
282290
del self.tcp_frags[ident]
283291
pay.underlayer.remove_payload()
284292
if IP in pkt:

test/pcaps/tls_tcp_frag.pcap.gz

3.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)