Thelastede reported on https://github.com/thelastede/FreeImage-cve-poc/tree/master/CVE-2023-47997 an infinite loop in PluginTIFF.cpp::Load in FreeImage 3.18.0, that could be used to conduct denial-of-service attacks.
This is mostly a copy of the original report, aimed to forward the report upstream. For full details, please refer to the link of the original report, mentioned above.
When loading a TIFF image, it will end up in the Load function. Due to the length of the function, only the key code related to the vulnerability is shown below.
const BYTE* const src_line_end = src_line_begin + src_line;
for (BYTE* src_bits = src_line_begin, * dst_bits = dst_line_begin; src_bits < src_line_end; src_bits += Bpc, dst_bits += Bpp) {
// actually assigns channel
AssignPixel(dst_bits + channelOffset, src_bits, Bpc);
} // line
Notice that the loop termination condition is src_bits < src_line_end, with Bpc added to src_bits each time it loops. The AssignPixel function is defined as follows.
inline void
AssignPixel(BYTE* dst, const BYTE* src, unsigned bytesperpixel) {
switch (bytesperpixel) {
case 1: // FIT_BITMAP (8-bit)
*dst = *src;
break;
case 2: // FIT_UINT16 / FIT_INT16 / 16-bit
*(reinterpret_cast<WORD*>(dst)) = *(reinterpret_cast<const WORD*> (src));
break;
case 3: // FIT_BITMAP (24-bit)
*(reinterpret_cast<WORD*>(dst)) = *(reinterpret_cast<const WORD*> (src));
dst[2] = src[2];
break;
case 4: // FIT_BITMAP (32-bit) / FIT_UINT32 / FIT_INT32 / FIT_FLOAT
*(reinterpret_cast<DWORD*>(dst)) = *(reinterpret_cast<const DWORD*> (src));
break;
case 6: // FIT_RGB16 (3 x 16-bit)
*(reinterpret_cast<DWORD*>(dst)) = *(reinterpret_cast<const DWORD*> (src));
*(reinterpret_cast<WORD*>(dst + 4)) = *(reinterpret_cast<const WORD*> (src + 4));
break;
// the rest can be speeded up with int64
case 8: // FIT_RGBA16 (4 x 16-bit)
*(reinterpret_cast<DWORD*>(dst)) = *(reinterpret_cast<const DWORD*> (src));
*(reinterpret_cast<DWORD*>(dst + 4)) = *(reinterpret_cast<const DWORD*> (src + 4));
break;
case 12: // FIT_RGBF (3 x 32-bit IEEE floating point)
*(reinterpret_cast<float*>(dst)) = *(reinterpret_cast<const float*> (src));
*(reinterpret_cast<float*>(dst + 4)) = *(reinterpret_cast<const float*> (src + 4));
*(reinterpret_cast<float*>(dst + 8)) = *(reinterpret_cast<const float*> (src + 8));
break;
case 16: // FIT_RGBAF (4 x 32-bit IEEE floating point)
*(reinterpret_cast<float*>(dst)) = *(reinterpret_cast<const float*> (src));
*(reinterpret_cast<float*>(dst + 4)) = *(reinterpret_cast<const float*> (src + 4));
*(reinterpret_cast<float*>(dst + 8)) = *(reinterpret_cast<const float*> (src + 8));
*(reinterpret_cast<float*>(dst + 12)) = *(reinterpret_cast<const float*> (src + 12));
break;
default:
assert(FALSE);
}
}
When Bpc equals 0, it goes to the default segment. Under the Release version, assert is equal to a space, so the function returns directly. However, since Bpc is equal to 0, src_bits += Bpc does not change, so the loop condition is always satisfied, making it enter an infinite loop.
The original report includes instructions and an input file to reproduce the issue. FTR, I have been unable to reproduce the infinite loop on linux so far.