Talos Vulnerability Report

TALOS-2023-1816

GTKWave VZT longest_len value allocation integer overflow vulnerability

January 8, 2024
CVE Number

CVE-2023-35004

SUMMARY

An integer overflow vulnerability exists in the VZT longest_len value allocation functionality of GTKWave 3.3.115. A specially crafted .vzt 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. So, for example, it’s enough for a victim to double-click on a wave file received by e-mail to cause the gtkwave program to be executed and load a potentially malicious file.

VZT (Verilog Zipped Trace) files are parsed by the functions found in vzt_read.c. These functions are used in the vzt2vcd file conversion utility, vztminer, and by the GUI portion of GTKwave. Thus both are affected by the issue described in this report.

To parse VZT files, the function vzt_rd_init_smp is called:

     struct vzt_rd_trace *vzt_rd_init_smp(const char *name, unsigned int num_cpus) {
[1]      struct vzt_rd_trace *lt = (struct vzt_rd_trace *)calloc(1, sizeof(struct vzt_rd_trace));
         ...

[2]      if (!(lt->handle = fopen(name, "rb"))) {
             vzt_rd_close(lt);
             lt = NULL;
         } else {
             vztint16_t id = 0, version = 0;
             ...
[3]          if (!fread(&id, 2, 1, lt->handle)) {
                 id = 0;
             }
             if (!fread(&version, 2, 1, lt->handle)) {
                 id = 0;
             }
             if (!fread(&lt->granule_size, 1, 1, lt->handle)) {
                 id = 0;
             }
         ...

At [1] the lt structure is initialized. This is the structure that will contain all the information about the input file.
The input file is opened [2] and 3 fields are read [3] to make sure the input file is a supported VZT file.

         ...
         rcf = fread(&lt->numfacs, 4, 1, lt->handle);
[4]      lt->numfacs = rcf ? vzt_rd_get_32(&lt->numfacs, 0) : 0;
         ...
         rcf = fread(&lt->numfacbytes, 4, 1, lt->handle);
         lt->numfacbytes = rcf ? vzt_rd_get_32(&lt->numfacbytes, 0) : 0;
         rcf = fread(&lt->longestname, 4, 1, lt->handle);
         lt->longestname = rcf ? vzt_rd_get_32(&lt->longestname, 0) : 0;
         rcf = fread(&lt->zfacnamesize, 4, 1, lt->handle);
         lt->zfacnamesize = rcf ? vzt_rd_get_32(&lt->zfacnamesize, 0) : 0;
         rcf = fread(&lt->zfacname_predec_size, 4, 1, lt->handle);
         lt->zfacname_predec_size = rcf ? vzt_rd_get_32(&lt->zfacname_predec_size, 0) : 0;
         rcf = fread(&lt->zfacgeometrysize, 4, 1, lt->handle);
         lt->zfacgeometrysize = rcf ? vzt_rd_get_32(&lt->zfacgeometrysize, 0) : 0;
         rcf = fread(&lt->timescale, 1, 1, lt->handle);
         ...

Several fields are then read from the file [4]:

  • numfacs: the number of facilities (elements in facnames)
  • numfacbytes: unused
  • longestname: keeps the longest length of all defined facilities’ names
  • zfacnamesize: compressed size of facnames
  • zfacname_predec_size: decompressed size of facnames
  • zfacgeometrysize: compressed size of facgeometry

Then, the facnames and facgeometry structures are extracted. They can be compressed with either gzip, bzip2 or lzma, depending on the first 2 bytes within the structure buffer.

For this advisory, we’re interested in the extraction and parsing of facgeometry:

switch (vzt_rd_det_gzip_type(lt->handle)) {
    case VZT_RD_IS_GZ:
        lt->zhandle = gzdopen(dup(fileno(lt->handle)), "rb");
        t = lt->numfacs * 4 * sizeof(vztint32_t);
        m = (char *)malloc(t);
        rc = gzread(lt->zhandle, m, t);
        gzclose(lt->zhandle);
        lt->zhandle = NULL;
        break;

    case VZT_RD_IS_BZ2:
        lt->zhandle = BZ2_bzdopen(dup(fileno(lt->handle)), "rb");
        t = lt->numfacs * 4 * sizeof(vztint32_t);
        m = (char *)malloc(t);
        rc = BZ2_bzread(lt->zhandle, m, t);
        BZ2_bzclose(lt->zhandle);
        lt->zhandle = NULL;
        break;

    case VZT_RD_IS_LZMA:
    default:
        lt->zhandle = LZMA_fdopen(dup(fileno(lt->handle)), "rb");
        t = lt->numfacs * 4 * sizeof(vztint32_t);
        m = (char *)malloc(t);
        rc = LZMA_read(lt->zhandle, m, t);
        LZMA_close(lt->zhandle);
        lt->zhandle = NULL;
        break;
}

The decompressed facgeometry structure is now pointed by m.
Each geometry has 4 fields associated: rows, msb, lsb, flags. These are extracted in the following code, together with len and vindex_offset arrays, which are calculated based on the 4 fields just mentioned.

     ...
     for (i = 0; i < lt->numfacs; i++) {
         int j;

         lt->rows[i] = vzt_rd_get_32(m + i * 16, 0);
[5]      lt->msb[i] = vzt_rd_get_32(m + i * 16, 4);
         lt->lsb[i] = vzt_rd_get_32(m + i * 16, 8);
         lt->flags[i] = vzt_rd_get_32(m + i * 16, 12) & VZT_RD_SYM_MASK; /* strip out unsupported bits */

         j = (!(lt->flags[i] & VZT_RD_SYM_F_ALIAS)) ? i : vzt_rd_get_alias_root(lt, i);

         if (!(lt->flags[i] & (VZT_RD_SYM_F_INTEGER | VZT_RD_SYM_F_STRING | VZT_RD_SYM_F_DOUBLE))) {
[6]          lt->len[i] = (lt->msb[j] <= lt->lsb[j]) ? (lt->lsb[j] - lt->msb[j] + 1) : (lt->msb[j] - lt->lsb[j] + 1);
         } else {
             lt->len[i] = (lt->flags[j] & (VZT_RD_SYM_F_INTEGER | VZT_RD_SYM_F_STRING)) ? 32 : 64;
         }

[7]      if (lt->len[i] > lt->longest_len) {
             lt->longest_len = lt->len[i];
         }
     }
     ...

In the code above, several fields are extracted from m (the decompressed facgeometry). In particular, msb and lsb [5] are used to fill a len array, which is used to keep the signed difference between msb and lsb [6].
Additionally, a longest_len variable is used to keep track of the largest len value encountered [7].

     ...
[8]  lt->value_current_sector = malloc(lt->longest_len + 1);
     lt->value_previous_sector = malloc(lt->longest_len + 1);
     ...

longest_len is then used as a size to allocate value_current_sector and value_previous_sector buffers [8].
We can already notice the issue at this point: if longest_len is 0xffffffff, the size operation will overflow and malloc(0) will be called, resulting in the allocation of a very small buffer (depending on the heap implementation).
In order to have longest_len set to 0xffffffff, it’s enough to have an msb of 0x7fffffff and an lsb of 0x80000001, which leads to the operation 0x7fffffff - 0x80000001 + 1 == -1. Other combinations of msb and lsb are possible.

After facgeometry, the file is expected to contain a sequence of blocks.
Upon return from the current vzt_rd_init_smp function, each block is parsed inside vzt_rd_iter_blocks, which in turn calls vzt_rd_process_block.

     int vzt_rd_process_block(struct vzt_rd_trace *lt, struct vzt_rd_block *b) {
         unsigned int i, i2;
         vztint32_t idx;
[11]     char *pnt = lt->value_current_sector, *pnt2 = lt->value_previous_sector;
         char buf[32];
         char *bufpnt;

         struct vzt_ncycle_autosort **autosort;
         struct vzt_ncycle_autosort *deadlist = NULL;
         struct vzt_ncycle_autosort *autofacs = calloc(lt->numrealfacs ? lt->numrealfacs : 1, sizeof(struct vzt_ncycle_autosort)); /* fix for scan-build on lt->numrealfacs */

         vzt_rd_block_vch_decode(lt, b);
         vzt_rd_pthread_mutex_lock(lt, &b->mutex);

         autosort = calloc(b->num_time_ticks, sizeof(struct vzt_ncycle_autosort *));
         for (i = 0; i < b->num_time_ticks; i++) autosort[i] = NULL;
         deadlist = NULL;

[9]      for (idx = 0; idx < lt->numrealfacs; idx++) {
             int process_idx = idx / 8;
             int process_bit = idx & 7;

             if (lt->process_mask[process_idx] & (1 << process_bit)) {
                 i = 0;

[10]             vzt_rd_fac_value(lt, b, i, idx, pnt);
                 ...

In this function, for each facility in the block [9], vzt_rd_fac_value is called [10], passing the pointer to lt->value_current_sector as last argument, which corresponds to value.

     int vzt_rd_fac_value(struct vzt_rd_trace *lt, struct vzt_rd_block *b, vztint32_t time_offset, vztint32_t facidx, char *value) {
[12]     vztint32_t len = lt->len[facidx];
         ...

         if (!(lt->flags[facidx] & VZT_RD_SYM_F_SYNVEC)) {
             vztint32_t vindex_offset = lt->vindex_offset[facidx];

             if (b->multi_state) {
                 vztint32_t vindex_offset_x = vindex_offset + lt->total_values;
                 vztint32_t *valpnt_x;
                 int which;

[13]            for (i = 0; i < len; i++) {
                     valpnt = val_base + (b->vindex[vindex_offset++] * row_size);
                     valpnt_x = val_base + (b->vindex[vindex_offset_x++] * row_size);

                     which = (((*valpnt_x >> bit) & 1) << 1) | ((*valpnt >> bit) & 1);
[14]                 value[i] = "01xz"[which];
                 }
             } else {
[13]             for (i = 0; i < len; i++) {
                     valpnt = val_base + (b->vindex[vindex_offset++] * row_size);
[14]                 value[i] = '0' | ((*valpnt >> bit) & 1);
                 }
             }
         } else {
             vztint32_t vindex_offset;

             if (b->multi_state) {
                 vztint32_t vindex_offset_x;
                 vztint32_t *valpnt_x;
                 int which;

[13]             for (i = 0; i < len; i++) {
                     if ((facidx + i) >= lt->numfacs) break;

                     vindex_offset = lt->vindex_offset[facidx + i];
                     vindex_offset_x = vindex_offset + lt->total_values;

                     valpnt = val_base + (b->vindex[vindex_offset] * row_size);
                     valpnt_x = val_base + (b->vindex[vindex_offset_x] * row_size);

                     which = (((*valpnt_x >> bit) & 1) << 1) | ((*valpnt >> bit) & 1);
[14]                 value[i] = "01xz"[which];
                 }
             } else {
[13]             for (i = 0; i < len; i++) {
                     if ((facidx + i) >= lt->numfacs) break;

                     vindex_offset = lt->vindex_offset[facidx + i];

                     valpnt = val_base + (b->vindex[vindex_offset] * row_size);
[14]                 value[i] = '0' | ((*valpnt >> bit) & 1);
                 }
             }
         }
         value[i] = 0;

         return (1);
     }

At [12] one len value is extracted, which is arbitrarily controlled by the file as shown at [6]. Then, there is a series of loops [13] iterating over the len value, writing to the value buffer [14], which corresponds to lt->value_current_sector.
As value has an overly small size because of the overflow at [8], any of these writes [14] will write out-of-bounds in the heap.

Since the parsing of blocks can be easily controlled via lt->flags, and since the blocks can appear in any order, the number of bytes written out-of-bounds can be carefully controlled, which allows arbitrary code execution.

Crash Information

==401254==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5e00571 at pc 0x5655af53 bp 0xffffd4e8 sp 0xffffd4dc
WRITE of size 1 at 0xf5e00571 thread T0
    #0 0x5655af52 in vzt_rd_fac_value src/helpers/vzt_read.c:627
    #1 0x5655cd55 in vzt_rd_process_block src/helpers/vzt_read.c:833
    #2 0x565619d4 in vzt_rd_iter_blocks src/helpers/vzt_read.c:1513
    #3 0x5656c5fc in process_vzt src/helpers/vzt2vcd.c:299
    #4 0x5656cf15 in main src/helpers/vzt2vcd.c:464
    #5 0xf7611294 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #6 0xf7611357 in __libc_start_main_impl ../csu/libc-start.c:381
    #7 0x565574f6 in _start (vzt2vcd+0x24f6)

0xf5e00571 is located 0 bytes to the right of 1-byte region [0xf5e00570,0xf5e00571)
allocated by thread T0 here:
    #0 0xf7a55ffb in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x56565f9c in vzt_rd_init_smp src/helpers/vzt_read.c:1905
    #2 0x565697f3 in vzt_rd_init src/helpers/vzt_read.c:2194
    #3 0x5656be5b in process_vzt src/helpers/vzt2vcd.c:176
    #4 0x5656cf15 in main src/helpers/vzt2vcd.c:464
    #5 0xf7611294 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-buffer-overflow src/helpers/vzt_read.c:627 in vzt_rd_fac_value
Shadow bytes around the buggy address:
  0x3ebc0050: fa fa 00 fa fa fa fd fa fa fa 00 05 fa fa fd fd
  0x3ebc0060: fa fa fd fa fa fa fd fd fa fa fd fa fa fa fd fd
  0x3ebc0070: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fd
  0x3ebc0080: fa fa fd fa fa fa fd fd fa fa fd fa fa fa fd fd
  0x3ebc0090: fa fa fd fd fa fa 00 04 fa fa 00 05 fa fa 00 04
=>0x3ebc00a0: fa fa 00 04 fa fa 00 04 fa fa 01 fa fa fa[01]fa
  0x3ebc00b0: fa fa 00 fa fa fa 00 fa fa fa 00 fa fa fa 00 fa
  0x3ebc00c0: fa fa 00 fa fa fa 00 fa fa fa fd fa fa fa 00 00
  0x3ebc00d0: fa fa fd fa fa fa 01 fa fa fa 04 fa fa fa 00 04
  0x3ebc00e0: fa fa 00 03 fa fa 00 04 fa fa 00 05 fa fa 00 04
  0x3ebc00f0: 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-08-02 - Vendor Disclosure
2023-12-31 - Vendor Patch Release
2024-01-08 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.