DTLS zero-length fragment for non-empty handshake can trigger out-of-bounds read
Hi all,
I believe that I have discovered a remotely reachable DTLS parsing issue in lib/buffers.c. A malicious peer can send a malformed DTLS handshake fragment with a non-zero handshake message length, a non-zero fragment_offset, and fragment_length == 0. This input is accepted by the DTLS handshake header parser and can later cause an out-of-bounds read during handshake reassembly.
In parse_handshake_header(), DTLS fragment metadata is taken directly from the received handshake header:
hsk->length = read_uint24(dataptr + 1);
hsk->start_offset = read_uint24(dataptr + 6);
frag_size = read_uint24(dataptr + 9);
If frag_size is zero, end_offset is set to zero and the fragment is still accepted:
if (frag_size > 0)
hsk->end_offset = hsk->start_offset + frag_size - 1;
else
hsk->end_offset = 0;
Later, _gnutls_parse_record_buffered_msgs() computes the amount of fragment data to copy using end_offset - start_offset + 1:
data_size = MIN(tmp.length, tmp.end_offset - tmp.start_offset + 1);
ret = _gnutls_buffer_append_data(&tmp.data,
_mbuffer_get_udata_ptr(bufel),
data_size);
If start_offset > 0 and end_offset == 0, the subtraction underflows and data_size becomes tmp.length. If the buffered DTLS record contains only the handshake header, or otherwise fewer bytes than data_size, the parser may read past the available record data.
This seems invalid at the protocol level as well: for a non-empty DTLS handshake message, fragment_length == 0 does not represent a valid fragment of the message.
- Start a DTLS client or server using GnuTLS.
- During the handshake, send one DTLS handshake record with:
- length = 0x000010
- fragment_offset = 0x000001
- fragment_length = 0x000000
- Include only the DTLS handshake header and no fragment body, or fewer payload bytes than the declared handshake length.
- Let the peer parse and buffer the record.
- Observe that the malformed fragment is accepted and the later reassembly path computes an oversized copy length from the underflowed offset range.
To fix this, I propose rejecting zero-length fragments for non-empty handshake messages and validate fragment bounds before using them. A minimal patch could look like this:
diff --git a/lib/buffers.c b/lib/buffers.c
--- a/lib/buffers.c
+++ b/lib/buffers.c
@@
- if (frag_size > 0)
- hsk->end_offset = hsk->start_offset + frag_size - 1;
- else
- hsk->end_offset = 0;
+ if (hsk->length > 0 && frag_size == 0)
+ return GNUTLS_E_UNEXPECTED_PACKET_LENGTH;
+
+ if (hsk->start_offset >= hsk->length)
+ return GNUTLS_E_UNEXPECTED_PACKET_LENGTH;
+
+ if (frag_size > hsk->length - hsk->start_offset)
+ return GNUTLS_E_UNEXPECTED_PACKET_LENGTH;
+
+ hsk->end_offset = hsk->start_offset + frag_size - 1;
@@
- data_size = MIN(tmp.length, tmp.end_offset - tmp.start_offset + 1);
+ data_size = MIN(tmp.length, tmp.end_offset - tmp.start_offset + 1);
+ data_size = MIN(data_size, _mbuffer_get_udata_size(bufel));
Cheers, Josh