CVE-2022-41649
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.
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
OpenImageIO - https://github.com/OpenImageIO/oiio
7.5 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
CWE-125 - Out-of-bounds Read
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).
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
2022-10-19 - Initial Vendor Contact
2022-10-20 - Vendor Disclosure
2022-11-01 - Vendor Patch Release
2022-12-22 - Public Release
Discovered by Lilith >_> of Cisco Talos.