Talos Vulnerability Report

TALOS-2018-0520

Simple DirectMedia Layer SDL2_Image load_xcf_tile_rle Information Disclosure Vulnerability

April 10, 2018
CVE Number

CVE-2018-3838

Summary

An exploitable information vulnerability exists in the XCF image rendering functionality of SDL2_image-2.0.2. A specially crafted XCF image can cause an out-of-bounds read on the heap, resulting in information disclosure. 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

5.3 - CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N

CWE

CWE-126: Buffer Over-read

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 parsing and storing an XCF file (the native file type for Gimp), the LibSDL2 library implements a custom RLE inflater for the compressed XCF data. The code that handles this is listed below:

//IMG_xcf.c
static unsigned char * load_xcf_tile_rle (SDL_RWops * src, Uint32 len, int bpp, int x, int y) 
    unsigned char * load, * t, * data, * d;
    Uint32 reallen;
            int i, size, count, j, length;
    unsigned char val;

    t = load = (unsigned char *) SDL_malloc (len);  //[1]
    reallen = SDL_RWread (src, t, 1, len); //[2] 

    data = (unsigned char *) SDL_malloc (x*y*bpp);  //[3]
    [...]

From the above, it should be noted that the bpp,x, and y parameters are all under the file's control, along with the len field, which is derived from the calling function as such:

