Talos Vulnerability Report

TALOS-2022-1657

OpenImageIO Project OpenImageIO ZfileOutput::close() denial of service vulnerability

December 22, 2022
CVE Number

CVE-2022-43603

SUMMARY

A denial of service vulnerability exists in the ZfileOutput::close() functionality of OpenImageIO Project OpenImageIO v2.4.4.2. A specially crafted ImageOutput Object can lead to denial of service. An attacker can provide a malicious file to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

OpenImageIO Project OpenImageIO v2.4.4.2

PRODUCT URLS

OpenImageIO - https://github.com/OpenImageIO/oiio

CVSSv3 SCORE

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

CWE

CWE-476 - NULL Pointer Dereference

DETAILS

OpenImageIO is an image processing library with easy-to-use interfaces and a sizable number of supported image formats. Useful for conversion and processing and even image comparison, this library is utilized by 3D-processing software from AliceVision (including Meshroom), as well as Blender for reading Photoshop .psd files.

Along with parsing files of various formats, libOpenImageIO is also capable of creating new files in these formats. For instance, if we look briefly at the OpenImageIO iconvert utility as an example, there are two functions capable of doing this image creation:

static bool
convert_file(const std::string& in_filename, const std::string& out_filename)
{
    // [...]

    // Find an ImageIO plugin that can open the input file, and open it.
    auto in = ImageInput::open(in_filename);                              // [1]
    // [...]
    ImageSpec inspec         = in->spec();                                // [2]

    // Find an ImageIO plugin that can open the output file, and open it
    auto out = ImageOutput::create(tempname);                             // [3]
   
    // [...]

        if (!nocopy) { 
            ok = out->copy_image(in.get());                                   // [4]
            if (!ok)
                std::cerr << "iconvert ERROR copying \"" << in_filename
                          << "\" to \"" << out_filename << "\" :\n\t"
                          << out->geterror() << "\n";
        } else {
            // Need to do it by hand for some reason.  Future expansion in which
            // only a subset of channels are copied, or some such.
            std::vector<char> pixels((size_t)outspec.image_bytes(true));
            ok = in->read_image(subimage, miplevel, 0, outspec.nchannels,     // [5]
                                outspec.format, &pixels[0]);
            if (!ok) {
                std::cerr << "iconvert ERROR reading \"" << in_filename
                          << "\" : " << in->geterror() << "\n";
            } else {
                ok = out->write_image(outspec.format, &pixels[0]);              // [6]
                if (!ok)
                    std::cerr << "iconvert ERROR writing \"" << out_filename
                              << "\" : " << out->geterror() << "\n";
            }
      
       }
       
      ++miplevel;
    } while (ok && in->seek_subimage(subimage, miplevel, inspec));
}

out->close(); // [7]
in->close();

The most important pieces are that we have an ImageInput object [1], an input specification [2] and an output image (whose type is determined by the filename extension) [3]. An output specification can be copied from the input specification and modified in case of incompatibilities with the output format. Subsequently we can either call ImageOutput::copy_image(in.get()) [4] or read the input into a buffer at [5] and then write the buffer to our ImageOutput at [6]. Now, it’s worth noting we cannot really know how libOpenImageIO will get its input images and specifications, and so the ImageOutput vulnerabilities are all applicable only in situations where an attacker can control the input file or specification that is then used to generate an ImageOutput object (like above).

If we end up creating a .zfile output file, we necessarily hit the ZfileOutput::open() function:

