CVE-2022-43593
A denial of service vulnerability exists in the DPXOutput::close() functionality of OpenImageIO Project OpenImageIO v2.4.4.2. A specially crafted ImageOutput Object can lead to null pointer dereference. An attacker can provide malicious input 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.4.4.2
OpenImageIO - https://github.com/OpenImageIO/oiio
5.9 - CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-476 - NULL Pointer Dereference
OpenImageIO is an image processing library with easy to use interfaces and a sizable amount 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) and is also used by Blender for reading Photoshop .psd files.
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], 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 hitting the functions to output a .dpx
file, a curious code flow can occur upon hitting the necessary out->close()
at [7]:
bool
DPXOutput::close()
{
if (!m_stream) { // already closed
init();
return true;
}
bool ok = true;
if (m_spec.tile_width) {
// Handle tile emulation -- output the buffered pixels
OIIO_DASSERT(m_tilebuffer.size());
ok &= write_scanlines(m_spec.y, m_spec.y + m_spec.height, 0, // [8]
m_spec.format, &m_tilebuffer[0]);
std::vector<unsigned char>().swap(m_tilebuffer);
}
ok &= write_buffer();
m_dpx.Finish();
init(); // Reset to initial state
return ok;
}
Assuming that our output specification has a .tile_width
, we end up hitting the ImageOutput::write_scanlines
function at [8] such that our buffered pixels can actually be written to our output file:
bool
ImageOutput::write_scanlines(int ybegin, int yend, int z, TypeDesc format,
const void* data, stride_t xstride,
stride_t ystride)
{
// Default implementation: write each scanline individually
stride_t native_pixel_bytes = (stride_t)m_spec.pixel_bytes(true);
if (format == TypeDesc::UNKNOWN && xstride == AutoStride)
xstride = native_pixel_bytes;
stride_t zstride = AutoStride;
m_spec.auto_stride(xstride, ystride, zstride, format, m_spec.nchannels,
m_spec.width, yend - ybegin);
bool ok = true;
for (int y = ybegin; ok && y < yend; ++y) {
ok &= write_scanline(y, z, format, data, xstride); // [9]
data = (char*)data + ystride;
}
return ok;
}
Since the ImageOutput
class is generic, it must call into the more specific DpxOutput::write_scanline
function [9] to actually know how to write each scanline:
bool
DPXOutput::write_scanline(int y, int z, TypeDesc format, const void* data,
stride_t xstride)
{
m_write_pending = true; // [10]
m_spec.auto_stride(xstride, format, m_spec.nchannels);
const void* origdata = data;
data = to_native_scanline(format, data, xstride, m_scratch, m_dither, y, z);
if (data == origdata) {
m_scratch.assign((unsigned char*)data,
(unsigned char*)data + m_spec.scanline_bytes());
data = &m_scratch[0];
}
unsigned char* dst = &m_buf[(y - m_spec.y) * m_bytes];
if (m_rawcolor)
// fast path - just dump the scanline into the buffer
memcpy(dst, data, m_spec.scanline_bytes());
else if (!dpx::ConvertToNative(m_desc, m_datasize, m_cmetr, m_spec.width, 1,
data, dst))
return false;
return true;
}
We don’t particularly care about much in this function except that m_write_pending
is set to true immediately at [10], no matter what. Regardless of what else occurs, we eventually return back up to DPXoutput::close():
bool
DPXOutput::close()
{
if (!m_stream) { // already closed
init();
return true;
}
bool ok = true;
if (m_spec.tile_width) {
// Handle tile emulation -- output the buffered pixels
OIIO_DASSERT(m_tilebuffer.size());
ok &= write_scanlines(m_spec.y, m_spec.y + m_spec.height, 0, // [11]
m_spec.format, &m_tilebuffer[0]);
std::vector<unsigned char>().swap(m_tilebuffer);
}
ok &= write_buffer(); // [12]
m_dpx.Finish();
init(); // Reset to initial state
return ok;
}
With m_write_pending
set to true in [11], we then hit DPXOutput::writebuffer
:
bool
DPXOutput::write_buffer()
{
bool ok = true;
if (m_write_pending) {
ok = m_dpx.WriteElement(m_subimage, &m_buf[0], m_datasize); // [13]
if (!ok) {
const char* err = strerror(errno);
errorf("DPX write failed (%s)",
(err && err[0]) ? err : "unknown error");
}
m_write_pending = false;
}
return ok;
}
Without getting too deep into the libdpx code, it suffices to say that the m_buf
variable gets written inside m_dpx.WriteElement
[13]. There are never really any checks to make sure that m_buf
has been initialized. This ends up being our vulnerability, in that data gets written to a null dereferenced pointer. But how do we manage to get this far into the code without m_buf
ever being initialized? Normally the m_buf
is allocated inside DPXOutput::prep_subimage
, so let us briefly look at the function:
bool
DPXOutput::prep_subimage(int s, bool allocate)
{
m_spec = m_subimage_specs[s]; // stash the spec
// determine descriptor
m_desc = get_image_descriptor(); // [13]
// [...]
// check if the client is giving us raw data to write
m_rawcolor = m_spec.get_int_attribute("dpx:RawColor")
|| m_spec.get_int_attribute("dpx:RawData") // deprecated
|| m_spec.get_int_attribute("oiio:RawColor");
// see if we'll need to convert color space or not
if (m_desc == dpx::kRGB || m_desc == dpx::kRGBA || m_spec.nchannels == 1) {
// shortcut for RGB/RGBA, and for 1-channel images that don't
// need to decode color representations.
m_bytes = m_spec.scanline_bytes();
m_rawcolor = true;
} else {
m_bytes = dpx::QueryNativeBufferSize(m_desc, m_datasize, m_spec.width, // [14]
1);
if (m_bytes == 0 && !m_rawcolor) { // [15]
errorf("Unable to deliver native format data from source data");
return false;
} else if (m_bytes < 0) {
// no need to allocate another buffer
if (!m_rawcolor)
m_bytes = m_spec.scanline_bytes();
else
m_bytes = -m_bytes;
}
}
if (m_bytes < 0)
m_bytes = -m_bytes;
// allocate space for the image data buffer
if (allocate)
m_buf.resize(m_bytes * m_spec.height); // [16]
return true;
}
At [16], we clearly see the allocation we needed to hit to avoid the crash. However, there also exists an early exit from DPXOutput::prep_subimage
at [15], which is entirely dependent on the m_desc
variable. Since dpx::QueryNativeBufferSize
[14] essentially is a switch-case based on the input m_desc
, we also need to quickly look at how get_image_descriptor
behaves at [13]:
dpx::Descriptor
DPXOutput::get_image_descriptor()
{
switch (m_spec.nchannels) {
case 1: {
std::string name = m_spec.channelnames.size() ? m_spec.channelnames[0]
: "";
if (m_spec.z_channel == 0 || name == "Z")
return dpx::kDepth;
else if (m_spec.alpha_channel == 0 || name == "A")
return dpx::kAlpha;
else if (name == "R")
return dpx::kRed;
else if (name == "B")
return dpx::kBlue;
else if (name == "G")
return dpx::kGreen;
else
return dpx::kLuma;
}
case 3: return dpx::kRGB;
case 4: return dpx::kRGBA;
default:
if (m_spec.nchannels <= 8)
return (dpx::Descriptor)((int)dpx::kUserDefined2Comp // [17]
+ m_spec.nchannels - 2);
return dpx::kUndefinedDescriptor;
}
}
Given an m_spec.nchannels
of two or five through eight, we end up hitting an m_desc
of anywhere from 0x96 to 0x90 at [17], all of which cause QueryNativeBufferSize
to return 0, error out in DPXOutput::prep_subimage
and eventually reach the null pointer dereference when DPXOutput
is called either explicitly or during the DPXOutput
destructor.
***********************************************************************************
***********************************************************************************
rax : 0x62100140ed00 | rip[L] : 0x7fffeada8f4d <__memmove_evex_unal
rbx : 0x25800 | eflags : 0x10202
rcx : 0xc00 | cs : 0x33
rdx : 0x1000 | ss : 0x2b
rsi : 0x0 | ds : 0x0
rdi : 0x62100140dd00 | es : 0x0
rbp : 0x615000003f00 | fs : 0x0
rsp[S] : 0x7fffffffa948 | gs : 0x0
r8 : 0x78 | k0 : 0xfffffffe
r9 : 0xa0 | k1 : 0xffffff
r10 : 0x0 | k2 : 0xffffffff
r11 : 0x61d000a0f140 | k3 : 0x0
r12 : 0x1000 | k4 : 0x0
r13 : 0x1000 | k5 : 0x0
r14 : 0x0 | k6 : 0x0
r15 : 0x24800 | k7 : 0x0
***********************************************************************************
0x7fffeada8f40 <__memmove_evex_unaligned_erms>: endbr64
0x7fffeada8f44 <__memmove_evex_unaligned_erms+4>: mov rax,rdi
0x7fffeada8f47 <__memmove_evex_unaligned_erms+7>: cmp rdx,0x20
0x7fffeada8f4b <__memmove_evex_unaligned_erms+11>: jb 0x7fffeada8f80 <__memmove_evex_unaligned_erms+64>
=> 0x7fffeada8f4d <__memmove_evex_unaligned_erms+13>: vmovdqu64 ymm16,YMMWORD PTR [rsi]
0x7fffeada8f53 <__memmove_evex_unaligned_erms+19>: cmp rdx,0x40
0x7fffeada8f57 <__memmove_evex_unaligned_erms+23>: ja 0x7fffeada9000 <__memmove_evex_unaligned_erms+192>
0x7fffeada8f5d <__memmove_evex_unaligned_erms+29>: vmovdqu64 ymm17,YMMWORD PTR [rsi+rdx*1-0x20]
0x7fffeada8f65 <__memmove_evex_unaligned_erms+37>: vmovdqu64 YMMWORD PTR [rdi],ymm16
***********************************************************************************
#0 __memmove_evex_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:317
#1 0x00007fffeac856e5 in _IO_new_file_xsputn (n=153600, data=?, f=?) at ./libio/fileops.c:1235
#2 _IO_new_file_xsputn (f=0x615000003f00, data=?, n=153600) at ./libio/fileops.c:1196
#3 0x00007fffeac7a057 in __GI__IO_fwrite (buf=0x0, size=1, count=153600, fp=0x615000003f00) at/libioP.h:947
#4 0x00005555555a10c0 in fwrite ()
#5 0x00007fffebb8e703 in OpenImageIO_v2_4::Filesystem::IOFile::write (this=0x60e00003a7a0, buf=0x0, size=153600) at/filesystem.cpp:1198
#6 0x00007ffff3a6438a in OutStream::Write (this=0x602000003610, buf=0x0, size=153600) at/OutStream.cpp:60
#7 0x00007ffff3ae2208 in OutStream::WriteCheck (this=0x602000003610, buf=0x0, size=153600) at/DPXStream.h:186
#8 0x00007ffff3af2d05 in dpx::Writer::WriteThrough (this=0x61d000a0f140, data=0x0, width=160, height=120, noc=2, bytes=4, eolnPad=0, eoimPad=0, blank=0x0) at/Writer.cpp:407
#9 0x00007ffff3aec1cf in dpx::Writer::WriteElement (this=0x61d000a0f140, element=0, data=0x0, size=dpx::kFloat) at/Writer.cpp:298
#10 0x00007ffff3a34ecc in OpenImageIO_v2_4::DPXOutput::write_buffer (this=0x61d000a0f080) at/dpxoutput.cpp:571
#11 0x00007ffff3a1dfc8 in OpenImageIO_v2_4::DPXOutput::close (this=0x61d000a0f080) at/dpxoutput.cpp:601
#12 0x0000555555649a6e in convert_file (in_filename=Python Exception <class 'gdb.error'>: There is no member named _M_p.
, out_filename=Python Exception <class 'gdb.error'>: There is no member named _M_p.
) at ./iconvert.cpp:475
#13 0x00005555556453c9 in main (argc=3, argv=0x7fffffffe598) at ./iconvert.cpp:523
***********************************************************************************
2022-11-14 - Vendor Disclosure
2022-12-03 - Vendor Patch Release
2022-12-22 - Public Release
Discovered by Lilith >_> of Cisco Talos.