Talos Vulnerability Report

TALOS-2023-1797

GTKWave FST fstReaderIterBlocks2 temp_signal_value_buf allocation integer overflow vulnerability

January 8, 2024
CVE Number

CVE-2023-36864

SUMMARY

An integer overflow vulnerability exists in the fstReaderIterBlocks2 temp_signal_value_buf allocation 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 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.

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-190 - Integer Overflow or Wraparound

DETAILS

GTKWave is a wave viewer, often used to analyze FPGA simulations and logic analyzer captures. It includes a GUI to view and analyze traces, as well as convert 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. For example, it’s enough for a victim to double-click on a wave file received by e-mail to trigger the vulnerability 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. This big (about 65KB) structure is 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 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 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.

After parsing the header, fstReaderInit then proceeds to parse other blocks, among them, the FST_BL_GEOM block:

     else if(sectype == FST_BL_GEOM)
             {
             if(!hdr_incomplete)
                     {
                     uint64_t clen = seclen - 24;
                     uint64_t uclen = fstReaderUint64(xc->f);
                     unsigned char *ucdata = (unsigned char *)malloc(uclen);
                     unsigned char *pnt = ucdata;
                     unsigned int i;

                     xc->contains_geom_section = 1;
                     xc->maxhandle = fstReaderUint64(xc->f);
[15]                 xc->longest_signal_value_len = 32; /* arbitrarily set at 32...this is much longer than an expanded double */

                     free(xc->process_mask);
                     xc->process_mask = (unsigned char *)calloc(1, (xc->maxhandle+7)/8);

[10]                 if(clen != uclen)
                             {
                             unsigned char *cdata = (unsigned char *)malloc(clen);
                             unsigned long destlen = uclen;
                             unsigned long sourcelen = clen;
                             int rc;

                             fstFread(cdata, clen, 1, xc->f);
                             rc = uncompress(ucdata, &destlen, cdata, sourcelen);

                             if(rc != Z_OK)
                                     {
                                     fprintf(stderr, FST_APIMESS "fstReaderInit(), geom uncompress rc = %d, exiting.\n", rc);
                                     exit(255);
                                     }

                             free(cdata);
                             }
                             else
                             {
                             fstFread(ucdata, uclen, 1, xc->f);
                             }

                     free(xc->signal_lens);
[11]                 xc->signal_lens = (uint32_t *)malloc(sizeof(uint32_t) * xc->maxhandle);
                     free(xc->signal_typs);
                     xc->signal_typs = (unsigned char *)malloc(sizeof(unsigned char) * xc->maxhandle);

                     for(i=0;i<xc->maxhandle;i++)
                             {
                             int skiplen;
[12]                         uint64_t val = fstGetVarint32(pnt, &skiplen);

                             pnt += skiplen;

                             if(val)
                                     {
[13]                                 xc->signal_lens[i] = (val != 0xFFFFFFFF) ? val : 0;
                                     xc->signal_typs[i] = FST_VT_VCD_WIRE;
                                     if(xc->signal_lens[i] > xc->longest_signal_value_len)
                                             {
[14]                                         xc->longest_signal_value_len = xc->signal_lens[i];
                                             }
                                     }
                                     else
                                     {
                                     xc->signal_lens[i] = 8; /* backpatch in real */
                                     xc->signal_typs[i] = FST_VT_VCD_REAL;
                                     /* xc->longest_signal_value_len handled above by overly large init size */
                                     }
                             }

                     free(xc->temp_signal_value_buf);
[16]                 xc->temp_signal_value_buf = (unsigned char *)malloc(xc->longest_signal_value_len + 1);

                     free(ucdata);
                     }
             }

At [10], the FST_BL_GEOM section is optionally decompressed with zlib. At [11] the signal_lens buffer is allocated, with xc->maxhandle elements. Finally, the signal_lens is filled [13] by reading 32-bit LEB128-encoded integers [12] from file. At [14], xc->longest_signal_value_len is set to keep track of the largest xc->signal_lens. Note that by default xc->longest_signal_value_len is set to 32. Finally at [16] the xc->temp_signal_value_buf is allocated, adding 1 to xc->longest_signal_value_len. In this code, there’s no integer overflow issues because xc->longest_signal_value_len can’t be 0xffffffff, thanks to the check at [13].

However, this is not the only way to define signal_lens. In fact, if there’s no FST_BL_GEOM section, the signal_lens is filled by fstReaderProcessHier.
Also, upon return from fstReaderOpen, gtkwave eventually calls fstReaderIterBlocks2.
If, however, the file is opened by fst2vcd, a call to fstReaderProcessHier happens first. In this case the “hier” section will overwrite any signal_lens structure defined in the FST_BL_GEOM section.

