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.