Talos Vulnerability Report

TALOS-2022-1656

OpenImageIO Project OpenImageIO IFFOutput wild write vulnerability

December 22, 2022
CVE Number

CVE-2022-43601,CVE-2022-43600,CVE-2022-43599,CVE-2022-43602

SUMMARY

Multiple code execution vulnerabilities exist in the IFFOutput::close() functionality of OpenImageIO Project OpenImageIO v2.4.4.2. A specially crafted ImageOutput Object can lead to a heap buffer overflow. An attacker can provide malicious input to trigger these vulnerabilities.

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

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

CWE

CWE-122 - Heap-based Buffer Overflow

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.

We will be looking at four instances of a similar vulnerability; the first description will look at the general details of the bugs and the specifics of the first, whilst the descriptions of the subsequent vulnerabilities will focus on the differences.

CVE-2022-43599 - TypeDesc::UINT8 xmax

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 generating a .iff file, then we appropriately end up hitting code inside src/iff.imageio/iffoutput.cpp. Upon opening the output file via IffOutput::open, assorted m_iff_header fields are set and our object’s scratch m_buf is resized to accommodate any images we wish to convert. It’s only upon IffOutput::close(), however, that this image data is flushed into the scratch buffers and eventually written to our resultant file. Starting with inline bool IffOutput::close(void):

   if (m_fd && m_buf.size()) {
        // [...]
        
        // write y-tiles
        for (uint32_t ty = 0; ty < tile_height_size(m_spec.height); ty++) {  // [7]
            // write x-tiles
            for (uint32_t tx = 0; tx < tile_width_size(m_spec.width); tx++) {  // [8]
                // channels
                uint8_t channels = m_iff_header.pixel_channels;

                // set tile coordinates
                uint16_t xmin, xmax, ymin, ymax;

                // set xmin and xmax
                xmin = tx * tile_width();
                xmax = std::min(xmin + tile_width(), m_spec.width) - 1;         // [9]

                // set ymin and ymax
                ymin = ty * tile_height();
                ymax = std::min(ymin + tile_height(), m_spec.height) - 1;       // [10]

The code flow goes as one might expect. We iterate over the tiles of each row and also each column, writing pixels as we go along. At [7] and [8], we find the bounds of our image, with the tile_height_size(m_spec.height) reducing essentially down to (m_spec.height + 64 -1 ) / 64 and tile_width_size reducing similarly to (m_spec.width + 64 - 1 ) / 64. Worth noting here that m_spec.height and m_spec.width are both uint32_t, for an idea of how many loops we can hit. Continuing on, we find were our current tile starts and ends at [9] and [10] with the xmin, xmax, ymin and ymax variables, all of which are importantly uint16_t variables. Since we know that m_spec.width and m_spec.height are uint32_t, it follows that xmax and ymax can both be anywhere from 0x0 to 0xFFFF. Continuing further into IffOutput::close():

      // write y-tiles
        for (uint32_t ty = 0; ty < tile_height_size(m_spec.height); ty++) { 
            // write x-tiles
            for (uint32_t tx = 0; tx < tile_width_size(m_spec.width); tx++) { 
            
                uint8_t channels = m_iff_header.pixel_channels;
                
                // [...]
                // set width and height
                uint32_t tw = xmax - xmin + 1;     // [9] 
                uint32_t th = ymax - ymin + 1;     // [10]
                // [...]

                // length.
                uint32_t length = tw * th * m_spec.pixel_bytes(); // [11]

                // tile length.
                uint32_t tile_length = length;
               // [...]

                // tile compression.
                bool tile_compress = (m_iff_header.compression == RLE); 

                 // [..]
                
                // handle 8-bit data
                if (m_spec.format == TypeDesc::UINT8) {
                    if (tile_compress) {  // [12]

For each tile, the size is calculated at [9] and [10], and the appropriate tile_length is propagated via the calculation at [11]. Assuming our image only uses one byte for each pixel—also importantly, if our image specification deems us to need RLE compression—then tile_compression is set and we hit the branch at [12]:

            if (m_spec.format == TypeDesc::UINT8) {
                 if (tile_compress) {  
                        // [...]
                        
                        // map: RGB(A) to BGRA
                        for (int c = (channels * m_spec.channel_bytes()) - 1; 
                             c >= 0; --c) {
                            std::vector<uint8_t> in(tw * th); // [13]
                            uint8_t* in_p = &in[0];

                            // set tile
                            for (uint16_t py = ymin; py <= ymax; py++) {    // [14]
                                const uint8_t* in_dy
                                    = &m_buf[0]
                                      + (py * m_spec.width)
                                            * m_spec.pixel_bytes();

                                for (uint16_t px = xmin; px <= xmax; px++) {  // [15]
                                    // get pixel
                                    uint8_t pixel;
                                    const uint8_t* in_dx
                                        = in_dy + px * m_spec.pixel_bytes() + c;
                                    memcpy(&pixel, in_dx, 1);
                                    // set pixel
                                    *in_p++ = pixel;     // [16]
                                }
                            }

                            // compress rle channel
                            size = compress_rle_channel(&in[0], &tmp[0] + index,   // [17]
                                                        tw * th);
                            index += size;
                        }

We don’t particularly care how many loops for the channel we hit, but for each pixel of our current tile [14, 15], we read image data into the in vector of size ( tw * th ) [13] at [16] with the in_p++ = pixel; line. After all the data has been read for the current channel, it’s compressed into a different scratch buffer at [17] before eventual further processing. But for now we’ve already gone past our vulnerability, as a curious condition can be hit. For either loop at [14] or [15], we’ve already established that the ymax and xmax variable can be anywhere from 0x0 to 0xFFFF. If either of these variables happens to be 0xFFFF, the given corresponding loop strictly cannot exit, since both px and py are also uint16_t and the conditional is <=. As such, when px or py reach 0xFFFF, they wrap around to 0x0 upon the next iteration, continuing the writes to our in buffer at [16], resulting in a wild write on the heap. Also worth noting is that since libOpenImageIO is a library, it would not be impossible to come across a situation in which this vulnerability is fully exploitable.

Crash Information

=================================================================
==816417==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6200007ffe80 at pc 0x7ffff3bffe2e bp 0x7fffffffae70 sp 0x7fffffffae68
WRITE of size 1 at 0x6200007ffe80 thread T0
[Detaching after fork from child process 817514]
    #0 0x7ffff3bffe2d in OpenImageIO_v2_4::IffOutput::close() /oiio/oiio-2.4.4.2/src/iff.imageio/iffoutput.cpp:283:45
    #1 0x555555649a6d in convert_file(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /oiio/fuzzing_release/.
/iconvert.cpp:475:10
    #2 0x5555556453c8 in main /oiio/fuzzing_release/./iconvert.cpp:523:14
    #3 0x7fffeac23d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #4 0x7fffeac23e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #5 0x555555584ed4 in _start (/oiio/fuzzing/triage/iconvert_testing_dir/iconvert+0x30ed4) (BuildId: 8d9c54aeaee5ba79c4320b01f97dc76bf6e7ce61)

0x6200007ffe80 is located 0 bytes to the right of 3584-byte region [0x6200007ff080,0x6200007ffe80)
allocated by thread T0 here:
    #0 0x555555642aed in operator new(unsigned long) (/oiio/fuzzing/triage/iconvert_testing_dir/iconvert+0xeeaed) (BuildId: 8d9c54aeaee5ba79c4320b01f97dc76bf6e7ce61)
    #1 0x7ffff160e411 in __gnu_cxx::new_allocator<unsigned char>::allocate(unsigned long, void const*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/new_allocator.h:127:27
    #2 0x7ffff160e293 in std::allocator_traits<std::allocator<unsigned char> >::allocate(std::allocator<unsigned char>&, unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/alloc_traits.h:464:20
    #3 0x7ffff160cd2b in std::_Vector_base<unsigned char, std::allocator<unsigned char> >::_M_allocate(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:346:20
    #4 0x7ffff169bfb8 in std::_Vector_base<unsigned char, std::allocator<unsigned char> >::_M_create_storage(unsigned long) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:361:33
    #5 0x7ffff169b551 in std::_Vector_base<unsigned char, std::allocator<unsigned char> >::_Vector_base(unsigned long, std::allocator<unsigned char> const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:305:9
    #6 0x7ffff1699fbe in std::vector<unsigned char, std::allocator<unsigned char> >::vector(unsigned long, std::allocator<unsigned char> const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:511:9
    #7 0x7ffff3bfedb7 in OpenImageIO_v2_4::IffOutput::close() /oiio/oiio-2.4.4.2/src/iff.imageio/iffoutput.cpp:266:50
    #8 0x555555649a6d in convert_file(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /oiio/fuzzing_release/.
/iconvert.cpp:475:10
    #9 0x5555556453c8 in main /oiio/fuzzing_release/./iconvert.cpp:523:14
    #10 0x7fffeac23d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow /oiio/oiio-2.4.4.2/src/iff.imageio/iffoutput.cpp:283:45 in OpenImageIO_v2_4::IffOutput::close()
Shadow bytes around the buggy address:
  0x0c40800f7f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c40800f7f90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c40800f7fa0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c40800f7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c40800f7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c40800f7fd0:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c40800f7fe0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c40800f7ff0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c40800f8000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c40800f8010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c40800f8020: 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
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==816417==ABORTING

CVE-2022-43600 - TypeDesc::UINT16 xmax

A similar vulnerability is applicable if the m_spec.format is TypeDesc::UINT16, as the code starting at [18] is essentially the same as the above. We can also see our unbreakable loop at [20] below for when the xmax variable is set to 0xFFFF:

            // handle 16-bit data
            else if (m_spec.format == TypeDesc::UINT16) {
                if (tile_compress) {
                    uint32_t index = 0, size = 0;
                    std::vector<uint8_t> tmp;

                    // set bytes.
                    tmp.resize(tile_length * 2);

                    // set map
                    std::vector<uint8_t> map;
                    if (littleendian()) {
                        int rgb16[]  = { 0, 2, 4, 1, 3, 5 };
                        int rgba16[] = { 0, 2, 4, 7, 1, 3, 5, 6 };
                        if (m_iff_header.pixel_channels == 3) {
                            map = std::vector<uint8_t>(rgb16, &rgb16[6]);
                        } else {
                            map = std::vector<uint8_t>(rgba16, &rgba16[8]);
                        }

                    } else {
                        int rgb16[]  = { 1, 3, 5, 0, 2, 4 };
                        int rgba16[] = { 1, 3, 5, 7, 0, 2, 4, 6 };
                        if (m_iff_header.pixel_channels == 3) {
                            map = std::vector<uint8_t>(rgb16, &rgb16[6]);
                        } else {
                            map = std::vector<uint8_t>(rgba16, &rgba16[8]);
                        }
                    }

                    // map: RRGGBB(AA) to BGR(A)BGR(A)
                    for (int c = (channels * m_spec.channel_bytes()) - 1;       // [18]
                         c >= 0; --c) {
                        int mc = map[c];

                        std::vector<uint8_t> in(tw * th);
                        uint8_t* in_p = &in[0];

                        // set tile
                        for (uint16_t py = ymin; py <= ymax; py++) { // [19]
                            const uint8_t* in_dy
                                = &m_buf[0]
                                  + (py * m_spec.width)
                                        * m_spec.pixel_bytes();

                            for (uint16_t px = xmin; px <= xmax; px++) { // [20]
                                // get pixel
                                uint8_t pixel;
                                const uint8_t* in_dx
                                    = in_dy + px * m_spec.pixel_bytes()
                                      + mc;
                                memcpy(&pixel, in_dx, 1);
                                // set pixel.
                                *in_p++ = pixel;
                            }
                        }

                        // compress rle channel
                        size = compress_rle_channel(&in[0], &tmp[0] + index,
                                                    tw * th);
                        index += size;
                    }

CVE-2022-43601 - TypeDesc::UINT16 ymax

A similar vulnerability is applicable if the m_spec.format is TypeDesc::UINT16, as the code starting at [18] is essentially the same as the above. We can also see our unbreakable loop at [19] below for when the ymax variable is set to 0xFFFF:

            // handle 16-bit data
            else if (m_spec.format == TypeDesc::UINT16) {
                if (tile_compress) {
                    uint32_t index = 0, size = 0;
                    std::vector<uint8_t> tmp;

                    // set bytes.
                    tmp.resize(tile_length * 2);

                    // set map
                    std::vector<uint8_t> map;
                    if (littleendian()) {
                        int rgb16[]  = { 0, 2, 4, 1, 3, 5 };
                        int rgba16[] = { 0, 2, 4, 7, 1, 3, 5, 6 };
                        if (m_iff_header.pixel_channels == 3) {
                            map = std::vector<uint8_t>(rgb16, &rgb16[6]);
                        } else {
                            map = std::vector<uint8_t>(rgba16, &rgba16[8]);
                        }

                    } else {
                        int rgb16[]  = { 1, 3, 5, 0, 2, 4 };
                        int rgba16[] = { 1, 3, 5, 7, 0, 2, 4, 6 };
                        if (m_iff_header.pixel_channels == 3) {
                            map = std::vector<uint8_t>(rgb16, &rgb16[6]);
                        } else {
                            map = std::vector<uint8_t>(rgba16, &rgba16[8]);
                        }
                    }

                    // map: RRGGBB(AA) to BGR(A)BGR(A)
                    for (int c = (channels * m_spec.channel_bytes()) - 1;       // [18]
                         c >= 0; --c) {
                        int mc = map[c];

                        std::vector<uint8_t> in(tw * th);
                        uint8_t* in_p = &in[0];

                        // set tile
                        for (uint16_t py = ymin; py <= ymax; py++) { // [19]
                            const uint8_t* in_dy
                                = &m_buf[0]
                                  + (py * m_spec.width)
                                        * m_spec.pixel_bytes();

                            for (uint16_t px = xmin; px <= xmax; px++) { // [20]
                                // get pixel
                                uint8_t pixel;
                                const uint8_t* in_dx
                                    = in_dy + px * m_spec.pixel_bytes()
                                      + mc;
                                memcpy(&pixel, in_dx, 1);
                                // set pixel.
                                *in_p++ = pixel;
                            }
                        }

                        // compress rle channel
                        size = compress_rle_channel(&in[0], &tmp[0] + index,
                                                    tw * th);
                        index += size;
                    }

CVE-2022-43602 - TypeDesc::UINT8 ymax

A similar vulnerability is applicable if the m_spec.format is TypeDesc::UINT8 and the ymax[14] variable is set to 0xFFFF:

     if (m_spec.format == TypeDesc::UINT8) {
                 if (tile_compress) {  
                        // [...]
                        
                        // map: RGB(A) to BGRA
                        for (int c = (channels * m_spec.channel_bytes()) - 1; 
                             c >= 0; --c) {
                            std::vector<uint8_t> in(tw * th); // [13]
                            uint8_t* in_p = &in[0];

                            // set tile
                            for (uint16_t py = ymin; py <= ymax; py++) {    // [14]
                                const uint8_t* in_dy
                                    = &m_buf[0]
                                      + (py * m_spec.width)
                                            * m_spec.pixel_bytes();

                                for (uint16_t px = xmin; px <= xmax; px++) {  // [15]
                                    // get pixel
                                    uint8_t pixel;
                                    const uint8_t* in_dx
                                        = in_dy + px * m_spec.pixel_bytes() + c;
                                    memcpy(&pixel, in_dx, 1);
                                    // set pixel
                                    *in_p++ = pixel;     // [16]
                                }
                            }

                            // compress rle channel
                            size = compress_rle_channel(&in[0], &tmp[0] + index,   // [17]
                                                        tw * th);
                            index += size;
                        }
TIMELINE

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

Credit

Discovered by Lilith >_> of Cisco Talos.