Talos Vulnerability Report

TALOS-2023-1783

GTKWave FST LEB128 varint stack-based buffer overflow vulnerabilities

January 8, 2024
CVE Number

CVE-2023-35704,CVE-2023-35703,CVE-2023-35702

SUMMARY

Multiple stack-based buffer overflow vulnerabilities exist in the FST LEB128 varint functionality of GTKWave 3.3.115. A specially crafted .fst file can lead to arbitrary code execution. A victim would need to open a malicious file to trigger these vulnerabilities.

CONFIRMED VULNERABLE VERSIONS

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

GTKWave 3.3.115

PRODUCT URLS

GTKWave - https://gtkwave.sourceforge.net

CVSSv3 SCORE

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

CWE

CWE-121 - Stack-based Buffer Overflow

DETAILS

GTKWave is a wave viewer, often used to analyze FPGA simulations and logic analyzer captures. It uses a graphical user interface to convert the traces across several file formats (.lxt, .lxt2, .vzt, .fst, .ghw, .vcd, .evcd) either by using the UI or its command line tools. GTKWave is available for Linux, Windows and MacOS. Trace files can be shared within teams or organizations, for example to compare results of simulation runs across different design implementations, to analyze protocols captured with logic analyzers or just as a reference when porting design implementations.

GTKWave sets up mime types for its supported extensions. So, for example, it’s enough for a victim to double-click on a wave file received by e-mail to trigger the vulnerabilities described in this advisory.

