Lucene search

K
talosTalos IntelligenceTALOS-2020-1222
HistoryApr 21, 2021 - 12:00 a.m.

Prusa Research PrusaSlicer Admesh stl_fix_normal_directions() out-of-bounds write vulnerability

2021-04-2100:00:00
Talos Intelligence
www.talosintelligence.com
38
prusa research
prusaslicer
admesh
out-of-bounds write
vulnerability
amf file
code execution
3-d printer
slicing program
gcode
heap-based buffer overflow
trianglemesh
stl file
amfparsercontext
xml
cwe-122

CVSS2

6.8

Attack Vector

NETWORK

Attack Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P

CVSS3

7.8

Attack Vector

LOCAL

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

EPSS

0.001

Percentile

37.4%

Summary

An out-of-bounds write vulnerability exists in the Admesh stl_fix_normal_directions() functionality of Prusa Research PrusaSlicer 2.2.0 and Master (commit 4b040b856). A specially crafted AMF file can lead to code execution. An attacker can provide a malicious file to trigger this vulnerability.

Tested Versions

Prusa Research PrusaSlicer 2.2.0
Prusa Research PrusaSlicer Master (commit 4b040b856)

Product URLs

<https://www.prusa3d.com/prusaslicer/&gt;

CVSSv3 Score

8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-122 - Heap-based Buffer Overflow

Details

Prusa Slicer is an open-source 3-D printer slicing program forked off Slic3r that can convert various 3-D model file formats and can output corresponding 3-D printer-readable Gcode.

After normalizing a given .stl, .obj, .3mf, .amf, .amf.xml, .3mf.xml or .prusa file, assuming basic requirements are met, we end up creating a TriangleMesh object, which is then further processed/acted upon. For demonstration purposes, let us examine how the .amf file format behaves in this regard. Upon finding a closing &lt;/volume&gt; tag within the XML, we hit the following code:

