CVE-2022-41999
A denial of service vulnerability exists in the DDS native tile reading functionality of OpenImageIO Project OpenImageIO v2.3.19.0 and v2.4.4.2. A specially-crafted .dds can lead to denial of service. 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 Project OpenImageIO v2.4.4.2
OpenImageIO - https://github.com/OpenImageIO/oiio
7.5 - CVSS:3.0/AV:N/AC:L/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 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.
The DirectDraw Surface file format (.dds) is another one of the file formats that libOpenImageIO can handle. It’s primarily used for DirectX and can contain a large number of textures, both compressed and uncompressed. When using libOpenImageIO to interact with .dds files, the same basic work flow occurs, in that we create a generic ImageInput object and then call ReadImage()
on our input file. After a certain amount of generic ImageInput
files, we end up hitting the DDSInput
object’s more specific handlers, so let us take a quick look at DDSInput::read_native_tile
:
bool
DDSInput::read_native_tile(int subimage, int miplevel, int x, int y, int z,
void* data)
{
lock_guard lock(*this);
if (!seek_subimage(subimage, miplevel))
return false;
// static ints to keep track of the current cube face and re-seek and
// re-read face
static int lastx = -1, lasty = -1, lastz = -1;
// don't proceed if not a cube map - use scanlines then instead
if (!(m_dds.caps.flags2 & DDS_CAPS2_CUBEMAP))
return false;
// make sure we get the right dimensions
if (x % m_spec.tile_width || y % m_spec.tile_height
|| z % m_spec.tile_width)
return false;
if (m_buf.empty() || x != lastx || y != lasty || z != lastz) { // [1]
lastx = x;
lasty = y;
lastz = z;
unsigned int w = 0, h = 0, d = 0;
#ifdef DDS_3X2_CUBE_MAP_LAYOUT
internal_seek_subimage(((x / m_spec.tile_width) << 1)
+ y / m_spec.tile_height,
m_miplevel, w, h, d);
#else // 1x6 layout
internal_seek_subimage(y / m_spec.tile_height, m_miplevel, w, h, d);
#endif // DDS_3X2_CUBE_MAP_LAYOUT
if (!w && !h && !d)
// face not present in file, black-pad the image
memset(&m_buf[0], 0, m_spec.tile_bytes()); // [2]
else
readimg_tiles();
}
memcpy(data, &m_buf[0], m_spec.tile_bytes()); // [3]
return true;
}
Over all, the function is supposed to copy the bytes out of m_buf
into the void *data
at [3]. But if the m_buf
vector (which is a DDSInput object variable) is empty [1] and no subimages can be found, then the vector is cleared out at [2]. But interestingly, let us quickly look at where the m_buf
vector gets populated:
bool
DDSInput::readimg_scanlines()
{
//std::cerr << "[dds] readimg: " << ftell() << "\n";
// resize destination buffer
m_buf.resize(m_spec.scanline_bytes() * m_spec.height * m_spec.depth
/*/ (1 << m_miplevel)*/);
return internal_readimg(&m_buf[0], m_spec.width, m_spec.height,
m_spec.depth);
}
bool
DDSInput::readimg_tiles()
{
// resize destination buffer
m_buf.resize(m_spec.tile_bytes());
return internal_readimg(&m_buf[0], m_spec.tile_width, m_spec.tile_height,
m_spec.tile_depth);
}
As we can see, both the DDSInput::readimg_scanlines()
and DDSInput::readimg_tiles()
functions resize the m_buf
and read in data, but let’s quickly look at DDSInput::read_native_scanline
so we can compare it with the above listed DDSInput::read_native_tile
:
bool
DDSInput::read_native_scanline(int subimage, int miplevel, int y, int z,
void* data)
{
lock_guard lock(*this);
if (!seek_subimage(subimage, miplevel))
return false;
// don't proceed if a cube map - use tiles then instead
if (m_dds.caps.flags2 & DDS_CAPS2_CUBEMAP)
return false;
if (m_buf.empty()) // [4]
readimg_scanlines();
size_t size = spec().scanline_bytes();
memcpy(data, &m_buf[0] + z * m_spec.height * size + y * size, size);
return true;
}
At [4], we clearly see that if m_buf
is empty, then the readimg_scanlines
function is called to correctly insert data into the m_buf
vector before we use it. So let’s look again at how DDSInput::read_native_tiles
behaves if m_buf
is empty:
bool
DDSInput::read_native_tile(int subimage, int miplevel, int x, int y, int z,
void* data)
{
// [...]
if (m_buf.empty() || x != lastx || y != lasty || z != lastz) { // [5]
lastx = x;
lasty = y;
lastz = z;
unsigned int w = 0, h = 0, d = 0;
#ifdef DDS_3X2_CUBE_MAP_LAYOUT
internal_seek_subimage(((x / m_spec.tile_width) << 1)
+ y / m_spec.tile_height,
m_miplevel, w, h, d);
#else // 1x6 layout
internal_seek_subimage(y / m_spec.tile_height, m_miplevel, w, h, d);
#endif // DDS_3X2_CUBE_MAP_LAYOUT
if (!w && !h && !d) // [6]
// face not present in file, black-pad the image
memset(&m_buf[0], 0, m_spec.tile_bytes()); // [7]
else
readimg_tiles();
We enter the branch at [5] if m_buf
is empty, then the code attempts to populate the w
, h
and d
variables inside of internal_seek_subimage
. If this is unsuccessful, we call memset
on the m_buf
vector, which, if we’ll remember, has not been populated yet, resulting in a write to the null page and a quick denial of service [7]. As for how we get to this particular code path, it just seems like we need the image to have a spec.tile_width
, such that we enter the tiled-image codepaths. This is done via the initial opening of the .dds file, in which the first 0x80 bytes are read directly into a dds_header
struct:
/// DDS file header.
typedef struct {
uint32_t fourCC; ///< file four-character code
uint32_t size; ///< structure size, must be 124
uint32_t flags; ///< flags to indicate valid fields
uint32_t height; ///< image height
uint32_t width; ///< image width
uint32_t pitch; ///< bytes per scanline (uncmp.)/total byte size (cmp.)
uint32_t depth; ///< image depth (for 3D textures)
uint32_t mipmaps; ///< number of mipmaps
uint32_t unused0[11];
dds_pixformat fmt; ///< pixel format
dds_caps caps; ///< DirectDraw Surface caps
uint32_t unused1;
} dds_header;
Subsequently the width
is copied into the m_spec.tile_width
inside DDSInput::seek_subimage
:
bool
DDSInput::seek_subimage(int subimage, int miplevel)
{
// [...]
m_spec.tile_width = m_spec.full_width = w;
m_spec.tile_height = m_spec.full_height = h;
m_spec.tile_depth = m_spec.full_depth = d;
The second requirement to hit the memset
is the branch at [6], but this seems easily doable as long as our dds_header.caps.flags2
(offset 0x70 in file, little endian) are correctly set:
// NOTE: This function has no sanity checks! It's a private method and relies
// on the input being correct and valid!
void
DDSInput::internal_seek_subimage(int cubeface, int miplevel, unsigned int& w,
unsigned int& h, unsigned int& d)
{
// early out for cubemaps that don't contain the requested face
if (m_dds.caps.flags2 & DDS_CAPS2_CUBEMAP
&& !(m_dds.caps.flags2 & (DDS_CAPS2_CUBEMAP_POSITIVEX << cubeface))) {
w = h = d = 0;
return;
}
Running: ./mini_crash.dds
/usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:1046:9: runtime error: reference binding to null pointer of type 'unsigned char'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:1046:9 in
/oiio/oiio-2.4.4.2/src/dds.imageio/ddsinput.cpp:870:20: runtime error: null pointer passed as argument 1, which is declared to never be null
/usr/include/string.h:61:62: note: nonnull attribute specified here
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /oiio/oiio-2.4.4.2/src/dds.imageio/ddsinput.cpp:870:20 in
Thread 1 "fuzz_oiio.bin" received signal SIGSEGV, Segmentation fault.
__memset_evex_unaligned_erms () at ../sysdeps/x86_64/multiarch/memset-vec-unaligned-erms.S:283
283 ../sysdeps/x86_64/multiarch/memset-vec-unaligned-erms.S: No such file or directory.
[^_^] SIGSEGV
***********************************************************************************
***********************************************************************************
rax : 0x0 | rip[L] : 0x7fffec45bbce <__memset_evex_unali
rbx : 0x0 | eflags : 0x10283
rcx : 0x0 | cs : 0x33
rdx : 0x80 | ss : 0x2b
rsi : 0x0 | ds : 0x0
rdi : 0x0 | es : 0x0
rbp[S] : 0x7fffffffa430 | fs : 0x0
rsp[S] : 0x7fffffff9bf8 | gs : 0x0
r8 : 0x7fff8010 | k0 : 0xffff0000
r9 : 0x6 | k1 : 0xffff
r10 : 0xf | k2 : 0x7
r11 : 0x206 | k3 : 0x0
r12 : 0x617000000e80 | k4 : 0x0
r13 : 0x1 | k5 : 0x0
r14 : 0x0 | k6 : 0x0
r15 : 0x80 | k7 : 0x0
***********************************************************************************
0x7fffec45bbbe <__memset_evex_unaligned_erms+126>: rol bl,1
0x7fffec45bbc0 <__memset_evex_unaligned_erms+128>: cmp rdx,QWORD PTR [rip+0x69829] # 0x7fffec4c53f0 <__x86_rep_stosb_threshold>
0x7fffec45bbc7 <__memset_evex_unaligned_erms+135>: ja 0x7fffec45bbb0 <__memset_evex_unaligned_erms+112>
0x7fffec45bbc9 <__memset_evex_unaligned_erms+137>: lea rcx,[rdi+rdx*1-0x80]
=> 0x7fffec45bbce <__memset_evex_unaligned_erms+142>: vmovdqu64 YMMWORD PTR [rax],ymm16
0x7fffec45bbd4 <__memset_evex_unaligned_erms+148>: vmovdqu64 YMMWORD PTR [rax+0x20],ymm16
0x7fffec45bbdb <__memset_evex_unaligned_erms+155>: cmp rdx,0x80
0x7fffec45bbe2 <__memset_evex_unaligned_erms+162>: jbe 0x7fffec45bb70 <__memset_evex_unaligned_erms+48>
0x7fffec45bbe4 <__memset_evex_unaligned_erms+164>: vmovdqu64 YMMWORD PTR [rax+0x40],ymm16
***********************************************************************************
#0 __memset_evex_unaligned_erms () at ../sysdeps/x86_64/multiarch/memset-vec-unaligned-erms.S:283
#1 0x00005555556311c4 in __asan_memset ()
#2 0x00007ffff3792c20 in OpenImageIO_v2_4::DDSInput::read_native_tile (this=0x614000000c40, subimage=0, miplevel=0, x=0, y=0, z=0, data=0x60c000000b80) at/ddsinput.cpp:870
#3 0x00007ffff2d3f167 in OpenImageIO_v2_4::ImageInput::read_native_tiles (this=0x614000000c40, subimage=0, miplevel=0, xbegin=0, xend=16, ybegin=0, yend=8, zbegin=0, zend=1, data=0x617000000e80) at/imageinput.cpp:774
#4 0x00007ffff2d37840 in OpenImageIO_v2_4::ImageInput::read_tiles (this=0x614000000c40, subimage=0, miplevel=0, xbegin=0, xend=16, ybegin=0, yend=8, zbegin=0, zend=1, chbegin=0, chend=1, format=..., data=0x617000000e80, xstride=1, ystride=16, zstride=768) at/imageinput.cpp:620
#5 0x00007ffff2d47951 in OpenImageIO_v2_4::ImageInput::read_image (this=0x614000000c40, subimage=0, miplevel=0, chbegin=0, chend=1, format=..., data=0x617000000e80, xstride=1, ystride=16, zstride=768, progress_callback=0x0, progress_callback_data=0x0) at/imageinput.cpp:941
#6 0x000055555566fa76 in LLVMFuzzerTestOneInput (Data=0x60d000000110 "DDS |", Size=133) at/oiio_harness.cpp:90
#7 0x00005555555954e4 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) ()
#8 0x000055555557f260 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) ()
#9 0x0000555555584fb7 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) ()
#10 0x00005555555aedd3 in main ()
***********************************************************************************
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.