Menu

#359 CVE-2023-47994: integer overflow in LoadPixelDataRLE4 function in PluginBMP.cpp

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

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.

Discussion


Log in to post a comment.

MongoDB Logo MongoDB