Having a Tiff – A Complicated Love Affair with Vulnerability Research
Over the past few months, I've really pushed myself to uncover more vulnerabilities. Partly, this is to demonstrate I can consistently find interesting stuff, but more importantly, it’s because vulnerability research is genuinely thrilling. Recently, I’ve shifted my focus away from typical web application vulnerabilities towards deeper, lower-level issues involving binaries, libraries, and similarly intriguing targets.
As always, I strive to select targets that carry some degree of meaningful impact. Nobody particularly cares about a C program written twenty years ago, abandoned in 2003, and used maybe seven times. Therefore, my objective was clear: identify a target that's actively used, currently maintained, and has significant potential if weaponised.
I enlisted Lola Hackmore, AnchorSec’s new, improved, sassy, and AI-powered assistant, to help identify suitable candidates. Predictably, Lola gave me some back-chat exactly what you’d expect from an entity embodying the true spirit of a hacker but eventually produced a helpful list of libraries.
After a short deliberation, LibTiff emerged as the ideal candidate, and version 4.7.0, the latest available, was promptly downloaded.
About LibTiff
LibTiff is an open-source library for reading and writing TIFF (Tagged Image File Format) files. Widely utilised for handling high-quality, lossless image data, it's a staple in image processing, scanning, and document management applications. The library supports multiple compression schemes (such as LZW, JPEG, and ZIP) and flexible metadata through its tag system, making it especially valuable for scientific, archival, and graphic applications. Initially developed in the 1980s by Sam Leffler, LibTiff remains actively maintained, featuring robust capabilities like multi-page TIFF support, comprehensive colour space handling, and easy extensibility. Its compatibility across multiple platforms and seamless integration with tools like ImageMagick solidify its status as a developer favourite.
Finding the Issue
Effective vulnerability research often requires combining multiple approaches. My usual arsenal includes fuzzing, static code analysis, dynamic analysis, and essentially every colour in the vulnerability-discovery rainbow.
Without wasting time, I constructed a fuzzing harness to explore code paths handling TIFF files and deployed it across my fuzz-farm. To my surprise and delight, the harness quickly uncovered numerous crashes.
After following the standard procedures for triaging and minimising these crashes, I selected one for deeper analysis.
▶ 0 0x55555565d9d5 put8bitcmaptile+117
1 0x55555565b7ee gtStripContig+1054
2 0x55555565a50c TIFFRGBAImageGet+188
3 0x55555565a5b0 TIFFReadRGBAImageOriented+144
4 0x55555565a657 TIFFReadRGBAImage+55
5 0x55555563aba7 main+1223
6 0x7ffff782a1ca __libc_start_call_main+122
7 0x7ffff782a28b __libc_start_main+139
The stack trace indicated the vulnerability occurred in the function put8bitcmaptile()
. Naturally, examining the registers at the point of the crash seemed prudent. This revealed several peculiarities. The resulting call path was as follows:
main()
└── TIFFReadRGBAImage(tif, w, h, raster, 0)
└── TIFFReadRGBAImageOriented(...)
└── TIFFRGBAImageGet(...)
└── put8bitcmaptile(...)
The error appeared specifically in the following assembly instruction:
mov dword ptr [rax], ecx
(Note: occasionally, based on environmental settings, this appeared as [RSI], r15d instead.)
Clearly, the pointer in RAX was corrupted, and the values in RCX were distinctly abnormal. Correlating this back to the original source code, the problematic instruction aligned with the following segment:
(void)y;
for (; h > 0; --h)
{
for (x = w; x > 0; --x)
{
*cp++ = PALmap[*pp][0]; // Vulnerability here
pp += samplesperpixel;
}
cp += toskew;
pp += fromskew;
}
Anyway, looking at the backtrace gave me some more insight.
#0 0x000055555565d9d5 in put8bitcmaptile (img=0x7fffffffdab0, cp=0x8003f76f9404, x=256, y=65534, w=256, h=1,
fromskew=0, toskew=-512, pp=0x611000000040 '\001' <repeats 200 times>...)
#1 0x000055555565b7ee in gtStripContig (img=0x7fffffffdab0, raster=0x8003f36f9c00, w=256, h=65535)
#2 0x000055555565a50c in TIFFRGBAImageGet (img=0x7fffffffdab0, raster=0x8003f36f9c00, w=256, h=65535)
#3 0x000055555565a5b0 in TIFFReadRGBAImageOriented (tif=0x61a000000080, rwidth=256, rheight=256, raster=0x7ffff76b9800,
orientation=4, stop=0)
#4 0x000055555565a657 in TIFFReadRGBAImage (tif=0x61a000000080, rwidth=256, rheight=256, raster=0x7ffff76b9800, stop=0)
#5 0x000055555563aba7 in main (argc=2, argv=<optimized out>)
#6 0x00007ffff782a1ca in __libc_start_call_main (main=main@entry=0x55555563a6e0 <main>, argc=argc@entry=2,
argv=argv@entry=0x7fffffffe208)
#7 0x00007ffff782a28b in __libc_start_main_impl (main=0x55555563a6e0 <main>, argc=2, argv=0x7fffffffe208,
init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe1f8)
#8 0x000055555557aa25 in _start ()
The hawk-eyed among you might notice that the values passed into raster differ between TIFFReadRGBAImageOriented() and TIFFRGBAImageGet(), where raster changes from:
0x7ffff76b9800
0x8003f36f9c00
A quick look through the code for TIFFReadRGBAImageOriented reveals why this change in value occurs
if (TIFFRGBAImageBegin(&img, tif, stop, emsg))
{
img.req_orientation = (uint16_t)orientation;
/* XXX verify rwidth and rheight against width and height */
ok = TIFFRGBAImageGet(&img, raster + (rheight - img.height) * rwidth,
rwidth, img.height);
TIFFRGBAImageEnd(&img);
}
else
There’s a calculation being performed on the raster value in the form of:
raster + (rheight - img.height) * rwidth
In this expression:
raster is the base pointer to the raster buffer, provided by the application.
rheight is the height of the image as expected by the application.
img.height is the height of the image as determined by parsing the TIFF file.
rwidth is the width of the image.
If an attacker crafts a TIFF file with a very large img.height, the calculation can result in a cp pointer that ends up outside the bounds of the allocated raster buffer. Consequently, the subsequent write operation can overwrite arbitrary memory locations, which appear to be controllable from within the TIFF file.
When aligning this with the disassembly, it becomes clear that we can control the destination of the write.
But can we control the value that gets written? Well, actually, yes.
The what in this vulnerability can be manipulated by defining palette entries within the TIFF file, which directly influence the value written via the offending instruction. In palette-based TIFFs, each pixel references an entry in PALmap. The code takes the palette value (one uint8_t per channel) and constructs a 32-bit word composed of four bytes: Red, Green, Blue, and Alpha. By crafting palette entries with the values 0x41, 0x41, 0x41, 0x41, an attacker sets the relevant register to this exact value, directly controlling the data being written.
Taken together, this means that with a maliciously crafted TIFF file, one can control both the destination address and the value being written. This conclusively proves exploitability.
RAX 0x8003f76f9400
RBX 0
RCX 0xff414141 ◂— 0
RDX 0x8003f76f9404
RAX and RCX Registers under control.
The ability to perform arbitrary writes in memory opens the door to various attack vectors. By overwriting function pointers or return addresses, an attacker can redirect execution flow and achieve code execution. Alternatively, overwriting critical data structures can cause the application to crash.
The severity of the impact depends on the application's environment and the presence of mitigations such as Address Space Layout Randomisation (ASLR), RELRO, and others. Furthermore, to bypass such features, this vulnerability would have to be chained with others, such as a memory leak – in order to determine a valid address.
This write-what-where vulnerability in LibTiff highlights the importance of rigorous input validation and secure coding practices, particularly in libraries that handle complex file formats. For software developers, utilising a vulnerable version of LibTiff in software could mean that their application becomes a target for attackers, should the vulnerable code get exposed.
In summary, this vulnerability could lead to:
Remote Code Execution
Denial of Service
In software applications utilising the library
Conclusion and Next Steps
This was submitted to the developers and fixed within four weeks. At the time of fixing, no CVE number had been assigned - something I assume is due to MITRE’s backlog and related issues.
The developers were responsive, helpful, and quite fast in addressing the problem.
While the vulnerability was identified, the root cause analysed, and exploitability proven, I did not weaponise the vulnerability; that is something for the future.