CVE-2023-39443,CVE-2023-39444
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.
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-119 - Improper Restriction of Operations within the Bounds of a Memory Buffer
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(<->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.
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.
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.
=================================================================
==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]
$ ./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
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 and Dave McDaniel of Cisco Talos.