do_layer_surface(SDL_Surface * surface, SDL_RWops * src, xcf_header * head, xcf_layer * layer, load_tile_type load_tile) {

        SDL_RWseek(src, layer->hierarchy_file_offset, RW_SEEK_SET); 
            hierarchy = read_xcf_hierarchy(src); // [1]
        level = NULL;
        for (i = 0; hierarchy->level_file_offsets[i]; i++) {
            SDL_RWseek(src, hierarchy->level_file_offsets[i], RW_SEEK_SET);
                level = read_xcf_level(src); //[2]

                ty = tx = 0;
                for (j = 0; level->tile_file_offsets[j]; j++) {
                    SDL_RWseek(src, level->tile_file_offsets[j], RW_SEEK_SET);
                    [...]

                    if (level->tile_file_offsets[j + 1]) { 
                    tile = load_tile(src, level->tile_file_offsets[j + 1] - level->tile_file_offsets[j], hierarchy->bpp, ox, oy);  // [3] 
            } else { 
                         tile = load_tile(src, ox * oy * 6, hierarchy->bpp, ox, oy);
                    }
[...]

To sum up the above, each XCF surface can have multiple layers, and these layers have a hierarchy, which is read in at [1] and dictates the order in which the image layers are rendered. Then, each layer has a set of tiles (which are just blocks of image data), which must be read in first before rendering. These tiles can be different sizes, so an array of file offsets are read in at [2], such that the parsing can be done correctly. After this, the library loops over the tile offsets, and seeks for the tile data in the file, reading the tile data at [3]. 'load_tile' is a function pointer to the load_xcf_tile_rle function that we were at before. Of most note for this bug, is the fact that the 'len' parameter from the load_xcf_tile_rle function is calculated from level->tile_file_offsets[j + 1] – level->tile_file_offsets[j], as long as level->tile_file_offsets[j+1] is != 0.

Thus, if we make our XCF file have two consecutive tile_file_offsets that are equivalent, our call to load_xcf_tile_rle becomes load_xcf_tile_rle(src,0,hierarchy->bpp,ox,oy). This causes issues further down inside load_xcf_tile_rle as we end up doing a call to malloc(0) for our source buffer:

static unsigned char * load_xcf_tile_rle (SDL_RWops * src, Uint32 len, int bpp, int x, int y)   {
unsigned char * load, * t, * data, * d;
Uint32 reallen;
int i, size, count, j, length;
unsigned char val;

t = load = (unsigned char *) SDL_malloc (len); // [1]

Since SDL_malloc is a simple wrapper around normal malloc, this depends on the specific implementation of malloc for the system, but at least on Debian Linux, m0560874168f86c7aeeee2edcf2cea202alloc(0) returns a valid buffer of the minimum chunk size possible for the given architecture, which is usually 16 bytes on a 64 byte system. But regardless of the exact byte count, the amount of bytes read in from this malloc(0) buffer for data is going to be far greater for any image of reasonable size, as the destination buffer's size is xybpp. This results in a large user-controlled amount of data being read from the heap and populated into the XCF tile for further processing, creating an out of bounds read.

Crash Information

(Note: Compiled with ASAN)

Program received signal SIGSEGV, Segmentation fault.
0x00007f692455e127 in load_xcf_tile_rle (src=0x60700000dfb0, len=0x0, bpp=0x3, x=0x40, y=0x40) at IMG_xcf.c:496
warning: Source file is more recent than executable.
496
-------------------------------------------------------------------------[ registers ]----
$rax   : 0x0000602000010000 -> 0x0000000000000000 -> 0x0000000000000000
$rbx   : 0x00007ffd66fd8c30 -> 0x00007ffd66fda37f -> 0x3134373830363530 -> 0x3134373830363530 ("05608741"?)
$rcx   : 0x22ffffff00000000 -> 0x22ffffff00000000
$rdx   : 0x0000602000010001 -> 0x0000000000000000 -> 0x0000000000000000
$rsp   : 0x00007ffd66fd88d0 -> 0x00007ffd66fd8940 -> 0x00007ffd66fd89f0 -> 0x00007ffd66fd8ab0 -> 0x00007ffd66fd8af0 -> 0x00007ffd66fd8b20 
-> 0x0000000000000002 ->   
0x0000000000000002
$rbp   : 0x00007ffd66fd8940 -> 0x00007ffd66fd89f0 -> 0x00007ffd66fd8ab0 -> 0x00007ffd66fd8af0 -> 0x00007ffd66fd8b20 ->     
0x0000000000000002 -> 0x0000000000000002
$rsi   : 0x0000000000000013 -> 0x0000000000000013
$rdi   : 0x00007f6924ae8540 -> 0x0000000000000014 -> 0x0000000000000014
$rip   : 0x00007f692455e127 -> <load_xcf_tile_rle+168> movzx eax, BYTE PTR [rax]
$r8    : 0x0000000000000000 -> 0x0000000000000000
$r9    : 0x000000000000001e -> 0x000000000000001e
$r10   : 0x00000000004bd553 -> 0x6349643375c08548 -> 0x6349643375c08548
$r11   : 0x0000000000000008 -> 0x0000000000000008
$r12   : 0x00000000004bd3e4 -> <_start+0> xor ebp, ebp
$r13   : 0x00007ffd66fd8c20 -> 0x0000000000000002 -> 0x0000000000000002
$r14   : 0xfffffffffffffff8 -> 0xfffffffffffffff8
$r15   : 0x0000000000000000 -> 0x0000000000000000
$eflags: [carry PARITY adjust zero sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
-----------------------------------------------------------------------------[ stack ]----
0x00007ffd66fd88d0|+0x00: 0x00007ffd66fd8940 -> 0x00007ffd66fd89f0 -> 0x00007ffd66fd8ab0 -> 0x00007ffd66fd8af0 -> 0x00007ffd66fd8b20 -> 
0x0000000000000002 -> 0x0000000000000002           
<-$rsp
0x00007ffd66fd88d8|+0x08: 0x0000004000000040 -> 0x0000000000000000 -> 0x0000000000000000
0x00007ffd66fd88e0|+0x10: 0x0000000000000003 -> 0x0000000000000003
0x00007ffd66fd88e8|+0x18: 0x000060700000dfb0 -> 0x00007f69247c0e48 -> 0x20ec8348e5894855 -> 0x20ec8348e5894855
0x00007ffd66fd88f0|+0x20: 0x0000000000000000 -> 0x0000000000000000
0x00007ffd66fd88f8|+0x28: 0x00007f69236c097d -> 0x01c4f6038bc28948 -> 0x01c4f6038bc28948
0x00007ffd66fd8900|+0x30: 0x00006270001b2100 -> 0x0000be0000be0000 -> 0x0000be0000be0000
0x00007ffd66fd8908|+0x38: 0x00000000247c0f03 -> 0x00000000247c0f03
------------------------------------------------------------------[ code:i386:x86-64 ]----
0x7f692455e10f <load_xcf_tile_rle+144> mov    DWORD PTR [rbp-0x1c], 0x0
0x7f692455e116 <load_xcf_tile_rle+151> jmp    0x7f692455e22a <load_xcf_tile_rle+427>
0x7f692455e11b <load_xcf_tile_rle+156> mov    rax, QWORD PTR [rbp-0x8]
0x7f692455e11f <load_xcf_tile_rle+160> lea    rdx, [rax+0x1]
0x7f692455e123 <load_xcf_tile_rle+164> mov    QWORD PTR [rbp-0x8], rdx
->0x7f692455e127 <load_xcf_tile_rle+168> movzx  eax, BYTE PTR [rax]
0x7f692455e12a <load_xcf_tile_rle+171> mov    BYTE PTR [rbp-0x41], al
0x7f692455e12d <load_xcf_tile_rle+174> movzx  eax, BYTE PTR [rbp-0x41]
0x7f692455e131 <load_xcf_tile_rle+178> mov    DWORD PTR [rbp-0x24], eax
0x7f692455e134 <load_xcf_tile_rle+181> cmp    DWORD PTR [rbp-0x24], 0x7f
0x7f692455e138 <load_xcf_tile_rle+185> jle    0x7f692455e1b0 <load_xcf_tile_rle+305>
--------------------------------------------------------------[ source:IMG_xcf.c+496 ]----
492    for (i = 0; i < bpp; i++) { //ah, we write every 'i'th byte in the Surface
493      d    = data + i; //d is the offending variable...
494      size = x*y; //0x1000
495      count = 0;
-> 496   
497      while (size > 0) { //size == amount of pixels (=>total size == x*y*bpp)
498        val = *t++; // #! t => OOB. Size starts at 0x1000
499  
500        length = val; //length == length of consecutive pixels that are same color?
---------------------------------------------------------------------------[ threads ]----
[#0] Id 1, Name: "", stopped, reason: SIGSEGV
-----------------------------------------------------------------------------[ trace ]----
[#0] 0x7f692455e127->Name: load_xcf_tile_rle(src=0x60700000dfb0, len=0x0, bpp=0x3, x=0x40, y=0x40)
[#1] 0x7f692455e55d->Name: do_layer_surface(surface=0x60800000be20, src=0x60700000dfb0, head=0x60700000df40, layer=0x60600000eea0,    
load_tile=0x7f692455e07f <load_xcf_tile_rle>)
[#2] 0x7f692455ee24->Name: IMG_LoadXCF_RW(src=0x60700000dfb0)
[#3] 0x7f692453c7ef->Name: IMG_LoadTyped_RW(src=0x60700000dfb0, freesrc=0x1, type=0x0)
[#4] 0x7f692453c5e0->Name: IMG_Load(file=0x7ffd66fda37f "0560874168f86c7aeeee2edcf2cea202")
[#5] 0x4bd553->Name: main(argc=<optimized out>, argv=<optimized out>)
------------------------------------------------------------------------------------------

Timeline

2018-02-06 - Vendor Disclosure
2018-02-07 - Vendor patched
2018-04-10 - Public Release

Credit

Discovered by Lilith of Cisco Talos.