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.