Talos Vulnerability Report

TALOS-2022-1636

OpenImageIO Exif out-of-bounds write vulnerability

December 22, 2022
CVE Number

CVE-2022-41837

SUMMARY

An out-of-bounds write vulnerability exists in the OpenImageIO::add_exif_item_to_spec functionality of OpenImageIO Project OpenImageIO v2.4.4.2. Specially-crafted exif metadata can lead to stack-based memory corruption. 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

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

CWE

CWE-562 - Return of Stack Variable Address

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.

Many different file formats are capable of utilizing Exif metadata to provide extra context to the specifcation of the given file. In libOpenImageIO, we can get to the Exif metadata parsing code via jpeg files, photoshop .psd files and some others. Depending on the file format, we start at different offsets, but the parsing code converges at 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();              // [1]
    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);                              // [2]
    // 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);  //[3]

As the first item in Exif data should be a TIFFHeader, we cast the input data as a TiffHeader [1] 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 [2] before parsing them at [3] 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;                          // [4]
    if (swab)
        swap_endian(&ndirs);
    for (int d = 0; d < ndirs; ++d)
        read_exif_tag(spec,
                      (const TIFFDirEntry*)(ifd + 2 + d * sizeof(TIFFDirEntry)),            //[5]
                      buf, swab, offset_adjustment, ifd_offsets_seen, tag_map);
}

The count of directories is read in at [4] with a max of 0xFFFF, and we call read_exif_tag on each of these directories [5]:

type = const struct TIFFDirEntry {   // [6]
    uint16_t tdir_tag;
    uint16_t tdir_type;
    uint32_t tdir_count;
    uint32_t tdir_offset;
} *

static void
read_exif_tag(ImageSpec& spec, const TIFFDirEntry* dirp, cspan<uint8_t> buf,
              bool swab, int offset_adjustment,
              std::set<size_t>& ifd_offsets_seen, const TagMap& tagmap)
{
    if ((const uint8_t*)dirp < buf.data()                // [7]
        || (const uint8_t*)dirp + sizeof(TIFFDirEntry)
               >= buf.data() + buf.size()) {
        return;
    }

    const TagMap& exif_tagmap(exif_tagmap_ref());
    const TagMap& gps_tagmap(gps_tagmap_ref());

    // Make a copy of the pointed-to TIFF directory, swab the components
    // if necessary.
    TIFFDirEntry dir = *dirp;
    // [...]

    if (dir.tdir_tag == TIFFTAG_EXIFIFD || dir.tdir_tag == TIFFTAG_GPSIFD) {
       // [...]
    } else if (dir.tdir_tag == TIFFTAG_INTEROPERABILITYIFD) {
        // [...]
    } else {
        // Everything else -- use our table to handle the general case
        const TagInfo* taginfo = tagmap.find(dir.tdir_tag);
        if (taginfo && !spec.extra_attribs.contains(taginfo->name)) {
            if (taginfo->handler)
                taginfo->handler(*taginfo, dir, buf, spec, swab,
                                 offset_adjustment);
            else if (taginfo->tifftype != TIFF_NOTYPE)
                add_exif_item_to_spec(spec, taginfo->name, &dir, buf, swab,  // [9]
                                      offset_adjustment);
        } // [...]
    }
}

Each directory, size 0xC, is cast into the structure listed at [6], and a minor check occurs at [7] to validate that this directory is not out of bounds. Assuming that the dirp->tdir_tag is not one of the ones listed above, we hit the branch at [8] and call into add_exif_item_to_spec [9] for the vast majority of tiff tag types:

