Menu

#358 CVE-2023-47993: buffer out-of-bound read vulnerability in Exif.cpp::ReadInt32 in FreeImage 3.18.0

open
nobody
None
5
2024-09-23
2024-09-23
No

Thelastede reported on https://github.com/thelastede/FreeImage-cve-poc/tree/master/CVE-2023-47993 a vulnerability found on freeimage 3.18's Exif.cpp::ReadInt32.

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.

When loading a jpeg image via the FreeImage_LoadU function, a further call is made to the jpeg_read_exif_dir, whose 3.18 code is:

/**
Process Exif directory

@param dib Input FIBITMAP
@param tiffp Pointer to the TIFF header
@param dwOffsetIfd0 Offset to the 0th IFD (first IFD)
@param dwLength Length of the Exif file
@param dwProfileOffset File offset to be used when reading 'offset/value' tags
@param msb_order Endianness order of the Exif file (TRUE if big-endian, FALSE if little-endian)
@param starting_md_model Metadata model of the IFD (should be TagLib::EXIF_MAIN for a jpeg)
@return Returns TRUE if sucessful, returns FALSE otherwise
/
static BOOL
jpeg_read_exif_dir(FIBITMAP
dib, const BYTE *tiffp, DWORD dwOffsetIfd0, DWORD dwLength, DWORD dwProfileOffset, BOOL msb_order, TagLib::MDMODEL starting_md_model) {
WORD de, nde;

std::stack<WORD>            destack;    // directory entries stack
std::stack<const BYTE*>     ifdstack;   // IFD stack
std::stack<TagLib::MDMODEL> modelstack; // metadata model stack

// Keep a list of already visited IFD to avoid stack overflows 
// when recursive/cyclic directory structures exist. 
// This kind of recursive Exif file was encountered with Kodak images coming from 
// KODAK PROFESSIONAL DCS Photo Desk JPEG Export v3.2 W
std::map<DWORD, int> visitedIFD;

/*
"An Image File Directory (IFD) consists of a 2-byte count of the number of directory
entries (i.e. the number of fields), followed by a sequence of 12-byte field
entries, followed by a 4-byte offset of the next IFD (or 0 if none)."
The "next IFD" (1st IFD) is the thumbnail.
*/
#define DIR_ENTRY_ADDR(_start, _entry) (_start + 2 + (12 * _entry))

// set the metadata model to Exif

TagLib::MDMODEL md_model = starting_md_model;

// set the pointer to the first IFD (0th IFD) and follow it were it leads.

const BYTE *ifd0th = (BYTE*)tiffp + (size_t)dwOffsetIfd0;

const BYTE *ifdp = ifd0th;

de = 0;

