Talos Vulnerability Report

TALOS-2017-0414

Blender Sequencer avi_format_convert Integer Overflow Code Execution Vulnerability

January 11, 2018
CVE Number

CVE-2017-2907

Summary

An exploitable integer overflow exists in the animation playing functionality of the Blender open-source 3d creation suite version 2.78c. A specially created .avi 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 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-190 - Integer Overflow or Wraparound

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 renders a frame from an .avi file within the sequencer. When allocating space for a frame within the .avi 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 decode data from the video file into this buffer, a heap-based buffer overflow will occur.

When trying to play a file, the application will first try to determine what whether the file is an animation or a picture format. An animation is checked at [1] using the IMB_isanim function. Inside this function, the application will determine the animation type. This is done by the imb_get_anim_type function within the source/blender/imbuf/intern/util.c file [2]. The next function will dispatch to a number of tests in order to determine what format the animation is actually in. One of these tests is a call to the function is_avi [3]. This function is simply a wrapper that calls AVI_is_avi [4]. Once inside the AVI_is_avi function, the application will check various chunks within the file to ensure they are of sane values. When this is done, the application will have determined it is an .avi animation and will proceed to actually process the video.

source/blender/windowmanager/intern/wm_playanim.c:1217
    if (IMB_isanim(filepath)) {                                 // [1] \
        /* OCIO_TODO: support different input color spaces */
...
            ibuf = IMB_anim_absolute(anim, 0, IMB_TC_NONE, IMB_PROXY_NONE);
...
    }
\
source/blender/imbuf/intern/util.c:422
bool IMB_isanim(const char *filename)
{
...
    type = imb_get_anim_type(filename);                         // [2] \
...
}
\
source/blender/imbuf/intern/util.c:376
int imb_get_anim_type(const char *name)
{
    int type;
    BLI_stat_t st;

    BLI_assert(!BLI_path_is_rel(name));

    if (UTIL_DEBUG) printf("%s: %s\n", __func__, name);
...
    if (isavi(name)) return (ANIM_AVI);                         // [3] \
...
    return ANIM_NONE;
}
\
source/blender/imbuf/intern/util.c:376
static int isavi(const char *name)
{
#ifdef WITH_AVI
    return AVI_is_avi(name);                                    // [4]
#else
    (void)name;
    return false;
#endif
}

Returning back to the caller, the application will then call the IMB_anim_absolute function [5]. This function does two things. One of which is to open up the file and collect information from the header chunk that is needed to render it [6]. The second which occurs after determining the animation type is to actually fetch an image buffer and decode a frame [7].

source/blender/windowmanager/intern/wm_playanim.c:1217
    if (IMB_isanim(filepath)) {
    ...
        if (anim) {
            ibuf = IMB_anim_absolute(anim, 0, IMB_TC_NONE, IMB_PROXY_NONE); // [5] \
    ...
        }
    }
