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.

  1. Start a DTLS client or server using GnuTLS.
  2. During the handshake, send one DTLS handshake record with:
    • length = 0x000010
    • fragment_offset = 0x000001
    • fragment_length = 0x000000
  3. Include only the DTLS handshake header and no fragment body, or fewer payload bytes than the declared handshake length.
  4. Let the peer parse and buffer the record.
  5. 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

Assignee Loading
Time tracking Loading