RTKLIB turns raw satellite signals into centimeter-level position fixes for ships, drones, aircraft and self-driving machines. We found four memory-corruption bugs in its correction- and observation-stream decoders, reachable from the very data those machines are designed to trust.
Plain GPS is accurate to a few meters, fine for a phone, useless for landing a drone on a moving boat or keeping a tractor in a 2 cm furrow. Real-Time Kinematic (RTK) GNSS fixes that by comparing your receiver against a known base station and streaming correction data to cancel out atmospheric and orbital error. The result: centimeter accuracy, in real time.
RTKLIB is the de-facto open-source engine for this. Its tools (rtkrcv, rtknavi, rnx2rtkp, str2str) and its decoders are embedded across drones, survey gear, CORS networks, marine and automotive positioning stacks. To deliver centimeter accuracy it must continuously parse correction streams and observation files, and that parser is our attack surface.
Three formats carry the data RTKLIB ingests. Two of them, RTCM3 and RINEX, are where our bugs live.
The correction wire format. Compact binary messages (station coordinates, observations/MSM, ephemerides, SSR biases) streamed live to the rover. Framed with a 0xD3 preamble, a 10-bit length, a 12-bit message type, and a 24-bit CRC.
“Networked Transport of RTCM via Internet Protocol.” How RTCM3 reaches a rover that isn’t in radio range of the base: a caster relays mountpoint streams over the internet. Anyone able to MITM or impersonate a caster controls the bytes.
“Receiver Independent Exchange.” Human-readable observation/navigation files, distributed by the thousands over FTP/HTTP by public CORS networks and fed into post-processing tools. A single file is enough to reach the parser.
Every bug sits behind data RTKLIB is built to accept from outside. None require a login; all are reachable from the normal correction/observation paths.
RTK-grade GNSS is load-bearing for machines that move. Here’s what “the decoder crashed” translates to once it’s bolted onto something with mass and momentum.
RTK drives precision docking, dynamic positioning and uncrewed surface vessels. A poisoned NTRIP correction feed that crashes or corrupts the positioning stack mid-maneuver removes the very reference the autopilot is steering by, in a harbor, that’s a grounding or allision risk.
Lane-level localization fuses RTK GNSS with other sensors, fed by CORS/NTRIP services. A correction stream that can crash the localizer becomes a remote way to degrade or drop a safety- critical input across an entire fleet subscribed to the same caster.
Survey, mapping, agriculture and delivery UAVs lean on RTK/PPK for tight flight paths and precision landing, with corrections piped over 4G/telemetry. Corrupting the GNSS decoder on the companion computer can stall the position estimate, the difference between a landing and a flyaway.
National CORS networks publish RINEX by the terabyte; surveyors batch-process untrusted files in RTKLIB-based tools. One booby-trapped RINEX file in a dataset is enough to crash, or corrupt the memory of, whoever opens it, a quiet foothold in the geospatial supply chain.
The bugs are memory corruption inside the decode step. Here’s how an attacker chains position in the network into impact on the machine.
An RTCM3 type-1033 message carries five 8-bit length fields for antenna and receiver descriptor strings. The decoder copies that many bytes into fixed 64-byte fields of rtcm->sta with strncpy and never clamps the length against the destination size. A length of 255 writes ~191 bytes past a 64-byte buffer, five times over, corrupting adjacent decoder state.
Reach: any NTRIP/serial correction stream, base-station metadata is broadcast routinely; a valid CRC is trivially computed by the sender.
/* n,m,n1,n2,n3 read straight off the wire: 0..255, unchecked */
strncpy(rtcm->sta.antdes, des, n); rtcm->sta.antdes[n] = '\0'; // char[64], n up to 255
strncpy(rtcm->sta.antsno, sno, m); ... // + four more fields, same flaw
An RTCM3 type-1033 message carries five 8-bit length fields for antenna and receiver descriptor strings. The decoder copies that many bytes into fixed 64-byte fields of rtcm->sta with strncpy and never clamps the length against the destination size. A length of 255 writes ~191 bytes past a 64-byte buffer, five times over, corrupting adjacent decoder state.
Reach: RTCM3 SSR streams (types 1059/1065/1242/1248/1254/1260) over NTRIP/serial.
if (mode <= ncode) { // should be: mode < ncode
cbias[codes[mode] - 1] = bias; // codes[ncode] is out of bounds
}
A RINEX observation epoch declares how many satellites follow. decode_obsepoch returns that count unclamped; the record loop then writes data[n] and reads sats[i-1] for every declared satellite and the n < MAXOBS bound is checked only after the write. Declare 70 satellites (cap is 64) and the observation buffer overflows.
Reach: a single RINEX file, local, emailed, or auto-fetched from a CORS FTP/HTTP mirror.
data[n].time = time; // rinex.c:948 — written before the bound check
data[n].sat = sats[i-1]; // sats[MAXOBS] overrun
if (decode_obsdata(...) && n < MAXOBS) n++; // too late
When a RINEX header lists an observation code RTKLIB doesn’t recognise, code2obs hands back an empty string and index 0. getcodepri then computes codepris[i][j-1], a negative [-1] index, and reads one byte past the empty string constant.
Reach: a crafted RINEX header with a bogus obs code.
obs = code2obs(code, &j); // unknown code → "" , j = 0
... strchr(codepris[i][j-1], obs[1]); // codepris[i][-1] + read past ""
Centimeter-grade GNSS quietly became load-bearing for machines that move themselves, and RTKLIB is the engine most of them lean on. Its decoders are the front door: they parse correction streams and observation files before a single coordinate is ever computed, and that front door was never hardened against hostile input.
We found four memory-corruption bugs, each reachable from a single crafted RTCM3 message or RINEX file, an out-of-bounds write in the antenna-descriptor decoder and three out-of-bounds reads across the SSR, epoch-count and observation-code paths. No authentication, no handshake: an attacker only has to sit upstream of the antenna, a rogue NTRIP caster, a man-in-the-middled stream, or one booby-trapped file. And because RTKLIB is permissively licensed and forked everywhere, the same flaws ride quietly into closed firmware that never says the word “RTKLIB.”
The fix is unglamorous and well understood: clamp every wire-supplied length and count to its buffer before you touch it, and treat correction data as the untrusted input it has always been. Operators should authenticate their casters and sensor-fuse GNSS so a single poisoned stream can’t silently steer the platform. We reported all four findings upstream under coordinated disclosure (RTKLIB #796–799). The GNSS stack deserves the same adversarial scrutiny we’ve spent two decades giving browsers and kernels and at FuzzingLabs, we’re just getting started.
Patrick Ventuzelo / @Pat_Ventuzelo
Founded in 2021 and headquartered in Paris, FuzzingLabs is a cybersecurity startup specializing in vulnerability research, fuzzing, and blockchain security. We combine cutting-edge research with hands-on expertise to secure some of the most critical components in the blockchain ecosystem.
Contact us for an audit or long term partnership!
| Cookie | Duration | Description |
|---|---|---|
| cookielawinfo-checkbox-analytics | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics". |
| cookielawinfo-checkbox-functional | 11 months | The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional". |
| cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
| cookielawinfo-checkbox-others | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other. |
| cookielawinfo-checkbox-performance | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance". |
| viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |