CVE-2022-43601,CVE-2022-43600,CVE-2022-43599,CVE-2022-43602
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.
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
OpenImageIO - https://github.com/OpenImageIO/oiio
8.1 - CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-122 - Heap-based Buffer Overflow
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.
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.
=================================================================
==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
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;
}
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;
}
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;
}
2022-11-14 - Vendor Disclosure
2022-12-03 - Vendor Patch Release
2022-12-22 - Public Release
Discovered by Lilith >_> of Cisco Talos.