The function fstReaderOpen is used to parse .fst files.

     void *fstReaderOpen(const char *nam)
     {
[1]  struct fstReaderContext *xc = (struct fstReaderContext *)calloc(1, sizeof(struct fstReaderContext));
 
[2]  if((!nam)||(!(xc->f=fopen(nam, "rb"))))
             {
             free(xc);
             xc=NULL;
             }
             else
             {
             int flen = strlen(nam);
             char *hf = (char *)calloc(1, flen + 6);
             int rc;
 
     #if defined(FST_UNBUFFERED_IO)
             setvbuf(xc->f, (char *)NULL, _IONBF, 0);   /* keeps gzip from acting weird in tandem with fopen */
     #endif
 
             memcpy(hf, nam, flen);
             strcpy(hf + flen, ".hier");
             xc->fh = fopen(hf, "rb");
 
             free(hf);
             xc->filename = strdup(nam);
[3]          rc = fstReaderInit(xc);

At [1] the xc structure is created, a big (about 65KB) structure that’s used to hold information regarding the .fst file.
At [2] the xc->f field is set to the file pointer returned by fopen after opening the file being parsed.

Then, the function fstReaderInit is called [3] to parse the various .fst sections from the input file.

     int fstReaderInit(struct fstReaderContext *xc)
     {
     fst_off_t blkpos = 0;
     fst_off_t endfile;
     uint64_t seclen;
     int sectype;
     uint64_t vc_section_count_actual = 0;
     int hdr_incomplete = 0;
     int hdr_seen = 0;
     int gzread_pass_status = 1;

     sectype = fgetc(xc->f);
     ...
     if(gzread_pass_status)
             {
             fstReaderFseeko(xc, xc->f, 0, SEEK_END);
             endfile = ftello(xc->f);

             while(blkpos < endfile)
                     {
                     fstReaderFseeko(xc, xc->f, blkpos, SEEK_SET);

[4]                  sectype = fgetc(xc->f);
                     seclen = fstReaderUint64(xc->f);

                     if(sectype == EOF)
                             {
                             break;
                             }

[5]                  if((hdr_incomplete) && (!seclen))
                             {
                             break;
                             }

[6]                  if(!hdr_seen && (sectype != FST_BL_HDR))
                             {
                             break;
                             }

                     blkpos++;
                     ...

At [4], the sectype (1 byte) is read off the file. This variable will tell us which kind of sector we’re parsing. The checks at [5] and [6] are important because we don’t want to trigger them in order to reach the vulnerable code: we need to make sure hdr_incomplete stays 0 and that we start with a FST_BL_HDR sector, which will set hdr_seen to 1.

So, if the input file starts with a 0x00, we’ll land inside the following condition:

     ...
     if(sectype == FST_BL_HDR)
             {
             if(!hdr_seen)
                     {
                     int ch;
                     double dcheck;

                     xc->start_time = fstReaderUint64(xc->f);
                     xc->end_time = fstReaderUint64(xc->f);

[7]                  hdr_incomplete = (xc->start_time == 0) && (xc->end_time == 0);

                     ...
[8]                  hdr_seen = 1;

[9]                  xc->mem_used_by_writer = fstReaderUint64(xc->f);
                     xc->scope_count = fstReaderUint64(xc->f);
                     xc->var_count = fstReaderUint64(xc->f);
                     ...
                     }
             }
     ...

fstReaderUint64 is used to read a 64-bit unsigned integer (big endian) off the file, incrementing the current file offset.
We need to make sure that either start_time or end_time are not 0, to not set hdr_incomplete [7].
At [8] hdr_seen is set so that we can pass the check at [6] when parsing the next sections.
Finally at [9], other values are read off the file and saved into the xc structure.

Next, if the file contains the FST_BL_BLACKOUT (0x02) section, we’ll land inside the following block:

     ...
     else if(sectype == FST_BL_BLACKOUT)
             {
             uint32_t i;
             uint64_t cur_bl = 0;
             uint64_t delta;

[10]         xc->num_blackouts = fstReaderVarint32(xc->f);
             free(xc->blackout_times);
             xc->blackout_times = (uint64_t *)calloc(xc->num_blackouts, sizeof(uint64_t));
             free(xc->blackout_activity);
             xc->blackout_activity = (unsigned char *)calloc(xc->num_blackouts, sizeof(unsigned char));

             for(i=0;i<xc->num_blackouts;i++)
                     {
                     xc->blackout_activity[i] = fgetc(xc->f) != 0;
[11]                 delta = fstReaderVarint64(xc->f);
                     cur_bl += delta;
                     xc->blackout_times[i] = cur_bl;
                     }
             }

     blkpos += seclen;
     if(!hdr_seen) break;
     }

At [10] num_blackouts is read using fstReaderVarint32. Inside this function lies the issue, but keep in mind this is a generic function that implements LEB128 decoding. The same exact issue is present in fstReaderVarint64, which is called at [11], and in fstReaderVarint32WithSkip, which is triggered on a different code flow. These functions are called in multiple places during FST parsing. Below we describe the three vulnerable functions separately.

CVE-2023-35702 - fstReaderVarint32 stack-based buffer overflow

     static uint32_t fstReaderVarint32(FILE *f)
     {
[12] unsigned char buf[5];
     unsigned char *mem = buf;
     uint32_t rc = 0;
     int ch;

     do
             {
             ch = fgetc(f);
[13]         *(mem++) = ch;
[14]         } while(ch & 0x80);
     mem--;

     for(;;)
             {
             rc <<= 7;
             rc |= (uint32_t)(*mem & 0x7f);
             if(mem == buf)
                     {
                     break;
                     }
             mem--;
             }

     return(rc);
     }

fstReaderVarint32 reads a variable integer using LEB128 encoding from the input file f. A 5-byte buffer is allocated on the stack at [12], and the do-while loop reads each byte in the input file and puts it in buf [13] (via the temporary variable mem), stopping only when the MSB of the byte read from file is 1 [14].

This means that the stop condition of the loop is fully under attacker control, and this function can be exploited to write out-of-bounds off the stack variable buf, leading to arbitrary code execution. For example, it’s enough to write a crafted field like “FF FF FF FF FF FF FF” to write 2 bytes outside the bounds of buf.

CVE-2023-35703 - fstReaderVarint64 stack-based buffer overflow

     static uint64_t fstReaderVarint64(FILE *f)
     {
[12] unsigned char buf[16];
     unsigned char *mem = buf;
     uint64_t rc = 0;
     int ch;

     do
             {
             ch = fgetc(f);
[13]         *(mem++) = ch;
[14]         } while(ch & 0x80);
     mem--;

     for(;;)
             {
             rc <<= 7;
             rc |= (uint64_t)(*mem & 0x7f);
             if(mem == buf)
                     {
                     break;
                     }
             mem--;
             }

     return(rc);
     }

fstReaderVarint64 reads a variable integer using LEB128 encoding from the input file f. A 16-byte buffer is allocated on the stack at [12], and the do-while loop reads each byte in the input file and puts it in buf [13] (via the temporary variable mem), stopping only when the MSB of the byte read from file is 1 [14].

This means that the stop condition of the loop is fully under attacker control, and this function can be exploited to write out-of-bounds off the stack variable buf, leading to arbitrary code execution.

CVE-2023-35704 - fstReaderVarint32WithSkip stack-based buffer overflow

     static uint32_t fstReaderVarint32WithSkip(FILE *f, uint32_t *skiplen)
     {
[12] unsigned char buf[5];
     unsigned char *mem = buf;
     uint32_t rc = 0;
     int ch;

     do
             {
             ch = fgetc(f);
[13]         *(mem++) = ch;
[14]         } while(ch & 0x80);
     *skiplen = mem - buf;
     mem--;

     for(;;)
             {
             rc <<= 7;
             rc |= (uint32_t)(*mem & 0x7f);
             if(mem == buf)
                     {
                     break;
                     }
             mem--;
             }

     return(rc);
     }

fstReaderVarint32WithSkip reads a variable integer using LEB128 encoding from the input file f. A 5-byte buffer is allocated on the stack at [12], and the do-while loop reads each byte in the input file and puts it in buf [13] (via the temporary variable mem), stopping only when the MSB of the byte read from file is 1 [14].

This means that the stop condition of the loop is fully under attacker control, and this function can be exploited to write out-of-bounds off the stack variable buf, leading to arbitrary code execution.

The issue in fstReaderVarint32WithSkip can be triggered when parsing the FST’s VCDATA section. The parsing happens inside the fstReaderIterBlocks2 function:

     ...
     for (i = 0; i < idx; i++) {
         if (chain_table[i]) {
             int process_idx = i / 8;
             int process_bit = i & 7;

             if (xc->process_mask[process_idx] & (1 << process_bit)) {
                 int rc = Z_OK;
                 uint32_t val;
                 uint32_t skiplen;
                 uint32_t tdelta;

[15]             fstReaderFseeko(xc, xc->f, vc_start + chain_table[i], SEEK_SET);
[16]             val = fstReaderVarint32WithSkip(xc->f, &skiplen);
                 if (val) {
                     unsigned char *mu = mem_for_traversal + traversal_mem_offs; /* uncomp: dst */
                     unsigned char *mc;                                          /* comp:   src */
                     unsigned long destlen = val;
                     unsigned long sourcelen = chain_table_lengths[i];

                     if (mc_mem_len < chain_table_lengths[i]) {
                         free(mc_mem);
                         mc_mem = (unsigned char *)malloc(mc_mem_len = chain_table_lengths[i]);
                     }
                     mc = mc_mem;
     ...

At [15] the file cursor is placed at the start of the VCDATA section, offset depending on chain_table. To trigger this issue, it’s enough to start the VCDATA section with a crafted varint as described previously (a long sequence of “FF FF FF FF FF FF FF” is enough).

Crash Information

=================================================================
==9500==ERROR: AddressSanitizer: stack-buffer-overflow on address 0xffffd275 at pc 0x56580dfa bp 0xffffd218 sp 0xffffd20c
WRITE of size 1 at 0xffffd275 thread T0
    #0 0x56580df9 in fstReaderVarint32WithSkip fst/fstapi.c:590
    #1 0x565a2bf3 in fstReaderIterBlocks2 fst/fstapi.c:5538
    #2 0x5659ea84 in fstReaderIterBlocks fst/fstapi.c:4999
    #3 0x5655908d in main src/helpers/fst2vcd.c:185
    #4 0xf7659294 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #5 0xf7659357 in __libc_start_main_impl ../csu/libc-start.c:381
    #6 0x56558686 in _start (./fst2vcd+0x3686)

Address 0xffffd275 is located in stack of thread T0 at offset 37 in frame
    #0 0x56580d39 in fstReaderVarint32WithSkip fst/fstapi.c:581

  This frame has 1 object(s):
    [32, 37) 'buf' (line 582) <== Memory access at offset 37 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow fst/fstapi.c:590 in fstReaderVarint32WithSkip
Shadow bytes around the buggy address:
  0x3ffff9f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffa00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffa10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffa20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffa30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x3ffffa40: 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1[05]f3
  0x3ffffa50: f3 f3 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffa60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffa70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffa80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffa90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==9500==ABORTING
VENDOR RESPONSE

Fixed in version 3.3.118, available from https://sourceforge.net/projects/gtkwave/files/gtkwave-3.3.118/

TIMELINE

2023-07-10 - Initial Vendor Contact
2023-07-18 - Vendor Disclosure
2023-12-31 - Vendor Patch Release
2024-01-08 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.