CVE-2017-2907
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.
Blender v2.78c
http://www.blender.org
git://git.blender.org/blender.git
8.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CWE-190 - Integer Overflow or Wraparound
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;
}
(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
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
In order to mitigate this vulnerability, it is recommended to not use untrusted animation files as an asset when composing a scene.
2017-09-06 - Vendor Disclosure
2018-01-11 - Public Release
Discovered by a member of Cisco Talos.