Talos Vulnerability Report

TALOS-2018-0521

Simple DirectMedia Layer SDL2_Image load_xcf_tile_rle bpp Code Execution Vulnerability

April 10, 2018
CVE Number

CVE-2018-3839

Summary

An exploitable code execution 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 write on the heap, 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

6.5 - CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H CVSSv3 Calculator: https://www.first.org/cvss/calculator/3.0

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 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); 
    reallen = SDL_RWread (src, t, 1, len); 

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

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. The bpp parameter in particular is taken from the xcf_hierarchy struct.

typedef struct {
  Uint32 cwidth;
  Uint32 height;
  Uint32 bpp;

  Uint32 * level_file_offsets;
} xcf_hierarchy;

This structure has it's fields populated directly from the XCF image file being rendered as such:

static xcf_hierarchy * read_xcf_hierarchy (SDL_RWops * src) {
xcf_hierarchy * h;
int i;

h = (xcf_hierarchy *) SDL_malloc (sizeof (xcf_hierarchy));
h->width  = SDL_ReadBE32 (src);
h->height = SDL_ReadBE32 (src);
h->bpp    = SDL_ReadBE32 (src);

Thus, we know that the bpp variable can be anything from 0x0 to 0xffffffff, so going back to the load_xcf_tile_rle function, an issue quickly arises:

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); 
    reallen = SDL_RWread (src, t, 1, len); 

    data = (unsigned char *) SDL_malloc (x*y*bpp);  //[1]
    for (i = 0; i < bpp; i++) { //[2]
    [...]

Since x, y, and bpp are all of integers, when all multiplied at [1], the resulting value passed into SDL_malloc is also of type integer. As noted before, since bpp can be 0x0-0xffffffff, this results in an integer overflow for a high enough value of bpp, causing the value passed into SDL_malloc to be less than bpp itself. This causes the subsequent usages of bpp to be outside the bounds of the allocated buffer and possible memory corruption. Due to line [2], bpp does have some bounds, such that (bpp < 0) => causes the corruption loop to skip. The tricky part with this bug is how much one could actually get away with corrupting as the writes in memory occur with the following code:

while (length-- > 0) { 
            *d = *t++;  //[1]
            d += bpp;   //[2]
        }

So while it is technically a controlled write, since we control the value of t at [1], remember that bpp must be rather large, and also cannot be negative. Thus, when the destination pointer d is incremented by bpp at [2], the odds of hitting unmapped memory increase exponentially for each loop iteration. On a 32-bit machine, the probability of successful exploitation with this bug would increase tremendously though as the pointer d would wrap around a lot faster, leaving less chance for segfaulting due to unmapped memory access.

Crash Information

(Note: Compiled with ASAN)

Program received signal SIGSEGV, Segmentation fault.
0x00007fe914baf213 in load_xcf_tile_rle (src=0x60700000dfb0, len=0x309b, bpp=0x40000003, x=0x40, y=0x40) at IMG_xcf.c:527
warning: Source file is more recent than executable.
527             val = *t++; //pull next byte for value to write.
-----------------------------------------------------[ registers ]----
$rax   : 0x0000627040015103 -> 0x0000000000000000 -> 0x0000000000000000
$rbx   : 0x00007ffcb1150ba0 -> 0x00007ffcb1152462 -> 0x643530342f464358 -> 0x643530342f464358 ("XCF/405d"?)
$rdx   : 0x00000000000000e2 -> 0x00000000000000e2
$rsp   : 0x00007ffcb1150840 -> 0x00007ffcb11508b0 -> 0x00007ffcb1150960 -> 0x00007ffcb1150a20 -> 0x00007ffcb1150a60 -> 
 0x00007ffcb1150a90 -> 0x0000000000000002 -> 0x0000000000000002
$rbp   : 0x00007ffcb11508b0 -> 0x00007ffcb1150960 -> 0x00007ffcb1150a20 -> 0x00007ffcb1150a60 -> 0x00007ffcb1150a90 -> 
0x0000000000000002 -> 0x0000000000000002
$rsi   : 0x0000000000000013 -> 0x0000000000000013
$rdi   : 0x00007fe915139540 -> 0x0000000000000014 -> 0x0000000000000014
$rip   : 0x00007fe914baf213 -> 0x489848a0458b1088 -> 0x489848a0458b1088
$r8    : 0x0000000000000008 -> 0x0000000000000008
$r9    : 0x0000000000000000 -> 0x0000000000000000
$r10   : 0x0000000000000000 -> 0x0000000000000000
$r11   : 0x0000000000000008 -> 0x0000000000000008
$r12   : 0x00000000004bd3e4 -> <_start+0> xor ebp, ebp
$r13   : 0x00007ffcb1150b90 -> 0x0000000000000002 -> 0x0000000000000002
$r14   : 0xfffffffffffffff8 -> 0xfffffffffffffff8
$r15   : 0x0000000000000000 -> 0x0000000000000000
$eflags: [CARRY PARITY ADJUST zero SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
---------------------------------------------------------[ stack ]----
0x00007ffcb1150840|+0x00: 0x00007ffcb11508b0 -> 0x00007ffcb1150960 -> 0x00007ffcb1150a20 -> 0x00007ffcb1150a60 -> 
0x00007ffcb1150a90 -> 0x0000000000000002 -> 0x0000000000000002        <-  
$rsp
0x00007ffcb1150848|+0x08: 0x0000004000000040 -> 0x0000000000000000 -> 0x0000000000000000
0x00007ffcb1150850|+0x10: 0x0000309b40000003 -> 0x0000309b40000003
0x00007ffcb1150858|+0x18: 0x000060700000dfb0 -> 0x00007fe914e11e48 -> 0x20ec8348e5894855 -> 0x20ec8348e5894855
0x00007ffcb1150860|+0x20: 0x0000000000000000 -> 0x0000000000000000
0x00007ffcb1150868|+0x28: 0xe2007fe913d1197d
0x00007ffcb1150870|+0x30: 0x0000627000015100 -> 0xbebebebebebebee2
0x00007ffcb1150878|+0x38: 0x0000309b14e11f03 -> 0x0000309b14e11f03
----------------------------------------------[ code:i386:x86-64 ]----
0x7fe914baf1ff <load_xcf_tile_rle+384> mov    BYTE PTR [rbp-0x41], al
0x7fe914baf202 <load_xcf_tile_rle+387> mov    DWORD PTR [rbp-0x20], 0x0
0x7fe914baf209 <load_xcf_tile_rle+394> jmp    0x7fe914baf222 <load_xcf_tile_rle+419>
0x7fe914baf20b <load_xcf_tile_rle+396> mov    rax, QWORD PTR [rbp-0x10]
0x7fe914baf20f <load_xcf_tile_rle+400> movzx  edx, BYTE PTR [rbp-0x41]
->0x7fe914baf213 <load_xcf_tile_rle+404> mov    BYTE PTR [rax], dl
0x7fe914baf215 <load_xcf_tile_rle+406> mov    eax, DWORD PTR [rbp-0x60]
0x7fe914baf218 <load_xcf_tile_rle+409> cdqe   
0x7fe914baf21a <load_xcf_tile_rle+411> add    QWORD PTR [rbp-0x10], rax
0x7fe914baf21e <load_xcf_tile_rle+415> add    DWORD PTR [rbp-0x20], 0x1
0x7fe914baf222 <load_xcf_tile_rle+419> mov    eax, DWORD PTR [rbp-0x20]
------------------------------------------[ source:IMG_xcf.c+527 ]----
523  
524          count += length; //count var unused, lol
525          size -= length;
526  
            // t=0x00007ffcb11508a8 -> [...] -> 0xe3e3e4f6e201df01, val=0xe2L
-> 527           val = *t++; //pull next byte for value to write.
528  
529          for (j = 0; j < length; j++) { //Write pixel-consecutive bytes.
530            *d = val; // oob write here... 405d
531            d += bpp;
-------------------------------------------------------[ threads ]----
[#0] Id 1, Name: "", stopped, reason: SIGSEGV
---------------------------------------------------------[ trace ]----
[#0] 0x7fe914baf213->Name: load_xcf_tile_rle(src=0x60700000dfb0, len=0x309b, bpp=0x40000003, x=0x40, y=0x40)
[#1] 0x7fe914baf55d->Name: do_layer_surface(surface=0x60800000be20, src=0x60700000dfb0, head=0x60700000df40, layer=0x60600000eea0,     
 load_tile=0x7fe914baf07f <load_xcf_tile_rle>)
[#2] 0x7fe914bafe24->Name: IMG_LoadXCF_RW(src=0x60700000dfb0)
[#3] 0x7fe914b8d7ef->Name: IMG_LoadTyped_RW(src=0x60700000dfb0, freesrc=0x1, type=0x0)
[#4] 0x7fe914b8d5e0->Name: IMG_Load(file=0x7ffcb1152462 "boop")
[#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.