Thelastede reported on https://github.com/thelastede/FreeImage-cve-poc/tree/master/CVE-2023-47995 a vulnerability in freeimage 3.18's BitmapAccess.cpp::FreeImage_AllocateBitmap:
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.
A vulnerability of memory allocation with excessive size value exits in BitmapAccess.cpp::FreeImage_AllocateBitmap in FreeImage 3.18.0, which allows attackers to conduct denial-of-service attacks.
When an image is read in through LoadU, it goes to the Load function and performs a series of initialization and processing operations. When it comes to step 5b, the step that allocates memory to the dib and init header, if the image is an RGB or greyscale image, it will go to this branch below:
// RGB or greyscale image
dib = FreeImage_AllocateHeader(header_only, cinfo.output_width, cinfo.output_height, 8 * cinfo.output_components, FI_RGBA_RED_MASK, FI_RGBA_GREEN_MASK, FI_RGBA_BLUE_MASK);
if(!dib) throw FI_MSG_ERROR_DIB_MEMORY;
Notice that the call to FreeImage_AllocateHeader passes cinfo.output_width and cinfo.output_height as parameters, both of which are user input controllable. This is the code of
FreeImage_AllocateHeader:
static FIBITMAP *
FreeImage_AllocateBitmap(BOOL header_only, BYTE *ext_bits, unsigned ext_pitch, FREE_IMAGE_TYPE type, int width, int height, int bpp, unsigned red_mask, unsigned green_mask, unsigned blue_mask) {
// check input variables
width = abs(width);
height = abs(height);
if(!((width > 0) && (height > 0))) {
return NULL;
}
if(ext_bits) {
if(ext_pitch == 0) {
return NULL;
}
assert(header_only == FALSE);
}
// we only store the masks (and allocate memory for them) for 16-bit images of type FIT_BITMAP
BOOL need_masks = FALSE;
// check pixel bit depth
switch(type) {
case FIT_BITMAP:
switch(bpp) {
case 1:
case 4:
case 8:
break;
case 16:
need_masks = TRUE;
break;
case 24:
case 32:
break;
default:
bpp = 8;
break;
}
break;
case FIT_UINT16:
bpp = 8 * sizeof(unsigned short);
break;
case FIT_INT16:
bpp = 8 * sizeof(short);
break;
case FIT_UINT32:
bpp = 8 * sizeof(DWORD);
break;
case FIT_INT32:
bpp = 8 * sizeof(LONG);
break;
case FIT_FLOAT:
bpp = 8 * sizeof(float);
break;
case FIT_DOUBLE:
bpp = 8 * sizeof(double);
break;
case FIT_COMPLEX:
bpp = 8 * sizeof(FICOMPLEX);
break;
case FIT_RGB16:
bpp = 8 * sizeof(FIRGB16);
break;
case FIT_RGBA16:
bpp = 8 * sizeof(FIRGBA16);
break;
case FIT_RGBF:
bpp = 8 * sizeof(FIRGBF);
break;
case FIT_RGBAF:
bpp = 8 * sizeof(FIRGBAF);
break;
default:
return NULL;
}
FIBITMAP *bitmap = (FIBITMAP *)malloc(sizeof(FIBITMAP));
if (bitmap != NULL) {
// calculate the size of a FreeImage image
// align the palette and the pixels on a FIBITMAP_ALIGNMENT bytes alignment boundary
// palette is aligned on a 16 bytes boundary
// pixels are aligned on a 16 bytes boundary
// when using a user provided pixel buffer, force a 'header only' allocation
size_t dib_size = FreeImage_GetInternalImageSize(header_only || ext_bits, width, height, bpp, need_masks);
if(dib_size == 0) {
// memory allocation will fail (probably a malloc overflow)
free(bitmap);
return NULL;
}
bitmap->data = (BYTE *)FreeImage_Aligned_Malloc(dib_size * sizeof(BYTE), FIBITMAP_ALIGNMENT);
if (bitmap->data != NULL) {
memset(bitmap->data, 0, dib_size);
// write out the FREEIMAGEHEADER
FREEIMAGEHEADER *fih = (FREEIMAGEHEADER *)bitmap->data;
fih->type = type;
memset(&fih->bkgnd_color, 0, sizeof(RGBQUAD));
fih->transparent = FALSE;
fih->transparency_count = 0;
memset(fih->transparent_table, 0xff, 256);
fih->has_pixels = header_only ? FALSE : TRUE;
// initialize FIICCPROFILE link
FIICCPROFILE *iccProfile = FreeImage_GetICCProfile(bitmap);
iccProfile->size = 0;
iccProfile->data = 0;
iccProfile->flags = 0;
// initialize metadata models list
fih->metadata = new(std::nothrow) METADATAMAP;
// initialize attached thumbnail
fih->thumbnail = NULL;
// store a pointer to user provided pixel buffer (if any)
fih->external_bits = ext_bits;
fih->external_pitch = ext_pitch;
// write out the BITMAPINFOHEADER
BITMAPINFOHEADER *bih = FreeImage_GetInfoHeader(bitmap);
bih->biSize = sizeof(BITMAPINFOHEADER);
bih->biWidth = width;
bih->biHeight = height;
bih->biPlanes = 1;
bih->biCompression = need_masks ? BI_BITFIELDS : BI_RGB;
bih->biBitCount = (WORD)bpp;
bih->biClrUsed = CalculateUsedPaletteEntries(bpp);
bih->biClrImportant = bih->biClrUsed;
bih->biXPelsPerMeter = 2835; // 72 dpi
bih->biYPelsPerMeter = 2835; // 72 dpi
if(bpp == 8) {
// build a default greyscale palette (very useful for image processing)
RGBQUAD *pal = FreeImage_GetPalette(bitmap);
for(int i = 0; i < 256; i++) {
pal[i].rgbRed = (BYTE)i;
pal[i].rgbGreen = (BYTE)i;
pal[i].rgbBlue = (BYTE)i;
}
}
// just setting the masks (only if needed) just like the palette.
if (need_masks) {
FREEIMAGERGBMASKS *masks = FreeImage_GetRGBMasks(bitmap);
masks->red_mask = red_mask;
masks->green_mask = green_mask;
masks->blue_mask = blue_mask;
}
return bitmap;
}
free(bitmap);
}
return NULL;
}
Notice that this function gets the dib_size via FreeImage_GetInternalImageSize. The relevant definition is as follows:
static size_t
FreeImage_GetInternalImageSize(BOOL header_only, unsigned width, unsigned height, unsigned bpp, BOOL need_masks) {
size_t dib_size = sizeof(FREEIMAGEHEADER);
dib_size += (dib_size % FIBITMAP_ALIGNMENT ? FIBITMAP_ALIGNMENT - dib_size % FIBITMAP_ALIGNMENT : 0);
dib_size += FIBITMAP_ALIGNMENT - sizeof(BITMAPINFOHEADER) % FIBITMAP_ALIGNMENT;
dib_size += sizeof(BITMAPINFOHEADER);
// palette is aligned on a 16 bytes boundary
dib_size += sizeof(RGBQUAD) * CalculateUsedPaletteEntries(bpp);
// we both add palette size and masks size if need_masks is true, since CalculateUsedPaletteEntries
// always returns 0 if need_masks is true (which is only true for 16 bit images).
dib_size += need_masks ? sizeof(DWORD) * 3 : 0;
dib_size += (dib_size % FIBITMAP_ALIGNMENT ? FIBITMAP_ALIGNMENT - dib_size % FIBITMAP_ALIGNMENT : 0);
if(!header_only) {
const size_t header_size = dib_size;
// pixels are aligned on a 16 bytes boundary
dib_size += (size_t)CalculatePitch(CalculateLine(width, bpp)) * (size_t)height;
// check for possible malloc overflow using a KISS integer overflow detection mechanism
{
const double dPitch = floor( ((double)bpp * width + 31.0) / 32.0 ) * 4.0;
const double dImageSize = (double)header_size + dPitch * height;
if(dImageSize != (double)dib_size) {
// here, we are sure to encounter a malloc overflow: try to avoid it ...
return 0;
}
/*
The following constant take into account the additionnal memory used by
aligned malloc functions as well as debug malloc functions.
It is supposed here that using a (8 * FIBITMAP_ALIGNMENT) risk margin will be enough
for the target compiler.
*/
const double FIBITMAP_MAX_MEMORY = (double)((size_t)-1) - 8 * FIBITMAP_ALIGNMENT;
if(dImageSize > FIBITMAP_MAX_MEMORY) {
// avoid possible overflow inside C allocation functions
return 0;
}
}
}
return dib_size;
}
One of the statements exists as follows. it will calculate the value based on width, bpp, height and add it to dib_size.
dib_size += (size_t)CalculatePitch(CalculateLine(width, bpp)) * (size_t)height;
CalculatePitch and CalculateLine are defined below.
inline unsigned
CalculateLine(const unsigned width, const unsigned bitdepth) {
return (unsigned)( ((unsigned long long)width * bitdepth + 7) / 8 );
}
inline unsigned
CalculatePitch(const unsigned line) {
return (line + 3) & ~3;
}
Subsequently, a heap is dynamically requested based on the dib_size, when in fact the size of the height and width can be carefully constructed so that a heap of dib_size can be requested to run out of computer memory. Therefore, when the dib_size value is returned and FreeImage_Aligned_Malloc is called, the computer may run out of memory, leading to a Dos attack.
bitmap->data = (BYTE *)FreeImage_Aligned_Malloc(dib_size * sizeof(BYTE), FIBITMAP_ALIGNMENT);
void* FreeImage_Aligned_Malloc(size_t amount, size_t alignment) {
assert(alignment == FIBITMAP_ALIGNMENT);
return _aligned_malloc(amount, alignment);
}
Instructions to reproduce the bug can be found in the original report.