do {
    // if there is anything on the stack then pop it off
    if(!destack.empty()) {
        ifdp        = ifdstack.top();   ifdstack.pop();
        de          = destack.top();    destack.pop();
        md_model    = modelstack.top(); modelstack.pop();
    }

    // remember that we've visited this directory and entry so that we don't visit it again later
    DWORD visited = (DWORD)( (((size_t)ifdp & 0xFFFF) << 16) | (size_t)de );
    if(visitedIFD.find(visited) != visitedIFD.end()) {
        continue;
    } else {
        visitedIFD[visited] = 1;    // processed
    }

    // determine how many entries there are in the current IFD
    nde = ReadUint16(msb_order, ifdp);
    if (((size_t)(ifdp - tiffp) + 12 * nde) > (size_t)dwLength) {
        // suspicious IFD offset, ignore
        continue;
    }

    for(; de < nde; de++) {
        char *pde = NULL;   // pointer to the directory entry
        char *pval = NULL;  // pointer to the tag value

        // create a tag
        FITAG *tag = FreeImage_CreateTag();
        if(!tag) return FALSE;

        // point to the directory entry
        pde = (char*) DIR_ENTRY_ADDR(ifdp, de);

        // get the tag ID
        WORD tag_id = ReadUint16(msb_order, pde);
        FreeImage_SetTagID(tag, tag_id);

        // get the tag type
        WORD tag_type = (WORD)ReadUint16(msb_order, pde + 2);
        if((tag_type - 1) >= EXIF_NUM_FORMATS) {
            // a problem occured : delete the tag (not free'd after)
            FreeImage_DeleteTag(tag);
            // break out of the for loop
            break;
        }
        FreeImage_SetTagType(tag, (FREE_IMAGE_MDTYPE)tag_type);

        // get number of components
        DWORD tag_count = ReadUint32(msb_order, pde + 4);
        FreeImage_SetTagCount(tag, tag_count);

        // check that tag length (size of the tag value in bytes) will fit in a DWORD
        unsigned tag_data_width = FreeImage_TagDataWidth(FreeImage_GetTagType(tag));
        if (tag_data_width != 0 && FreeImage_GetTagCount(tag) > ~(DWORD)0 / tag_data_width) {
            FreeImage_DeleteTag(tag);
            // jump to next entry
            continue;
        }
        FreeImage_SetTagLength(tag, FreeImage_GetTagCount(tag) * tag_data_width);

        if(FreeImage_GetTagLength(tag) <= 4) {
            // 4 bytes or less and value is in the dir entry itself
            pval = pde + 8;
        } else {
            // if its bigger than 4 bytes, the directory entry contains an offset               
            DWORD offset_value = ReadUint32(msb_order, pde + 8);
            // the offset can be relative to tiffp or to an external reference (see JPEG-XR)
            if(dwProfileOffset) {
                offset_value -= dwProfileOffset;
            }
            // first check if offset exceeds buffer, at this stage FreeImage_GetTagLength may return invalid data
            if(offset_value > dwLength) {
                // a problem occured : delete the tag (not free'd after)
                FreeImage_DeleteTag(tag);
                // jump to next entry
                continue;
            }
            // now check that length does not exceed the buffer size
            if(FreeImage_GetTagLength(tag) > dwLength - offset_value){
                // a problem occured : delete the tag (not free'd after)
                FreeImage_DeleteTag(tag);
                // jump to next entry
                continue;
            }
            pval = (char*)(tiffp + offset_value);
        }

        // check for a IFD offset
        BOOL isIFDOffset = FALSE;
        switch(FreeImage_GetTagID(tag)) {
            case TAG_EXIF_OFFSET:
            case TAG_GPS_OFFSET:
            case TAG_INTEROP_OFFSET:
            case TAG_MAKER_NOTE:
                isIFDOffset = TRUE;
                break;
        }
        if(isIFDOffset) {
            DWORD sub_offset = 0;
            TagLib::MDMODEL next_mdmodel = md_model;
            const BYTE *next_ifd = ifdp;

            // get offset and metadata model
            if (FreeImage_GetTagID(tag) == TAG_MAKER_NOTE) {
                processMakerNote(dib, pval, msb_order, &sub_offset, &next_mdmodel);
                next_ifd = (BYTE*)pval + sub_offset;
            } else {
                processIFDOffset(tag, pval, msb_order, &sub_offset, &next_mdmodel);
                next_ifd = (BYTE*)tiffp + sub_offset;
            }

            if((sub_offset < dwLength) && (next_mdmodel != TagLib::UNKNOWN)) {
                // push our current directory state onto the stack
                ifdstack.push(ifdp);
                // jump to the next entry
                de++;
                destack.push(de);

                // push our current metadata model
                modelstack.push(md_model);

                // push new state onto of stack to cause a jump
                ifdstack.push(next_ifd);
                destack.push(0);

                // select a new metadata model
                modelstack.push(next_mdmodel);

                // delete the tag as it won't be stored nor deleted in the for() loop
                FreeImage_DeleteTag(tag);

                break; // break out of the for loop
            }
            else {
                // unsupported camera model, canon maker tag or something unknown
                // process as a standard tag
                processExifTag(dib, tag, pval, msb_order, md_model);
            }

        } else {
            // process as a standard tag
            processExifTag(dib, tag, pval, msb_order, md_model);
        }

        // delete the tag
        FreeImage_DeleteTag(tag);

    } // for(nde)

    // additional thumbnail data is skipped

} while (!destack.empty());

