Talos Vulnerability Report

TALOS-2017-0490

Simple DirectMedia Layer SDL2_image ICO Pitch Handling Code Execution Vulnerability

March 1, 2018
CVE Number

CVE-2017-14441

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

Credit

Discovered by Lilith <(x_x)> of Cisco Talos.