CVE-2020-28599
A stack-based buffer overflow vulnerability exists in the import_stl.cc:import_stl() functionality of Openscad openscad-2020.12-RC2. A specially crafted STL file can lead to code execution. An attacker can provide a malicious file to trigger this vulnerability.
Openscad openscad-2020.12-RC2
https://github.com/openscad/openscad
8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-121 - Stack-based Buffer Overflow
Openscad is an open-source program for creating 3-D CAD models, available for all platforms. Aside from describing and creating objects from scripts, it’s also possible to import existing .stl, .amf, .3mf, .svg and .dxf files into a scene for rendering.
When importing a given .stl
file into a scene via the import("file.stl");
command, the first stl-specific function we hit is PolySet *import_stl(const std::string &filename, const Location &loc)
:
PolySet *import_stl(const std::string &filename, const Location &loc)
{
PolySet *p = new PolySet(3);
// Open file and position at the end
std::ifstream f(filename.c_str(), std::ios::in | std::ios::binary | std::ios::ate); // [1]
if (!f.good()) {
LOG(message_group::Warning,Location::NONE,"","Can't open import file '%1$s', import() at line %2$d",filename,loc.firstLine());
return p;
}
boost::regex ex_sfe("solid|facet|endloop"); // [2]
boost::regex ex_outer("outer loop");
boost::regex ex_vertex("vertex");
boost::regex ex_vertices("\\s*vertex\\s+([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)"); // [3]
bool binary = false;
std::streampos file_size = f.tellg();
f.seekg(80);
if (f.good() && !f.eof()) { // [4]
uint32_t facenum = 0;
f.read((char *)&facenum, sizeof(uint32_t));
#if BOOST_ENDIAN_BIG_BYTE
uint32_byte_swap( facenum );
#endif
if (file_size == static_cast<std::streamoff>(80 + 4 + 50*facenum)) {
binary = true;
}
}
At [1], our input file is opened, and at [2] through [3] we notice some important regexes that will be used further on. Assuming we pass the check at [4], which makes sure our file is at least 80 bytes, then we move on to the following code:
PolySet *import_stl(const std::string &filename, const Location &loc)
{
// [...]
char data[5];
f.read(data, 5);
if (!binary && !f.eof() && f.good() && !memcmp(data, "solid", 5)) {
int i = 0;
double vdata[3][3]; // [1]
std::string line;
std::getline(f, line);
while (!f.eof()) { // [2]
std::getline(f, line);
boost::trim(line);
if (boost::regex_search(line, ex_sfe)) { // "solid|facet|endloop" // [3]
continue;
}
if (boost::regex_search(line, ex_outer)) { // "outer loop" // [4]
i = 0;
continue;
}
boost::smatch results;
if (boost::regex_search(line, results, ex_vertices)) { // "\\s*vertex\\s+([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)" // [5]
//[...]
}
}
}
At [2] we hit our parsing loop, iterating over each line of the input .stl
file, looking for different regexes as we go along. Lines matching the regex at [3], "solid|facet|endloop"
, are completely ignored, lines matching at [4], "outerloop"
, reset the i
variable, but that’s about it. The only regex that is actually read in is at [5], "\\s*vertex\\s+([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)"
. To give an example:
facet normal 1.000000e+00 0.000000e+00 -0.000000e+00
outer loop
vertex 2.000000e+01 2.000000e+01 0.000000e+00
vertex 2.000000e+01 2.000000e+01 2.000000e+01
vertex 2.000000e+01 0.000000e+00 2.000000e+01
endloop
endfacet
To proceed, let us now examine the code hit when the ex_vertices
regex is hit:
if (boost::regex_search(line, results, ex_vertices)) { // "\\s*vertex\\s+([^\\s]+)\\s+([^\\s]+)\\s+([^\\s]+)"
try {
for (int v=0; v<3; ++v) {
vdata[i][v] = boost::lexical_cast<double>(results[v+1]); // [1]
}
}
catch (const boost::bad_lexical_cast &blc) {
LOG(message_group::Warning,Location::NONE,"","Can't parse vertex line '%1$s', import() at line %2$d",line,loc.firstLine());
i = 10;
continue;
}
if (++i == 3) { // [2]
p->append_poly();
p->append_vertex(vdata[0][0], vdata[0][1], vdata[0][2]);
p->append_vertex(vdata[1][0], vdata[1][1], vdata[1][2]);
p->append_vertex(vdata[2][0], vdata[2][1], vdata[2][2]);
}
}
Each of the vertex numbers are populated into the vdata
variable at [1], and if we have three vertexes read in (forming a triangle) at [2], we append these vertexes into the PolySet *p
object. Interestingly, the only thing that resets the i
variable is, as mentioned before, when we hit the "outer loop"
regex:
if (boost::regex_search(line, ex_outer)) { // "outer loop"
i = 0;
continue;
}
Thus, if our .stl file has more than three vertexes in a given outer loop
tag, i
keeps incrementing, and if we look again at double vdata[3][3];
, we quickly realize that this is an arbitrary stack-based buffer overflow, resulting in potential code execution.
==2559056==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffee6cc0ec8 at pc 0x7f2417b8938f bp 0x7ffee6cc0a30 sp 0x7ffee6cc0a28
WRITE of size 8 at 0x7ffee6cc0ec8 thread T0
#0 0x7f2417b8938e in import_stl(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Location const&) //boop/assorted_fuzzing/openscad/openscad-openscad-2020.12-RC2/openscad-openscad-2020.12-RC2/src/import_stl.cc:114:19
#1 0x55bb6c in LLVMFuzzerTestOneInput //boop/assorted_fuzzing/openscad/openscad-openscad-2020.12-RC2/./fuzz_stl_harness.cpp:71:21
#2 0x461ae1 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (//boop/assorted_fuzzing/openscad/openscad-openscad-2020.12-RC2/stl_fuzzdir/stl_harness.bin+0x461ae1)
#3 0x44d252 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (//boop/assorted_fuzzing/openscad/openscad-openscad-2020.12-RC2/stl_fuzzdir/stl_harness.bin+0x44d252)
#4 0x452d06 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (//boop/assorted_fuzzing/openscad/openscad-openscad-2020.12-RC2/stl_fuzzdir/stl_harness.bin+0x452d06)
#5 0x47b9c2 in main (//boop/assorted_fuzzing/openscad/openscad-openscad-2020.12-RC2/stl_fuzzdir/stl_harness.bin+0x47b9c2)
#6 0x7f24159380b2 in __libc_start_main /build/glibc-ZN95T4/glibc-2.31/csu/../csu/libc-start.c:308:16
#7 0x42791d in _start (//boop/assorted_fuzzing/openscad/openscad-openscad-2020.12-RC2/stl_fuzzdir/stl_harness.bin+0x42791d)
Address 0x7ffee6cc0ec8 is located in stack of thread T0 at offset 1160 in frame
#0 0x7f2417b87daf in import_stl(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Location const&) //boop/assorted_fuzzing/openscad/openscad-openscad-2020.12-RC2/openscad-openscad-2020.12-RC2/src/import_stl.cc:63
This frame has 25 object(s):
[32, 36) 'agg.tmp'
[48, 568) 'f' (line 67)
[704, 708) 'ref.tmp' (line 69)
[720, 752) 'ref.tmp7' (line 69)
[784, 785) 'ref.tmp8' (line 69)
[800, 804) 'ref.tmp11' (line 69)
[816, 832) 'ex_sfe' (line 73)
[848, 864) 'ex_outer' (line 74)
[880, 896) 'ex_vertex' (line 75)
[912, 928) 'ex_vertices' (line 76)
[944, 960) 'file_size' (line 79)
[976, 992) 'agg.tmp30'
[1008, 1012) 'facenum' (line 82)
[1024, 1040) 'agg.tmp56'
[1056, 1061) 'data' (line 93)
[1088, 1160) 'vdata' (line 97) <== Memory access at offset 1160 overflows this variable
[1200, 1232) 'line' (line 98)
[1264, 1272) 'ref.tmp93' (line 102)
[1296, 1376) 'results' (line 110)
[1408, 1409) 'ref.tmp106' (line 110)
[1424, 1428) 'ref.tmp125' (line 118)
[1440, 1472) 'ref.tmp126' (line 118)
[1504, 1505) 'ref.tmp127' (line 118)
[1520, 1524) 'ref.tmp130' (line 118)
[1536, 1588) 'facet' (line 135)
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 //boop/assorted_fuzzing/openscad/openscad-openscad-2020.12-RC2/openscad-openscad-2020.12-RC2/src/import_stl.cc:114:19 in import_stl(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Location const&)
Shadow bytes around the buggy address:
0x10005cd90180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f2
0x10005cd90190: f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 f2
0x10005cd901a0: f8 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f2 f8 f2 00 00
0x10005cd901b0: f2 f2 00 00 f2 f2 00 00 f2 f2 00 00 f2 f2 00 00
0x10005cd901c0: f2 f2 00 00 f2 f2 f8 f2 00 00 f2 f2 05 f2 f2 f2
=>0x10005cd901d0: 00 00 00 00 00 00 00 00 00[f2]f2 f2 f2 f2 00 00
0x10005cd901e0: 00 00 f2 f2 f2 f2 f8 f2 f2 f2 00 00 00 00 00 00
0x10005cd901f0: 00 00 00 00 f2 f2 f2 f2 f8 f2 f8 f2 f8 f8 f8 f8
0x10005cd90200: f2 f2 f2 f2 f8 f2 f8 f2 f8 f8 f8 f8 f8 f8 f8 f3
0x10005cd90210: f3 f3 f3 f3 00 00 00 00 00 00 00 00 00 00 00 00
0x10005cd90220: 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
2021-01-08 - Vendor Disclosure
2021-01-31 - Vendor Patched
2021-02-23 - Public Release
Discovered by Lilith >_> of Cisco Talos.