Thelastede reported on https://github.com/thelastede/FreeImage-cve-poc/tree/master/CVE-2023-47994 a vulnerability found in freeimage 3.18's LoadPixelDataRLE4.
This is a summary of the report, aimed to forward the report upstream. For full details, please refer to the link of the original report, mentioned above.
An integer overflow vulnerability exits in PluginBMP.cpp::LoadPixelDataRLE4 in FreeImage 3.18.0, which may further cause buffer overflow that could allow attackers to conduct information disclosure, denial-of-service attacks and arbitrary code execution.
When parsing a bmp image, it will go to LoadWindowsBMP, the source code of this function is as follows
static FIBITMAP *
LoadWindowsBMP(FreeImageIO *io, fi_handle handle, int flags, unsigned bitmap_bits_offset, int type) {
FIBITMAP *dib = NULL;
try {
BOOL header_only = (flags & FIF_LOAD_NOPIXELS) == FIF_LOAD_NOPIXELS;
// load the info header
BITMAPINFOHEADER bih;
io->read_proc(&bih, sizeof(BITMAPINFOHEADER), 1, handle);
#ifdef FREEIMAGE_BIGENDIAN
SwapInfoHeader(&bih);
#endif
// keep some general information about the bitmap
unsigned used_colors = bih.biClrUsed;
int width = bih.biWidth;
int height = bih.biHeight; // WARNING: height can be < 0 => check each call using 'height' as a parameter
unsigned bit_count = bih.biBitCount;
unsigned compression = bih.biCompression;
unsigned pitch = CalculatePitch(CalculateLine(width, bit_count));
switch (bit_count) {
case 1 :
case 4 :
case 8 :
{
if ((used_colors == 0) || (used_colors > CalculateUsedPaletteEntries(bit_count))) {
used_colors = CalculateUsedPaletteEntries(bit_count);
}
// allocate enough memory to hold the bitmap (header, palette, pixels) and read the palette
dib = FreeImage_AllocateHeader(header_only, width, height, bit_count);
if (dib == NULL) {
throw FI_MSG_ERROR_DIB_MEMORY;
}
// set resolution information
FreeImage_SetDotsPerMeterX(dib, bih.biXPelsPerMeter);
FreeImage_SetDotsPerMeterY(dib, bih.biYPelsPerMeter);
// seek to the end of the header (depending on the BMP header version)
// type == sizeof(BITMAPVxINFOHEADER)
switch(type) {
case 40: // sizeof(BITMAPINFOHEADER) - all Windows versions since Windows 3.0
break;
case 52: // sizeof(BITMAPV2INFOHEADER) (undocumented)
case 56: // sizeof(BITMAPV3INFOHEADER) (undocumented)
case 108: // sizeof(BITMAPV4HEADER) - all Windows versions since Windows 95/NT4 (not supported)
case 124: // sizeof(BITMAPV5HEADER) - Windows 98/2000 and newer (not supported)
io->seek_proc(handle, (long)(type - sizeof(BITMAPINFOHEADER)), SEEK_CUR);
break;
}
// load the palette
io->read_proc(FreeImage_GetPalette(dib), used_colors * sizeof(RGBQUAD), 1, handle);
#if FREEIMAGE_COLORORDER == FREEIMAGE_COLORORDER_RGB
RGBQUAD *pal = FreeImage_GetPalette(dib);
for(int i = 0; i < used_colors; i++) {
INPLACESWAP(pal[i].rgbRed, pal[i].rgbBlue);
}
#endif
if(header_only) {
// header only mode
return dib;
}
// seek to the actual pixel data.
// this is needed because sometimes the palette is larger than the entries it contains predicts
io->seek_proc(handle, bitmap_bits_offset, SEEK_SET);
// read the pixel data
switch (compression) {
case BI_RGB :
if( LoadPixelData(io, handle, dib, height, pitch, bit_count) ) {
return dib;
} else {
throw "Error encountered while decoding BMP data";
}
break;
case BI_RLE4 :
if( LoadPixelDataRLE4(io, handle, width, height, dib) ) {
return dib;
} else {
throw "Error encountered while decoding RLE4 BMP data";
}
break;
case BI_RLE8 :
if( LoadPixelDataRLE8(io, handle, width, height, dib) ) {
return dib;
} else {
throw "Error encountered while decoding RLE8 BMP data";
}
break;
default :
throw FI_MSG_ERROR_UNSUPPORTED_COMPRESSION;
}
}
break; // 1-, 4-, 8-bit
case 16 :
{
int use_bitfields = 0;
if (bih.biCompression == BI_BITFIELDS) use_bitfields = 3;
else if (bih.biCompression == BI_ALPHABITFIELDS) use_bitfields = 4;
else if (type == 52) use_bitfields = 3;
else if (type >= 56) use_bitfields = 4;
if (use_bitfields > 0) {
DWORD bitfields[4];
io->read_proc(bitfields, use_bitfields * sizeof(DWORD), 1, handle);
dib = FreeImage_AllocateHeader(header_only, width, height, bit_count, bitfields[0], bitfields[1], bitfields[2]);
} else {
dib = FreeImage_AllocateHeader(header_only, width, height, bit_count, FI16_555_RED_MASK, FI16_555_GREEN_MASK, FI16_555_BLUE_MASK);
}
if (dib == NULL) {
throw FI_MSG_ERROR_DIB_MEMORY;
}
// set resolution information
FreeImage_SetDotsPerMeterX(dib, bih.biXPelsPerMeter);
FreeImage_SetDotsPerMeterY(dib, bih.biYPelsPerMeter);
if(header_only) {
// header only mode
return dib;
}
// seek to the actual pixel data
io->seek_proc(handle, bitmap_bits_offset, SEEK_SET);
// load pixel data and swap as needed if OS is Big Endian
LoadPixelData(io, handle, dib, height, pitch, bit_count);
return dib;
}
break; // 16-bit
case 24 :
case 32 :
{
int use_bitfields = 0;
if (bih.biCompression == BI_BITFIELDS) use_bitfields = 3;
else if (bih.biCompression == BI_ALPHABITFIELDS) use_bitfields = 4;
else if (type == 52) use_bitfields = 3;
else if (type >= 56) use_bitfields = 4;
if (use_bitfields > 0) {
DWORD bitfields[4];
io->read_proc(bitfields, use_bitfields * sizeof(DWORD), 1, handle);
dib = FreeImage_AllocateHeader(header_only, width, height, bit_count, bitfields[0], bitfields[1], bitfields[2]);
} else {
if( bit_count == 32 ) {
dib = FreeImage_AllocateHeader(header_only, width, height, bit_count, FI_RGBA_RED_MASK, FI_RGBA_GREEN_MASK, FI_RGBA_BLUE_MASK);
} else {
dib = FreeImage_AllocateHeader(header_only, width, height, bit_count, FI_RGBA_RED_MASK, FI_RGBA_GREEN_MASK, FI_RGBA_BLUE_MASK);
}
}
if (dib == NULL) {
throw FI_MSG_ERROR_DIB_MEMORY;
}
// set resolution information
FreeImage_SetDotsPerMeterX(dib, bih.biXPelsPerMeter);
FreeImage_SetDotsPerMeterY(dib, bih.biYPelsPerMeter);
if(header_only) {
// header only mode
return dib;
}
// Skip over the optional palette
// A 24 or 32 bit DIB may contain a palette for faster color reduction
// i.e. you can have (FreeImage_GetColorsUsed(dib) > 0)
// seek to the actual pixel data
io->seek_proc(handle, bitmap_bits_offset, SEEK_SET);
// read in the bitmap bits
// load pixel data and swap as needed if OS is Big Endian
LoadPixelData(io, handle, dib, height, pitch, bit_count);
// check if the bitmap contains transparency, if so enable it in the header
FreeImage_SetTransparent(dib, (FreeImage_GetColorType(dib) == FIC_RGBALPHA));
return dib;
}When parsing a bmp image, it will go to LoadWindowsBMP, the source code of this function is as follows
break; // 24-, 32-bit
}
} catch(const char *message) {
if(dib) {
FreeImage_Unload(dib);
}
if(message) {
FreeImage_OutputMessageProc(s_format_id, message);
}
}
return NULL;
}
The function reads the height and width of the image in the following statement and writes them to bih.biHeight and bih.biWidth, and then assigns the values to height and width:
io->read_proc(&bih, sizeof(BITMAPINFOHEADER), 1, handle);
When compression is BI_RLE4, it ends up in LoadPixelDataRLE4. However, it is worth noting that height and width are four-byte unsigned integers and can be arbitrarily defined by the user.
The LoadPixelDataRLE4 function assigns a buffer size to pixel, this is done by calculating width * height * sizeof(BYTE), which also results in a 4-byte unsigned integer. However, both width and height are input-controllable 4-byte unsigned integers. Thus an integer overflow can easily occur by simply constructing a larger value on input, making the input argument to malloc a smaller value.
pixels = (BYTE*)malloc(width * height * sizeof(BYTE));
Finally, the following loop will be executed, where src is a pointer to (BYTE*)pixels + y * width, however, if height is a larger value, the address pointed to by src will cross the range of the heap buffer that was just allocated by the malloc after a number of loops, which will result in a heap out-of-bounds read, as well as an out-of-bounds write to dst:
for(int y = 0; y < height; y++) {
const BYTE *src = (BYTE*)pixels + y * width; // Heap out-of-bounds read
BYTE *dst = FreeImage_GetScanLine(dib, y);
BOOL hinibble = TRUE;
for (int cols = 0; cols < width; cols++){
if (hinibble) {
dst[cols >> 1] = (src[cols] << 4);
} else {
dst[cols >> 1] |= src[cols];
}
hinibble = !hinibble;
}
}
The original report includes instructions about how to reproduce the vulnerability.