Talos Vulnerability Report

TALOS-2017-0499

Simple DirectMedia Layer SDL2_Image LWZ Decompression Buffer Overflow Vulnerability

March 1, 2018
CVE Number

CVE-2017-14450

Summary

A buffer overflow vulnerability exists in the GIF image parsing functionality of SDL2_image-2.0.2. A specially crafted GIF image can lead to a buffer overflow on a global section. An attacker can display an 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

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

CWE

CWE-121: Stack-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 a GIF image, LibSDL2_Image allocates a buffer of appropriate size and then proceeds to populate the buffer with the appropriate pixel data, as expected. When dealing with GIF images, the color data is LZW compressed, and decompression must naturally occur before the image data buffer can be populated. For an in-depth overview of LZW, please refer to http://giflib.sourceforge.net/whatsinagif/lzwimagedata.html.

Needless to say, the code for this is a little complex, so it will hopefully broken down into easier pieces below. The code starts here:

// IMG_gif.c:541
static Image * ReadImage(SDL_RWops * src, int len, int height, int cmapSize,
     unsigned char cmap[3][MAXCOLORMAPSIZE],
    int gray, int interlace, int ignore)
{
        Image *image;
        unsigned char c;
        int i, v;
        int xpos = 0, ypos = 0, pass = 0;

     /*
     **  Initialize the compression routines
      */
        if (!ReadOK(src, &c, 1)) {
        RWSetMsg("EOF / read error on image data");
         return NULL;
        }
        if (LWZReadByte(src, TRUE, c) < 0) {        //[1]
            RWSetMsg("error reading image");
            return NULL;
    [...]
        image = ImageNewCmap(len, height, cmapSize);

        for (i = 0; i < cmapSize; i++)
            ImageSetCmap(image, i, cmap[CM_RED][i],

    cmap[CM_GREEN][i], cmap[CM_BLUE][i]);

        while ((v = LWZReadByte(src, FALSE, c)) >= 0) {   //[2]
            #ifdef USED_BY_SDL
                ((Uint8 *)image->pixels)[xpos + ypos * image->pitch] = v;
            #else
                image->data[xpos + ypos * len] = v;
    [...]

The ReadImage function decompresses the GIF image via the LWZReadBytes function called at [1] and [2]. At [1], the first call is just used to initialize the state of the LWZReadBytes function, and all of the static variables inside, this is what the 'TRUE' bool is used for. After this, LWZReadBytes is called inside of a loop at [2], to actually grab the data from the file and begin to populate the pixel data, whose code we will ignore, as the bug path leads into the LWZReadByte funtion:

    static int LWZReadByte(SDL_RWops *src, int flag, int input_code_size)
{
        static int fresh = FALSE; 
    int code, incode;                        //[1]
        static int code_size, set_code_size;                 //[2]
        static int max_code, max_code_size;              
        static int firstcode, oldcode;
        static int clear_code, end_code;
    static int table[2][(1 << MAX_LWZ_BITS)];        //[4]
        static int stack[(1 << (MAX_LWZ_BITS)) * 2], *sp;   //[5]
        register int i;

        /* Fixed buffer overflow found by Michael Skladnikiewicz */
        if (input_code_size > MAX_LWZ_BITS)
         return -1;

        if (flag) { //Flag => initialization
    […]

    if (sp > stack)
                return *--sp;

        while ((code = GetCode(src, code_size, FALSE)) >= 0) { //[3]

As mentioned, the code is going to get somewhat complex here, so heres a quick overview of the more relevant variables above: At [1], the code is given, which describes the current LZW code. This can be a value in the range of (1, 1<<codesize), which in this case is (0x1,0x200). This limit is enforced inside of the GetCode(src, codesize,FALSE) call at [3]. The table variable at [4] is used to store the known LZW code sequences, and is typically known as a LZW code table. The stack variable at [5] is used to store a stack of LZW sequences, with the *sp pointer being assigned to the address of 'stack' at initialization. Regardless of of all of this, the bug just mainly involves the following code segment:

while ((code = GetCode(src, code_size, FALSE)) >= 0) {
[...]
// not clear code and not end code
        while (code >= clear_code) {    // [1]
            /* Guard against buffer overruns */
            if (code < 0 || code >= (1 << MAX_LWZ_BITS)) { //[2]
                RWSetMsg("invalid LWZ data");
                return -3;
            }
            *sp++ = table[1][code];     //[3]
            if (code == table[0][code])
                    RWSetMsg("circular table entry BIG ERROR");
            code = table[0][code];      //[4]
}

Assuming that the LWZ code found is greater than the clear code (0x100) at [1], the decompression algorithm will start to unpack the LWZ codes into the LWZ stack, which is done at [3]. The underlying issue lies in that, even though there's a check on buffer overruns at [2], if we insert a value into table[0][code] such that (table[0][code] == code && code >= clear_code), then the while loop at [1] will never exit, and *sp will be continually advanced past the bounds of stack variable, causing an overflow.

It should be noted that there are quite a few restrictions to this OOB write, since the 'stack' variable is a static variable, the OOB write actually occurs in the global variable section in memory for the LibSDL2_image library, and not the stack itself, such that only other global library variables below 'stack' can be written.

Also, the values written can only be valid LWZ codes, such that the range is restricted to values (1,1<<(code_size)), as mentioned before, which is (0x1,0x200). Also of note is that the pointer being incremented is a (int ) pointer, and not a (char ) pointer, so the values in memory will look as such (for code 0x77):

<(^_^)> x/40gx sp-0x40
0x7ffff7a63f04: 0x0000007700000077      0x0000007700000077
0x7ffff7a63f14: 0x0000007700000077      0x0000007700000077
0x7ffff7a63f24: 0x0000007700000077      0x0000007700000077
0x7ffff7a63f34: 0x0000007700000077      0x0000007700000077

Crash Information

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff782b552 in LWZReadByte (src=0xc63e70, flag=0x0, input_code_size=0x8) at IMG_gif.c:499
499             /* Guard against buffer overruns */
---------------[ registers ]----
$rax   : 0x00007ffff7a64000 -> 0x00010102464c457f
$rbx   : 0x0000000000000000
$rcx   : 0x00000000000044f4
$rdx   : 0x0000000000000077
$rsp   : 0x00007fffffffdb40 -> 0x0000000000000008
$rbp   : 0x00007fffffffdc70 -> 0x00007fffffffdcc0 -> 0x00007fffffffe050 -> 0x00007fffffffe090 -> 0x00007fffffffe0c0 -> 0x00007fffffffe100 ->    
0x0000000000000000
$rsi   : 0x00007fffffffd880 -> "circular table entry BIG ERROR"
$rdi   : 0x0000000000000001
$rip   : 0x00007ffff782b552 -> <LWZReadByte+1002> mov DWORD PTR [rax], edx
$r8    : 0x000000000000ffff
$r9    : 0x65875f9a7a257d5e
$r10   : 0x691bd05d945a5a55
$r11   : 0x0000000000000206
$r12   : 0x0000000000400a10 -> <_start+0> xor ebp, ebp
$r13   : 0x00007fffffffe1e0 -> 0x0000000000000002
$r14   : 0x0000000000000000
$r15   : 0x0000000000000000
$eflags: [carry parity adjust zero sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
-------------------[ stack ]----
0x00007fffffffdb40|+0x00: 0x0000000000000008    <-$rsp
0x00007fffffffdb48|+0x08: 0x0000000000c63e70 -> 0x00007ffff7aace48 -> <stdio_size+0> push rbp
0x00007fffffffdb50|+0x10: 0x0000000000000080
0x00007fffffffdb58|+0x18: 0x0000000000000001
0x00007fffffffdb60|+0x20: 0x00007fffffffdb90 -> 0x00007fffffffdbe0 -> 0x00007fffffffdc20 -> 0x00007fffffffdc60 -> 0x00007fffffffdcc0 ->    
0x00007fffffffe050 -> 0x00007fffffffe090
0x00007fffffffdb68|+0x28: 0x0000000000c28960 -> 0x0000000000000000
0x00007fffffffdb70|+0x30: 0x00007fffffffdb90 -> 0x00007fffffffdbe0 -> 0x00007fffffffdc20 -> 0x00007fffffffdc60 -> 0x00007fffffffdcc0 ->    
0x00007fffffffe050 -> 0x00007fffffffe090
0x00007fffffffdb78|+0x38: 0x00007ffff7b59d50 -> <SDL_AllocBlitMap+23> mov QWORD PTR [rbp-0x8], rax
--------[ code:i386:x86-64 ]----

0x7ffff782b536 <LWZReadByte+974> movsxd rdx, edx 0x7ffff782b539 <LWZReadByte+977> add rdx, 0x1000 0x7ffff782b540 <LWZReadByte+984> lea rcx, [rdx4+0x0] 0x7ffff782b548 <LWZReadByte+992> lea rdx, [rip+0x2282f1] # 0x7ffff7a53840 <table.9973> 0x7ffff782b54f <LWZReadByte+999> mov edx, DWORD PTR [rcx+rdx1] ->0x7ffff782b552 <LWZReadByte+1002> mov DWORD PTR [rax], edx 0x7ffff782b554 <LWZReadByte+1004> mov eax, DWORD PTR [rbp-0x14] 0x7ffff782b557 <LWZReadByte+1007> cdqe
0x7ffff782b559 <LWZReadByte+1009> lea rdx, [rax4+0x0] 0x7ffff782b561 <LWZReadByte+1017> lea rax, [rip+0x2282d8] # 0x7ffff7a53840 <table.9973> 0x7ffff782b568 <LWZReadByte+1024> mov eax, DWORD PTR [rdx+rax1]

----[ source:IMG_gif.c+499 ]----
495          *sp++ = firstcode;
496          code = oldcode;
497      }
498      while (code >= clear_code) {
-> 499           /* Guard against buffer overruns */
500          if (code < 0 || code >= (1 << MAX_LWZ_BITS)) {
501              RWSetMsg("invalid LWZ data");
502              return -3;
503          }
-----------------[ threads ]----
[#0] Id 5, Name: "img_read_plain", stopped, reason: SIGSEGV
[#1] Id 4, Name: "img_read_plain", stopped, reason: SIGSEGV
[#2] Id 3, Name: "img_read_plain", stopped, reason: SIGSEGV
[#3] Id 2, Name: "img_read_plain", stopped, reason: SIGSEGV
[#4] Id 1, Name: "img_read_plain", stopped, reason: SIGSEGV
-------------------[ trace ]----
[#0] 0x7ffff782b552->Name: LWZReadByte(src=0xc63e70, flag=0x0, input_code_size=0x8)
[#1] 0x7ffff782b9a2->Name: ReadImage(src=0xc63e70, len=0x200, height=0x200, cmapSize=0x100, cmap=0x7ffff7a53288 <GifScreen+8>,    
gray=0x0, interlace=0x0, ignore=0x0)
[#2] 0x7ffff782ac66->Name: IMG_LoadGIF_RW(src=0xc63e70)
[#3] 0x7ffff78287ef->Name: IMG_LoadTyped_RW(src=0xc63e70, freesrc=0x1, type=0x0)
[#4] 0x7ffff78285e0->Name: IMG_Load(file=0x7fffffffe4e0 "gif_crashes/0345ae792a4ed85e785571105a430417")
[#5] 0x400b85->Name: main(argc=0x2, argv=0x7fffffffe1e8)
------------------------------------------------------------------------------------------------------------------------

Timeline

2017-11-27 - Vendor Disclosure
2018-03-01 - Public Release

Credit

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