CVE-2023-38618,CVE-2023-38621,CVE-2023-38620,CVE-2023-38619,CVE-2023-38623,CVE-2023-38622
Multiple integer overflow vulnerabilities exist in the VZT facgeometry parsing 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 these vulnerabilities.
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
GTKWave - https://gtkwave.sourceforge.net
7.8 - CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-190 - Integer Overflow or Wraparound
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, which are thus all 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(<->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(<->numfacs, 4, 1, lt->handle);
[4] lt->numfacs = rcf ? vzt_rd_get_32(<->numfacs, 0) : 0;
...
rcf = fread(<->numfacbytes, 4, 1, lt->handle);
lt->numfacbytes = rcf ? vzt_rd_get_32(<->numfacbytes, 0) : 0;
rcf = fread(<->longestname, 4, 1, lt->handle);
lt->longestname = rcf ? vzt_rd_get_32(<->longestname, 0) : 0;
rcf = fread(<->zfacnamesize, 4, 1, lt->handle);
lt->zfacnamesize = rcf ? vzt_rd_get_32(<->zfacnamesize, 0) : 0;
rcf = fread(<->zfacname_predec_size, 4, 1, lt->handle);
lt->zfacname_predec_size = rcf ? vzt_rd_get_32(<->zfacname_predec_size, 0) : 0;
rcf = fread(<->zfacgeometrysize, 4, 1, lt->handle);
lt->zfacgeometrysize = rcf ? vzt_rd_get_32(<->zfacgeometrysize, 0) : 0;
rcf = fread(<->timescale, 1, 1, lt->handle);
...
Several fields are then read from the file [4]:
numfacs
: the number of facilities (elements in facnames
)numfacbytes
: unusedlongestname
: keeps the longest length of all defined facilities’ nameszfacnamesize
: 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.
...
[5] lt->rows = malloc(lt->numfacs * sizeof(vztint32_t));
[6] lt->msb = malloc(lt->numfacs * sizeof(vztsint32_t));
[7] lt->lsb = malloc(lt->numfacs * sizeof(vztsint32_t));
[8] lt->flags = malloc(lt->numfacs * sizeof(vztint32_t));
[9] lt->len = malloc(lt->numfacs * sizeof(vztint32_t));
[10] lt->vindex_offset = malloc(lt->numfacs * sizeof(vztint32_t));
lt->longest_len = 32; /* big enough for decoded double in vzt_rd_process_block_single_factime() */
[11] for (i = 0; i < lt->numfacs; i++) {
int j;
[12] lt->rows[i] = vzt_rd_get_32(m + i * 16, 0);
[13] lt->msb[i] = vzt_rd_get_32(m + i * 16, 4);
[14] lt->lsb[i] = vzt_rd_get_32(m + i * 16, 8);
[15] 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))) {
[16] lt->len[i] = (lt->msb[j] <= lt->lsb[j]) ? (lt->lsb[j] - lt->msb[j] + 1) : (lt->msb[j] - lt->lsb[j] + 1);
} else {
[16] lt->len[i] = (lt->flags[j] & (VZT_RD_SYM_F_INTEGER | VZT_RD_SYM_F_STRING)) ? 32 : 64;
}
if (lt->len[i] > lt->longest_len) {
lt->longest_len = lt->len[i];
}
}
vindex_offset = 0; /* offset in value table */
for (lt->numrealfacs = 0; lt->numrealfacs < lt->numfacs; lt->numrealfacs++) {
if (lt->flags[lt->numrealfacs] & VZT_RD_SYM_F_ALIAS) {
break;
}
[17] lt->vindex_offset[lt->numrealfacs] = vindex_offset;
vindex_offset += lt->len[lt->numrealfacs];
}
...
In the code above, there are 6 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
[11] which is writing to all of those allocated buffers, leading to multiple out-of-bounds 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.
The size for the rows
array allocation [5] may wrap-around, leading to an out-of-bounds write at [12].
The size for the msb
array allocation [6] may wrap-around, leading to an out-of-bounds write at [13].
The size for the lsb
array allocation [7] may wrap-around, leading to an out-of-bounds write at [14].
The size for the flags
array allocation [8] may wrap-around, leading to an out-of-bounds write at [15].
The size for the len
array allocation [9] may wrap-around, leading to an out-of-bounds write at [16].
The size for the vindex_offset
array allocation [10] may wrap-around, leading to an out-of-bounds write at [17].
==401154==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5003e80 at pc 0x5656506d bp 0xffffd628 sp 0xffffd61c
WRITE of size 4 at 0xf5003e80 thread T0
#0 0x5656506c in vzt_rd_init_smp src/helpers/vzt_read.c:1868
#1 0x565697f3 in vzt_rd_init src/helpers/vzt_read.c:2194
#2 0x5656be5b in process_vzt src/helpers/vzt2vcd.c:176
#3 0x5656cf15 in main src/helpers/vzt2vcd.c:464
#4 0xf7611294 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#5 0xf7611357 in __libc_start_main_impl ../csu/libc-start.c:381
#6 0x565574f6 in _start (vzt2vcd+0x24f6)
0xf5003e80 is located 0 bytes to the right of 256-byte region [0xf5003d80,0xf5003e80)
allocated by thread T0 here:
#0 0xf7a55ffb in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x56564d0a in vzt_rd_init_smp src/helpers/vzt_read.c:1856
#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:1868 in vzt_rd_init_smp
Shadow bytes around the buggy address:
0x3ea00780: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x3ea00790: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x3ea007a0: 00 00 00 00 00 00 00 00 fa fa fa fa fa fa fa fa
0x3ea007b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x3ea007c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x3ea007d0:[fa]fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x3ea007e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x3ea007f0: 00 00 00 00 fa fa fa fa fa fa fa fa fa fa fa fa
0x3ea00800: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ea00810: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ea00820: 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
Fixed in version 3.3.118, available from https://sourceforge.net/projects/gtkwave/files/gtkwave-3.3.118/
2023-08-02 - Vendor Disclosure
2023-12-31 - Vendor Patch Release
2024-01-08 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.