Talos Vulnerability Report

TALOS-2019-0842

SDL_image XCF Image Code Execution Vulnerability

July 29, 2019
CVE Number

CVE-2019-5058

Summary

An exploitable code execution vulnerability exists in the XCF image rendering functionality of SDL2_image 2.0.4. A specially crafted XCF image can cause a heap overflow, resulting in code execution. An attacker can display a specially crafted image to trigger this vulnerability.

Tested Versions

SDL_image 2.0.4

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 number of video games, software and emulators. The LibSDL2_Image library is an optional component that deals specifically with parsing and displaying a variety of image file formats, creating a single, uniform API for image processing, regardless of the type.

When parsing an XCF image, the pitch of a surface is calculated from the width of the image itself. The idea to note here is the pitch is directly affected by the width of the image, which is attacker controlled [0].

SDL/src/video/SDL_surface.c:38

/*
* Calculate the pad-aligned scanline width of a surface
*/
static int
SDL_CalculatePitch(Uint32 format, int width)
{
    int pitch;

    /* Surface should be 4-byte aligned for speed */
    pitch = width * SDL_BYTESPERPIXEL(format); [0]
    switch (SDL_BITSPERPIXEL(format)) {
    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;
}

SDL allocates a buffer to hold the pixels [2] in the image sized to parameters provided in the XCF image, including the aforementioned pitch [1].

SDL/src/video/SDL_surface.c:112

/* Get the pixels */
if (surface->w && surface->h) {
    /* Assumptions checked in surface_size_assumptions assert above */
    Sint64 size = ((Sint64)surface->h * surface->pitch); [1]
    if (size < 0 || size > SDL_MAX_SINT32) {
        /* Overflow... */
        SDL_FreeSurface(surface);
        SDL_OutOfMemory();
        return NULL;
    }

    surface->pixels = SDL_SIMDAlloc((size_t)size); [2]
    if (!surface->pixels) {
        SDL_FreeSurface(surface);
        SDL_OutOfMemory();
        return NULL;
    }

Later, an XCF level structure is parsed containing the level width and height, which is also attacker-controlled [3].

SDL_image/IMG_xcf.c:491

static xcf_level * read_xcf_level (SDL_RWops * src, const xcf_header * h) {
    xcf_level * l;
    int i;

    l = (xcf_level *) SDL_malloc (sizeof (xcf_level));
    l->width  = SDL_ReadBE32 (src); [3]
    l->height = SDL_ReadBE32 (src); [3]

    l->tile_file_offsets = NULL;
    i = 0;
    do {
        l->tile_file_offsets = (Uint32 *) SDL_realloc (l->tile_file_offsets, sizeof (Uint32) * (i+1));
        l->tile_file_offsets [i] = read_offset (src, h);
    } while (l->tile_file_offsets [i++]);

    return l;
}

Using the level width and height, a tile is loaded from the image. The pixels in an XCF are organized in a grid of tiles. These tiles have, at most, a width and height of 64 [4].

SDL_image/IMG_xcf.c:721

SDL_RWseek(src, level->tile_file_offsets[j], RW_SEEK_SET);
ox = tx + 64 > level->width ? level->width % 64 : 64;   [4]
oy = ty + 64 > level->height ? level->height % 64 : 64; [4]
length = ox*oy*6;

if (level->tile_file_offsets[j + 1] > level->tile_file_offsets[j]) {
    length = level->tile_file_offsets[j + 1] - level->tile_file_offsets[j];
}

tile = load_tile(src, length, hierarchy->bpp, ox, oy);

From the loaded tile, an offset into the pixels buffer is calculated to prepare the population of the pixels buffer from the tile data itself [5].

SDL_image/IMG_xcf.c:747

p8 = tile;
p16 = (Uint16 *) p8;
p = (Uint32 *) p8;

for (y = ty; y < ty + oy; y++) {
    if ((ty >= surface->h) || ((tx+ox) > surface->w)) {
        break;
    }

    row = (Uint32 *) ((Uint8 *) surface->pixels + y * surface->pitch + tx * 4); [5]

This row offset is populated based on the bpp value of the XCF image from the parsed hierarchy structure [6].

SDL_image/IMG_xcf.c:761

switch (hierarchy->bpp) {
case 4:
    for (x = tx; x < tx + ox; x++)
        *row++ = Swap32(*p++);  [6]
    break;
case 3:
    for (x = tx; x < tx + ox; x++) {
        *row = 0xFF000000;      [6]
        *row |= ((Uint32)*p8++ << 16);
        *row |= ((Uint32)*p8++ << 8);
        *row |= ((Uint32)*p8++ << 0);
        row++;
    }
    break;
...
case 2:
...
case 1:
...

If an attacker sets the correct level values to mismatch the image values, the row pointer will be calculated beyond the bounds of the pixels buffer. Once this pointer is written to, it will be writing out of bounds of the heap buffer, causing a heap overflow, potentially resulting in code execution.

Crash Information

=================================================================
==2023==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x61a00001f148 at pc 0x0000004b7d14 bp 0x7ffed79cf300 sp 0x7ffed79cf2f0
WRITE of size 4 at 0x61a00001f148 thread T0
    #0 0x4b7d13 in do_layer_surface ../IMG_xcf.c:766
    #1 0x4b7d13 in IMG_LoadXCF_RW ../IMG_xcf.c:930
    #2 0x40653e in IMG_LoadTyped_RW ../IMG.c:195
    #3 0x404de0 in main ../showimage.c:56
    #4 0x7f3447bd382f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
    #5 0x405438 in _start (/root/vmshare/targets/sdl/SDL_image/build-afl-asan/showimage+0x405438)

0x61a00001f148 is located 0 bytes to the right of 1224-byte region [0x61a00001ec80,0x61a00001f148)
allocated by thread T0 here:
    #0 0x7f344873f602 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x98602)
    #1 0x4f8092 in SDL_malloc_REAL /root/vmshare/targets/sdl/SDL/src/stdlib/SDL_malloc.c:5387

SUMMARY: AddressSanitizer: heap-buffer-overflow ../IMG_xcf.c:766 do_layer_surface
Shadow bytes around the buggy address:
0x0c347fffbdd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fffbde0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fffbdf0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fffbe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fffbe10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c347fffbe20: 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa fa
0x0c347fffbe30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c347fffbe40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c347fffbe50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fffbe60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c347fffbe70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable:           00
Partially addressable: 01 02 03 04 05 06 07 
Heap left redzone:       fa
Heap right redzone:      fb
Freed heap region:       fd
Stack left redzone:      f1
Stack mid redzone:       f2
Stack right redzone:     f3
Stack partial redzone:   f4
Stack after return:      f5
Stack use after scope:   f8
Global redzone:          f9
Global init order:       f6
Poisoned by user:        f7
Container overflow:      fc
Array cookie:            ac
Intra object redzone:    bb
ASan internal:           fe

Timeline

2019-05-30 - Vendor Disclosure
2019-07-03 - Vendor Patched
2019-07-29 - Public Release

Credit

Discovered by Cory Duplantis of Cisco Talos.