Talos Vulnerability Report

TALOS-2022-1631

OpenImageIO TIFF file IPTC data information disclosure vulnerability

December 22, 2022
CVE Number

CVE-2022-41649

SUMMARY

A heap out of bounds read vulnerability exists in the handling of IPTC data while parsing TIFF images in OpenImageIO v2.3.19.0. A specially-crafted TIFF file can cause a read of adjacent heap memory, which can leak sensitive process information. 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.3.19.0

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:H/I:N/A:N

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.

With OpenImageIO, handling of different file formats is a relatively simple matter. auto inp = ImageInput::open(filename) takes care of all the intricate details of figuring out which file format we’re dealing with and redirects the codeflow to the appropriate file handlers. For the TIFF file format, we first end up hitting the TIFFOpen function inside of the opensource LibTIFF library, which provides a parsed TIFF object to OpenImageIO inside of TIFFInput:seek_subimage:

bool
TIFFInput::seek_subimage(int subimage, int miplevel)
{
 // [...]
    if (!m_tif) {
        if (ioproxy_opened()) {
            static_assert(sizeof(thandle_t) == sizeof(void*),
                          "thandle_t must be same size as void*");
            // Strutil::print("\n\nOpening client \"{}\"\n", m_filename);
            ioseek(0);
            m_tif = TIFFClientOpen(m_filename.c_str(), "rm", ioproxy(),
                                   reader_readproc, reader_writeproc,
                                   reader_seekproc, reader_closeproc,
                                   reader_sizeproc, reader_mapproc,
                                   reader_unmapproc);
        } else {
#ifdef _WIN32
            std::wstring wfilename = Strutil::utf8_to_utf16wstring(m_filename);
            m_tif                  = TIFFOpenW(wfilename.c_str(), "rm");
#else
            m_tif = TIFFOpen(m_filename.c_str(), "rm");  // [1]
#endif
        }

We first hit the code path at [1] since we don’t have a m_tif object yet, which eventually gets us to the lengthy LibTIFF file parsing inside TIFFReadDirectory. For brevity’s sake, instead of going through the code we will just examine a sample hexdump to explain the file format:

00000000   49 49 2A 00  08 00 00 00  18 00 00 01  03 00 01 00  II*............. // [2]
00000010   00 00 80 00  00 00 01 01  03 00 01 00  00 00 00 02  ................
00000020   00 00 02 01  03 00 03 00  00 00 2E 01  00 00 03 01  ................
00000030   03 00 01 00  00 00 08 00  00 00 06 01  03 00 01 00  ................
00000040   00 00 02 00  00 00 12 01  03 00 01 00  00 00 01 00  ................
00000050   00 00 15 01  03 00 01 00  00 00 03 00  00 00 16 01  ................
00000060   03 00 01 00  00 00 20 00  00 00 1C 01  03 00 01 00  ...... .........
00000070   00 00 01 00  00 00 1E 01  05 00 01 00  00 00 34 01  ..............4.
00000080   00 00 1F 01  05 00 01 00  00 00 3C 01  00 00 31 01  ..........<...1.
00000090   02 00 23 00  00 00 44 01  00 00 32 01  02 00 14 00  ..#...D...2.....
000000A0   00 00 68 01  00 00 3D 01  03 00 01 00  00 00 02 00  ..h...=.........
000000B0   00 00 42 01  03 00 01 00  00 00 40 00  00 00 43 01  ..B.......@...C.
000000C0   03 00 01 00  00 00 40 00  00 00 44 01  04 00 10 00  ......@...D.....
000000D0   00 00 7C 01  00 00 45 01  04 00 10 00  00 00 BC 01  ..|...E.........
000000E0   00 00 53 01  03 00 03 00  00 00 FC 01  00 00 BC 02  ..S.............
000000F0   01 00 24 02  00 00 02 02  00 00 16 82  02 00 0E 00  ..$.............
00000100   00 00 26 04  00 00 17 82  02 00 0C 00  00 00 34 04  ..&...........4.
00000110   00 00 18 82  0B 00 01 00  00 00 00 00  80 3E BB 83  .............>..
00000120   04 00 0A 00  00 00 40 04  00 00 F8 09  00 00 08 00  ......@.........


#define TIFF_BIGENDIAN      0x4d4d
#define TIFF_LITTLEENDIAN   0x4949
#define MDI_LITTLEENDIAN    0x5045
#define MDI_BIGENDIAN       0x4550


typedef struct {
    uint16_t tiff_magic;      /* magic number (defines byte order) */
    uint16_t tiff_version;    /* TIFF version number */
    uint32_t tiff_diroff;     /* byte offset to first directory */
} TIFFHeaderClassic; // [3]

typedef struct {
    uint16_t tiff_magic;      /* magic number (defines byte order) */
    uint16_t tiff_version;    /* TIFF version number */
    uint16_t tiff_offsetsize; /* size of offsets, should be 8 */
    uint16_t tiff_unused;     /* unused word, should be 0 */
    uint64_t tiff_diroff;     /* byte offset to first directory */
} TIFFHeaderBig;     // [4]

Since our first 2 bytes match the TIFF_LITTLEENDIAN header of 0x4949 [2], we deal with the TiffHeaderClassic[3] instead of the TIFFHeaderBig [4]. Thus, the uint32_t at offset 0x4 is our tiff_diroff, i.e. the offset at which we find our tiff directories (0x8). TIFFReadDirectory then goes to that offset and reads in the number of directories we have. In this case, at offset 0x8 at [2], we can see that our uint16_t dircount is 0x18. Since we are not dealing with a TIFFHeaderBig, each of these “directories” is 0xC bytes long and looks as such:

[-.-]> ptype TIFFDirEntry
type = struct TIFFDirEntry {
    uint16_t tdir_tag;
    uint16_t tdir_type;
    uint32_t tdir_count;
    uint32_t tdir_offset;
}

As such, TiffReadDirectory reads in the dircount16 * dirsize bytes immediately following our dircount. In our case, this would be 0x18 * 0xc, resulting in 0x120 bytes being read. In the hexdump above, this would mean that our directories come directly from the bytes at offset (0xA, 0x12A). For example, if we look at the directories at offset 0x8E get parsed, we see the following:

00000080                                             31 01  |..........<...1.|
00000090  02 00 23 00 00 00 44 01  00 00 // start of second directory
                                         32 01 02 00 14 00  |..#...D...2.....|
000000a0  00 00 68 01 00 00

[^~^]> p/x *direntry
$45 = {tdir_tag = 0x131, tdir_type = 0x2, tdir_count = 0x23, tdir_offset = {toff_short = 0x144, toff_long = 0x144, toff_long8 = 0x144}, tdir_ignore = 0x0}

[^.^]> p/x *(direntry+1)
$46 = {tdir_tag = 0x132, tdir_type = 0x2, tdir_count = 0x14, tdir_offset = {toff_short = 0x168, toff_long = 0x168, toff_long8 = 0x168}, tdir_ignore = 0x0}

The tdir_tag field is used to sort the array of directories, along with identifying the data. The tdir_type field designates the data’s type, and the tdir_count designates the length of the data. Finally, the tdir_offset field points to the offset inside the TIFF file where we can actually find the data. So when LibOpenImageIO wants to find the data for a given tag, it first consults the hardcoded LibTiff array of possible field types and then binary searches through the directories that it has loaded in from the file. A sample of predefined LibTiff fields is given below:

static const TIFFField
tiffFields[] = {
    { TIFFTAG_SUBFILETYPE, 1, 1, TIFF_LONG, 0, TIFF_SETGET_UINT32, TIFF_SETGET_UNDEFINED, FIELD_SUBFILETYPE, 1, 0, "SubfileType", NULL },
    { TIFFTAG_OSUBFILETYPE, 1, 1, TIFF_SHORT, 0, TIFF_SETGET_UNDEFINED, TIFF_SETGET_UNDEFINED, FIELD_SUBFILETYPE, 1, 0, "OldSubfileType", NULL },
    { TIFFTAG_IMAGEWIDTH, 1, 1, TIFF_LONG, 0, TIFF_SETGET_UINT32, TIFF_SETGET_UNDEFINED, FIELD_IMAGEDIMENSIONS, 0, 0, "ImageWidth", NULL },
    { TIFFTAG_IMAGELENGTH, 1, 1, TIFF_LONG, 0, TIFF_SETGET_UINT32, TIFF_SETGET_UNDEFINED, FIELD_IMAGEDIMENSIONS, 1, 0, "ImageLength", NULL },
    // [...]
}    

These directories are for the most part utilized inside of the void TIFFInput::readspec(bool read_meta) function, which calls the various TIFFGetField function to grab the needed data from each directory. In the most basic case that looks like so:

void
TIFFInput::readspec(bool read_meta)
{
    uint32_t width = 0, height = 0, depth = 0;
    TIFFGetField(m_tif, TIFFTAG_IMAGEWIDTH, &width);
    TIFFGetField(m_tif, TIFFTAG_IMAGELENGTH, &height);
    TIFFGetFieldDefaulted(m_tif, TIFFTAG_IMAGEDEPTH, &depth);
    TIFFGetFieldDefaulted(m_tif, TIFFTAG_SAMPLESPERPIXEL, &m_inputchannels);
    // [...] 

Much further down inside of void TIFFInput::readspec(bool read_meta) we see a parsing of the TIFFTAG_RICHTIFFIPTC directory, where the tag is 0x83bb:

    int iptcsize         = 0;
    const void* iptcdata = NULL;
    if (TIFFGetField(m_tif, TIFFTAG_RICHTIFFIPTC, &iptcsize, &iptcdata)) { // [3]
        std::vector<uint32_t> iptc((uint32_t*)iptcdata,   // [4]
                                   (uint32_t*)iptcdata + iptcsize);
        if (TIFFIsByteSwapped(m_tif))
            TIFFSwabArrayOfLong((uint32_t*)&iptc[0], iptcsize);
        decode_iptc_iim(&iptc[0], iptcsize * 4, m_spec);
    }

The data is pulled from the directory and thrown into the iptcsize and iptcdata variables at [3], and then a uint32_t vector is created with that data at [4] via the (start_ptr, end_ptr ) initializer. Now, quickly going back to our input hexdump, let’s find what the TIFFTAG_RICHTIFFIPTC looks like:

#define TIFFTAG_RICHTIFFIPTC        33723 (0x83bb)

0000020a    BB 83  04 00 0A 00  00 00 40 04 // [5]
// [...]

As shown above at [5], we see the TIFFTAG_RICHTIFFIPTC tag at offset 0x20a, followed by the tdir_type, tdir_count and tdir_offset. Thus, the type of the data is TIFF_LONG. There are 0xa entries, and the data starts at offset 0x440. So if we look at offset 0x440, we can see the following:

00000440   59 00 00 00  59 00 00 00  59 00 00 00  59 00 00 00  59 00 00 00  59 00 00 00  Y...Y...Y...Y...Y...Y...
00000458   59 00 00 00  59 00 00 00  59 00 00 00  59 00 00 00  59 00 00 00  59 00 00 00  Y...Y...Y...Y...Y...Y...
00000470   59 00 00 00  59 00 00 00  01 00 01 00  01 00 3C 3F  78 70 61 63  6B 65 74 20  Y...Y.........

This sample data corresponds to what we see in a debugger inside of iptcdata and iptcsize after TIFFGetField [3] finishes:

[x.x]> x/1s iptcdata
0x602000000270: "YYYYYYYYYY"

[^~^]> p/x iptcsize
$8 = 0xa

Thus, let us look back at the line at [4]:

         std::vector<uint32_t> iptc((uint32_t*)iptcdata,   // [4]
                                   (uint32_t*)iptcdata + iptcsize);

Since iptcdata is already converted into a char * array by LibTiff, and since it gets cast into a uint32_t array in this line, the amount of data read into our resultant iptc is actually four times the size of our actual iptcdata (i.e. (sizeof(uint32_t) * iptcsize)), resulting in an out-of-bounds read on the heap. This data subsequently can be stored into the m_spec itself and potentially exported once this data gets converted or utilized in some manner by arbitrary functions within the libOpenImageIO codebase. Combined with another vulnerability, it could be used as an information leak exploit component used to bypass mitigations.

It’s worth noting that this vulnerability has been fixed within the current master branch (Commit ID 9aeec7a).

Crash Information

INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2162285448
INFO: Loaded 3 modules   (1146234 inline 8-bit counters): 102986 [0x7fcb540eb0c0, 0x7fcb5410430a), 1043099 [0x7fcb62363f60, 0x7fcb624629fb), 149 [0x563a116d4108, 0x563a116d419d), 
INFO: Loaded 3 PC tables (1146234 PCs): 102986 [0x7fcb54104310,0x7fcb542967b0), 1043099 [0x7fcb62462a00,0x7fcb6344d3b0), 149 [0x563a116d41a0,0x563a116d4af0), 
./fuzz_oiio.bin: Running 1 inputs 1 time(s) each.
Running: ./crash-b46c6897ad2ec6279f7b8f14fa77bb964a85e97c
/oiio/oiio-2.3.19.0/src/libutil/stb_sprintf.h:427:17: runtime error: store to misaligned address 0x7ffdd27734a3 for type 'unsigned int', which requires 4 byte alignment
0x7ffdd27734a3: note: pointer points here
 6d  61 74 22 d1 fd 7f 00 00  58 35 77 d2 fd 7f 00 00  00 00 00 00 00 00 00 00  70 12 00 00 60 61 00
              ^ 
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /oiio/oiio-2.3.19.0/src/libutil/stb_sprintf.h:427:17 in 
=================================================================
==273892==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000027a at pc 0x563a1164f2df bp 0x7ffdd276fc80 sp 0x7ffdd276f450
READ of size 40 at 0x60200000027a thread T0
    #0 0x563a1164f2de in __asan_memmove (/oiio/fuzzing/fuzz_oiio.bin+0xdd2de) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #1 0x7fcb58b8b286 in unsigned int* std::__copy_move<false, true, std::random_access_iterator_tag>::__copy_m<unsigned int>(unsigned int const*, unsigned int const*, unsigned int*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:431:6
    #2 0x7fcb58b8b19c in unsigned int* std::__copy_move_a2<false, unsigned int*, unsigned int*>(unsigned int*, unsigned int*, unsigned int*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:494:14
    #3 0x7fcb58b8b0dc in unsigned int* std::__copy_move_a1<false, unsigned int*, unsigned int*>(unsigned int*, unsigned int*, unsigned int*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:522:14
    #4 0x7fcb58b8af93 in unsigned int* std::__copy_move_a<false, unsigned int*, unsigned int*>(unsigned int*, unsigned int*, unsigned int*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:530:3
    #5 0x7fcb58b8ade1 in unsigned int* std::copy<unsigned int*, unsigned int*>(unsigned int*, unsigned int*, unsigned int*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_algobase.h:619:14
    #6 0x7fcb58b8ad3c in unsigned int* std::__uninitialized_copy<true>::__uninit_copy<unsigned int*, unsigned int*>(unsigned int*, unsigned int*, unsigned int*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_uninitialized.h:110:18
    #7 0x7fcb58b8a740 in unsigned int* std::uninitialized_copy<unsigned int*, unsigned int*>(unsigned int*, unsigned int*, unsigned int*) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_uninitialized.h:148:14
    #8 0x7fcb5d2ff5e8 in unsigned int* std::__uninitialized_copy_a<unsigned int*, unsigned int*, unsigned int>(unsigned int*, unsigned int*, unsigned int*, std::allocator<unsigned int>&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_uninitialized.h:333:14
    #9 0x7fcb5ee67f7d in void std::vector<unsigned int, std::allocator<unsigned int> >::_M_range_initialize<unsigned int*>(unsigned int*, unsigned int*, std::forward_iterator_tag) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:1585:6
    #10 0x7fcb5ee16356 in std::vector<unsigned int, std::allocator<unsigned int> >::vector<unsigned int*, void>(unsigned int*, unsigned int*, std::allocator<unsigned int> const&) /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:657:4
    #11 0x7fcb5ee03623 in OpenImageIO_v2_3::TIFFInput::readspec(bool) /oiio/oiio-2.3.19.0/src/tiff.imageio/tiffinput.cpp:1234:31
    #12 0x7fcb5ede93cb in OpenImageIO_v2_3::TIFFInput::seek_subimage(int, int) /oiio/oiio-2.3.19.0/src/tiff.imageio/tiffinput.cpp:775:9
    #13 0x7fcb5ede6b38 in OpenImageIO_v2_3::TIFFInput::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/tiff.imageio/tiffinput.cpp:687:15
    #14 0x7fcb5edee11c in OpenImageIO_v2_3::TIFFInput::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/tiff.imageio/tiffinput.cpp:713:12
    #15 0x7fcb5dcf4659 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
    #16 0x7fcb5dbd3579 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
    #17 0x563a1168d40f in LLVMFuzzerTestOneInput /oiio/fuzzing/./oiio_harness.cpp:77:16
    #18 0x563a115b34e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing/fuzz_oiio.bin+0x414e3) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #19 0x563a1159d25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing/fuzz_oiio.bin+0x2b25f) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #20 0x563a115a2fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing/fuzz_oiio.bin+0x30fb6) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #21 0x563a115ccdd2 in main (/oiio/fuzzing/fuzz_oiio.bin+0x5add2) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #22 0x7fcb54307d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #23 0x7fcb54307e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #24 0x563a11597b24 in _start (/oiio/fuzzing/fuzz_oiio.bin+0x25b24) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)

0x60200000027a is located 0 bytes to the right of 10-byte region [0x602000000270,0x60200000027a)
allocated by thread T0 here:
    #0 0x563a1164ff86 in __interceptor_realloc (/oiio/fuzzing/fuzz_oiio.bin+0xddf86) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25)
    #1 0x7fcb528694e7 in _TIFFCheckRealloc (/lib/x86_64-linux-gnu/libtiff.so.5+0xa4e7) (BuildId: 387930755156c8b853ab7ea3e516e1f3bf49b761)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/oiio/fuzzing/fuzz_oiio.bin+0xdd2de) (BuildId: e9d97e110da8ca7129ca0569fb37dfa703dccc25) in __asan_memmove
Shadow bytes around the buggy address:
  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff8000: fa fa fd fa fa fa fd fd fa fa fd fa fa fa fd fa
  0x0c047fff8010: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
  0x0c047fff8020: fa fa fd fd fa fa fd fd fa fa 00 00 fa fa fd fa
  0x0c047fff8030: fa fa fd fa fa fa fd fd fa fa 01 fa fa fa fd fd
=>0x0c047fff8040: fa fa 03 fa fa fa 04 fa fa fa fd fd fa fa 00[02]
  0x0c047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8090: 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
==273892==ABORTING
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.