//
// --- handle thumbnail data ---
//

const WORD entriesCount0th = ReadUint16(msb_order, ifd0th);

DWORD next_offset = ReadUint32(msb_order, DIR_ENTRY_ADDR(ifd0th, entriesCount0th));
if((next_offset == 0) || (next_offset >= dwLength)) {
    return TRUE; //< no thumbnail
}

const BYTE* const ifd1st = (BYTE*)tiffp + next_offset;
const WORD entriesCount1st = ReadUint16(msb_order, ifd1st);

unsigned thCompression = 0;
unsigned thOffset = 0; 
unsigned thSize = 0;

for(int e = 0; e < entriesCount1st; e++) {

    // point to the directory entry
    const BYTE* base = DIR_ENTRY_ADDR(ifd1st, e);

    // check for buffer overflow
    const size_t remaining = (size_t)base + 12 - (size_t)tiffp;
    if(remaining >= dwLength) {
        // bad IFD1 directory, ignore it
        return FALSE;
    }

    // get the tag ID
    WORD tag = ReadUint16(msb_order, base);
    // get the tag type
    /*WORD type = */ReadUint16(msb_order, base + sizeof(WORD));
    // get number of components
    /*DWORD count = */ReadUint32(msb_order, base + sizeof(WORD) + sizeof(WORD));
    // get the tag value
    DWORD offset = ReadUint32(msb_order, base + sizeof(WORD) + sizeof(WORD) + sizeof(DWORD));

    switch(tag) {
        case TAG_COMPRESSION:
            // Tiff Compression Tag (should be COMPRESSION_OJPEG (6), but is not always respected)
            thCompression = offset;
            break;
        case TAG_JPEG_INTERCHANGE_FORMAT:
            // Tiff JPEGInterchangeFormat Tag
            thOffset = offset;
            break;
        case TAG_JPEG_INTERCHANGE_FORMAT_LENGTH:
            // Tiff JPEGInterchangeFormatLength Tag
            thSize = offset;
            break;
        // ### X and Y Resolution ignored, orientation ignored
        case TAG_X_RESOLUTION:      // XResolution
        case TAG_Y_RESOLUTION:      // YResolution
        case TAG_RESOLUTION_UNIT:   // ResolutionUnit
        case TAG_ORIENTATION:       // Orientation
            break;
        default:
            break;
    }
}

if(/*thCompression != 6 ||*/ thOffset == 0 || thSize == 0) {
    return TRUE;
}

if(thOffset + thSize > dwLength) {
    return TRUE;
}

// load the thumbnail

const BYTE *thLocation = tiffp + thOffset;

FIMEMORY* hmem = FreeImage_OpenMemory(const_cast<BYTE*>(thLocation), thSize);
FIBITMAP* thumbnail = FreeImage_LoadFromMemory(FIF_JPEG, hmem);
FreeImage_CloseMemory(hmem);

// store the thumbnail
FreeImage_SetThumbnail(dib, thumbnail);
// then delete it
FreeImage_Unload(thumbnail);

return TRUE;

}

When handling thumbnail data, the function makes this call:

DWORD next_offset = ReadUint32(msb_order, DIR_ENTRY_ADDR(ifd0th, entriesCount0th));

The value of the entriesCount0th variable comes from ifd0th that comes from the input file name and the controllable. jpeg_read_exif_dir calls to ReadUint32 (and in turn ReadInt32) without checking entriesCount0th, potentially making it possible to arbitrarily read access, that could cause a crash if reading into unallocated memory, and an information leak if the address is carefully constructed.

Discussion


Log in to post a comment.