bool
ZfileOutput::open(const std::string& name, const ImageSpec& userspec,
                  OpenMode mode)
{
    if (mode != Create) {
        errorf("%s does not support subimages or MIP levels", format_name());
        return false;
    }

    close();  // Close any already-opened file
    m_gz   = 0;
    m_file = NULL;
    m_spec = userspec;  // Stash the spec

    // Check for things this format doesn't support
    if (m_spec.width < 1 || m_spec.height < 1) {                               // [7]
        errorf("Image resolution must be at least 1x1, you asked for %d x %d",
               m_spec.width, m_spec.height);
        return false;
    }
    if (m_spec.width > 32767 || m_spec.height > 32767) {                       // [8]
        errorf("zfile image resolution maximum is 32767, you asked for %d x %d",
               m_spec.width, m_spec.height);
        return false;
    }
    if (m_spec.depth < 1)
        m_spec.depth = 1;
    if (m_spec.depth > 1) {                                                   // [9]
        errorf("%s does not support volume images (depth > 1)", format_name());
        return false;
    }

    if (m_spec.nchannels != 1) {                                              // [10]
        errorf("Zfile only supports 1 channel, not %d", m_spec.nchannels);
        return false;
    }


// [...]

    // If user asked for tiles -- which this format doesn't support, emulate
    // it by buffering the whole image.this form
    if (m_spec.tile_width && m_spec.tile_height)
        m_tilebuffer.resize(m_spec.image_bytes());                              // [11]

    return true;
}

As shown above at multiple spots, there are quite a few different validation checks ([7], [8], [9], and [10]), and the possibility of exiting from this function early is presented at each of these. Assuming we do return false, the line at [11] is skipped and our ZfileOutput::m_tilebuffer vector remains uninitialized with size 0 and a null pointer for its data. It’s also quite important that this is the only place that m_tilebuffer can be resized, and so continuing on to ZfileOutput::close():

bool
ZfileOutput::close()
{
    bool ok = true;
    if (m_spec.tile_width) {
        // We've been emulating tiles; now dump as scanlines.
        OIIO_DASSERT(m_tilebuffer.size());                            // [12]
        ok &= write_scanlines(m_spec.y, m_spec.y + m_spec.height, 0,  
                              m_spec.format, &m_tilebuffer[0]);                  // [13]
        std::vector<unsigned char>().swap(m_tilebuffer);       
    }

    if (m_gz) {
        gzclose(m_gz);
        m_gz = 0;
    }
    if (m_file) {
        fclose(m_file);
        m_file = nullptr;
    }

    init();  // re-initialize
    return ok;
}

We quickly see a reference to our ZfileOutput::m_tilebuffer at [13], which we know is uninitialized. While there is an explicit assert check at [12], this only occurs if the library was built in debug mode. Assuming we’re in release mode, the write_scanlines function attempts to flush the data from the m_tilebuffer into our output file. We know it’s possible for m_tilebuffer to be uninitialized, so a NULL pointer dereference and a denial of service occurs as the program will attempt to copy from around the NULL page. It’s important to note as well that the ZfileOutput::close() function does not need to be explicitly called by whatever is using libOpenImageIO, as the ZfileOutput::~ZfileOutput() destructor will call ZfileOutput::close() as soon as the object goes out of scope.

Crash Information

