Menu

#360 CVE-2023-47995: memory allocation with excessive size value in BitmapAccess.cpp::FreeImage_AllocateBitmap

open
nobody
None
5
2024-10-03
2024-10-03
No

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.

Related

Patches: #164

Discussion


Log in to post a comment.

MongoDB Logo MongoDB