Talos Vulnerability Report

TALOS-2023-1826

GTKWave LXT2 lxt2_rd_get_facname decompression out-of-bounds write vulnerabilities

January 8, 2024
CVE Number

CVE-2023-39443,CVE-2023-39444

SUMMARY

Multiple out-of-bounds write vulnerabilities exist in the LXT2 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-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.

GTKWave sets up mime types for its supported extensions. For example, it’s enough for a victim to double-click on a wave file received by e-mail to trigger the vulnerability described in this advisory.

An LXT2 file is parsed through various functions. In lxt2vcd.c, the process_lxt() function calls lxt2_rd_get_fac_geometry() at line 240:

178 int process_lxt(char *fname)
179 {
180 struct lxt2_rd_trace *lt;
181 char *netname;
...
238   for(i=0;i<numfacs;i++)
239     {
240     struct lxt2_rd_geometry *g = lxt2_rd_get_fac_geometry(lt, i); [0]

In lxt2_read.c, the geometry is populated with the values as set in the lxt file:

1103 struct lxt2_rd_geometry *lxt2_rd_get_fac_geometry(struct lxt2_rd_trace *lt, lxtint32_t facidx)
1104 {
1105 if((lt)&&(facidx<lt->numfacs))
1106   {
1107   lt->geometry.rows = lt->rows[facidx];
1108   lt->geometry.msb = lt->msb[facidx];
1109   lt->geometry.lsb = lt->lsb[facidx];
1110   lt->geometry.flags = lt->flags[facidx];
1111   lt->geometry.len = lt->len[facidx];
1112   return(&lt->geometry);
1113   }
1114   else
1115   {
1116   return(NULL);
1117   }
1118 }

Our geometry now looks like this:

gef➤  p lt->geometry 
$4 = {
  rows = 0x42424242,
  msb = 0x43434343,
  lsb = 0x44444444,
  flags = 0x45454548, 
  len = 0x20
}

After this step, we return to process_lxt() which then calls lxt2_rd_get_alias_root():

...
241     lxtint32_t newindx = lxt2_rd_get_alias_root(lt, i);
...

In lxt2_read.c we can see that if our flags pass the check at line 1193, we can control the facidx:

1189 _LXT2_RD_INLINE lxtint32_t lxt2_rd_get_alias_root(struct lxt2_rd_trace *lt, lxtint32_t facidx)
1190 {
1191 if((lt)&&(facidx<lt->numfacs))
1192   {
1193   while(lt->flags[facidx] & LXT2_RD_SYM_F_ALIAS)
1194     {
1195     facidx = lt->rows[facidx];  /* iterate to next alias */
1196     }
1197   return(facidx);
1198   }
1199   else
1200   {
1201   return(~((lxtint32_t)0));
1202   }
1203 }

We can see rows specified in the file is expecting an offset to the next alias:

gef➤  p lt->flags[facidx]
$3 = 0x45454548

gef➤  p LXT2_RD_SYM_F_ALIAS
$5 = 0x8

gef➤  p (lt->flags[facidx] & LXT2_RD_SYM_F_ALIAS)
$4 = 0x8

gef➤  p lt->rows[facidx]
$3 = 0x42424242

Processing is continued upon return to process_lxt() where lxt2_rd_get_facname() is called:

...
242 
243                 if(!flat_earth)
244                         {
245                         netname = fv_output_hier(fv, lxt2_rd_get_facname(lt, i));
246                         }
247                         else
248                         {
249                         netname = lxt2_rd_get_facname(lt, i);
250                         }
251 
252                 if(g->flags & LXT2_RD_SYM_F_DOUBLE)
253                   {
254                         fprintf(fv, "$var real 1 %s %s $end\n", vcdid(newindx), netname);
255                         }
256                 else
257                 if(g->flags & LXT2_RD_SYM_F_STRING)
258                        {
259                        fprintf(fv, "$var real 1 %s %s $end\n", vcdid(newindx), netname);
260                        }
...

From here, we can see that line 1250 will take our zfacnames data, which was extracted previously at lines 861 and 875 within the lxt2_rd_init() function. The length of this data is determined by the value specified for zfacname_predec_size. At lines 879 and 880, we can see the lt->faccache->bufcurr and lt->faccache->bufprev get allocated based off of the longestname value specified in the file.

 762 struct lxt2_rd_trace *lxt2_rd_init(const char *name)
 763 {
...
 861       m=(char *)malloc(lt->zfacname_predec_size);
 862       rc=gzread(lt->zhandle, m, lt->zfacname_predec_size);
 863       gzclose(lt->zhandle); lt->zhandle=NULL;
 ...
 875       lt->zfacnames = m; 
 877       lt->faccache = calloc(1, sizeof(struct lxt2_rd_facname_cache));
 878       lt->faccache->old_facidx = lt->numfacs;   /* causes lxt2_rd_get_facname to initialize its unroll ptr as this is always invalid */
 879       lt->faccache->bufcurr = malloc(lt->longestname+1);
 880       lt->faccache->bufprev = malloc(lt->longestname+1);
...
1234 /*
1235  * extract facname from prefix-compressed table.  this
1236  * performs best when extracting facs with monotonically
1237  * increasing indices...
1238  */
1239 char *lxt2_rd_get_facname(struct lxt2_rd_trace *lt, lxtint32_t facidx)
1240 {
1241 char *pnt;
1242 lxtint32_t clone, j;
1243 
1244 if(lt)
1245   {
1246   if((facidx==(lt->faccache->old_facidx+1))||(!facidx))
1247     {
1248     if(!facidx)
1249       {
1250       lt->faccache->n = lt->zfacnames;
1251       lt->faccache->bufcurr[0] = 0;
1252       lt->faccache->bufprev[0] = 0;
1253       }
1254 
1255     if(facidx!=lt->numfacs) // loop for each facility
1256       {
1257       pnt = lt->faccache->bufcurr;
1258       lt->faccache->bufcurr = lt->faccache->bufprev;
1259       lt->faccache->bufprev = pnt;
...

Later, at line 1261, 2 bytes are retrieved from lt->faccache->n, which is our lt->zfacnames value mentioned previously.
At this point we have a loop at line 1266, which will take data from lt->faccache->bufprev[j] and write each byte to pnt, a pointer to the lt->faccache->bufcurr data.

Note the sizes used for the buffers when allocated previously at lines 879 and 880. In this example, our zfacnames value (which is placed in lt->faccache->n), is used as the maximum index used in the loop at lines 1264 and 1266.

...
1261       clone=lxt2_rd_get_16(lt->faccache->n, 0);  lt->faccache->n+=2;
1262       pnt=lt->faccache->bufcurr;
1263
1264       for(j=0;j<clone;j++)
1265         {
1266         *(pnt++) = lt->faccache->bufprev[j]; 
1267         }
1268
1269       while((*(pnt++)=lxt2_rd_get_byte(lt->faccache->n++,0)));
...

In our example, the 2-byte value retrieved for the clone value is 0x4141. This will lead to an out-of-bounds read and write for both the lt->faccache->bufcurr and the lt->faccache->bufprev members of the structure, which were only allocated at size lt->longestname+1, which was set to 0x17 in our PoC. This will trigger a buffer over-read on lt->faccache->bufprev when j is a large enough value. Likewise, this will also trigger an out-of-bounds write on lt->faccache->bufcur when the pointer is incremented beyond the bounds of the allocated memory.

gef➤  p clone
$2 = 0x4141

gef➤  p *lt->faccache 
$3 = {
  n = 0x6020000000f2 "AAAAAAAAAA",
  bufprev = 0x603000000100 "",
  bufcurr = 0x603000000130 "",
  old_facidx = 0x2
}

It’s important to note that we have other members of the lxt_rd_trace structure in nearby heap memory.
The buffer that will overflow in this example lives at 0x0x555555561020 and has a usable size of 0x18 bytes.

gef➤  heap chunk lt->faccache->bufcurr
Chunk(addr=0x555555561020, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk size: 32 (0x20)
Usable size: 24 (0x18)
Previous chunk size: 0 (0x0)
PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA

gef➤  hexdump (lt->faccache->bufcurr)-8
0x0000555555561018     21 00 00 00 00 00 00 00 00 06 02 17 20 60 00 b2    !........... `..
0x0000555555561028     41 d8 1d 08 3c 80 00 c4 06 00 01 99 77 82 20 00    A...<.......w. .
0x0000555555561038     21 00 00 00 00 00 00 00 43 43 43 43 47 47 47 47    !.......CCCCGGGG
0x0000555555561048     00 00 00 00 00 00 00 00 53 53 54 54 54 54 55 55    ........SSTTTTUU

We have other important heap objects nearby as well.

lt->faccache->bufcurr

Chunk(addr=0x555555561020, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000555555561020     00 06 02 17 20 60 00 b2 41 d8 1d 08 3c 80 00 c4    .... `..A...<...]

lt->msb

Chunk(addr=0x555555561040, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000555555561040     43 43 43 43 47 47 47 47 00 00 00 00 00 00 00 00    CCCCGGGG........]

lt->rows

Chunk(addr=0x555555561060, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000555555561060     08 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00    ................]

In fact, we can show that we have now clobbered the heap layout and overwritten the values previously stored in the lxt_rd_trace structure used throughout parsing.

lt->msb (note the size has been overwritten as well as the values)

Chunk(addr=0x555555561040, size=0x30, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000555555561040     c2 0e 56 55 55 55 00 00 d0 0f 56 55 55 55 00 00    ..VUUU....VUUU..]

lt->rows (note the chunk address is now corrupted )

Chunk(addr=0x555555561070, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000555555561070     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]

Following this, at line 1269 we are then able to write the contents of our uncompressed facility name data.

1269       while((*(pnt++)=lxt2_rd_get_byte(lt->faccache->n++,0)));
1270       lt->faccache->old_facidx = facidx;
1271       return(lt->faccache->bufcurr);

For example, if we want to overwrite the data displayed during one of the parsing events:

Chunk(addr=0x555555561200, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000555555561200     24 74 69 6d 65 73 63 61 6c 65 20 31 73 20 20 24    $timescale 1s  $]

gef➤  hexdump 0x555555561200
0x0000555555561200     24 74 69 6d 65 73 63 61 6c 65 20 31 73 20 20 24    $timescale 1s  $
0x0000555555561210     65 6e 64 0a 6e 64 0a 32 33 0a 00 00 00 00 00 00    end.nd.23.......
0x0000555555561220     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x0000555555561230     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................

We just set our offset byte, previously 0x4141, to 0x555555561200-0x555555561020, which is 0x1e0. Our data should overflow the heap buffer, land in the buffer containing this data and overwrite it. All the data in-between will also get overwritten by the loop at line 1266.

gef➤  hexdump 0x555555561200
0x0000555555561200     69 57 72 6f 74 65 44 61 74 61 00 31 73 20 20 24    iWroteData.1s  $
0x0000555555561210     65 6e 64 0a 6e 64 0a 32 33 0a 00 00 00 00 00 00    end.nd.23.......
0x0000555555561220     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
0x0000555555561230     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................

These out-of-bounds writes would also allow an attacker to modify the heap metadata or other sensitive structures leading to potential arbitrary code execution.

CVE-2023-39443 - prefix copy loop

The copy loop at line 1266 does not check that the writes are performed within the bounds of the pnt buffer, which may allow an out-of-bounds write in the heap, leading to arbitrary code execution.

CVE-2023-39444 - string copy loop

The string copy loop at line 1269 does not check that the writes are performed within the bounds of the pnt buffer, which may allow an out-of-bounds write in the heap, leading to arbitrary code execution.

Crash Information

=================================================================
==1506941==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000000118 at pc 0x555555563133 bp 0x7fffffffda30 sp 0x7fffffffda20
READ of size 1 at 0x603000000118 thread T0
    #0 0x555555563132 in lxt2_rd_get_facname src/helpers/lxt2_read.c:1266
    #1 0x555555566c55 in process_lxt src/helpers/lxt2vcd.c:245
    #2 0x5555555679d9 in main src/helpers/lxt2vcd.c:458
    #3 0x7ffff71e7082 in __libc_start_main ../csu/libc-start.c:308
    #4 0x5555555576ed in _start (lxt2vcd+0x36ed)

0x603000000118 is located 0 bytes to the right of 24-byte region [0x603000000100,0x603000000118)
allocated by thread T0 here:
    #0 0x7ffff7675808 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:144
    #1 0x55555555f583 in lxt2_rd_init src/helpers/lxt2_read.c:879
    #2 0x555555566956 in process_lxt src/helpers/lxt2vcd.c:183
    #3 0x5555555679d9 in main src/helpers/lxt2vcd.c:458
    #4 0x7ffff71e7082 in __libc_start_main ../csu/libc-start.c:308

SUMMARY: AddressSanitizer: heap-buffer-overflow src/helpers/lxt2_read.c:1266 in lxt2_rd_get_facname
Shadow bytes around the buggy address:
  0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff8000: fa fa 00 00 00 fa fa fa 00 00 00 fa fa fa 00 00
  0x0c067fff8010: 00 fa fa fa fd fd fd fa fa fa 00 00 00 00 fa fa
=>0x0c067fff8020: 00 00 00[fa]fa fa 00 00 00 fa fa fa fd fd fd fa
  0x0c067fff8030: fa fa fd fd fd fd fa fa 00 00 04 fa fa fa 00 00
  0x0c067fff8040: 04 fa fa fa 00 00 04 fa fa fa 00 00 04 fa fa fa
  0x0c067fff8050: 00 00 04 fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8070: 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
  Shadow gap:              cc
==1506941==ABORTING
[Inferior 1 (process 1506941) exited with code 01]    

Exploit Proof of Concept

$ ./lxt2vcd ../../out.lxt 
LXTLOAD | 2 facilities
LXTLOAD | Read 1 block header OK
LXTLOAD | [5931894172722287186] start time
LXTLOAD | [6004234345560363859] end time
LXTLOAD | 
$date
    Thu Jun 15 10:27:31 2023
$end
$version
    lxt2vcd
$end
$timescale 1s  $end
malloc(): mismatching next->prev_size (unsorted)
[1]    1511157 abort (core dumped)  ./lxt2vcd ../../out.lxt
VENDOR RESPONSE

Fixed in version 3.3.118, available from https://sourceforge.net/projects/gtkwave/files/gtkwave-3.3.118/

TIMELINE

2023-08-11 - Vendor Disclosure
2023-12-31 - Vendor Patch Release
2024-01-08 - Public Release

Credit

Discovered by Claudio Bozzato and Dave McDaniel of Cisco Talos.