Thread 1 "fancy_fuzz_rele" received signal SIGSEGV, Segmentation fault.
0x0000555555652950 in __sanitizer::internal_memmove(void*, void const*, unsigned long) ()
<(^.^)>#bt
#0  0x0000555555652950 in __sanitizer::internal_memmove(void*, void const*, unsigned long) ()
#1  0x000055555563b616 in __asan_memmove ()
#2  0x00007ffff3673f83 in std::__copy_move<false, true, std::random_access_iterator_tag>::__copy_m<unsigned char> (__first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>, __result=0x61300005ce00 '\276' <repeats 200 times>...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:431
#3  0x00007ffff3826efd in std::__copy_move_a2<false, unsigned char*, unsigned char*> (__first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>, __result=0x61300005ce00 '\276' <repeats 200 times>...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:494
#4  0x00007ffff3826e6d in std::__copy_move_a1<false, unsigned char*, unsigned char*> (__first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>, __result=0x61300005ce00 '\276' <repeats 200 times>...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:522
#5  0x00007ffff3826d84 in std::__copy_move_a<false, unsigned char*, unsigned char*> (__first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>, __result=0x61300005ce00 '\276' <repeats 200 times>...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:530
#6  0x00007ffff3826992 in std::copy<unsigned char*, unsigned char*> (__first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>, __result=0x61300005ce00 '\276' <repeats 200 times>...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:619
#7  0x00007ffff382720d in std::__uninitialized_copy<true>::__uninit_copy<unsigned char*, unsigned char*> (__first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>, __result=0x61300005ce00 '\276' <repeats 200 times>...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_uninitialized.h:110
#8  0x00007ffff3827181 in std::uninitialized_copy<unsigned char*, unsigned char*> (__first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>, __result=0x61300005ce00 '\276' <repeats 200 times>...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_uninitialized.h:148
#9  0x00007ffff3826ba9 in std::__uninitialized_copy_a<unsigned char*, unsigned char*, unsigned char> (__first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>, __result=0x61300005ce00 '\276' <repeats 200 times>...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_uninitialized.h:333
#10 0x00007ffff38267e3 in std::vector<unsigned char, std::allocator<unsigned char> >::_M_allocate_and_copy<unsigned char*> (this=0x6120000182a8, __n=384, __first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:1514
#11 0x00007ffff3824291 in std::vector<unsigned char, std::allocator<unsigned char> >::_M_assign_aux<unsigned char*> (this=0x6120000182a8, __first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/vector.tcc:309
#12 0x00007ffff3823f0d in std::vector<unsigned char, std::allocator<unsigned char> >::_M_assign_dispatch<unsigned char*> (this=0x6120000182a8, __first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:1628
#13 0x00007ffff380e586 in std::vector<unsigned char, std::allocator<unsigned char> >::assign<unsigned char*, void> (this=0x6120000182a8, __first=0x0, __last=0x180 <error: Cannot access memory at address 0x180>) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:769
#14 0x00007ffff3f710d4 in OpenImageIO_v2_4::ZfileOutput::write_scanline (this=0x6120000181c0, y=0, format=..., data=0x0, xstride=3) at /oiio/oiio-2.4.4.2/src/zfile.imageio/zfile.cpp:375
#15 0x00007ffff2eaab16 in OpenImageIO_v2_4::ImageOutput::write_scanlines (this=0x6120000181c0, ybegin=0, yend=512, z=0, format=..., data=0x0, xstride=3, ystride=384) at /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageoutput.cpp:113
#16 0x00007ffff3f6ea92 in OpenImageIO_v2_4::ZfileOutput::close (this=0x6120000181c0) at /oiio/oiio-2.4.4.2/src/zfile.imageio/zfile.cpp:346
#17 0x00007ffff3f72af3 in OpenImageIO_v2_4::ZfileOutput::~ZfileOutput (this=0x6120000181c0) at /oiio/oiio-2.4.4.2/src/zfile.imageio/zfile.cpp:88
#18 0x00007ffff3f72c76 in OpenImageIO_v2_4::ZfileOutput::~ZfileOutput (this=0x6120000181c0) at /oiio/oiio-2.4.4.2/src/zfile.imageio/zfile.cpp:88
#19 0x000055555568cd31 in std::default_delete<OpenImageIO_v2_4::ImageOutput>::operator() (this=0x7fffffffd130, __ptr=0x6120000181c0) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/unique_ptr.h:85
#20 0x000055555568169f in std::unique_ptr<OpenImageIO_v2_4::ImageOutput, std::default_delete<OpenImageIO_v2_4::ImageOutput> >::~unique_ptr (this=0x7fffffffd130) at /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/unique_ptr.h:361
#21 0x000055555567c6cc in LLVMFuzzerTestOneInput (Data=0x61e000000c80 "II*", Size=2629) at ./fancy_oiio.cpp:323
#22 0x000055555559f734 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) ()
#23 0x00005555555894b0 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) ()
#24 0x000055555558f207 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) ()
#25 0x00005555555b9023 in main ()
TIMELINE

2022-11-14 - Vendor Disclosure
2022-12-03 - Vendor Patch Release
2022-12-22 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.