Talos Vulnerability Report

TALOS-2023-1818

GTKWave LXT2 facgeometry parsing integer overflow vulnerabilities

January 8, 2024
CVE Number

CVE-2023-39273,CVE-2023-39271,CVE-2023-39274,CVE-2023-39275,CVE-2023-39272,CVE-2023-39270

SUMMARY

Multiple integer overflow vulnerabilities exist in the LXT2 facgeometry parsing functionality of GTKWave 3.3.115. A specially crafted .lxt2 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-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.

LXT2 (InterLaced eXtensible Trace Version 2) files are parsed by the functions found in lxt2_read.c. These functions are used in the lxt2vcd file conversion utility, rtlbrowse, lxt2miner, and by the GUI portion of GTKwave, which are thus all affected by the issue described in this report.

To parse LXT2 files, the function lxt2_rd_init is called:

     struct lxt2_rd_trace *lxt2_rd_init(const char *name) {
[1]      struct lxt2_rd_trace *lt = (struct lxt2_rd_trace *)calloc(1, sizeof(struct lxt2_rd_trace));
         lxtint32_t i;

[2]      if (!(lt->handle = fopen(name, "rb"))) {
             lxt2_rd_close(lt);
             lt = NULL;
         } else {
             lxtint16_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 LXT2 file.

         ...
[4]      rcf = fread(&lt->numfacbytes, 4, 1, lt->handle);
         lt->numfacbytes = rcf ? lxt2_rd_get_32(&lt->numfacbytes, 0) : 0;
         rcf = fread(&lt->longestname, 4, 1, lt->handle);
         lt->longestname = rcf ? lxt2_rd_get_32(&lt->longestname, 0) : 0;
         rcf = fread(&lt->zfacnamesize, 4, 1, lt->handle);
         lt->zfacnamesize = rcf ? lxt2_rd_get_32(&lt->zfacnamesize, 0) : 0;
         rcf = fread(&lt->zfacname_predec_size, 4, 1, lt->handle);
         lt->zfacname_predec_size = rcf ? lxt2_rd_get_32(&lt->zfacname_predec_size, 0) : 0;
         rcf = fread(&lt->zfacgeometrysize, 4, 1, lt->handle);
         lt->zfacgeometrysize = rcf ? lxt2_rd_get_32(&lt->zfacgeometrysize, 0) : 0;
         rcf = fread(&lt->timescale, 1, 1, lt->handle);
         if (!rcf) lt->timescale = 0; /* no swap necessary */
         ...

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. Both structures are compressed with gzip.

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

     fseeko(lt->handle, pos = pos + lt->zfacnamesize, SEEK_SET);
     /* fprintf(stderr, LXT2_RDLOAD"seeking to geometry at %d (0x%08x)\n", pos, pos); */
     lt->zhandle = gzdopen(dup(fileno(lt->handle)), "rb");

[5]  t = lt->numfacs * 4 * sizeof(lxtint32_t);
[6]  m = (char *)malloc(t);
[7]  rc = gzread(lt->zhandle, m, t);
     gzclose(lt->zhandle);
     lt->zhandle = NULL;
     if (rc != t) {
         fprintf(stderr, LXT2_RDLOAD "*** geometry section mangled %d (act) vs %d (exp)\n", rc, t);
         ...
     }

The decompressed facgeometry structure is pointed to by m [6] and decompressed at [7] using gzread. The decompressed size is expected to be numfacs * 16 [5].

Each geometry has 4 fields associated: rows, msb, lsb, flags. These are extracted in the following code, together with len and value arrays, which are calculated based on the 4 fields just mentioned.

     ...
[8]  lt->rows = malloc(lt->numfacs * sizeof(lxtint32_t));
[9]  lt->msb = malloc(lt->numfacs * sizeof(lxtsint32_t));
[10] lt->lsb = malloc(lt->numfacs * sizeof(lxtsint32_t));
[11] lt->flags = malloc(lt->numfacs * sizeof(lxtint32_t));
[12] lt->len = malloc(lt->numfacs * sizeof(lxtint32_t));
[13] lt->value = malloc(lt->numfacs * sizeof(char *));
[14] lt->next_radix = malloc(lt->numfacs * sizeof(void *));

[15] for (i = 0; i < lt->numfacs; i++) {
[16]     lt->rows[i] = lxt2_rd_get_32(m + i * 16, 0);
[17]     lt->msb[i] = lxt2_rd_get_32(m + i * 16, 4);
[18]     lt->lsb[i] = lxt2_rd_get_32(m + i * 16, 8);
[19]     lt->flags[i] = lxt2_rd_get_32(m + i * 16, 12);

         if (!(lt->flags[i] & LXT2_RD_SYM_F_INTEGER)) {
[20]         lt->len[i] = (lt->msb[i] <= lt->lsb[i]) ? (lt->lsb[i] - lt->msb[i] + 1) : (lt->msb[i] - lt->lsb[i] + 1);
         } else {
[20]         lt->len[i] = 32;
         }
[21]     lt->value[i] = calloc(lt->len[i] + 1, sizeof(char));
     }
     ...

In the code above, there are 7 identical issues, only affecting 32-bit mode. The size of the buffers is calculated by multiplying numfacs by 4. When numfacs is bigger than 0x40000000, this multiplication will wrap around in 32-bit mode, leading to calling malloc with a smaller size.

Right after these allocations, there’s a loop over numfacs [15,] which writes to all of those allocated buffers, leading to multiple out-of-bound writes on heap. As lt is also allocated on heap, by carefully manipulating heap allocations, these out-of-bounds writes can be used to overwrite pointers inside lt, which can in turn be used to write to arbitrary locations. For this reason this issue can lead to arbitrary code execution.

We are not assigning a CVE for the next_radix allocation [14], even though the allocation size wraps around, because in order to exploit this issue successfully the allocations need to be exploited at the same time, as next_radix is written to only in later parsing functions of the LXT2 file.

CVE-2023-39270 - rows array allocation

The size for the rows array allocation [8] may wrap around, leading to an out-of-bounds write at [16].

CVE-2023-39271 - msb array allocation

The size for the msb array allocation [9] may wrap around, leading to an out-of-bounds write at [17].

CVE-2023-39272 - lsb array allocation

The size for the lsb array allocation [10] may wrap around, leading to an out-of-bounds write at [18].

CVE-2023-39273 - flags array allocation

The size for the flags array allocation [11] may wrap around, leading to an out-of-bounds write at [19].

CVE-2023-39274 - len array allocation

The size for the len array allocation [12] may wrap around, leading to an out-of-bounds write at [20].

CVE-2023-39275 - value array allocation

The size for the value array allocation [13] may wrap around, leading to an out-of-bounds write at [21].

Crash Information

==818482==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xec903980 at pc 0x56562a75 bp 0xffffd658 sp 0xffffd64c
WRITE of size 4 at 0xec903980 thread T0
    #0 0x56562a74 in lxt2_rd_init src/helpers/lxt2_read.c:925
    #1 0x5656afba in process_lxt src/helpers/lxt2vcd.c:183
    #2 0x5656bfd7 in main src/helpers/lxt2vcd.c:458
    #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 0x565573a6 in _start (lxt2vcd+0x23a6)

0xec903980 is located 0 bytes to the right of 256-byte region [0xec903880,0xec903980)
allocated by thread T0 here:
    #0 0xf7a55ffb in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x56562097 in lxt2_rd_init src/helpers/lxt2_read.c:907
    #2 0x5656afba in process_lxt src/helpers/lxt2vcd.c:183
    #3 0x5656bfd7 in main src/helpers/lxt2vcd.c:458
    #4 0xf7659294 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-buffer-overflow src/helpers/lxt2_read.c:925 in lxt2_rd_init
Shadow bytes around the buggy address:
  0x3d9206e0: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x3d9206f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3d920700: 00 00 00 00 00 00 00 00 fa fa fa fa fa fa fa fa
  0x3d920710: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3d920720: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x3d920730:[fa]fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x3d920740: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3d920750: 00 00 00 00 00 00 00 00 fa fa fa fa fa fa fa fa
  0x3d920760: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3d920770: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3d920780: fa fa fa fa fa fa fa fa 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
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.