Headline
CVE-2017-14441: TALOS-2017-0490 || Cisco Talos Intelligence Group
An exploitable code execution vulnerability exists in the ICO image rendering functionality of SDL2_image-2.0.2. A specially crafted ICO image can cause an integer overflow, cascading to a heap overflow resulting in code execution. An attacker can display a specially crafted image to trigger this vulnerability.
Summary
An exploitable code execution vulnerability exists in the ICO image rendering functionality of SDL2_image-2.0.2. A specially crafted ICO image can cause an integer overflow, cascading to a heap overflow resulting in code execution. An attacker can display a specially crafted image to trigger this vulnerability.
Tested Versions
Simple DirectMedia Layer SDL2_image 2.0.2
Product URLs
https://www.libsdl.org/projects/SDL_image/
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-122: Heap-based Buffer Overflow
Details
LibSDL is a multi-platform library for easy access to low level hardware and graphics, providing support for a large amount of games, software, and emulators. The last known count of software using LibSDL (from 2012) listed the number at upwards of 120. The LibSDL2_Image library is an optional component that deals specifically with parsing and displaying a variety of image file formats, creating a single and uniform API for image processing, regardless of the type.
When reading in an ICO file, in order to allocate enough space for the input, the dimensions of the image, as read from the image headers, will obviously be used. The interesting thing is that for a lot of image types, this is not simply (height * width), as one would expect, it’s actually (width * pitch). Pitch is the distance in bytes between two memory addresses that represent the beginning of one bitmap line and the next, essentially resulting in: pitch = (width + padding), which is usually just done for alignment reasons.
The pitch is calculated inside of LibSDL with the following function:
/*
* Calculate the pad-aligned scanline width of a surface
*/
int SDL_CalculatePitch(SDL_Surface * surface) {
int pitch;
/* Surface should be 4-byte aligned for speed */
pitch = surface->w * surface->format->BytesPerPixel;
switch (surface->format->BitsPerPixel) {
case 1:
pitch = (pitch + 7) / 8;
break;
case 4:
pitch = (pitch + 1) / 2;
break;
default:
break;
}
pitch = (pitch + 3) & ~3; /* 4-byte aligning */
return (pitch);
}
If we look at how the values surface->w and surface->format->BytesPerPixel are read in, we can see that there are not really any checks on the BytesPerPixel field:
// SDL_pixels.c
528 SDL_InitFormat(format=0x2af7bb0, pixel_format=0x16362004) //[1]
[...]
542 format->BitsPerPixel = bpp;
543 format->BytesPerPixel = (bpp + 7) / 8;
[1] The bpp variable = pixel_format & 0x0000FF00
Or the surface->w file (which is 4 bytes read straight from the image):
682 /* Read the Win32 BITMAPINFOHEADER */
683 biSize = SDL_ReadLE32(src); //offset 0x16 in ICO file
684 if (biSize == 40) {
685 biWidth = SDL_ReadLE32(src); //offset 0x1a...
[…]
741 surface =
742 NSDL_CreateRGBSurface(0, biWidth, biHeight, 32, 0x00FF0000,
0x0000FF00, 0x000000FF, 0xFF000000);
Thus, going back to how the pitch is generated:
pitch = surface->w * surface->format->BytesPerPixel;
We can easily input a width and BytesPerPixel that cause an integer overflow, for example, if width == 0x40000020, and BytesPerPixel == 0x80, then the resulting pitch will be (0x2000001000 & 0xFFFFFFFF), or 0x1000, since the pitch field is a 32-bit integer, resulting in a huge desync between the width and pitch variables, which will come into play in the following function:
if (surface->w && surface->h) {
/* Assumptions checked in surface_size_assumptions assert above */
Sint64 size = ((Sint64)surface->h * surface->pitch);
if (size < 0 || size > SDL_MAX_SINT32) {
The above code is the only check on the resulting size of the pallet buffer. Notice that the pitch is used to generate the allocated buffer, and also that there is no check on the width of the surface. This results in the following allocation at src/video/SDL_surface.c+107:
// 107 surface->pixels = SDL_malloc((size_t)size);
<(^_^)> info reg rax
rax 0x22a92b0 0x22a92b0
<(^_^)> heap chunk 0x22a92b0
Chunk(addr=0x22a92b0, size=0x1010, flags=PREV_INUSE)
Chunk size: 4112 (0x1010)
Usable size: 4104 (0x1008)
Previous chunk size: 1702521171 (0x657a6953)
PREV_INUSE flag: On
IS_MMAPPED flag: Off
NON_MAIN_ARENA flag: Off
// Looking at the bytes in memory:
<(^_^)> x/4gx $rax-0x10
0x22a92a0: 0x00000000657a6953 0x0000000000001011
0x22a92b0: 0x00007f999f25fca8 0x00007f999f25fca8
// Start of next chunk (0x2b3de50)
<(^_^)> x/4gx $rax-0x10+0x1010
0x22aa2b0: 0x0000068800000000 0x0000000000000291
0x22aa2c0: 0x00007f999f25f678 0x00007f999f25f678
The actual corruption of the heap is within the following loop:
761 bits = (Uint8 *) surface->pixels + (surface->h * surface->pitch); //[1]
[...]
//IMG_bmp.c:780
while (bits > (Uint8 *) surface->pixels) {
bits -= surface->pitch;
switch (ExpandBMP) {
case 1:
case 4:
case 8:
{
Uint8 pixel = 0;
int shift = (8 - ExpandBMP);
for (i = 0; i < surface->w; ++i) { // [2]
if (i % (8 / ExpandBMP) == 0) {
if (!SDL_RWread(src, &pixel, 1, 1)) {
IMG_SetError("Error reading from ICO");
was_error = SDL_TRUE;
goto done;
}
}
*((Uint32 *) bits + i) = (palette[pixel >> shift]); // [3]
pixel <<= ExpandBMP;
}
}
Starting at the bottom of the pixel buffer [1], the image is read in backwards, as it attempts to unpack the raw data into the allocated buffer. Unfortunately, it uses the surface->w parameter [2] to determine how long each bitmap line is, which was never checked or validated. Thus, when the program actually writes the data, the counter variable i will eventually pass the allocated heap boundaries causing an OOB write at [3]. An example of this in action is given below:
<(^_^)> x/4gx 0x22aa2b0
0x22aa2b0: 0x0192394f41414141 0x01c2989401b54750
0x22aa2c0: 0x00007f99019d4555 0x00007f999f25f678
Whereby the heap metadata struct becomes controlled by the attacker (0x22aa2b0->0x22aa2c0 in this example).
Crash Information
*** Error in `./img_read_plain': double free or corruption (!prev): 0x00000000022a92b0 ***
Program received signal SIGABRT, Aborted.
0x0000000070000002 in ?? ()
--------------------------------------------------------------------------[ registers ]----
$rax : 0x0000000000000000 -> 0x0000000000000000
$rbx : 0x00000000000000ea -> 0x00000000000000ea
$rcx : 0xffffffffffffffff
$rdx : 0x0000000000000006 -> 0x0000000000000006
$rsp : 0x00000000681ffe00 -> 0x00007f999f820598 -> xor ecx, ecx
$rbp : 0x00007ffebf1a9870 -> 0x00007ffebf1a9880 -> 0x3030303030303030 -> 0x3030303030303030 ("00000000"?)
$rsi : 0x00000000000155c7 -> 0x00000000000155c7
$rdi : 0x00000000000155c7 -> 0x00000000000155c7
$rip : 0x0000000070000002 -> 0x0fc3050fc3050fc3 -> 0x0fc3050fc3050fc3
$r8 : 0x3062323961323230 -> 0x3062323961323230 ("022a92b0"?)
$r9 : 0x6f6974707572726f -> 0x6f6974707572726f ("orruptio"?)
$r10 : 0x0000000000000008 -> 0x0000000000000008
$r11 : 0x0000000000000246 -> 0x0000000000000246
$r12 : 0x00007ffebf1a9680 -> 0x0000000000000000 -> 0x0000000000000000
$r13 : 0x0000000000000007 -> 0x0000000000000007
$r14 : 0x000000000000005b -> 0x000000000000005b
$r15 : 0x00000000681fffa0 -> 0x00000000000000ea -> 0x00000000000000ea
$eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification]
------------------------------------------------------------------------------[ stack ]----
0x00000000681ffe00|+0x00: 0x00007f999f820598 -> xor ecx, ecx <-$rsp
0x00000000681ffe08|+0x08: 0x0000000000000000 -> 0x0000000000000000
0x00000000681ffe10|+0x10: 0x0000000000000000 -> 0x0000000000000000
0x00000000681ffe18|+0x18: 0x00007f999f81d0f5 -> 0x1f0f66c328c48348 -> 0x1f0f66c328c48348
0x00000000681ffe20|+0x20: 0x6f6974707572726f -> 0x6f6974707572726f
0x00000000681ffe28|+0x28: 0x0000000070000000 -> 0x050fc3050fc3050f -> 0x050fc3050fc3050f
0x00000000681ffe30|+0x30: 0x0000000000000000 -> 0x0000000000000000
0x00000000681ffe38|+0x38: 0x0000000000000000 -> 0x0000000000000000
-------------------------------------------------------------------[ code:i386:x86-64 ]----
->0x70000002 ret
0x70000003 syscall
0x70000005 ret
0x70000006 syscall
0x70000008 ret
0x70000009 syscall
----------------------------------------------------------------------------[ threads ]----
[#0] Id 1, Name: "", stopped, reason: SIGABRT
------------------------------------------------------------------------------[ trace ]----
[#0] 0x70000002->ret
[#1] 0x7f999f820598->xor ecx, ecx
[#2] 0x7f999f81d0f5->add rsp, 0x28
[#3] 0x7f999f81e108->mov rbx, rax
[#4] 0x7f999f8205ca->mov rsp, rbx
[#5] 0x7f999f8205f3->ret
Timeline
2017-11-28 - Vendor Disclosure
2018-03-01 - Public Release
Discovered by Lilith <(x_x)> of Cisco Talos.