void AMFParserContext::endElement(const char * /* name */)
{
    switch (m_path.back()) {

        // Constellation transformation:
        case NODE_TYPE_DELTAX:
        case NODE_TYPE_DELTAX:
        case NODE_TYPE_DELTAX:
    //[...]
    
       // Closing the current volume. Create an STL from m_volume_facets pointing to m_object_vertices.
    case NODE_TYPE_VOLUME:
    {
        assert(m_object && m_volume);
        TriangleMesh  mesh;            // [1]
        stl_file     &stl = mesh.stl;
        stl.stats.type = inmemory;
        stl.stats.number_of_facets = int(m_volume_facets.size() / 3);   // [2]
        stl.stats.original_num_facets = stl.stats.number_of_facets;
        stl_allocate(&stl);

        bool has_transform = ! m_volume_transform.isApprox(Transform3d::Identity(), 1e-10);
        for (size_t i = 0; i &lt; m_volume_facets.size();) {       // [3]
            stl_facet &facet = stl.facet_start[i/3];
            for (unsigned int v = 0; v &lt; 3; ++v)
            {
                unsigned int tri_id = m_volume_facets[i++] * 3;
                facet.vertex[v] = Vec3f(m_object_vertices[tri_id + 0], m_object_vertices[tri_id + 1], m_object_vertices[tri_id + 2]);
            }
        }
        stl_get_size(&stl);
        mesh.repair();             // [4]
//[...]

At [1], we see our desired TriangleMesh object being instantiated, and at [2], an important variable stl.stats.number_of_facets is set as the amount of m_volume_facets.size() / 3; m_volume_facets is just a collection of all of the co-ordinates of all the triangles from our input. So if m_volume_facets looks like std::vector of length 9, capacity 16 = {0x2, 0x3, 0x1, 0x2, 0x3, 0x0, 0x4, 0x1, 0x4}, then this just means we have three triangle objects with three vertices each, each number representing the vertex index. Carrying on in the above example, at [3], the stl.facet_start vector is populated with m_volume_facets.size() elements, and at [4], we check the resultant set of facets to see if they make sense as a TriangleMesh and to repair if not. For the most part TriangleMesh::repair() consists of checks and assertions, but for our purposes, the only one that matters is here:

    // normal_directions
#ifdef SLIC3R_TRACE_REPAIR
    BOOST_LOG_TRIVIAL(trace) &lt;&lt; "\tstl_fix_normal_directions";
#endif /* SLIC3R_TRACE_REPAIR */
    stl_fix_normal_directions(&stl);  // [1]
    assert(stl_validate(&this-&gt;stl));

The assumption is that certain facets in the list might be reversed, and normalization is enforced by [1]. Examining admesh/normals.cpp:stl_fix_normal_directions():

void stl_fix_normal_directions(stl_file *stl)
{
    if (stl-&gt;stats.number_of_facets == 0)
        return;
    
    //[...]
    // Initialize linked list.
    boost::object_pool&lt;stl_normal&gt; pool;
    stl_normal *head = pool.construct();
    stl_normal *tail = pool.construct();
    head-&gt;next = tail;
    tail-&gt;next = tail;

    // Initialize list that keeps track of already fixed facets.
    std::vector&lt;char&gt; norm_sw(stl-&gt;stats.number_of_facets, 0); // stats.number_of_facets % 3 != 0 =&gt; oob write.
    // Initialize list that keeps track of reversed facets.
    std::vector&lt;int&gt;  reversed_ids(stl-&gt;stats.number_of_facets, 0);

The first important characteristic of this function is that we allocate two vectors with a size of stl-&gt;stats.number_of_facets, which is of size m_volume_facets.size() / 3, i.e. the amount of triangles read in from our input. For completeness, this is what a ‘Triangle’ looks like from a .amf or .3mf file:

  &lt;volume materialid="2"&gt;/triangle&gt;
    &lt;triangle&gt;&lt;v1&gt;0&lt;/v1&gt;&lt;v2&gt;1&lt;/v2&gt;&lt;v3&gt;4&lt;/v3&gt;&lt;/triangle&gt;
    &lt;triangle&gt;&lt;v1&gt;4&lt;/v1&gt;&lt;v2&gt;1&lt;/v2&gt;&lt;v3&gt;2&lt;/v3&gt;&lt;/triangle&gt;
    &lt;triangle&gt;&lt;v1&gt;1&lt;/v1&gt;&lt;v2&gt;3&lt;/v2&gt;&lt;v3&gt;2&lt;/v3&gt;&lt;/triangle&gt;
  &lt;/volume&gt;

Elements &lt;v1&gt;, &lt;v2&gt;, &lt;v3&gt; all point to different vertex index, which look like such:

  &lt;vertices&gt;
    &lt;vertex&gt;&lt;coordinates&gt;&lt;x&gt;11&lt;/x&gt;&lt;y&gt;0&lt;/y&gt;&lt;z&gt;0&lt;/z&gt;&lt;/coordinates&gt;&lt;/vertex&gt;   // index 0
    &lt;vertex&gt;&lt;coordinates&gt;&lt;x&gt;0&lt;/x&gt;&lt;y&gt;0&lt;/y&gt;&lt;z&gt;0&lt;/z&gt;&lt;/coordinates&gt;&lt;/vertex&gt;
    &lt;vertex&gt;&lt;coordinates&gt;&lt;x&gt;1&lt;/x&gt;&lt;y&gt;1&lt;/y&gt;&lt;z&gt;0&lt;/z&gt;&lt;/coordinates&gt;&lt;/vertex&gt;
    &lt;vertex&gt;&lt;coordinates&gt;&lt;x&gt;5&lt;/x&gt;&lt;y&gt;2&lt;/y&gt;&lt;z&gt;0&lt;/z&gt;&lt;/coordinates&gt;&lt;/vertex&gt;
    &lt;vertex&gt;&lt;coordinates&gt;&lt;x&gt;5&lt;/x&gt;&lt;y&gt;2&lt;/y&gt;&lt;z&gt;2&lt;/z&gt;&lt;/coordinates&gt;&lt;/vertex&gt;
  &lt;/vertices&gt;

Thus, to reiterate, stl-&gt;stats.number_of_facets can be thought of as the number of valid &lt;triangle&gt; objects in our input file. Continuing in admesh/normals.cpp:stl_fix_normal_directions():

    for (;;) {
        // Add neighbors_to_list. Add unconnected neighbors to the list.
        bool force_exit = false;
        for (int j = 0; j &lt; 3; ++ j) {   // [1]
            // Reverse the neighboring facets if necessary.
            if (stl-&gt;neighbors_start[facet_num].which_vertex_not[j] &gt; 2) {
                // If the facet has a neighbor that is -1, it means that edge isn't shared by another facet
                if (stl-&gt;neighbors_start[facet_num].neighbor[j] != -1) {
                    if (norm_sw[stl-&gt;neighbors_start[facet_num].neighbor[j]] == 1) {
                       
                        for (int id = reversed_count - 1; id &gt;= 0; -- id)
                            reverse_facet(stl, reversed_ids[id]);
                        force_exit = true;
                        break;
                    }
                    reverse_facet(stl, stl-&gt;neighbors_start[facet_num].neighbor[j]); // if amount of 
                    reversed_ids[reversed_count ++] = stl-&gt;neighbors_start[facet_num].neighbor[j];  // [2] 
                }
            }
    //[..]

The only thing that we must pay attention to: until the for(;;) loop breaks, [1] always executes three times (assuming we don’t hit the break). Thus, the statement at [2] can potentially be hit three times max per for (;;) iteration, assuming that a given facet has enough valid neighbors. As mentioned/shown above, the reversed_ids vector’s length is equivalent to the amount of triangles in the input file, and also there’s no guarantee that the (reversed_count % 3) == 0.
Thus, for example, assume we have an input file in which there’s only four triangles that are connected (e.g. a pyramid) and our stl-&gt;neightbors_start vector looks like such:

[o.o]&gt; p/x stl-&gt;neighbors_start
$17 = std::vector of length 4, capacity 5 = {{neighbor = {0x1, 0x3, 0x2}, which_vertex_not = {0x2, 0x4, 0x3}}, {neighbor = {0x0, 0x2, 0x3}, which_vertex_not = {0x2, 0x2, 0x3}}, {neighbor = {0x1, 0x0, 0x3}, which_vertex_not = {0x0, 0x4, 0x5}}, {neighbor = {0x2, 0x1, 0x0}, which_vertex_not = {0x4, 0x4, 0x3}}}

Since each facet/triangle has three neighbors, if each of these neighboring facets needs to be reversed, we can quickly exceed the amount of elements in the reversed_ids vector, which again was allocated with stl-&gt;number_of_facets elements. Given a specific layout of facets/triangles, the same facet may be reversed multiple times, causing the assignment at [2] to write out of bounds, resulting in a out-of-bounds heap write and possible code execution.

A last important note: while this vulnerability is in admesh/normals.cpp, it seems that this “admesh” library is a re-write or alternate of the standard “admesh” library, which is written in C. It does not appear that this vulnerability applies to the standard “admesh” library.

Crash Information

=================================================================
==2302481==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200002dae0 at pc 0x7f4ae8a14209 bp 0x7fffea4d5fb0 sp 0x7fffea4d5fa8
WRITE of size 4 at 0x60200002dae0 thread T0
    #0 0x7f4ae8a14208 in stl_fix_normal_directions(stl_file*) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/admesh/normals.cpp:168:47
    #1 0x7f4ae5dfe888 in Slic3r::TriangleMesh::repair(bool) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/libslic3r/TriangleMesh.cpp:178:5
    #2 0x7f4ae4d9d106 in Slic3r::AMFParserContext::endElement(char const*) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/libslic3r/Format/AMF.cpp:642:14
    #3 0x7f4ae4da672c in Slic3r::AMFParserContext::endElement(void*, char const*) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/libslic3r/Format/AMF.cpp:97:14
    #4 0x7f4adf19d9d9  (/lib/x86_64-linux-gnu/libexpat.so.1+0xb9d9)
    #5 0x7f4adf19e6af  (/lib/x86_64-linux-gnu/libexpat.so.1+0xc6af)
    #6 0x7f4adf19bb82  (/lib/x86_64-linux-gnu/libexpat.so.1+0x9b82)
    #7 0x7f4adf19d04d  (/lib/x86_64-linux-gnu/libexpat.so.1+0xb04d)
    #8 0x7f4adf1a0dbf in XML_ParseBuffer (/lib/x86_64-linux-gnu/libexpat.so.1+0xedbf)
    #9 0x7f4ae4da59cf in Slic3r::load_amf_file(char const*, Slic3r::DynamicPrintConfig*, Slic3r::Model*) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/libslic3r/Format/AMF.cpp:877:13
    #10 0x7f4ae4da8763 in Slic3r::load_amf(char const*, Slic3r::DynamicPrintConfig*, Slic3r::Model*, bool) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/libslic3r/Format/AMF.cpp:1048:16
    #11 0x565a98 in LLVMFuzzerTestOneInput //boop/assorted_fuzzing/prusaslicer/./fuzz_amf_harness.cpp:82:20
    #12 0x46be11 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (//boop/assorted_fuzzing/prusaslicer/amf_fuzzdir/fuzzamf.bin+0x46be11)
    #13 0x457582 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (//boop/assorted_fuzzing/prusaslicer/amf_fuzzdir/fuzzamf.bin+0x457582)
    #14 0x45d036 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (//boop/assorted_fuzzing/prusaslicer/amf_fuzzdir/fuzzamf.bin+0x45d036)
    #15 0x485cf2 in main (//boop/assorted_fuzzing/prusaslicer/amf_fuzzdir/fuzzamf.bin+0x485cf2)
    #16 0x7f4ae0a3e0b2 in __libc_start_main /build/glibc-ZN95T4/glibc-2.31/csu/../csu/libc-start.c:308:16
    #17 0x431c4d in _start (//boop/assorted_fuzzing/prusaslicer/amf_fuzzdir/fuzzamf.bin+0x431c4d)

0x60200002dae0 is located 0 bytes to the right of 16-byte region [0x60200002dad0,0x60200002dae0)
allocated by thread T0 here:
    #0 0x5610cd in operator new(unsigned long) (//boop/assorted_fuzzing/prusaslicer/amf_fuzzdir/fuzzamf.bin+0x5610cd)
    #1 0x7f4ae49ac5cb in __gnu_cxx::new_allocator&lt;int&gt;::allocate(unsigned long, void const*) /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/ext/new_allocator.h:114:27
    #2 0x7f4ae49ac4f8 in std::allocator_traits&lt;std::allocator&lt;int&gt; &gt;::allocate(std::allocator&lt;int&gt;&, unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/alloc_traits.h:444:20
    #3 0x7f4ae49abf6f in std::_Vector_base&lt;int, std::allocator&lt;int&gt; &gt;::_M_allocate(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/stl_vector.h:343:20
    #4 0x7f4ae49ad4eb in std::_Vector_base&lt;int, std::allocator&lt;int&gt; &gt;::_M_create_storage(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/stl_vector.h:358:33
    #5 0x7f4ae49acf5f in std::_Vector_base&lt;int, std::allocator&lt;int&gt; &gt;::_Vector_base(unsigned long, std::allocator&lt;int&gt; const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/stl_vector.h:302:9
    #6 0x7f4ae5e9c937 in std::vector&lt;int, std::allocator&lt;int&gt; &gt;::vector(unsigned long, int const&, std::allocator&lt;int&gt; const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/stl_vector.h:521:9
    #7 0x7f4ae8a139e3 in stl_fix_normal_directions(stl_file*) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/admesh/normals.cpp:136:20
    #8 0x7f4ae5dfe888 in Slic3r::TriangleMesh::repair(bool) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/libslic3r/TriangleMesh.cpp:178:5
    #9 0x7f4ae4d9d106 in Slic3r::AMFParserContext::endElement(char const*) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/libslic3r/Format/AMF.cpp:642:14
    #10 0x7f4ae4da672c in Slic3r::AMFParserContext::endElement(void*, char const*) //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/libslic3r/Format/AMF.cpp:97:14
    #11 0x7f4adf19d9d9  (/lib/x86_64-linux-gnu/libexpat.so.1+0xb9d9)

SUMMARY: AddressSanitizer: heap-buffer-overflow //boop/assorted_fuzzing/prusaslicer/PrusaSlicer/src/admesh/normals.cpp:168:47 in stl_fix_normal_directions(stl_file*)
Shadow bytes around the buggy address:
  0x0c047fffdb00: fa fa 00 fa fa fa 00 00 fa fa 00 07 fa fa 00 fa
  0x0c047fffdb10: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fd
  0x0c047fffdb20: fa fa fd fa fa fa fd fa fa fa fd fd fa fa fd fd
  0x0c047fffdb30: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fa
  0x0c047fffdb40: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fd
=&gt;0x0c047fffdb50: fa fa fd fa fa fa 04 fa fa fa 00 00[fa]fa fa fa
  0x0c047fffdb60: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffdb70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffdb80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffdb90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fffdba0: 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
==2302481==ABORTING

Timeline

2021-01-08 - Vendor Disclosure
2021-04-21 - Public Release

CVSS2

6.8

Attack Vector

NETWORK

Attack Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P

CVSS3

7.8

Attack Vector

LOCAL

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

EPSS

0.001

Percentile

37.4%

Related for TALOS-2020-1222