Talos Vulnerability Report

TALOS-2022-1632

OpenImageIO PSD format image file directory denial of service vulnerability

December 22, 2022
CVE Number

CVE-2022-41684

SUMMARY

A heap out of bounds read vulnerability exists in the OpenImageIO master-branch-9aeece7a when parsing the image file directory part of a PSD image file. A specially-crafted .psd file can cause a read of arbitrary memory address which 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 master-branch-9aeece7a

PRODUCT URLS

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

CVSSv3 SCORE

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

CWE

CWE-125 - Out-of-bounds Read

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.

When dealing with Photoshop .psd files, libOpenImageIO starts from the following function:

bool
PSDInput::open(const std::string& name, ImageSpec& newspec)
{
    m_filename = name;

    Filesystem::open(m_file, name, std::ios::binary);

    if (!m_file) {
        errorf("\"%s\": failed to open file", name);
        return false;
    }

    // File Header
    if (!load_header()) {      // [1]
        errorf("failed to open \"%s\": failed load_header", name);
        return false;
    }

    // Color Mode Data
    if (!load_color_data()) {  // [2]
        errorf("failed to open \"%s\": failed load_color_data", name);
        return false;
    }

    // Image Resources
    if (!load_resources()) {   // [3]
        errorf("failed to open \"%s\": failed load_resources", name);
        return false;
    }

The .psd file headers are read in at [1], populating the struct shown below:

[o.O]> ptype m_header
type = struct OpenImageIO_v2_3::psd_pvt::FileHeader {
    char signature[4];
    uint16_t version;
    uint16_t channel_count;
    uint32_t height;
    uint32_t width;
    uint16_t depth;
    uint16_t color_mode;
}

Even though the psd_pvt::FileHeader struct is only 0x14 bytes long, we end up skipping over six bytes between version and channel_count, so our file pointer ends up at offset 0x1a when we start reading the color data at [2] into a struct that looks like so:

type = struct OpenImageIO_v2_3::psd_pvt::ColorModeData {
    uint32_t length;
    std::string data;
}

The struct read is intuitive, with the 4 bytes for the length being read, then that many bytes being read for the ColorModeData.data. After this, we get to the more interesting resource data at [3] in load_resources():

bool
PSDInput::load_resources()
{
    uint32_t length;
    read_bige<uint32_t>(length);   // [4]

    if (!check_io())
        return false;

    ImageResourceBlock block;
    ImageResourceMap resources;                               // map of uint16_t to ImageResourceBlocks  
    std::streampos begin = m_file.tellg();   
    std::streampos end   = begin + (std::streampos)length;
    while (m_file && m_file.tellg() < end) {
        if (!read_resource(block) || !validate_resource(block)) //  [5] // validate ony checks for "8BIM" signature
            return false;

        resources.insert(std::make_pair(block.id, block));    // [6]
    }
    if (!check_io()) // if (!m_file)
        return false;

    if (!handle_resources(resources))  // [7]
        return false;

    m_file.seekg(end);
    return check_io();
}

Our read at [4] determines the total length of the resources in the file, and then the blocks are read in at [5] and subsequently added to the resources map at [6] before being processed in handle_resources [7]. For more context, the resource struct is given below:

type = struct OpenImageIO_v2_3::psd_pvt::ImageResourceBlock { 
    char signature[4];                               
    uint16_t id;                                     
    std::string name;                                // 1-256 bytes read in
    uint32_t length;                                 // [8]
    std::streampos pos;                              
}    

As one would expect, all of these struct members are simply read in order from the file with the exception of std::string name, where the length is read in via read_pascal_string(). This boils down to a single byte read for the string length, then reading that many bytes, and finally padding out to an even length if needed. The length field at [8] designates the length of the resource data itself, and the offset of it in the file is saved to the std::streampos pos member before the file seeks forward length bytes to find the next ImageResourceBlock. Once all these objects have been read in, we go to the handle_resources function:

bool
PSDInput::handle_resources(ImageResourceMap& resources)
{
    // Loop through each of our resource loaders
    const ImageResourceMap::const_iterator end(resources.end());
    for (const ResourceLoader& loader : resource_loaders) {
        ImageResourceMap::const_iterator it(resources.find(loader.resource_id));
        // If a resource with that ID exists in the file, call the loader
        if (it != end) {
            m_file.seekg(it->second.pos);
            if (!check_io())
                return false;

            loader.load(this, it->second.length);
            if (!check_io())
                return false;
        }
    }
    return true;
}

The library contains a static list of PSDInput::ResourceLoader[] objects that determine which ImageResourceBlocks are actually parsed, which we see below:

const PSDInput::ResourceLoader PSDInput::resource_loaders[]
    = { ADD_LOADER(1005), ADD_LOADER(1006), ADD_LOADER(1010), ADD_LOADER(1033),
        ADD_LOADER(1036), ADD_LOADER(1039), ADD_LOADER(1047), ADD_LOADER(1058),
        ADD_LOADER(1059), ADD_LOADER(1060), ADD_LOADER(1064) };
#undef ADD_LOADER

As such, we only actually hit the handlers for ImageResourceBlocks where the id field matches one of the above numbers. The handlers all have a definition looking like bool PSDInput::load_resource_XXXX(uint32_t length) whereby the Xs correspond to the ImageResourceBlock.id. For today’s vulnerability we only really care about PSDInput::load_resource_1058:

bool
PSDInput::load_resource_1058(uint32_t length)
{
    std::string data(length, 0);
    if (!m_file.read(&data[0], length)) // [9]
        return false;

    if (!decode_exif(data, m_composite_attribs) // [10]
        || !decode_exif(data, m_common_attribs)) {
        errorf("Failed to decode Exif data");
        return false;
    }
    return true;
}

Apparently .psd files can contain Exif data, so the ImageResourceBlock data is read in at [9] and we parse it at [10] inside decode_exif():

bool
decode_exif(cspan<uint8_t> exif, ImageSpec& spec)
{
    // Sometimes an exif blob starts with "Exif". Skip it.
    if (exif.size() >= 6 && exif[0] == 'E' && exif[1] == 'x' && exif[2] == 'i'
        && exif[3] == 'f' && exif[4] == 0 && exif[5] == 0) {
        exif = exif.subspan(6);
    }

    // [...]
    
    TIFFHeader head = *(const TIFFHeader*)exif.data();              // [11]
    if (head.tiff_magic != 0x4949 && head.tiff_magic != 0x4d4d)
        return false;
    bool host_little = littleendian();
    bool file_little = (head.tiff_magic == 0x4949);
    bool swab        = (host_little != file_little);
    if (swab)
        swap_endian(&head.tiff_diroff);

    const unsigned char* ifd = ((const unsigned char*)exif.data()
                                + head.tiff_diroff);                              // [12]
    // keep track of IFD offsets we've already seen to avoid infinite
    // recursion if there are circular references.
    std::set<size_t> ifd_offsets_seen;
    decode_ifd(ifd, exif, spec, exif_tagmap_ref(), ifd_offsets_seen, swab);  //[13]

As the first item in Exif data should be a TIFFHeader, we cast the start of the ImageResourceBlock data as a TiffHeader and start parsing:

 type = struct TIFFHeader {
    uint16_t tiff_magic;
    uint16_t tiff_version;
    uint32_t tiff_diroff;
}
[-.-]> p/x *(struct TIFFHeader *)data.data()
$44 = {tiff_magic = 0x4949, tiff_version = 0x92a, tiff_diroff = 0x800}

As with normal tiff files, the TIFFHeader.tiff_diroff points to the start of the Tiff Directories, and so the code points the ifd variable to these directories at [12] before parsing them at [13] in decode_ifd():

// Decode a raw Exif data block and save all the metadata in an
// ImageSpec.  Return true if all is ok, false if the exif block was
// somehow malformed.
void
pvt::decode_ifd(const unsigned char* ifd, cspan<uint8_t> buf, ImageSpec& spec,
                const TagMap& tag_map, std::set<size_t>& ifd_offsets_seen,
                bool swab, int offset_adjustment)
{
    // Read the directory that the header pointed to.  It should contain
    // some number of directory entries containing tags to process.
    unsigned short ndirs = *(const unsigned short*)ifd;                          // [14]
    if (swab)
        swap_endian(&ndirs);
    for (int d = 0; d < ndirs; ++d)
        read_exif_tag(spec,
                      (const TIFFDirEntry*)(ifd + 2 + d * sizeof(TIFFDirEntry)),            //[15]
                      buf, swab, offset_adjustment, ifd_offsets_seen, tag_map);
}

Since there’s no checking of the ifd variable between [12] and [14], we actually end up with an out-of-bounds read condition at [14]. Unfortunately there is immediately bounds checking inside of read_exif_tag at [15]. As such, the impact of this read is pretty limited, but since we can read an arbitrary uint32_t from the file for our head.tiff_diroff at [12], we can at least turn it into an unmapped memory read, causing a crash and denial-of-service.

Crash Information

=================================================================
==67078==ERROR: AddressSanitizer: SEGV on unknown address 0x614002001848 (pc 0x7f9357df94ef bp 0x7fff87ae2f60 sp 0x7fff87ae2cc0 T0)
==67078==The signal is caused by a READ memory access.
    #0 0x7f9357df94ef in OpenImageIO_v2_3::pvt::decode_ifd(unsigned char const*, OpenImageIO_v2_3::span<unsigned char const, -1l>, OpenImageIO_v2_3::ImageSpec&, OpenImageIO_v2_3::pvt::TagMap const&, std::set<unsigned long, std::less<unsigned long>, std::allocator<unsigned long> >&, bool, int) /oiio/oiio-2.3.19.0/src/libOpenImageIO/exif.cpp:1012:28
    #1 0x7f9357e039d9 in OpenImageIO_v2_3::decode_exif(OpenImageIO_v2_3::span<unsigned char const, -1l>, OpenImageIO_v2_3::ImageSpec&) /oiio/oiio-2.3.19.0/src/libOpenImageIO/exif.cpp:1143:5
    #2 0x7f9357e02781 in OpenImageIO_v2_3::decode_exif(OpenImageIO_v2_3::string_view, OpenImageIO_v2_3::ImageSpec&) /oiio/oiio-2.3.19.0/src/libOpenImageIO/exif.cpp:1091:12
    #3 0x7f9359580904 in OpenImageIO_v2_3::PSDInput::load_resource_1058(unsigned int) /oiio/oiio-2.3.19.0/src/psd.imageio/psdinput.cpp:1210:10
    #4 0x7f9359608752 in bool std::__invoke_impl<bool, bool (OpenImageIO_v2_3::PSDInput::*&)(unsigned int), OpenImageIO_v2_3::PSDInput*, unsigned int>(std::__invoke_memfun_deref, bool (OpenImageIO_v2_3::PSDInput::*&)(unsigned int), OpenImageIO_v2_3::PSDInput*&&, unsigned int&&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/invoke.h:74:14
    #5 0x7f9359607b85 in std::__invoke_result<bool (OpenImageIO_v2_3::PSDInput::*&)(unsigned int), OpenImageIO_v2_3::PSDInput*, unsigned int>::type std::__invoke<bool (OpenImageIO_v2_3::PSDInput::*&)(unsigned int), OpenImageIO_v2_3::PSDInput*, unsigned int>(bool (OpenImageIO_v2_3::PSDInput::*&)(unsigned int), OpenImageIO_v2_3::PSDInput*&&, unsigned int&&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/invoke.h:96:14
    #6 0x7f9359607481 in bool std::_Bind<bool (OpenImageIO_v2_3::PSDInput::* (std::_Placeholder<1>, std::_Placeholder<2>))(unsigned int)>::__call<bool, OpenImageIO_v2_3::PSDInput*&&, unsigned int&&, 0ul, 1ul>(std::tuple<OpenImageIO_v2_3::PSDInput*&&, unsigned int&&>&&, std::_Index_tuple<0ul, 1ul>) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/functional:420:11
    #7 0x7f9359606ec1 in bool std::_Bind<bool (OpenImageIO_v2_3::PSDInput::* (std::_Placeholder<1>, std::_Placeholder<2>))(unsigned int)>::operator()<OpenImageIO_v2_3::PSDInput*, unsigned int, bool>(OpenImageIO_v2_3::PSDInput*&&, unsigned int&&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/functional:503:17
    #8 0x7f93596069f5 in bool std::__invoke_impl<bool, std::_Bind<bool (OpenImageIO_v2_3::PSDInput::* (std::_Placeholder<1>, std::_Placeholder<2>))(unsigned int)>&, OpenImageIO_v2_3::PSDInput*, unsigned int>(std::__invoke_other, std::_Bind<bool (OpenImageIO_v2_3::PSDInput::* (std::_Placeholder<1>, std::_Placeholder<2>))(unsigned int)>&, OpenImageIO_v2_3::PSDInput*&&, unsigned int&&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/invoke.h:61:14
    #9 0x7f9359606365 in std::enable_if<__and_<std::__not_<std::is_void<bool> >, std::is_convertible<std::__invoke_result<std::_Bind<bool (OpenImageIO_v2_3::PSDInput::* (std::_Placeholder<1>, std::_Placeholder<2>))(unsigned int)>&, OpenImageIO_v2_3::PSDInput*, unsigned int>::type, bool> >::value, bool>::type std::__invoke_r<bool, std::_Bind<bool (OpenImageIO_v2_3::PSDInput::* (std::_Placeholder<1>, std::_Placeholder<2>))(unsigned int)>&, OpenImageIO_v2_3::PSDInput*, unsigned int>(std::_Bind<bool (OpenImageIO_v2_3::PSDInput::* (std::_Placeholder<1>, std::_Placeholder<2>))(unsigned int)>&, OpenImageIO_v2_3::PSDInput*&&, unsigned int&&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/invoke.h:142:14
    #10 0x7f9359604fac in std::_Function_handler<bool (OpenImageIO_v2_3::PSDInput*, unsigned int), std::_Bind<bool (OpenImageIO_v2_3::PSDInput::* (std::_Placeholder<1>, std::_Placeholder<2>))(unsigned int)> >::_M_invoke(std::_Any_data const&, OpenImageIO_v2_3::PSDInput*&&, unsigned int&&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/std_function.h:290:9
    #11 0x7f93595d0424 in std::function<bool (OpenImageIO_v2_3::PSDInput*, unsigned int)>::operator()(OpenImageIO_v2_3::PSDInput*, unsigned int) const /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/std_function.h:590:9
    #12 0x7f93595ccc46 in OpenImageIO_v2_3::PSDInput::handle_resources(std::map<unsigned short, OpenImageIO_v2_3::psd_pvt::ImageResourceBlock, std::less<unsigned short>, std::allocator<std::pair<unsigned short const, OpenImageIO_v2_3::psd_pvt::ImageResourceBlock> > >&) /oiio/oiio-2.3.19.0/src/psd.imageio/psdinput.cpp:1075:13
    #13 0x7f935958bb74 in OpenImageIO_v2_3::PSDInput::load_resources() /oiio/oiio-2.3.19.0/src/psd.imageio/psdinput.cpp:1020:10
    #14 0x7f935958781e in OpenImageIO_v2_3::PSDInput::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, OpenImageIO_v2_3::ImageSpec&) /oiio/oiio-2.3.19.0/src/psd.imageio/psdinput.cpp:559:10
    #15 0x7f93595a11da in OpenImageIO_v2_3::PSDInput::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, OpenImageIO_v2_3::ImageSpec&, OpenImageIO_v2_3::ImageSpec const&) /oiio/oiio-2.3.19.0/src/psd.imageio/psdinput.cpp:615:12
    #16 0x7f9358782669 in OpenImageIO_v2_3::ImageInput::create(OpenImageIO_v2_3::string_view, bool, OpenImageIO_v2_3::ImageSpec const*, OpenImageIO_v2_3::Filesystem::IOProxy*, OpenImageIO_v2_3::string_view) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageioplugin.cpp:758:27
    #17 0x7f9358661589 in OpenImageIO_v2_3::ImageInput::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, OpenImageIO_v2_3::ImageSpec const*, OpenImageIO_v2_3::Filesystem::IOProxy*) /oiio/oiio-2.3.19.0/src/libOpenImageIO/imageinput.cpp:105:16
    #18 0x55e4dcdae40f in LLVMFuzzerTestOneInput /oiio/fuzzing/./oiio_harness.cpp:77:16
    #19 0x55e4dccd44e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing/fuzz_oiio.bin+0x414e3) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #20 0x55e4dccbe25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing/fuzz_oiio.bin+0x2b25f) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #21 0x55e4dccc3fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing/fuzz_oiio.bin+0x30fb6) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #22 0x55e4dcceddd2 in main (/oiio/fuzzing/fuzz_oiio.bin+0x5add2) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #23 0x7f934ed80d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #24 0x7f934ed80e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #25 0x55e4dccb8b24 in _start (/oiio/fuzzing/fuzz_oiio.bin+0x25b24) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /oiio/oiio-2.3.19.0/src/libOpenImageIO/exif.cpp:1012:28 in OpenImageIO_v2_3::pvt::decode_ifd(unsigned char const*, OpenImageIO_v2_3::span<unsigned char const, -1l>, OpenImageIO_v2_3::ImageSpec&, OpenImageIO_v2_3::pvt::TagMap const&, std::set<unsigned long, std::less<unsigned long>, std::allocator<unsigned long> >&, bool, int)
==67078==ABORTING
TIMELINE

2022-10-19 - Initial Vendor Contact
2022-10-20 - Vendor Disclosure
2022-12-22 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.