Talos Vulnerability Report

TALOS-2022-1635

OpenImageIO DDS native tile reading denial of service vulnerability

December 22, 2022
CVE Number

CVE-2022-41999

SUMMARY

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.

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
OpenImageIO Project OpenImageIO v2.4.4.2

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

CWE

CWE-476 - NULL Pointer Dereference

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.

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;
    }

Crash Information

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 ()
***********************************************************************************
TIMELINE

2022-10-19 - Initial Vendor Contact
2022-10-20 - Vendor Disclosure
2022-12-22 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.