This is how signal_lens are processed inside fstReaderProcessHier:

     int fstReaderProcessHier(void *ctx, FILE *fv) {
         struct fstReaderContext *xc = (struct fstReaderContext *)ctx;
         ...

         if (!xc) return (0);

[17]     xc->longest_signal_value_len = 32; /* arbitrarily set at 32...this is much longer than an expanded double */

         ...

         xc->maxhandle = 0;
         xc->num_alias = 0;

         ...

         fstReaderFseeko(xc, xc->fh, 0, SEEK_SET);
         while (!feof(xc->fh)) {
             int tag = fgetc(xc->fh);
             switch (tag) {
                 ...
[18]             case FST_VT_VCD_EVENT:
                 case FST_VT_VCD_INTEGER:
                 case FST_VT_VCD_PARAMETER:
                 case FST_VT_VCD_REAL:
                 case FST_VT_VCD_REAL_PARAMETER:
                 case FST_VT_VCD_REG:
                 case FST_VT_VCD_SUPPLY0:
                 case FST_VT_VCD_SUPPLY1:
                 case FST_VT_VCD_TIME:
                 case FST_VT_VCD_TRI:
                 case FST_VT_VCD_TRIAND:
                 case FST_VT_VCD_TRIOR:
                 case FST_VT_VCD_TRIREG:
                 case FST_VT_VCD_TRI0:
                 case FST_VT_VCD_TRI1:
                 case FST_VT_VCD_WAND:
                 case FST_VT_VCD_WIRE:
                 case FST_VT_VCD_WOR:
                 case FST_VT_VCD_PORT:
                 case FST_VT_VCD_SPARRAY:
                 case FST_VT_VCD_REALTIME:
                 case FST_VT_GEN_STRING:
                 case FST_VT_SV_BIT:
                 case FST_VT_SV_LOGIC:
                 case FST_VT_SV_INT:
                 case FST_VT_SV_SHORTINT:
                 case FST_VT_SV_LONGINT:
                 case FST_VT_SV_BYTE:
                 case FST_VT_SV_ENUM:
                 case FST_VT_SV_SHORTREAL:
[19]                 vartype = tag;
                     /* vardir = */ fgetc(xc->fh); /* unused in VCD reader, but need to advance read pointer */
                     pnt = str;
                     cl = 0;
                     while ((ch = fgetc(xc->fh))) {
                         if (cl < FST_ID_NAM_ATTR_SIZ) {
                             pnt[cl++] = ch;
                         }
                     }; /* varname */
                     pnt[cl] = 0;
[20]                 len = fstReaderVarint32(xc->fh);
                     alias = fstReaderVarint32(xc->fh);

                     if (!alias) {
                         if (xc->maxhandle == num_signal_dyn) {
                             num_signal_dyn *= 2;
                             xc->signal_lens = (uint32_t *)realloc(xc->signal_lens, num_signal_dyn * sizeof(uint32_t));
                             xc->signal_typs = (unsigned char *)realloc(xc->signal_typs, num_signal_dyn * sizeof(unsigned char));
                         }
[21]                     xc->signal_lens[xc->maxhandle] = len;
                         xc->signal_typs[xc->maxhandle] = vartype;

                         /* maxvalpos+=len; */
[22]                     if (len > xc->longest_signal_value_len) {
                             xc->longest_signal_value_len = len;
                         }

                         ...
                         xc->maxhandle++;
                     } else {
                        ...
                     }

                     break;

                 default:
                     break;
             }
         }

         ...

         free(xc->temp_signal_value_buf);
[23]     xc->temp_signal_value_buf = (unsigned char *)malloc(xc->longest_signal_value_len + 1);

         xc->var_count = xc->maxhandle + xc->num_alias;

         free(str);
         return (1);
     }

At [17], xc->longest_signal_value_len is initialized to 32. The HIER section is then scanned, and for any matching tag of the list [18], we enter the block at [19].
Two 32-bit varint values are read into len and alias [20]. If alias is not zero, len is assigned to the current signal_lens [21], and the biggest signal_lens value is saved to xc->longest_signal_value_len [22]. Note that in this case xc->longest_signal_value_len can have any arbitrary value as there are no checks.
Finally at [23], the xc->temp_signal_value_buf is allocated using xc->longest_signal_value_len + 1. If xc->longest_signal_value_len is 0xffffffff, the addition will wrap around and a call to malloc(0) will be made, which will allocate a small buffer (depending on the malloc implementation).

