Talos Vulnerability Report

TALOS-2017-0409

Blender Sequencer dpxOpen Buffer Overflow Code Execution Vulnerability

January 11, 2018
CVE Number

CVE-2017-2902

Summary

An exploitable integer overflow exists in the DPX loading functionality of the Blender open-source 3d creation suite version 2.78c. A specially crafted .cin file can cause an integer overflow resulting in a buffer overflow which can allow for code execution under the context of the application. An attacker can convince a user to use the file as an asset via the sequencer in order to trigger this vulnerability.

Tested Versions

Blender v2.78c

Product URLs

http://www.blender.org git://git.blender.org/blender.git

CVSSv3 Score

8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-120 - Buffer Copy without Checking Size of Input (‘Classic Buffer Overflow’)

Details

Blender is a professional, open-source 3d computer graphics application. It is used for creating animated films, visual effects, art, 3d printed applications, and video games. It is also capable of doing minimalistic video editing and sequencing as needed by the user. There are various features that it provides which allow for a user to perform a multitude of actions as required by a particular project.

This vulnerability exists with how the Blender application loads a DPX file as a resource for the video sequencer. When allocating space for the image data within a .cin file, the application will perform some arithmetic which can overflow. This result will then be used to perform an allocation which can allow for an undersized buffer. Later when the application attempts to render the image data into this buffer, a heap-based buffer overflow will occur.

When loading an image file, the function IMB_loadiffname in the source/blender/imbuf/intern/readimage.c file will be called. Inside this function, the application will first open the file and then call the IMB_loadifffile function [2].

