Talos Vulnerability Report

TALOS-2023-1708

OpenImageIO Project OpenImageIO TGAInput::decode_pixel() out-of-bounds read vulnerability

March 30, 2023
CVE Number

CVE-2023-22845

SUMMARY

An out-of-bounds read vulnerability exists in the TGAInput::decode_pixel() functionality of OpenImageIO Project OpenImageIO v2.4.7.1. A specially crafted targa file can lead to information disclosure. 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.4.7.1

PRODUCT URLS

OpenImageIO - https://github.com/OpenImageIO/oiio

CVSSv3 SCORE

7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

CWE

CWE-125 - Out-of-bounds Read

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.

When reading in Targa (.tga) files, the libOpenImageIO code flows are pretty quick and easy, matching the relative simpleness of the file format itself. Starting with TGAInput::open(const std::stirng &name, ImageSpec& newspec):

bool
TGAInput::open(const std::string& name, ImageSpec& newspec)
{
    m_filename = name;

    DBG("TGA opening {}\n", name);
    uint64_t filesize = Filesystem::file_size(name);
    m_file            = Filesystem::fopen(name, "rb");
    if (!m_file) {
        errorf("Could not open file \"%s\"", name);
        return false;
    }

    // Due to struct packing, we may get a corrupt header if we just load the
    // struct from file; to address that, read every member individually save
    // some typing. Byte swapping is done automatically. If any fail, the file
    // handle is closed and we return false from open().
    if (!(read(m_tga.idlen) && read(m_tga.cmap_type) && read(m_tga.type)
          && read(m_tga.cmap_first) && read(m_tga.cmap_length)
          && read(m_tga.cmap_size) && read(m_tga.x_origin)
          && read(m_tga.y_origin) && read(m_tga.width) && read(m_tga.height)
          && read(m_tga.bpp) && read(m_tga.attr))) {
        errorfmt("Could not read full header");
        return false;
    }

The file is opened and as shown above, and we start reading the appropriate fields into the following struct:

<(^.^)>#ptype m_tga
type = struct {
    uint8_t idlen;
    uint8_t cmap_type;
    uint8_t type;
    uint16_t cmap_first;
    uint16_t cmap_length;
    uint8_t cmap_size;
    uint16_t x_origin;
    uint16_t y_origin;
    uint16_t width;
    uint16_t height;
    uint8_t bpp1
    uint8_t attr;
}

Soon after this, we enter bool TGAInput::readimg(), to read in the actual image data that we’re here for, referencing the forementioned headers to format it appropriately:

bool TGAInput::readimg()
{
    // how many bytes we actually read
    // for 15-bit read 2 bytes and ignore the 16th bit
    int bytespp    = (m_tga.bpp == 15) ? 2 : (m_tga.bpp / 8);
    int palbytespp = (m_tga.cmap_size == 15) ? 2 : (m_tga.cmap_size / 8);  // [1]
    int alphabits  = m_tga.attr & 0x0F;
    if (alphabits == 0 && m_tga.bpp == 32)
        alphabits = 8;

/// [...] 
    // read palette, if there is any
    std::unique_ptr<unsigned char[]> palette;
    if (is_palette()) {
        palette.reset(new unsigned char[palbytespp * m_tga.cmap_length]);   // [2]
        if (!ioread(palette.get(), palbytespp, m_tga.cmap_length))          // [3]
            return false;
    }

    unsigned char pixel[4] = { 0, 0, 0, 0 };
    if (m_tga.type < TYPE_PALETTED_RLE) {
        // uncompressed image data
        DBG("TGA readimg, reading uncompressed image data\n");
        unsigned char in[4];
        for (int64_t y = m_spec.height - 1; y >= 0; y--) {
            for (int64_t x = 0; x < m_spec.width; x++) {
                if (!ioread(in, bytespp, 1))
                    return false;
                decode_pixel(in, pixel, palette.get(), bytespp, palbytespp);   // [4]
                memcpy(m_buf.get() + y * m_spec.width * m_spec.nchannels
                           + x * m_spec.nchannels,
                       pixel, m_spec.nchannels);
            }
        }
    } else {
        // Run Length Encoded image
        unsigned char in[5];
        DBG("TGA readimg, reading RLE image data\n");
        int packet_size;
        for (int64_t y = m_spec.height - 1; y >= 0; y--) {
            for (int64_t x = 0; x < m_spec.width; x++) {
                if (!ioread(in, 1 + bytespp, 1)) {
                    DBG("Failed on scanline {}\n", y);
                    return false;
                }
                packet_size = 1 + (in[0] & 0x7f);
                decode_pixel(&in[1], pixel, palette.get(), bytespp, palbytespp);   // [5]
                if (in[0] & 0x80) {  // run length packet
                    // DBG("[tga] run length packet size {} @ ({},{})\n",
                    //     packet_size, x, y);

At [1], our library determines the size of the int palbytespp variable, which it then uses at [2] to know how large of a temporary buffer to allocate. Immediately after, the correct amount of bytes is read into our palette struct, at [3]. Since image data typically ends up being compressed, we can then use this palette struct to generate our uncompressed data at [4] or [5], inside the decode_pixel function:

inline void
TGAInput::decode_pixel(unsigned char* in, unsigned char* out,
                       unsigned char* palette, int bytespp, int palbytespp)
{
    unsigned int k = 0;
    // I hate nested switches...
    switch (m_tga.type) {
    case TYPE_PALETTED:
    case TYPE_PALETTED_RLE:
        for (int i = 0; i < bytespp; ++i)
            k |= in[i] << (8 * i);  // Assemble it in little endian order
        k = (m_tga.cmap_first + k) * palbytespp;                         // [6] 
        switch (palbytespp) {
            case 2:
                // see the comment for 16bpp RGB below for an explanation of this
                out[0] = bit_range_convert<5, 8>((palette[k + 1] & 0x7C) >> 2);     // [7]
                out[1] = bit_range_convert<5, 8>(((palette[k + 0] & 0xE0) >> 5)
                                                 | ((palette[k + 1] & 0x03) << 3));
                out[2] = bit_range_convert<5, 8>(palette[k + 0] & 0x1F);
                break;
            case 3:
                out[0] = palette[k + 2];
                out[1] = palette[k + 1];
                out[2] = palette[k + 0];
                break;
            case 4:
                out[0] = palette[k + 2];
                out[1] = palette[k + 1];
                out[2] = palette[k + 0];
                out[3] = palette[k + 3];
                break;
        }
        break;
    // [...]

We first populate the k variable at [6] to know where our palette data starts within our targa image, and then we start to populate uncompressed data into our single pixel starting with the code at [7]. It is here we have hit our vulnerability, however, as an important fact has yet to be mentioned: our cmap_first variable, which comes from the initial targa headers, has no validation on it whatsoever. As such, there are no checks to see if cmap_first is even within the bounds of our image file, and we can end up reading out of bounds heap data for our palette, resulting in an information disclosure.

Crash Information

Running: crash-355fc8ab0e18a3024ad2775157eaff1dda2e0994
=================================================================
==530887==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61500000417d at pc 0x7ffff3c1139a bp 0x7fffffff8a50 sp 0x7fffffff8a48
READ of size 1 at 0x61500000417d thread T0
[Detaching after fork from child process 530919]
    #0 0x7ffff3c11399 in OpenImageIO_v2_4::TGAInput::decode_pixel(unsigned char*, unsigned char*, unsigned char*, int, int) /oiio/oiio-2.4.7.1/src/targa.imageio/targainput.cpp:574:47
    #1 0x7ffff3c19bae in OpenImageIO_v2_4::TGAInput::readimg() /oiio/oiio-2.4.7.1/src/targa.imageio/targainput.cpp:745:17
    #2 0x7ffff3c24f09 in OpenImageIO_v2_4::TGAInput::read_native_scanline(int, int, int, int, void*) /oiio/oiio-2.4.7.1/src/targa.imageio/targainput.cpp:881:14
    #3 0x7ffff2bd6fc8 in OpenImageIO_v2_4::ImageInput::read_native_scanlines(int, int, int, int, int, void*) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:399:19
    #4 0x7ffff2bd7cfd in OpenImageIO_v2_4::ImageInput::read_native_scanlines(int, int, int, int, int, int, int, void*) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:420:16
    #5 0x7ffff2bd3780 in OpenImageIO_v2_4::ImageInput::read_scanlines(int, int, int, int, int, int, int, OpenImageIO_v2_4::TypeDesc, void*, long, long) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:336:15
    #6 0x7ffff2bf57d9 in OpenImageIO_v2_4::ImageInput::read_image(int, int, int, int, OpenImageIO_v2_4::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:967:23
    #7 0x7ffff2bef13a in OpenImageIO_v2_4::ImageInput::read_image(OpenImageIO_v2_4::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:864:12
    #8 0x55555566fab4 in LLVMFuzzerTestOneInput /oiio/fuzzing/./oiio_harness.cpp:90:18
    #9 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing/fuzz_oiio.bin+0x414e3) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)
    #10 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing/fuzz_oiio.bin+0x2b25f) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)
    #11 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing/fuzz_oiio.bin+0x30fb6) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)
    #12 0x5555555aedd2 in main (/oiio/fuzzing/fuzz_oiio.bin+0x5add2) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)
    #13 0x7fffebfc9d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #14 0x7fffebfc9e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #15 0x555555579b24 in _start (/oiio/fuzzing/fuzz_oiio.bin+0x25b24) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)

0x61500000417d is located 125 bytes to the right of 512-byte region [0x615000003f00,0x615000004100)
allocated by thread T0 here:
    #0 0x55555566ca1d in operator new[](unsigned long) (/oiio/fuzzing/fuzz_oiio.bin+0x118a1d) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)
    #1 0x7ffff3c17256 in OpenImageIO_v2_4::TGAInput::readimg() /oiio/oiio-2.4.7.1/src/targa.imageio/targainput.cpp:713:23
    #2 0x7ffff3c24f09 in OpenImageIO_v2_4::TGAInput::read_native_scanline(int, int, int, int, void*) /oiio/oiio-2.4.7.1/src/targa.imageio/targainput.cpp:881:14
    #3 0x7ffff2bd6fc8 in OpenImageIO_v2_4::ImageInput::read_native_scanlines(int, int, int, int, int, void*) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:399:19
    #4 0x7ffff2bd7cfd in OpenImageIO_v2_4::ImageInput::read_native_scanlines(int, int, int, int, int, int, int, void*) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:420:16
    #5 0x7ffff2bd3780 in OpenImageIO_v2_4::ImageInput::read_scanlines(int, int, int, int, int, int, int, OpenImageIO_v2_4::TypeDesc, void*, long, long) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:336:15
    #6 0x7ffff2bf57d9 in OpenImageIO_v2_4::ImageInput::read_image(int, int, int, int, OpenImageIO_v2_4::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:967:23
    #7 0x7ffff2bef13a in OpenImageIO_v2_4::ImageInput::read_image(OpenImageIO_v2_4::TypeDesc, void*, long, long, long, bool (*)(void*, float), void*) /oiio/oiio-2.4.7.1/src/libOpenImageIO/imageinput.cpp:864:12
    #8 0x55555566fab4 in LLVMFuzzerTestOneInput /oiio/fuzzing/./oiio_harness.cpp:90:18
    #9 0x5555555954e3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/oiio/fuzzing/fuzz_oiio.bin+0x414e3) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)
    #10 0x55555557f25f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/oiio/fuzzing/fuzz_oiio.bin+0x2b25f) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)
    #11 0x555555584fb6 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/oiio/fuzzing/fuzz_oiio.bin+0x30fb6) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)
    #12 0x5555555aedd2 in main (/oiio/fuzzing/fuzz_oiio.bin+0x5add2) (BuildId: c47f4a88ed888e6af376abd149e3cfe6bd24ceea)
    #13 0x7fffebfc9d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16



SUMMARY: AddressSanitizer: heap-buffer-overflow /oiio/oiio-2.4.6.0/src/targa.imageio/targainput.cpp:574:47 in OpenImageIO_v2_4::TGAInput::decode_pixel(unsigned char*, unsigned char*, unsigned char*, int,
 int)
Shadow bytes around the buggy address:
  0x0c2a7fff87d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff87e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c2a7fff87f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c2a7fff8800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c2a7fff8810: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c2a7fff8820: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa[fa]
  0x0c2a7fff8830: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff8840: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff8850: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff8860: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c2a7fff8870: 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
==447210==ABORTING
[Thread 0x7fffe66f9640 (LWP 447213) exited]
[Inferior 1 (process 447210) exited with code 01]
TIMELINE

2023-02-07 - Vendor Disclosure
2023-02-13 - Vendor Patch Release
2023-03-30 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.