Talos Vulnerability Report

TALOS-2023-1792

GTKWave FST fstReaderIterBlocks2 time_table tsec_nitems integer overflow vulnerability

January 8, 2024
CVE Number

CVE-2023-35128

SUMMARY

An integer overflow vulnerability exists in the fstReaderIterBlocks2 time_table tsec_nitems functionality of GTKWave 3.3.115. A specially crafted .fst file can lead to memory corruption. 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.0 - CVSS:3.1/AV:L/AC:H/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);
                     ...
                     }
             }
     ...

After parsing the header, fstReaderInit proceeds to parse other blocks that aren’t relevant for this advisory.

Upon return from fstReaderOpen, GTKWave eventually calls fstReaderIterBlocks2 to parse all VCDATA blocks, as they haven’t been fully parsed in fstReaderInit.

fstReaderIterBlocks2 is a pretty large function, so we’ll focus on the interesting lines only:

     int fstReaderIterBlocks2(void *ctx,
                              void (*value_change_callback)(void *user_callback_data_pointer, uint64_t time, fstHandle facidx, const unsigned char *value),
                              void (*value_change_callback_varlen)(void *user_callback_data_pointer, uint64_t time, fstHandle facidx, const unsigned char *value, uint32_t len),
                              void *user_callback_data_pointer, FILE *fv) {
         struct fstReaderContext *xc = (struct fstReaderContext *)ctx;

         uint64_t previous_time = UINT64_MAX;
         uint64_t *time_table = NULL;
         uint64_t tsec_nitems;
         unsigned int secnum = 0;
         int blocks_skipped = 0;
         fst_off_t blkpos = 0;
         uint64_t seclen, beg_tim;
         uint64_t end_tim;
         uint64_t frame_uclen, frame_clen, frame_maxhandle, vc_maxhandle;
         fst_off_t vc_start;
         fst_off_t indx_pntr, indx_pos;
         fst_off_t *chain_table = NULL;
         uint32_t *chain_table_lengths = NULL;
         unsigned char *chain_cmem;
         unsigned char *pnt;
         long chain_clen;
         fstHandle idx, pidx = 0, i;
         uint64_t pval;
         uint64_t vc_maxhandle_largest = 0;
         uint64_t tsec_uclen = 0, tsec_clen = 0;
         int sectype;
         uint64_t mem_required_for_traversal;
         unsigned char *mem_for_traversal = NULL;
         uint32_t traversal_mem_offs;
         uint32_t *scatterptr, *headptr, *length_remaining;
         uint32_t cur_blackout = 0;
         int packtype;
         unsigned char *mc_mem = NULL;
         uint32_t mc_mem_len; /* corresponds to largest value encountered in chain_table_lengths[i] */
         int dumpvars_state = 0;

         ...
[10]     for (;;) {
             uint32_t *tc_head = NULL;
             traversal_mem_offs = 0;

             fstReaderFseeko(xc, xc->f, blkpos, SEEK_SET);

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

             if ((sectype == EOF) || (sectype == FST_BL_SKIP)) {
     #ifdef FST_DEBUG
                 fprintf(stderr, FST_APIMESS "<< EOF >>\n");
     #endif
                 break;
             }

             blkpos++;
[12]         if ((sectype != FST_BL_VCDATA) && (sectype != FST_BL_VCDATA_DYN_ALIAS) && (sectype != FST_BL_VCDATA_DYN_ALIAS2)) {
                 blkpos += seclen;
                 continue;
             }

             if (!seclen) break;
         ...

As previously said, all blocks are scanned [10], and only the blocks with a sectype [11] containing VCDATA are parsed [12]. These blocks are FST_BL_VCDATA, FST_BL_VCDATA_DYN_ALIAS and FST_BL_VCDATA_DYN_ALIAS2.

Later on, the code extracts the time block:

[13]         uint64_t tsec_nitems;
             ...
             /* process time block */
             {
             unsigned char *ucdata;
             unsigned char *cdata;
             unsigned long destlen /* = tsec_uclen */; /* scan-build */
             unsigned long sourcelen /*= tsec_clen */; /* scan-build */
             int rc;
             unsigned char *tpnt;
             uint64_t tpval;
             unsigned int ti;

             if(fstReaderFseeko(xc, xc->f, blkpos + seclen - 24, SEEK_SET) != 0) break;
             tsec_uclen = fstReaderUint64(xc->f);
             tsec_clen = fstReaderUint64(xc->f);
[14]         tsec_nitems = fstReaderUint64(xc->f);
     #ifdef FST_DEBUG
             fprintf(stderr, FST_APIMESS "time section unc: %d, com: %d (%d items)\n",
                     (int)tsec_uclen, (int)tsec_clen, (int)tsec_nitems);
     #endif
             if(tsec_clen > seclen) break; /* corrupted tsec_clen: by definition it can't be larger than size of section */
             ucdata = (unsigned char *)malloc(tsec_uclen);
             if(!ucdata) break; /* malloc fail as tsec_uclen out of range from corrupted file */
             destlen = tsec_uclen;
             sourcelen = tsec_clen;

             fstReaderFseeko(xc, xc->f, -24 - ((fst_off_t)tsec_clen), SEEK_CUR);

[15]         if(tsec_uclen != tsec_clen)
                     {
                     cdata = (unsigned char *)malloc(tsec_clen);
                     fstFread(cdata, tsec_clen, 1, xc->f);

                     rc = uncompress(ucdata, &destlen, cdata, sourcelen);

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

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

             free(time_table);
[16]         time_table = (uint64_t *)calloc(tsec_nitems, sizeof(uint64_t));
             tpnt = ucdata;
             tpval = 0;
[17]         for(ti=0;ti<tsec_nitems;ti++)
                     {
                     int skiplen;
                     uint64_t val = fstGetVarint64(tpnt, &skiplen);
[18]                 tpval = time_table[ti] = tpval + val;
                     tpnt += skiplen;
                     }

             tc_head = (uint32_t *)calloc(tsec_nitems /* scan-build */ ? tsec_nitems : 1, sizeof(uint32_t));
             free(ucdata);
             }

At [14] tsec_nitems is read from file. Along with tsec_clen, tsec_uclen represents the size respectively of compressed and uncompressed time block data.
tsec_nitems represents the number of elements in the time table.

Right after these 3 fields, we have the time table block data, which is a series of 64-bit LEB128-encoded varints. This section can however be zlib-compressed if tsec_uclen is different from tsec_clen [15].

At [16] the time_table is allocated using calloc, with an element count of tsec_nitems and an element size of sizeof(uint64_t). calloc internally multiplies tsec_nitems by sizeof(uint64_t), which could lead to an integer overflow, depending on the calloc implementation: some implementations (for example glibc) will detect the overflow and return 0, leading to just crashing GTKWave. If the calloc implementation, however, doesn’t detect the overflow, calloc may end up allocating a very small buffer. For example on 64-bit systems, a tsec_nitems value of 0x2000000000000000 will lead to allocating a small buffer (again, the actual size depends on the implementation). In this case, the buffer will be too small to fit tsec_nitems, so the loop at [17], which extracts varints from the file and writes them in time_table[ti], will eventually write out-of-bounds in the heap, with attacker-controlled values, leading to memory corruption. Because of the multi-threaded nature of GTKWave, an attacker may be able to exploit this issue to execute arbitrary code.

A special note for 32-bit systems: As calloc expects size_t arguments, and tsec_nitems is of type uint64_t [13], tsec_nitems will be implicitly cast to a 32-bit value. So, on 32-bit systems this issue is triggerable even if calloc performs overflow checks, as the casting happens before the call, leading to calling calloc(0, 8).

Crash Information

==32993==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5f00550 at pc 0x5659f8a7 bp 0xffffd228 sp 0xffffd21c
WRITE of size 8 at 0xf5f00550 thread T0
    #0 0x5659f8a6 in fstReaderIterBlocks2 fst/fstapi.c:5164
    #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)

0xf5f00551 is located 0 bytes to the right of 1-byte region [0xf5f00550,0xf5f00551)
allocated by thread T0 here:
    #0 0xf7a55bab in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:77
    #1 0x5659f77c in fstReaderIterBlocks2 fst/fstapi.c:5157
    #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

SUMMARY: AddressSanitizer: heap-buffer-overflow fst/fstapi.c:5164 in fstReaderIterBlocks2
Shadow bytes around the buggy address:
  0x3ebe0050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  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 fa fa
=>0x3ebe00a0: fa fa fa fa fa fa fa fa fa fa[01]fa fa fa 05 fa
  0x3ebe00b0: fa fa 00 fa fa fa 00 fa fa fa 00 fa fa fa 01 fa
  0x3ebe00c0: fa fa 02 fa fa fa 00 fa fa fa fd fa fa fa fd fa
  0x3ebe00d0: fa fa fd fa fa fa fd fa fa fa 00 fa 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
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.