CVE-2021-21899
A code execution vulnerability exists in the dwgCompressor::copyCompBytes21 functionality of LibreCad libdxfrw 2.2.0-rc2-19-ge02f3580. A specially-crafted .dwg file can lead to a heap buffer overflow. An attacker can provide 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.
LibreCad libdxfrw 2.2.0-rc2-19-ge02f3580
libdxfrw - https://librecad.org/
8.8 - CVSS:3.0/AV:N/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
Libdfxfw is an opensource library facilitating the reading and writing of .dxf and .dwg files, the primary vector graphics file formats of CAD software. Libdxfrw is contained in and primarily used by LibreCAD for the aforementioned purposes.
Libdxfrw is capable of reading in many different versions of .dwg files. The backwards compatibility is quite extensive, and likewise the particulars for each given version are also quite extensive. For today’s vulnerability, we deal with the AC1021
version, the AutoCAD .dwg standard from 2007 through 2009.
To keep things relatively short, let’s examine how libdxfrw reads in AC1021
.dwg headers:
bool dwgReader21::readFileHeader() {
//DRW_DBG("\n\ndwgReader21::parsing file header\n");
if (! fileBuf->setPosition(0x80)) // [1]
return false;
duint8 fileHdrRaw[0x2FD];//0x3D8
fileBuf->getBytes(fileHdrRaw, 0x2FD); // [2]
duint8 fileHdrdRS[0x2CD];
dwgRSCodec::decode239I(fileHdrRaw, fileHdrdRS, 3); // [3]
As shown by [1], .dwg file headers start at offset 0x80, an example of which will soon be posted. At [2], the headers are read into a static buffer, and at [3] the headers are decoded. Thus, for an input file of:
00000000 41 43 31 30 32 31 00 00 00 00 00 25 03 c0 00 1d |AC1021.....%....|
00000010 25 00 01 00 00 00 00 00 00 20 01 00 00 00 00 00 |%........ ......|
00000020 00 80 00 00 00 e0 11 02 00 00 15 02 00 00 00 01 |................|
00000030 01 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 |................|
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000070 00 00 00 00 00 00 00 00 00 68 f8 f7 92 2a b5 ef |.........h...*..|
00000080 18 dd 0b f1 f1 bb e9 eb 00 00 00 01 00 00 00 09 |................|
00000090 00 41 00 75 00 74 00 6f 00 64 00 65 00 73 00 6b |.A.u.t.o.d.e.s.k|
000000a0 06 50 04 31 40 40 4d 5c a7 22 98 1d 02 54 01 48 |.P.1@@M\."...T.H|
000000b0 01 18 44 ab 54 f9 2d d1 04 41 27 d4 22 78 3b a1 |..D.T.-..A'."x;.|
000000c0 22 bd 22 91 9d e1 8b 1f da 54 9a fc 5f |"."......T.._|
000000cd
We end up with a decoded input buffer of:
[~.~]> x/191wx fileHdrdRS
0x7fff66b00b70: 0x00e9f118 0x74000900 0x50006500 0x54985c40
0x7fff66b00b80: 0x412dab01 0xe122a122 0x0000fcda 0x00000000
0x7fff66b00b90: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00ba0: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00bb0: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00bc0: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00bd0: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00be0: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00bf0: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00c00: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00c10: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00c20: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00c30: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00c40: 0x00000000 0x00000000 0x00000000 0x00000000
0x7fff66b00c50: 0x00000000 0x00000000 0x00000000 0xdd000000
0x7fff66b00c60: 0x0000ebf1 0x64007500 0x40046b00 0x18011da7
0x7fff66b00c70: 0x7827d154 0x548b9122 0x0000005f 0x00000000
0x7fff66b00c80: 0x00000000 0x00000000 0x00000000 0x00000000
Immediately following the decoding, we populate two more variables, fileHdrCompLength
and fileHdrCompLength2
:
dwgRSCodec::decode239I(fileHdrRaw, fileHdrdRS, 3);
dint32 fileHdrCompLength = fileHdrBuf.getRawLong32();
dcint32 fileHdrCompLength2 = fileHdrBuf.getRawLong32();
Which get populated with 0x0000fcda and 0x00000000 respectively from the decoded buffer:
[~.~]> x/191wx fileHdrdRS
0x7fff66b00b70: 0x00e9f118 0x74000900 0x50006500 0x54985c40
0x7fff66b00b80: 0x412dab01 0xe122a122 0x0000fcda 0x00000000 //<- fileHdrCompLength/fileHdrCompLength2
Needless to say, the fileHdrCompLength
variable is completely controlled by the input file. Continuing on within dwgReader21::readFileHeader()
:
dwgRSCodec::decode239I(fileHdrRaw, fileHdrdRS, 3);
dint32 fileHdrCompLength = fileHdrBuf.getRawLong32();
dcint32 fileHdrCompLength2 = fileHdrBuf.getRawLong32();
int fileHdrDataLength = 0x110;
std::vector<duint8> fileHdrData;
if (fileHdrCompLength < 0) { // [4]
// [...] no compression
}else { // [5]
//DRW_DBG("\ndwgReader21:: file header are compresed:\n");
std::vector<duint8> compByteStr(fileHdrCompLength);
fileHdrBuf.getBytes(compByteStr.data(), fileHdrCompLength);
fileHdrData.resize(fileHdrDataLength); // [6]
dwgCompressor::decompress21(compByteStr.data(), &fileHdrData.front(), // [7]
fileHdrCompLength, fileHdrDataLength);
}
The branch at [4] is for a .dwg file without compression, but frankly that’s boring so it’s skipped. At [5] we deal with the much more fun and compressed .dwg files. Since we control fileHdrCompLength
, we have a choice of branch, and we go with compressed. At [6], a static sized buffer of 0x110 bytes is created to contain the resultant decompressed header bytes, and the decompression itself occurs at [7] in dwgCompressor::decompress21()
, which we now examine:
void dwgCompressor::decompress21(duint8 *cbuf, duint8 *dbuf, duint32 csize, duint32 dsize){
duint32 srcIndex=0; // controlled, 0x110 malloc, controlled, 0x110, always
duint32 dstIndex=0;
duint32 length=0;
duint32 sourceOffset;
duint8 opCode;
opCode = cbuf[srcIndex++];
if ((opCode >> 4) == 2){
srcIndex = srcIndex +2;
length = cbuf[srcIndex++] & 0x07;
}
while (srcIndex < csize && (dstIndex < dsize+1)){ // [8] //dstIndex < dsize to prevent crash more robust are needed
if (length == 0)
length = litLength21(cbuf, opCode, &srcIndex); // [9]
copyCompBytes21(cbuf, dbuf, length, srcIndex, dstIndex); // [10]
srcIndex += length;
dstIndex += length;
if (dstIndex >=dsize) break; // [11] //check if last chunk are compresed & terminate
To ellucidate and reiterate, the cbuf
and csize arguments are fully controlled by the input file, while dbuf
anddsize
are a 0x110 sized allocation and corresponding size. This hopefully gives away the vulnerability, as the only real length checks are at the aptly-commented [8] and [11]. At [9] we find the length
field to pass into the copyCompBytes21()
function at [10] (assuming that length is currently 0x0, something fully within the file’s control). To proceed, we first look at litLength21()
and then copyCompBytes21
:
duint32 dwgCompressor::litLength21(duint8 *cbuf, duint8 oc, duint32 *si){
duint32 srcIndex=*si;
duint32 length = oc + 8;
if (length == 0x17) {
duint32 n = cbuf[srcIndex++];
length += n;
if (n == 0xff) {
do {
n = cbuf[srcIndex++];
n |= (duint32)(cbuf[srcIndex++] << 8);
length += n;
} while (n == 0xffff);
}
}
*si = srcIndex;
return length;
}
Not worth going into too much detail; all we really care about is that the return length
can be any value from 0x0 to 0xFFFFFFFF. With that in mind we now peek at copyCompBytes21(cbuf, dbuf, length, srcIndex, dstIndex)
:
void dwgCompressor::copyCompBytes21(duint8 *cbuf, duint8 *dbuf, duint32 l, duint32 si, duint32 di){
duint32 length =l;
duint32 dix = di;
duint32 six = si;
while (length > 31){
//in doc: 16-31, 0-15
for (duint32 i = six+24; i<six+32; i++)
dbuf[dix++] = cbuf[i];
for (duint32 i = six+16; i<six+24; i++)
dbuf[dix++] = cbuf[i];
for (duint32 i = six+8; i<six+16; i++)
dbuf[dix++] = cbuf[i];
for (duint32 i = six; i<six+8; i++)
dbuf[dix++] = cbuf[i];
six = six + 32;
length = length -32;
}
switch (length) {
case 0:
break;
case 1: //Ok
dbuf[dix] = cbuf[six];
break;
case 2: //Ok
dbuf[dix++] = cbuf[six+1];
dbuf[dix] = cbuf[six];
break;
//[...]
case 31:
//in doc: six+30, 26-29, 18-25, 2-17, 0-1
dbuf[dix++] = cbuf[six+30];
for (int i = 26; i<30;i++)
dbuf[dix++] = cbuf[six+i];
for (int i = 18; i<26;i++)
dbuf[dix++] = cbuf[six+i];
/* for (int i = 2; i<18; i++)
dbuf[dix++] = cbuf[six+i];*/
for (int i = 10; i<18; i++)
dbuf[dix++] = cbuf[six+i];
for (int i = 2; i<10; i++)
dbuf[dix++] = cbuf[six+i];
dbuf[dix++] = cbuf[six+1];
dbuf[dix] = cbuf[six];
break;
default:
DRW_DBG("WARNING dwgCompressor::copyCompBytes21, bad output.\n");
break;
} }
As one can quite clearly see, still no length checks anywhere within this function. If we pass an input buffer that is greater than 0x110 bytes, or if we pass a compressed chunk when our dstIndex is at 0x10F, we end up copying bytes directly from our fully controlled input buffer into the fixed 0x110 size heap buffer, resulting in a fully controlled heap buffer overflow.
================================================================= ==996143==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6120000005d0 at pc 0x7f34d4675a85 bp 0x7fff66afecb0 sp 0x7fff66afeca8 WRITE of size 1 at 0x6120000005d0 thread T0 #0 0x7f34d4675a84 in dwgCompressor::copyCompBytes21(unsigned char, unsigned char, unsigned int, unsigned int, unsigned int) /root/boop/assorted_fuzzing/librecad/LibreCAD_master/libraries/libdxfrw/src/intern/dwgutil.cpp:423:25 #1 0x7f34d46722d5 in dwgCompressor::decompress21(unsigned char, unsigned char, unsigned int, unsigned int) /root/boop/assorted_fuzzing/librecad/LibreCAD_master/libraries/libdxfrw/src/intern/dwgutil.cpp:272:9 #2 0x7f34d461feb2 in dwgReader21::readFileHeader() /root/boop/assorted_fuzzing/librecad/LibreCAD_master/libraries/libdxfrw/src/intern/dwgreader21.cpp:167:9 #3 0x7f34d4128364 in dwgR::read(DRW_Interface, bool) /root/boop/assorted_fuzzing/librecad/LibreCAD_master/libraries/libdxfrw/src/libdwgr.cpp:156:24 #4 0x55073c in LLVMFuzzerTestOneInput /root/boop/assorted_fuzzing/librecad/fuzzing/dwg/./dwg_harness.cpp:65:9 #5 0x4587e1 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const, unsigned long) (/root/boop/assorted_fuzzing/librecad/fuzzing/dwg/triaged/dwg_fuzzer.bin+0x4587e1) #6 0x443f52 in fuzzer::RunOneTest(fuzzer::Fuzzer, char const, unsigned long) (/root/boop/assorted_fuzzing/librecad/fuzzing/dwg/triaged/dwg_fuzzer.bin+0x443f52) #7 0x449a06 in fuzzer::FuzzerDriver(int, char*, int ()(unsigned char const*, unsigned long)) (/root/boop/assorted_fuzzing/librecad/fuzzing/dwg/triaged/dwg_fuzzer.bin+0x449a06) #8 0x4726c2 in main (/root/boop/assorted_fuzzing/librecad/fuzzing/dwg/triaged/dwg_fuzzer.bin+0x4726c2) #9 0x7f34d37430b2 in __libc_start_main /build/glibc-eX1tMB/glibc-2.31/csu/../csu/libc-start.c:308:16 #10 0x41e61d in _start (/root/boop/assorted_fuzzing/librecad/fuzzing/dwg/triaged/dwg_fuzzer.bin+0x41e61d)
0x6120000005d0 is located 0 bytes to the right of 272-byte region [0x6120000004c0,0x6120000005d0)
allocated by thread T0 here:
#0 0x54da9d in operator new(unsigned long) (/root/boop/assorted_fuzzing/librecad/fuzzing/dwg/triaged/dwg_fuzzer.bin+0x54da9d)
#1 0x7f34d45ba38b in __gnu_cxx::new_allocator
SUMMARY: AddressSanitizer: heap-buffer-overflow /root/boop/assorted_fuzzing/librecad/LibreCAD_master/libraries/libdxfrw/src/intern/dwgutil.cpp:423:25 in dwgCompressor::copyCompBytes21(unsigned char, unsigned char, unsigned int, unsigned int, unsigned int) Shadow bytes around the buggy address: 0x0c247fff8060: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00 0x0c247fff8070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0c247fff8080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fa fa 0x0c247fff8090: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00 0x0c247fff80a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x0c247fff80b0: 00 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa 0x0c247fff80c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c247fff80d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c247fff80e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c247fff80f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x0c247fff8100: 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 ==996143==ABORTING
2021-08-04 - Vendor Disclosure
2021-11-10 - Vendor Patched
2021-11-17 - Public Release
Discovered by Lilith >_> of Cisco Talos.