CVE-2023-35989
An integer overflow vulnerability exists in the LXT2 zlib block allocation 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 this vulnerability.
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.
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(<->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(<->numfacbytes, 4, 1, lt->handle);
lt->numfacbytes = rcf ? lxt2_rd_get_32(<->numfacbytes, 0) : 0;
rcf = fread(<->longestname, 4, 1, lt->handle);
lt->longestname = rcf ? lxt2_rd_get_32(<->longestname, 0) : 0;
rcf = fread(<->zfacnamesize, 4, 1, lt->handle);
lt->zfacnamesize = rcf ? lxt2_rd_get_32(<->zfacnamesize, 0) : 0;
rcf = fread(<->zfacname_predec_size, 4, 1, lt->handle);
lt->zfacname_predec_size = rcf ? lxt2_rd_get_32(<->zfacname_predec_size, 0) : 0;
rcf = fread(<->zfacgeometrysize, 4, 1, lt->handle);
lt->zfacgeometrysize = rcf ? lxt2_rd_get_32(<->zfacgeometrysize, 0) : 0;
rcf = fread(<->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
: 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. Both structures are compressed with gzip.
Right after these two structures, there’s a sequence of blocks that can be arbitrarily long.
for (;;) {
...
[5] b = calloc(1, sizeof(struct lxt2_rd_block));
[6] rcf = fread(&b->uncompressed_siz, 4, 1, lt->handle);
b->uncompressed_siz = rcf ? lxt2_rd_get_32(&b->uncompressed_siz, 0) : 0;
rcf = fread(&b->compressed_siz, 4, 1, lt->handle);
b->compressed_siz = rcf ? lxt2_rd_get_32(&b->compressed_siz, 0) : 0;
rcf = fread(&b->start, 8, 1, lt->handle);
b->start = rcf ? lxt2_rd_get_64(&b->start, 0) : 0;
rcf = fread(&b->end, 8, 1, lt->handle);
b->end = rcf ? lxt2_rd_get_64(&b->end, 0) : 0;
...
if ((b->uncompressed_siz) && (b->compressed_siz) && (b->end)) {
/* fprintf(stderr, LXT2_RDLOAD"block [%d] %lld / %lld\n", lt->numblocks, b->start, b->end); */
fseeko(lt->handle, b->compressed_siz, SEEK_CUR);
lt->numblocks++;
[7] if (lt->block_curr) {
lt->block_curr->next = b;
lt->block_curr = b;
lt->end = b->end;
} else {
lt->block_head = lt->block_curr = b;
lt->start = b->start;
lt->end = b->end;
}
} else {
free(b);
break;
}
pos += b->compressed_siz;
}
At [5] the block structure is allocated on the heap. At [6] some fields are extracted. Finally, the block is saved inside a linked list [7].
From this code we can see the file structure for a block as follows:
uncompressed_siz
- unsigned big endian 32 bitcompressed_siz
- unsigned big endian 32 bitstart_time
- unsigned big endian 64 bitend_time
- unsigned big endian 64 bitcompressed_siz
Upon return from the current lxt2_rd_init
function, the blocks are parsed inside lxt2_rd_iter_blocks
by walking the linked list created at [7].
int lxt2_rd_iter_blocks(struct lxt2_rd_trace *lt,
void (*value_change_callback)(struct lxt2_rd_trace **lt, lxtint64_t *time, lxtint32_t *facidx, char **value),
void *user_callback_data_pointer) {
struct lxt2_rd_block *b;
int blk = 0, blkfinal = 0;
int processed = 0;
struct lxt2_rd_block *bcutoff = NULL, *bfinal = NULL;
int striped_kill = 0;
unsigned int real_uncompressed_siz = 0;
unsigned char gzid[2];
lxtint32_t i;
if (lt) {
...
b = lt->block_head;
blk = 0;
...
while (b) {
if ((!b->mem) && (!b->short_read_ignore) && (!b->exclude_block)) {
...
fseeko(lt->handle, b->filepos, SEEK_SET);
gzid[0] = gzid[1] = 0;
if (!fread(&gzid, 2, 1, lt->handle)) {
gzid[0] = gzid[1] = 0;
}
fseeko(lt->handle, b->filepos, SEEK_SET);
[8] if ((striped_kill = (gzid[0] != 0x1f) || (gzid[1] != 0x8b))) {
lxtint32_t clen, unclen, iter = 0;
char *pnt;
off_t fspos = b->filepos;
lxtint32_t zlen = 16;
[9] char *zbuff = malloc(zlen);
struct z_stream_s strm;
real_uncompressed_siz = b->uncompressed_siz;
pnt = b->mem = malloc(b->uncompressed_siz);
b->uncompressed_siz = 0;
lxt2_rd_regenerate_process_mask(lt);
while (iter != 0xFFFFFFFF) {
size_t rcf;
clen = unclen = iter = 0;
[10] rcf = fread(&clen, 4, 1, lt->handle);
clen = rcf ? lxt2_rd_get_32(&clen, 0) : 0;
rcf = fread(&unclen, 4, 1, lt->handle);
unclen = rcf ? lxt2_rd_get_32(&unclen, 0) : 0;
rcf = fread(&iter, 4, 1, lt->handle);
iter = rcf ? lxt2_rd_get_32(&iter, 0) : 0;
fspos += 12;
if ((iter == 0xFFFFFFFF) || (lt->process_mask_compressed[iter / LXT2_RD_PARTIAL_SIZE])) {
[11] if (clen > zlen) {
if (zbuff) free(zbuff);
[12] zlen = clen * 2;
[13] zbuff = malloc(zlen ? zlen : 1 /* scan-build */);
}
[14] if (!fread(zbuff, clen, 1, lt->handle)) {
clen = 0;
}
...
If the block does not start with the gzip magic [8], the block is decompressed directly using zlib.
To do this, zbuff
is allocated to contain the compressed contents of the block, currently with a size of 16 bytes [9].
Then, clen
, unclen
and iter
fields are extracted as 32-bit big-endian integers from the file [10].
Then, if clen
(the compressed len) is larger than zlen
(the current size of zbuff
) [11], the zbuff
buffer needs to be enlarged to fit the contents. So, at [12], zlen
is set to clen * 2
and zbuff
is allocate with a size of zlen
[13].
Finally, fread
is called to read clen
bytes into zbuff
.
At [12] an integer overflow can happen during the multiplication. For example, if clen
is 0x80000000, zlen
will be set to 0, which ends up calling malloc(1)
when allocating the zbuff
buffer at [13]. fread
is then called with a clen
of 0x80000000. fread
will stop early, when the end-of-file is reached, but not before having written out-of-bounds of zbuff
. Because the write can be carefully controlled, this issue can be used to execute arbitrary code.
LXTLOAD | 1 facilities
LXTLOAD | Read 2 block headers OK
LXTLOAD | [0] start time
LXTLOAD | [-4919131752989213765] end time
LXTLOAD |
LXTLOAD | block [0] processing 0 / 40
LXTLOAD | short read on subblock 0 vs 0 (exp), ignoring
munmap_chunk(): invalid pointer
Aborted
Fixed in version 3.3.118, available from https://sourceforge.net/projects/gtkwave/files/gtkwave-3.3.118/
2023-08-11 - Vendor Disclosure
2023-12-31 - Vendor Patch Release
2024-01-08 - Public Release
Discovered by Claudio Bozzato of Cisco Talos.