Message manipulation
Tlsfuzzer provides facilities to modify messages and records before sending, use them to create malformed messages. You can apply the modifiers on generator nodes, the ones that send messages to the peer.
Custom message generation
Tlsfuzzer provides support for sending arbitrary messages over established connections. It provides two nodes to achieve it: one to send messages unencrypted and one to send them using the current connection status.
Creating unencrypted messages
To send a record with a specific payload and type, irrespective of
active encryption or negotiated fragmentation, use
PlaintextMessageGenerator
.
It accepts two parameters to specify data sent to the other
peer (content_type
and data
) as well as one
used for debugging: description
, printed when sending of the message
failed.
Note
As it skips all the usual message processing steps, it also doesn’t update handshake hashes so values calculated for Finished and connection secrets in TLS 1.3 won’t match expected ones.
For example, to send an empty ClientHello message, write:
node = node.add_child(PlaintextMessageGenerator(
ContentType.handshake,
bytearray(b'\x01\x00\x00\x00')))
You can find a usage example in: test-aesccm.py.
Tip
If you want to send an otherwise valid message, only as plaintext, not encrypted, see the Clearing encryption settings section.
To write directly to the socket, without record layer encapsulation,
use the RawSocketWriteGenerator
.
It accepts two parameters, one to specify the data to write and another,
optional, used for debugging, the description
.
Creating arbitrary messages
To send messages with a specific payload and type, while using encryption
and record layer fragmentation, use
RawMessageGenerator
.
It accepts two parameters that specify data sent to the other side
(content_type
and data
) and one that stores message to print if
processing of the message fails: description
.
For example, to send an empty Finished message, write:
node = node.add_child(RawMessageGenerator(
ContentType.handshake,
bytearray(b'\x14\x00\x00\x00')
You can find a usage example in: test-invalid-content-type.py.
Modifying messages
Tlsfuzzer supports applying two operations to sent messages: modifying length and modifying contents of specific bytes.
Modifying length
Handshake messages include an internal header that identifies the message type and message length. Two methods can change their payload while modifying the header to match.
The pad_handshake()
function adds data at the
end of payload. The size
param specifies how many bytes and
the pad_byte
parameter specifies the value of the added bytes.
In the other calling convention, it accepts literal bytes to add to the payload
by using the pad
keyword argument.
For example, to add 10 bytes of value 0 at the end of ClientHello, write:
ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA]
exts = {ExtensionType.renegotiation_info: None}
msg_gen = ClientHelloGenerator(cipihers, extensions=exts)
node = node.add_child(pad_handshake(msg_gen, 10))
You can find a usage example in: test-truncating-of-client-hello.py.
If you want to remove bytes from the end of a message, you can either
specify a negative size
or use the
truncate_handshake()
function.
Note
The sender can format ClientHello in two ways: with and without extensions.
A ClientHello with an empty list of extensions differs from one without
extensions by two zero bytes (they encode the length of the extensions).
Thus adding 2 zero bytes to an extensions-less ClientHello or removing
enough bytes from a ClientHello with extensions to turn it into one
without extensions can cause the
pad_handshake()
to create a well-formed
message, despite modifying it.
Modifying content
The fuzz_message()
supports changing arbitrary
parts of sent messages.
Both optional parameters of the function, substitutions
and xors
expect
a dictionary as value.
The keys of the dictionary specify the bytes to change.
To specify the bytes counting from the end of the message use negative numbers.
For example, to change the type of a ClientHello message to that of ServerHello use the following code:
ciphers = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA]
exts = {ExtensionType.renegotiation_info: None}
msg_gen = ClientHelloGenerator(cipihers, extensions=exts)
node = node.add_child(fuzz_message(msg_gen,
{0: HandshakeType.server_hello}))
You can find a usage example in: test-invalid-client-hello.py.
Modifying records
The TLS protocol specifies four types of encrypted records: ones that use stream encryption, ones that use block encryption in MAC then encrypt mode, ones that use block encryption in encrypt then MAC mode, and ones that use AEAD ciphers. Each of them behaves differently on the record layer level, thus modifying the intermediate ciphertext requires the use of different functions.
Fuzzing the MAC
To change the authentication tag you need to use different functions depending on which cipher suite and extensions have been negotiated.
For ciphers that use HMAC you can change the authentication tag using
the fuzz_mac()
function.
Note
fuzz_mac()
works with stream ciphers and
block ciphers in CBC mode only. It doesn’t work for SSLv2
connections though.
You use fuzz_mac()
the same way as you use
fuzz_message()
: pass the message to change as the
first argument and use the other two to specify the bytes to either xor or
substitute.
Use the following code to invert the first and last bit of the :term`HMAC` in a record with a Finished message:
msg_gen = FinishedGenerator()
xors = {0: 0x80, -1: 0x01}
node = node.add_child(fuzz_mac(msg_gen, xors=xors))
You can find a usage example in: test-fuzzed-MAC.py.
Since both AEAD cipher suites and CBC cipher suites in “encrypt
then MAC“ mode don’t encrypt the authentication tag, you can use the
fuzz_encrypted_message()
function to change it.
As it allows modification of any part of encrypted message, not just the tag,
you need to know the size of the authentication tag to change the first byte
of it though.
Hint
AES-CCM8 uses tags 8 bytes long. AES-GCM, Chacha20-Poly1305, AES-CCM and MD5-HMAC use tags 16 bytes long. SHA1-HMAC uses tags 20 bytes long. SHA256-HMAC uses tags 32 bytes long. SHA384-HMAC uses tags 48 bytes long
Use the following code to invert the first and last bit of authentication tag in a record with a Finished message in an AES-GCM connection:
msg_gen = FinishedGenerator()
xors = {-17: 0x80, -1: 0x01}
node = node.add_child(fuzz_encrypted_message(msg_gen, xors=xors))
You can find a usage example in: test-chacha20.py.
Tlsfuzzer can go as far as changing the whole plaintext
right before encryption, this can change the HMAC for CBC
mode ciphers working in “encrypt then MAC“ mode.
Use the replace_plaintext()
function for that.
Hint
The length of the replacement plaintext must be a multiple of cipher’s block size: 8 bytes for 3DES and 16 bytes for other ciphers.
For example, to create a record with a plaintext with all bytes of the IV set to 1 (assuming AES cipher), all bytes of the payload set to 2, all bytes of the authentication tag set to 3 (assuming SHA1-HMAC), and a zero-length padding, use the following code:
iv_bytes = bytearray([1]*16)
payload_bytes = bytearray([2]*11)
mac_bytes = bytearray([3]*20)
pad_bytes = bytearray(b'\x00')
new_plaintext = iv_bytes + payload_bytes + mac_bytes + pad_bytes
assert len(new_plaintext) % 16 == 0
msg_gen = FinishedGenerator()
node = node.add_child(replace_plaintext(msg_gen, new_plaintext))
You can find a usage example in: test-fuzzed-plaintext.py.
While you can use the fuzz_plaintext()
function
to change the MAC, you need to know the length of padding to know
where MAC begins and ends in the plaintext.
Fuzzing the padding
The CBC mode ciphers require input with length that’s a multiple of the cipher block size. Since stream ciphers and AEAD ciphers dont’t require that, TLS 1.2 and earlier doesn’t define padding for them.
As a single byte encodes the length of the padding, 255 bytes is the max length (256 bytes including the byte encoding length).
TLS 1.3 defines padding differently, it combines it with content type specification for record payload, thus the max record length (214 or 16384 bytes) defines max padding.
The fuzz_padding()
function can change the
padding used by CBC cipher suites.
For example, to negate the last byte of padding of a record with Finished message (while ensuring non-zero length padding), use the following code:
msg_gen = FinishedGenerator()
node = node.add_child(fuzz_padding(msg_gen, min_length=1,
xors={-2: 0xff}))
You can find a usage example in: test-fuzzed-padding.py.
While you can use the fuzz_plaintext()
function
to change the padding, it doesn’t support specifying the min length
for the padding.
TLS 1.3 padding length
tlsfuzzer supports changing the padding in sent records through a callback
mechanism.
The SetPaddingCallback
node sets the
callback for calculating the padding size.
It includes two factory methods and one ready to use callback.
For example, to make all records send max supported padding in the connection, use the following code:
node = node.add_child(
SetPaddingCallback(SetPaddingCallback.fill_padding_cb))
You can find a usage example in: test-tls13-record-layer-limits.py.
Sending too big records
The TLS protocol specifies the max length of payload at 214
bytes.
To send records with larger payload use
SetMaxRecordSize
to increase that limit.
Note
This increases the max length of payload. With active encryption, records include IV, MAC and padding or AEAD tag, making them at least 16 bytes larger.
Warning
The TLS protocol specifies the length in record header as two bytes, as such, records larger than 216- 1 or 65535 bytes have no physical representation and tlsfuzzer doesn’t support sending them. IV, padding and authentication tag increase the size of record compared to the payload by at least 16 bytes and at most by 276 bytes.
With this limit unmodified, the record layer fragments a 16385 byte message into two records.
For example, to send an ApplicationData record 1 byte larger than the TLS specified limit, use the following code:
node = node.add_child(SetMaxRecordSize(2**16-1)) # "unlimited"
node = node.add_child(ApplicationDataGenerator(bytearray(b'A' * 16385)))
You can find a usage example in: test-record-size-limit.py.
Message fragmentation
Tlsfuzzer provides methods to control fragmentation and sending of the messages.
TCP fragmentation
Normally, the TLS messages are sent as soon as they are created during the execution of the decision graph. That means that every TLS message will be sent in an individual TCP fragment (if it fits in one). That means, if the script sends multiple messages, like Certificate, ClientKeyExchange, CertificateVerify, ChangeCipherSpec, and Finished, with an inconsistent value in either Certificate or ClientKeyExchange messages, the server may detect that inconsistency as soon as it processes those messages, or only when it decides that it needs to process them to be able to handle the ChangeCipherSpect/Finished messages. That means, if the script sends the messages in individual fragments, the sending may fail because the server has sent an Alert message and closed the TCP connection.
To work-around this to a certain degree, we can queue the TLS messages and
send them in a single write, hopefully ending up in a single TCP fragment.
To do that, we have three command nodes:
TCPBufferingEnable
,
TCPBufferingDisable
, and
TCPBufferingFlush
.
The first one starts bufferring all writes to the socket,
the second one disables buffering, and the third one flushes
the current contents of the buffer (buffering doesn’t have to be disabled
to flush the buffer).
You can find a usage example in: test-rsa-pss-sigs-on-certificate-verify.py.
Splitting messages
To send one higher level message in more than one record, you can use
split_message()
,
PopMessageFromList
, and
FlushMessageList
.
The split_message()
requires a list()
object to pass the created fragments to the other two nodes.
It sends the first fragment at that point.
PopMessageFromList
takes one fragment from
the list and sends it.
FlushMessageList
takes all remaining fragments
from the list and sends them in one record.
If a message has a post-send action, they execute it after sending the last
fragment.
For example, to send a ClientHello in two records, the first of 2 bytes length, use the following code:
ciphres = [CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
msg_gen = ClientHelloGenerator(ciphers)
fragment_list = []
node = node.add_child(split_message(msg_gen, fragment_list, 2))
node = node.add_child(FlushMessageList(fragment_list))
You can find a usage example in: test-large-hello.py.
Combining messages
While TLS allows for sending multiple messages with the same content type in a single record, for ease of debugging tlsfuzzer doesn’t do that by default.
But to verify that the other side of the connection can process such records (or that it rejects messages that must not be coalesced), it’s possible to combine (coalesce) multiple messages with the same record_type.
First, to queue a message instead of sending it, use the
queue_message()
decorator:
node = node.add_child(queue_message(CertificateGenerator(cert_chain)))
Then, to actually send the message, you can either send another message,
of any type (the queue is flushed if the content_type of it doesn’t match
new message; and regular writes first queue a message and then flush
the queue) or flush the queue manually using
FlushMessageQueue
:
node = node.add_child(FlushMessageQueue())
Note
The post_send
method is still executed right after the message is
queued, so if it has side effects, like updating the write state, the
actually sent record may be encrypted with wrong (i.e. future) keys.
Use RawMessageGenerator
to create the message without side-effects.
Or use the skip_post_send()
to disable it.
You can find a usage example in: test-tls13-keyupdate.py.