static void
add_exif_item_to_spec(ImageSpec& spec, const char* name,
                      const TIFFDirEntry* dirp, cspan<uint8_t> buf, bool swab,
                      int offset_adjustment = 0)
{
    OIIO_ASSERT(dirp);
    const uint8_t* dataptr = (const uint8_t*)pvt::dataptr(*dirp, buf,
                                                          offset_adjustment);
    if (!dataptr)
        return;
    TypeDesc type = tiff_datatype_to_typedesc(*dirp);
    if (dirp->tdir_type == TIFF_SHORT) {
        std::vector<uint16_t> d((const uint16_t*)dataptr,
                                (const uint16_t*)dataptr + dirp->tdir_count);
        if (swab)
            swap_endian(d.data(), d.size());
        spec.attribute(name, type, d.data());
        return;
    }
    if (dirp->tdir_type == TIFF_LONG) {
        // [...]
    }

This particular function examines the dirp->tdir_type to figure out what type of variable our current directory holds and then populates this data appropriately into the m_spec attributes for our resultant ImageSpec object. A problem quickly arises in this function, as there’s never really any concrete validation on our current dirp. The dirp->tdir_count variable is completely controlled by the input file. As such, let’s examine how we might utilize this fact:

static void
add_exif_item_to_spec(ImageSpec& spec, const char* name,
                      const TIFFDirEntry* dirp, cspan<uint8_t> buf, bool swab,
                      int offset_adjustment = 0)
{

        const uint8_t* dataptr = (const uint8_t*)pvt::dataptr(*dirp, buf,
                                                          offset_adjustment);
    if (!dataptr)
        return;
    TypeDesc type = tiff_datatype_to_typedesc(*dirp);
    if (dirp->tdir_type == TIFF_SHORT) {
        // [...]
    }
     if (dirp->tdir_type == TIFF_LONG) {
        // [...]
     }

    if (dirp->tdir_type == TIFF_RATIONAL) {
        int n    = dirp->tdir_count;  // How many
        float* f = OIIO_ALLOCA(float, n);                    // [10]
        for (int i = 0; i < n; ++i) {                        // [11]
            unsigned int num, den;
            num = ((const unsigned int*)dataptr)[2 * i + 0];
            den = ((const unsigned int*)dataptr)[2 * i + 1];  
            if (swab) {
                swap_endian(&num);
                swap_endian(&den);
            }
            f[i] = (float)((double)num / (double)den);
        }
        if (dirp->tdir_count == 1)
            spec.attribute(name, *f);
        else
            spec.attribute(name, TypeDesc(TypeDesc::FLOAT, n), f); // [12]
        return;
    }

At [10], we see our arbitrary value plugged directly into OIIO_ALLOCA. This is just a define wrapper for the alloca function, which subtracts an appropriate value from our stack pointer to allocate enough memory for a temporary buffer. The define is given as such:

#    define OIIO_ALLOCA(type, size) (assert(size < (1<<20)), (size) != 0 ? ((type*)alloca((size) * sizeof(type))) : nullptr)

And functionally this ends up looking like the following decompiled code:

00a4b3c8                      f_alloc = &top_of_stack - (((sx.q(tdir_count) << 2) + 0xf) & 0xfffffffffffffff0)

Since tdir_count is a uint32_t, we don’t have complete control over where our stack pointer can end up. We can easily move it into our current function (or previous functions) call stacks. If our input tdir_count is < 0x80000000, the stack pointer goes to a lower value and can potentially corrupt other threads. The loop ends up being rather unruly, and we usually crash in uncontrollable ways. But if our input tdir_count is > 0x80000000, the stack pointer increases, and we skip the loop at [11] (since it’s a signed comparison). Thus we get to the call at [12], where we can corrupt the stack via normal calling conventions, usually with new variables, with values we control being written therein. Clever corruption could potentially lead to code execution.

Crash Information

Running: ./stack_overflow_add_exif_to_spec/test_crash.jpg
=================================================================
==334652==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffffff75e0 at pc 0x5555556701f6 bp 0x7fffffff7510 sp 0x7fffffff7508
WRITE of size 1 at 0x7fffffff75e0 thread T0
[Detaching after fork from child process 334695]
    #0 0x5555556701f5 in OpenImageIO_v2_4::TypeDesc::TypeDesc(OpenImageIO_v2_4::TypeDesc::BASETYPE, OpenImageIO_v2_4::TypeDesc::AGGREGATE, OpenImageIO_v2_4::TypeDesc::VECSEMANTICS, int) /oiio/oiio-2.4.4.2/src/include/OpenImageIO/typedesc
.h:135:11
    #1 0x7ffff27a75ec in OpenImageIO_v2_4::ImageSpec::attribute(OpenImageIO_v2_4::basic_string_view<char, std::char_traits<char> >, OpenImageIO_v2_4::TypeDesc, void const*) /oiio/oiio-2.4.4.2/src/libOpenImageIO/formatspec.cpp:325:21
    #2 0x7ffff27466f6 in OpenImageIO_v2_4::add_exif_item_to_spec(OpenImageIO_v2_4::ImageSpec&, char const*, TIFFDirEntry const*, OpenImageIO_v2_4::span<unsigned char const, -1l>, bool, int) /oiio/oiio-2.4.4.2/src/libOpenImageIO/exif.cpp:
680:18
    #3 0x7ffff270364e in OpenImageIO_v2_4::read_exif_tag(OpenImageIO_v2_4::ImageSpec&, TIFFDirEntry const*, OpenImageIO_v2_4::span<unsigned char const, -1l>, bool, int, std::set<unsigned long, std::less<unsigned long>, std::allocator<unsigned long> >&, OpenImageIO_v2_4::
pvt::TagMap const&) /oiio/oiio-2.4.4.2/src/libOpenImageIO/exif.cpp:886:17
    #4 0x7ffff26ff3c1 in OpenImageIO_v2_4::pvt::decode_ifd(unsigned char const*, OpenImageIO_v2_4::span<unsigned char const, -1l>, OpenImageIO_v2_4::ImageSpec&, OpenImageIO_v2_4::pvt::TagMap const&, std::set<unsigned long, std::less<unsigned long>, std::allocator<unsigne
d long> >&, bool, int) /oiio/oiio-2.4.4.2/src/libOpenImageIO/exif.cpp:1019:9
    #5 0x7ffff2709299 in OpenImageIO_v2_4::decode_exif(OpenImageIO_v2_4::span<unsigned char const, -1l>, OpenImageIO_v2_4::ImageSpec&) /oiio/oiio-2.4.4.2/src/libOpenImageIO/exif.cpp:1146:5
    #6 0x7ffff2708041 in OpenImageIO_v2_4::decode_exif(OpenImageIO_v2_4::basic_string_view<char, std::char_traits<char> >, OpenImageIO_v2_4::ImageSpec&) /oiio/oiio-2.4.4.2/src/libOpenImageIO/exif.cpp:1094:12
    #7 0x7ffff39e8ba9 in OpenImageIO_v2_4::JpgInput::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, OpenImageIO_v2_4::ImageSpec&) /oiio/oiio-2.4.4.2/src/jpeg.imageio/jpeginput.cpp:288:13
    #8 0x7ffff39e1bdf in OpenImageIO_v2_4::JpgInput::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, OpenImageIO_v2_4::ImageSpec&, OpenImageIO_v2_4::ImageSpec const&) /oiio/oiio-2.4.4.2/src/jp