\
source/blender/imbuf/intern/anim_movie.c:1282
struct ImBuf *IMB_anim_absolute(struct anim *anim, int position,
                                IMB_Timecode_Type tc,
                                IMB_Proxy_Size preview_size)
{
...
    if (preview_size == IMB_PROXY_NONE) {
        if (anim->curtype == 0) {
            ibuf = anim_getnew(anim);                   // [6]
            if (ibuf == NULL) {
                return(NULL);
            }

            IMB_freeImBuf(ibuf); /* ???? */
            ibuf = NULL;
        }

        if (position < 0) return(NULL);
        if (position >= anim->duration) return(NULL);
    }
...
    switch (anim->curtype) {
        case ANIM_SEQUENCE:
...
        case ANIM_AVI:
            ibuf = avi_fetchibuf(anim, position);       // [7]
            if (ibuf)
                anim->curposition = position;
            break;

When opening the .avi file to collect information about the header, the anim_getnew function will be called. Inside this function, the application will again determine which animation type the file is of and then hand off the current animation state to the startavi function at [8]. Inside startavi, the movie will actually be opened in order to extract information needed to render the file followed by saving the dimensions and number of frames available [10]. Before this, however, the AVI_open_movie function will be called at [9].

source/blender/imbuf/intern/anim_movie.c:1208
static ImBuf *anim_getnew(struct anim *anim)
{
...
    anim->curtype = imb_get_anim_type(anim->name);

    switch (anim->curtype) {
        case ANIM_SEQUENCE:
...
        case ANIM_AVI:
            if (startavi(anim)) {                           // [8] \
                printf("couldnt start avi\n");
                return (NULL);
            }
            ibuf = IMB_allocImBuf(anim->x, anim->y, 24, 0);
            break;
\
source/blender/imbuf/intern/anim_movie.c:282
static int startavi(struct anim *anim)
{
...
    avierror = AVI_open_movie(anim->name, anim->avi);       // [9]

#if defined(_WIN32) && !defined(FREE_WINDOWS)
    if (avierror == AVI_ERROR_COMPRESSION) {
...
    }
...
    anim->duration = anim->avi->header->TotalFrames;
    anim->params = NULL;

    anim->x = anim->avi->header->Width;                     // [10]
    anim->y = anim->avi->header->Height;
    anim->interlacing = 0;
    anim->orientation = 0;
    anim->framesize = anim->x * anim->y * 4;
...
}

The AVI_open_movie function will open the file in order to prepare the number of streams encoded within the file [11]. Once opening the file, the initial headers will be checked in order to locate the AVI header (identified by the “hdrl” chunk) which contains general information about the .avi file [12]. This data which includes the video’s dimensions is then assigned to the movie->header field [13]. These fields can be up to 32-bits in size. It is these two fields that when multiplied will be overflown. After storing the general information about the file, the application will then proceed to read information about each sample from the sample headers within the file [14]. After that information is stored, the application will then save the file offset to the sample data [15] to the movi_offset and read_offset variables and then proceed to load the index from the file if one is included. After collecting all of this information about the file, this function will return back to anim_getnew and then back to IMB_anim_absolute.

src/source/blender/avi/intern/avi.c:438
AviError AVI_open_movie(const char *name, AviMovie *movie)
{
...
    movie->fp = BLI_fopen(name, "rb");                      // [11]
    movie->offset_table = NULL;
...
    if (GET_FCC(movie->fp) != FCC("RIFF") ||
        !(movie->size = GET_FCC(movie->fp)))
    {
...
    }
...
    if (GET_FCC(movie->fp) != FCC("AVI ") ||                // [12]
        GET_FCC(movie->fp) != FCC("LIST") ||
        !GET_FCC(movie->fp) ||
        GET_FCC(movie->fp) != FCC("hdrl") ||
        (movie->header->fcc = GET_FCC(movie->fp)) != FCC("avih") ||
        !(movie->header->size = GET_FCC(movie->fp)))
    {
...
    }
    
    movie->header->MicroSecPerFrame = GET_FCC(movie->fp);   // [13]
    movie->header->MaxBytesPerSec = GET_FCC(movie->fp);
    movie->header->PaddingGranularity = GET_FCC(movie->fp);
    movie->header->Flags = GET_FCC(movie->fp);
    movie->header->TotalFrames = GET_FCC(movie->fp);
    movie->header->InitialFrames = GET_FCC(movie->fp);
    movie->header->Streams = GET_FCC(movie->fp);
    movie->header->SuggestedBufferSize = GET_FCC(movie->fp);
    movie->header->Width = GET_FCC(movie->fp);
    movie->header->Height = GET_FCC(movie->fp);
    movie->header->Reserved[0] = GET_FCC(movie->fp);
    movie->header->Reserved[1] = GET_FCC(movie->fp);
    movie->header->Reserved[2] = GET_FCC(movie->fp);
    movie->header->Reserved[3] = GET_FCC(movie->fp);
...
    for (temp = 0; temp < movie->header->Streams; temp++) { // [14]
...
    }
...
    movie->movi_offset = ftell(movie->fp);                  // [15]
    movie->read_offset = movie->movi_offset;
    
    /* Read in the index if the file has one, otherwise create one */
    if (movie->header->Flags & AVIF_HASINDEX) {
...
    }
...
}

Back in the IMB_anim_absolute function, the information that was collected by anim_getnew will then be passed to the avi_fetchibuf function [16]. This function will take the fields that were assigned earlier and use them to allocate a buffer based on the video dimensions at [17]. Once allocating the buffer, the application will then proceed to read the first frame out of the file into the allocated buffer using the AVI_read_frame function. This function will first seek to the correct frame by navigating the index that was read earlier, and then hand off the sample stream to the avi_format_convert function at [18].

source/blender/imbuf/intern/anim_movie.c:1282
struct ImBuf *IMB_anim_absolute(struct anim *anim, int position,
                                IMB_Timecode_Type tc,
                                IMB_Proxy_Size preview_size)
{
...
    switch (anim->curtype) {
        case ANIM_SEQUENCE:
...
        case ANIM_AVI:
            ibuf = avi_fetchibuf(anim, position);                                                       // [16] \
            if (ibuf)
                anim->curposition = position;
            break;
\
source/blender/imbuf/intern/anim_movie.c:394
static ImBuf *avi_fetchibuf(struct anim *anim, int position)
{
...
        ibuf = IMB_allocImBuf(anim->x, anim->y, 24, IB_rect);

        tmp = AVI_read_frame(anim->avi, AVI_FORMAT_RGB32, position,                                     // [17] \
                             AVI_get_stream(anim->avi, AVIST_VIDEO, 0));
\
src/source/blender/avi/intern/avi.c:690
void *AVI_read_frame(AviMovie *movie, AviFormat format, int frame, int stream)
{
...
    while (rewind && frame > -1) {
...
    }
...
    buffer = avi_format_convert(movie, stream, buffer, movie->streams[stream].format, format, &temp);   // [18]

    return buffer;
}

The avi_format_convert function will determine the video’s format type and then use that to call one of functions at [19]. Each of these functions will allocate space for the destination of the sample data that is to be decoded and then will decompress the sample data from the file into said buffer. The provided proof-of-concept will actaully dispatch into the avi_converter_from_avi_rgb function.

source/blender/avi/intern/avi_codecs.c:42
void *avi_format_convert(AviMovie *movie, int stream, void *buffer, AviFormat from, AviFormat to, int *size)
{
...
    switch (to) {
        case AVI_FORMAT_RGB24:
            switch (from) {
                case AVI_FORMAT_AVI_RGB:
                    buffer = avi_converter_from_avi_rgb(movie, stream, buffer, size);   // [19]
                    break;
                case AVI_FORMAT_MJPEG:
                    buffer = avi_converter_from_mjpeg(movie, stream, buffer, size);     // [19]
                    break;
                case AVI_FORMAT_RGB32:
                    buffer = avi_converter_from_rgb32(movie, stream, buffer, size);     // [19]
                    break;
                default:
                    break;
            }
            break;
...
    return buffer;
}

Once inside the avi_converter_from_avi_rgb function, the application will actually decode the sample data into a frame. At [20], the application will allocate a buffer based on the Width and Height fields that were read earlier from the general AVI header. Due to the application not checking that the product of these fields with the size of 3 is larger than 32-bits, this allocation can be made to underflow which will result in an undersized buffer being assigned to the buf variable. Later at [21] when the application tries to decode the sample data into this buffer, both will write outside the bounds of the buffer which will cause a heap-based buffer overflow. This can lead to code execution under the context of the application.

source/blender/avi/intern/avi_rgb.c:45
void *avi_converter_from_avi_rgb(AviMovie *movie, int stream, unsigned char *buffer, int *size)
{
...
    bi = (AviBitmapInfoHeader *) movie->streams[stream].sf;
    if (bi) bits = bi->BitCount;

    if (bits == 16) {
...
    }
    else {
        buf = MEM_mallocN(movie->header->Height * movie->header->Width * 3, "fromavirgbbuf");   // [20]
...
        for (y = 0; y < movie->header->Height; y++) {                                           // [21]
            memcpy(&buf[y * movie->header->Width * 3], &buffer[((movie->header->Height - 1) - y) * rowstride], movie->header->Width * 3);
        }
    
        for (y = 0; y < movie->header->Height * movie->header->Width * 3; y += 3) {             // [21]
            i = buf[y];
            buf[y] = buf[y + 2];
            buf[y + 2] = i;
        }

Crash Information

(2e5c.564): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=143c9ffe ebx=14c2efbc ecx=00000002 edx=00000006 esi=143c9ffc edi=14c75000
eip=02acaf2a esp=00abf150 ebp=00abf174 iopl=0         nv up ei pl nz ac po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010213
blender!xmlListWalk+0x2181aa:
02acaf2a f3a4            rep movs byte ptr es:[edi],byte ptr [esi]

0:000> !heap -p -a @edi-1
    address 14c74fff found in
    _DPH_HEAP_ROOT @ b61000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                14b2330c:         14c74ff8                8 -         14c74000             2000

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 .avi file to.

$ python poc.py $FILENAME.avi

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.avi

Mitigation

In order to mitigate this vulnerability, it is recommended to not use untrusted animation files as an asset when composing a scene.

Timeline

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

Credit

Discovered by a member of Cisco Talos.