Talos Vulnerability Report

TALOS-2023-1803

GTKWave EVCD var len parsing improper array index validation vulnerability

January 8, 2024
CVE Number

CVE-2023-34087

SUMMARY

An improper array index validation vulnerability exists in the EVCD var len parsing functionality of GTKWave 3.3.115. A specially crafted .evcd 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-119 - Improper Restriction of Operations within the Bounds of a Memory Buffer

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.

The function evcd_main is used to parse .evcd files when using the evcd2vcd utility.

     JRB vcd_ids = NULL;
     
     ...

     int evcd_main(char *vname) {
         FILE *f;
         char *buf = NULL;
         size_t glen;
         int line = 0;
         ssize_t ss;
         JRB node;
[1]      char bin_fixbuff[32769];
         char bin_fixbuff2[32769];

         if (!strcmp("-", vname)) {
             f = stdin;
         } else {
[2]          f = fopen(vname, "rb");
         }

         ...

[3]      vcd_ids = make_jrb();

At [1] the bin_fixbuff buffer is created on the stack, with a size of 32769 bytes. At [2] the file is opened, and at [3] a vcd_ids hash table is created.

         while (!feof(f)) {
[4]          ss = getline_replace(&buf, &glen, f);
[5]          if (ss == -1) {
                 break;
             }
             line++;

[6]          if (!strncmp(buf, "$var", 4)) {
                 char *st = strtok(buf + 5, " \t");
                 int len;
                 char *nam;
                 unsigned int hash;
                 char *lbrack;

                 switch (st[0]) {
                     case 'p':
[7]                      if (!strcmp(st, "port")) {
                             break;
                         }
                         /* fallthrough */
                     default:
                         fprintf(stderr, "'%s' is an unsupported data type, exiting.\n", st);
                         exit(255);
                         break;
                 }

                 st = strtok(NULL, " \t");
[8]              if (*st == '[') /* VCS extension */
                 {
                     int p_hi = atoi(st + 1);
                     int p_lo = p_hi;
                     char *p_colon = strchr(st + 1, ':');
                     if (p_colon) {
                         p_lo = atoi(p_colon + 1);
                     }

                     if (p_hi > p_lo) {
                         len = p_hi - p_lo + 1;
                     } else {
                         len = p_lo - p_hi + 1;
                     }
                 } else {
[9]                  len = atoi(st);
                 }

At [4], a loop reads each line in the file. The details of getline_replace are not relevant for this advisory. Suffice to say, the loop exits if the end of file is reached or if the line only contains a null character [5].

In general, lines are read as a set of tokens separated by spaces or tabs, so we’ll see calls to strtok(<buffer>, " \t") throughout the code.

If the line starts with “$var”, we enter the block at [6], and the code makes sure the next token is exactly “port” [7].

A len variable is then set either by directly converting the current token via atoi [9] or by parsing it as a range-like format [a:b] [8] where a and b are numbers whose len is the result of the absolute difference between a and b.

                 st = strtok(NULL, " \t"); /* vcdid */
[10]              hash = vcdid_hash(st);

[11]             nam = strtok(NULL, " \t"); /* name */
[12]             st = strtok(NULL, " \t");  /* $end */

                 if (strncmp(st, "$end", 4)) {
                     *(st - 1) = ' ';
                 }

[13]             node = jrb_find_int(vcd_ids, hash);
                 if (!node) {
                     Jval val;
[14]                 jrb_insert_int(vcd_ids, hash, val)->val2.i = len;
                 }

                 ...

The next token is a string that gets hashed [10] before another two tokens are extracted ([11] and [12]). These tokens are not important for this advisory.
Next, the hash is added to the hash table [13] and len is saved into the field val2.i [14] of the new hash node.

After this loop terminates (either with a line containing just a “\x00” character or the string “$endd”), the code continues looping for each line on the rest of the file:

         while (!feof(f)) {
             unsigned int hash;
             size_t len;
             char *nl, *sp;

             ss = getline_replace(&buf, &len, f);
             if (ss == -1) {
                 break;
             }

             ...

             switch (buf[0]) {
[15]             case 'p': {
                     char *src = buf + 1;
                     char *pnt = bin_fixbuff;
                     int pchar = 0;

[16]                 for (;;) {
                         if (!*src) break;
                         if (isspace((int)(unsigned char)*src)) {
                             if (pchar != ' ') {
                                 *(pnt++) = pchar = ' ';
                             }
                             src++;
                             continue;
                         }
                         *(pnt++) = pchar = *(src++);
                     }
                     *pnt = 0;

[17]                 sp = strchr(bin_fixbuff, ' ');
                     sp = strchr(sp + 1, ' ');
                     sp = strchr(sp + 1, ' ');
                     *sp = 0;
[18]                 hash = vcdid_hash(sp + 1);

[19]                 node = jrb_find_int(vcd_ids, hash);
                     if (node) {
[20]                     bin_fixbuff[node->val2.i] = 0;
                         ...

The switch case at [15] is entered when the current line starts with p. The rest of the line is then copied in the bin_fixbuff buffer [16], and a few tokens are extracted [17].
Eventually, the last token is hashed, and that hash is searched into the previously populated vcd_ids hash table. If the hash is found, the corresponding node value is used to index the bin_fixbuff array.

Because the value of node->val2.i is arbitrarily set from file at [14], the line at [20] can be used to write a NULL byte anywhere in memory. Moreover, as this code is in a loop, the write can be triggered multiple times. This can be used by an attacker to corrupt memory and execute arbitrary code.

Crash Information

==133588==ERROR: AddressSanitizer: stack-buffer-overflow on address 0xffffd791 at pc 0x56558b84 bp 0xfffed4f8 sp 0xfffed4ec
WRITE of size 1 at 0xffffd791 thread T0
    #0 0x56558b83 in evcd_main helpers/evcd2vcd.c:358
    #1 0x565594be in main helpers/evcd2vcd.c:522
    #2 0xf7676294 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #3 0xf7676357 in __libc_start_main_impl ../csu/libc-start.c:381
    #4 0x565572d6 in _start (evcd2vcd+0x22d6)

Address 0xffffd791 is located in stack of thread T0 at offset 66065 in frame
    #0 0x56557994 in evcd_main helpers/evcd2vcd.c:139

  This frame has 6 object(s):
    [48, 52) 'buf' (line 141)
    [64, 68) 'glen' (line 142)
    [80, 84) 'len' (line 315)
    [96, 104) 'val' (line 236)
    [128, 32897) 'bin_fixbuff' (line 146)
    [33168, 65937) 'bin_fixbuff2' (line 147) <== Memory access at offset 66065 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow helpers/evcd2vcd.c:358 in evcd_main
Shadow bytes around the buggy address:
  0x3ffffaa0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffab0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffac0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffad0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffae0: 00 00 01 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3
=>0x3ffffaf0: f3 f3[f3]f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3 f3
  0x3ffffb00: f3 f3 f3 f3 00 00 00 00 00 00 00 00 00 00 f1 f1
  0x3ffffb10: f1 f1 f8 f3 f3 f3 00 00 00 00 00 00 00 00 00 00
  0x3ffffb20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffb30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x3ffffb40: 00 00 00 00 00 00 00 00 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-01 - Vendor Disclosure
2023-12-31 - Vendor Patch Release
2024-01-08 - Public Release

Credit

Discovered by Claudio Bozzato of Cisco Talos.