source/blender/imbuf/intern/readimage.c:212
ImBuf *IMB_loadiffname(const char *filepath, int flags, char colorspace[IM_MAX_SPACE])
{
...
    file = BLI_open(filepath_tx, O_BINARY | O_RDONLY, 0);                   // [1]
    if (file == -1)
        return NULL;

    ibuf = IMB_loadifffile(file, filepath, flags, colorspace, filepath_tx); // [2]

Inside the IMB_loadifffile function, the application will first map the whole file into memory using the mmap system-call [3]. After the file is successfully mapped into memory, the resulting pages will be passed to the IMB_ibImageFromMemory function [4]. This function is responsible for figuring out which file-format handlers to use, and then to call its respective loader.

source/blender/imbuf/intern/readimage.c:165
ImBuf *IMB_loadifffile(int file, const char *filepath, int flags, char colorspace[IM_MAX_SPACE], const char *descr)
{
...
    imb_mmap_lock();
    mem = mmap(NULL, size, PROT_READ, MAP_SHARED, file, 0);             // [3]
    imb_mmap_unlock();

    if (mem == (unsigned char *) -1) {
        fprintf(stderr, "%s: couldn't get mapping %s\n", __func__, descr);
        return NULL;
    }

    ibuf = IMB_ibImageFromMemory(mem, size, flags, colorspace, descr);  // [4]

Inside the following function, the application will iterate through a global list that contains different handlers for all of the image files that the application supports. At [5], the application will call the function responsible for loading the image out of memory.

source/blender/imbuf/intern/readimage.c:104
ImBuf *IMB_ibImageFromMemory(unsigned char *mem, size_t size, int flags, char colorspace[IM_MAX_SPACE], const char *descr)
{
...
    for (type = IMB_FILE_TYPES; type < IMB_FILE_TYPES_LAST; type++) {
        if (type->load) {
            ibuf = type->load(mem, size, flags, effective_colorspace);              // [5]
            if (ibuf) {
                imb_handle_alpha(ibuf, flags, colorspace, effective_colorspace);
                return ibuf;
            }
        }
    }

After determining that the file is of a DPX or CINEON file, the function at [6] will be called. This will execute the logImageOpenFromMemory function which will check the header of the file in order to determine whether to call logImageIsDpx or logImageIsCineon functions. If a DPX image file was detected, then the dpxOpen function will be called at [7]

source/blender/imbuf/intern/cineon/cineon_dpx.c:52
static struct ImBuf *imb_load_dpx_cineon(
        const unsigned char *mem, size_t size, int use_cineon, int flags,
        char colorspace[IM_MAX_SPACE])
{
...
    image = logImageOpenFromMemory(mem, size);          // [6] \
\
source/blender/imbuf/intern/cineon/logImageCore.c:118
LogImageFile *logImageOpenFromMemory(const unsigned char *buffer, unsigned int size)
{
    if (logImageIsDpx(buffer))
        return dpxOpen(buffer, 1, size);                // [7]
    else if (logImageIsCineon(buffer))
        return cineonOpen(buffer, 1, size);

    return NULL;
}

Once inside the dpxOpen function, the application will read the header from the file into a DpxMainHeader structure [8]. This structure is composed of 5 different headers that are constantly sized. At [9], the imageHeader field is declared as the DpxImageHeader structure. This structure contains a constant-sized array in the element field [10]. This structure only allocates up to 8 elements for the DpxElementHeader structure.

source/blender/imbuf/intern/cineon/dpxlib.c:133
LogImageFile *dpxOpen(const unsigned char *byteStuff, int fromMemory, size_t bufferSize)
{
    DpxMainHeader header;
    LogImageFile *dpx = (LogImageFile *)MEM_mallocN(sizeof(LogImageFile), __func__);
    const char *filename = (const char *)byteStuff;
    int i;

    if (dpx == NULL) {
        if (verbose) printf("DPX: Failed to malloc dpx file structure.\n");
        return NULL;
    }
...
    if (logimage_fread(&header, sizeof(header), 1, dpx) == 0) {     // [8]
        if (verbose) printf("DPX: Not enough data for header in \"%s\".\n", byteStuff);
        logImageClose(dpx);
        return NULL;
    }

source/blender/imbuf/intern/cineon/dpxlib.h:144
typedef struct {
    DpxFileHeader           fileHeader;
    DpxImageHeader          imageHeader;                            // [9]
    DpxOrientationHeader    orientationHeader;
    DpxFilmHeader           filmHeader;
    DpxTelevisionHeader     televisionHeader;
} DpxMainHeader;

source/blender/imbuf/intern/cineon/dpxlib.h:80
typedef struct {
    unsigned short      orientation;
    unsigned short      elements_per_image;
    unsigned int        pixels_per_line;
    unsigned int        lines_per_element;
    DpxElementHeader    element[8];                                 // [10]
    char                reserved[52];
} DpxImageHeader;

source/blender/imbuf/intern/cineon/dpxlib.h:63
typedef struct {
...
} DpxElementHeader;

After the application finishes reading the header, it will begin to check its magic at [11], and collect the image dimensions at [13]. At [12], however, the application will read a 16-bit unsigned integer from the header and use it to determine the number of elements to read from the header. Due to a lack of bounds checking, this value can be used to write outside the bounds of the elements field. If this value is larger than 8, then a buffer overflow can be made to occur.

source/blender/imbuf/intern/cineon/dpxlib.c:176
    if (header.fileHeader.magic_num == swap_uint(DPX_FILE_MAGIC, 1)) {                  // [11]
        dpx->isMSB = 1;
        if (verbose) printf("DPX: File is MSB.\n");
    }
    else if (header.fileHeader.magic_num == DPX_FILE_MAGIC) {
        dpx->isMSB = 0;
        if (verbose) printf("DPX: File is LSB.\n");
    }
...
    dpx->srcFormat = format_DPX;
    dpx->numElements = swap_ushort(header.imageHeader.elements_per_image, dpx->isMSB);  // [12]
    if (dpx->numElements == 0) {
        if (verbose) printf("DPX: Wrong number of elements: %d\n", dpx->numElements);
        logImageClose(dpx);
        return NULL;
    }

    dpx->width = swap_uint(header.imageHeader.pixels_per_line, dpx->isMSB);             // [13]
    dpx->height = swap_uint(header.imageHeader.lines_per_element, dpx->isMSB);

After storing the number of elements and the dimensions of the image, the following loop will be entered to extract information from the imageHeader.element field. Due to a missing bounds check on the dpx->numElements field, this loop can write outside the bounds of the dpx->element array [14].

source/blender/imbuf/intern/cineon/dpxlib.c:213
    for (i = 0; i < dpx->numElements; i++) {
        dpx->element[i].descriptor = header.imageHeader.element[i].descriptor;          // [14]

        switch (dpx->element[i].descriptor) {
...
        }

        if (dpx->depth == 0 || dpx->depth > 4) {
...
        }

        dpx->element[i].bitsPerSample = header.imageHeader.element[i].bits_per_sample;
        if (dpx->element[i].bitsPerSample != 1 && dpx->element[i].bitsPerSample != 8 &&
            dpx->element[i].bitsPerSample != 10 && dpx->element[i].bitsPerSample != 12 &&
            dpx->element[i].bitsPerSample != 16)
        {
...
        }

...
        if (dpx->element[i].dataOffset == 0) {
...
        }

        dpx->element[i].transfer = header.imageHeader.element[i].transfer;

...
    }

Crash Information

(25fc.27e0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00cbeed7 ecx=00020000 edx=00003f80 esi=14cb4e6c edi=14cb4fdc
eip=016f2cb5 esp=00cbe950 ebp=00cbf178 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
blender!osl_texture_set_swrap_code+0x47a25:
016f2cb5 f30f114728      movss   dword ptr [edi+28h],xmm0 ds:002b:14cb5004=????????

0:000> !heap -p -a @edi
    address 14cb4fdc found in
    _DPH_HEAP_ROOT @ 6f01000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                14222000:         14cb4e68              198 - 

Exploit Proof-of-Concept

Included with this advisory is a generator for the vulnerability. This proof-of-concept requires python and takes a single-argument which is the filename to write the .cin file to.

$ python poc.py $FILENAME.cin

To trigger the vulnerability, one can simply add it as an asset or they can pass it as an argument to the blender executable.

$ /path/to/blender.exe -a $FILENAME.cin

Mitigation

In order to mitigate this vulnerability, it is recommended to not use untrusted image files as an asset when using the sequencer.

Timeline

2017-09-06 - Vendor Disclosure
2018-01-11 - Public Release

Credit

Discovered by a member of Cisco Talos.