eg.imageio/jpeginput.cpp:173:12
    #9 0x7ffff2e413c9 in OpenImageIO_v2_4::ImageInput::create(OpenImageIO_v2_4::basic_string_view<char, std::char_traits<char> >, bool, OpenImageIO_v2_4::ImageSpec const*, OpenImageIO_v2_4::Filesystem::IOProxy*, OpenImageIO_v2_4::basic_string_view<char, std::char_traits<
char> >) /oiio/oiio-2.4.4.2/src/libOpenImageIO/imageioplugin.cpp:786:27
    #10 0x7ffff2d19289 in OpenImageIO_v2_4::ImageInput::open(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, OpenImageIO_v2_4::ImageSpec const*, OpenImageIO_v2_4::Filesystem::IOProxy*) /oiio/oiio-2
.4.4.2/src/libOpenImageIO/imageinput.cpp:112:16
    #11 0x55555566f40f in LLVMFuzzerTestOneInput /oiio/fuzzing_release/./oiio_harness.cpp:77:16
    #12 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing_release/fuzz_oiio.bin+0x414e3) (BuildId: 1cf10bcefd2178fbc0bf431c5ed1d874a392106b)                                                 #13 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing_release/fuzz_oiio.bin+0x2b25f) (BuildId: 1cf10bcefd2178fbc0bf431c5ed1d874a392106b)
    #14 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing_release/fuzz_oiio.bin+0x30fb6) (BuildId: 1cf10bcefd2178fbc0bf431c5ed1d874a392106b)
    #15 0x5555555aedd2 in main (/oiio/fuzzing_release/fuzz_oiio.bin+0x5add2) (BuildId: 1cf10bcefd2178fbc0bf431c5ed1d874a392106b)
    #16 0x7fffec2d5d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #17 0x7fffec2d5e3f in __libc_start_main csu/../csu/libc-start.c:392:3                                                                                                                                                                                                          #18 0x555555579b24 in _start (/oiio/fuzzing_release/fuzz_oiio.bin+0x25b24) (BuildId: 1cf10bcefd2178fbc0bf431c5ed1d874a392106b)
                                                                                                                                                                                                                                                                               Address 0x7fffffff75e0 is located in stack of thread T0 at offset 64 in frame
    #0 0x7ffff27a724f in OpenImageIO_v2_4::ImageSpec::attribute(OpenImageIO_v2_4::basic_string_view<char, std::char_traits<char> >, OpenImageIO_v2_4::TypeDesc, void const*) /oiio/oiio-2.4.4.2/src/libOpenImageIO/formatspec.cpp:321

  This frame has 6 object(s):
    [32, 48) 'agg.tmp'
    [64, 72) 'agg.tmp6' <== Memory access at offset 64 is inside this variable
    [96, 112) 'agg.tmp28'
    [128, 136) 'agg.tmp31'
    [160, 161) 'agg.tmp34'
    [176, 177) 'ref.tmp' (line 330)
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 /oiio/oiio-2.4.4.2/src/include/OpenImageIO/typedesc.h:135:11 in OpenImageIO_v2_4::TypeDesc::TypeDesc(OpenImageIO_v2_4::TypeDesc::BASETYPE, OpenImageIO_v2_4::TypeDesc::AGGREGATE, OpenImageI
O_v2_4::TypeDesc::VECSEMANTICS, int)
Shadow bytes around the buggy address:
  0x10007fff6e60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fff6e70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fff6e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007fff6e90: cb cb cb cb f1 f1 f1 f1 00 00 f2 f2 00 00 f2 f2
  0x10007fff6ea0: 00 f2 f2 f2 f8 f8 f8 f2 f2 f2 f2 f2 f8 f2 00 00
=>0x10007fff6eb0: f2 f2 00 f2 f1 f1 f1 f1 00 00 f2 f2[f2]f2 f2 f2
  0x10007fff6ec0: 00 00 f2 f2 00 f2 f2 f2 01 f2 f8 f3 00 00 f2 f2
  0x10007fff6ed0: 00 00 f2 f2 00 f2 f2 f2 f8 f2 f8 f2 00 00 f2 f2
  0x10007fff6ee0: 00 00 f2 f2 00 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2
  0x10007fff6ef0: f8 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f2 00 00 f2 f2
      0x10007fff6f00: 00 00 f2 f2 00 00 f3 f3 00 00 00 00 ca ca ca ca
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
TIMELINE

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

Credit

Discovered by Lilith >_> of Cisco Talos.