Later on, fstReaderIterBlocks2 is called to parse all VCDATA blocks, as they haven’t been fully parsed in fstReaderInit. Inside this function there are several spots that may write into xc->temp_signal_value_buf, which has a small size, leading to out-of-bounds writes in the heap. For example:

     for (i = 0; i < tsec_nitems; i++) {
         ...
         while (tc_head[i]) {
             idx = tc_head[i] - 1;
             vli = fstGetVarint32(mem_for_traversal + headptr[idx], &skiplen);

[24]         if (xc->signal_lens[idx] <= 1) {
                 ...
             } else {
[25]             uint32_t len = xc->signal_lens[idx];
                 unsigned char *vdata;

[26]             vli = fstGetVarint32(mem_for_traversal + headptr[idx], &skiplen);
                 /* tdelta = vli >> 1; */ /* scan-build */
                 vdata = mem_for_traversal + headptr[idx] + skiplen;

                 if (xc->signal_typs[idx] != FST_VT_VCD_REAL) {
                     if (!(vli & 1)) {
                         int byte = 0;
                         int bit;
                         unsigned int j;

[27]                     for (j = 0; j < len; j++) {
                             unsigned char ch;
                             byte = j / 8;
                             bit = 7 - (j & 7);
                             ch = ((vdata[byte] >> bit) & 1) | '0';
[28]                         xc->temp_signal_value_buf[j] = ch;
                         }
                         xc->temp_signal_value_buf[j] = 0;

The code above, inside the fstReaderIterBlocks2 function, processes the time_table and tc_head, to populate the temporary .hier file associated with the xc state.
If the signal_lens for the current idx is bigger than 1 [24], we enter the else block.
The value for the current signal_lens is stored in len [25], and a 32-bit varint is then read from file [26] into vli. Right after this varint we’ll have vdata in the file, so that’s a buffer completely under control.
If the current signal_typs is not FST_VT_VCD_REAL and the LSB of vli is 1, we enter a loop that iterates len times [27] (recall that as len is a signal_lens value, this field is arbitrarily controlled). Eventually, after enough loops, xc->temp_signal_value_buf[j] = ch; [28] will write out-of-bounds in the heap.

This out-of-bounds write can be controlled by defining two signal lens: one with a big value, enough to write out-of-bounds (for example, 0x7f is enough as a proof-of-concept), and a second signal_lens with a value of 0xffffffff, to trigger the integer overflow, which in turn leads to the small temp_signal_value_buf allocation. As vdata is also controlled, the out-of-bounds write is completely controlled, leading to arbitrary code execution.

Crash Information

==94891==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5f005b1 at pc 0x565a5464 bp 0xffffd258 sp 0xffffd24c
WRITE of size 1 at 0xf5f005b1 thread T0
    #0 0x565a5463 in fstReaderIterBlocks2 fst/fstapi.c:5783
    #1 0x5659ea84 in fstReaderIterBlocks fst/fstapi.c:4999
    #2 0x5655908d in main src/helpers/fst2vcd.c:185
    #3 0xf7659294 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #4 0xf7659357 in __libc_start_main_impl ../csu/libc-start.c:381
    #5 0x56558686 in _start (fst2vcd+0x3686)

0xf5f005b1 is located 0 bytes to the right of 1-byte region [0xf5f005b0,0xf5f005b1)
allocated by thread T0 here:
    #0 0xf7a55ffb in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x5659a629 in fstReaderProcessHier fst/fstapi.c:4555
    #2 0x56558ff7 in main src/helpers/fst2vcd.c:179
    #3 0xf7659294 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-buffer-overflow fst/fstapi.c:5783 in fstReaderIterBlocks2
Shadow bytes around the buggy address:
  0x3ebe0060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x3ebe0070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x3ebe0080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x3ebe0090: fa fa fa fa fa fa fa fa fa fa fa fa fa fa 00 04
  0x3ebe00a0: fa fa 02 fa fa fa fd fa fa fa 00 fa fa fa 00 fa
=>0x3ebe00b0: fa fa 00 fa fa fa[01]fa fa fa 01 fa fa fa 02 fa
  0x3ebe00c0: fa fa 00 fa fa fa fd fa fa fa fd fa fa fa fd fa
  0x3ebe00d0: fa fa fd fa fa fa 00 06 fa fa 00 06 fa fa 00 04
  0x3ebe00e0: fa fa 00 03 fa fa 00 04 fa fa 00 05 fa fa 00 04
  0x3ebe00f0: fa fa 00 05 fa fa 00 00 fa fa fa fa fa fa fa fa
  0x3ebe0100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
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
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.