Fuzzing Catimg

Allocation Failure via Integer Overflow with AFL++

2023.03.06 - Connor Shugg
Fuzzing Security AFL++ C

Want to display an image directly in the terminal? It just so happens there’s a nifty tool for that: catimg. It’s pretty simple: catimg accepts the path to an image file and prints the image to the terminal as accurately as possible:

Displaying my Alma Mater's logo with catimg

Fuzzing Catimg

Catimg is written in C. Let’s fuzz it with AFL++!

Building with AFL++

Building catimg is dead simple. All we need to do is clone the repository:

git clone https://github.com/posva/catimg

Then build with the AFL++ compiler:

cd catimg
CC=afl-clang-fast cmake ./
make

The resulting instrumented binary will be stored at catimg/bin/catimg.

Creating the Input Corpus

We need a small collection of images to use as an input corpus. We’ll find some by heading to unsplash to download a few stock photos:

Each of these images download as a .jpg. This is a good start, but considering there are a multitude of formats catimg supports, we’ll create copies of each one across four different image formats:

Placing these all inside a directory gives us our final set of inputs:

823002  Mar  5 17:16 angryman.bmp
89750   Mar  5 17:16 angryman.gif
27237   Mar  5 17:16 angryman.jpg
72473   Mar  5 17:16 angryman.png
1231962 Mar  5 17:16 astronaut.bmp
63818   Mar  5 17:16 astronaut.gif
22374   Mar  5 17:16 astronaut.jpg
146094  Mar  5 17:16 astronaut.png
1539162 Mar  5 17:16 cheese.bmp
237063  Mar  5 17:16 cheese.gif
70338   Mar  5 17:16 cheese.jpg
472064  Mar  5 17:16 cheese.png
823002  Mar  5 17:16 dog.bmp
118989  Mar  5 17:16 dog.gif
33805   Mar  5 17:16 dog.jpg
224437  Mar  5 17:16 dog.png
Fuzzing

Finally, we’ll fire up AFL++ with our input set:

afl-fuzz -D -i ./fuzz_inputs -o ./fuzz_run__0 ./catimg/bin/catimg @@

AFL++ interprets the @@ symbol as the location into which it places the path to its current input file. With each iteration, the fuzzer will drop in the path to a new file.

Sit back and relax. Nearly a day later, we’ve found ourselves a crash:

AFL++'s status screen, 20 hours later

Postmortem - Inspecting the Crash

Let’s examine the crash AFL++ reported. Our good friend GDB can assist us here:

gdb ./catimg/bin/catimg -ex "set args fuzz_run__0/default/crashes/id:000000*"

With the arguments set, we can run to observe the SIGSEGV (Segmentation Fault). Excellent.

GDB showing the segmentation fault

We can see it failed during a call to memset(), a standard C function used to fill a memory region with a single byte’s value. In this case, some struct, called g, is having its history field (apparently a pointer to some memory), zeroed out.

GDB showing a null pointer

Interesting. g->history is actually NULL, which explains the segfault. You can’t write bytes into address 0x0. This is a classic “failure to check for null” bug. Somehow, this input file caused g->history to be NULL, which was a situation the developer didn’t anticipate.

Digging Deeper

Let’s debug this a little bit. This code lives in src/stb_image.h - specifically in the stbi__gif_load_next() function. (We’re dealing with a GIF image file.) A few lines before the faulty memset(), g->history is assigned what appears to be a chunk of heap-allocated memory:

Examining source code

Nothing else happens to g->history between these two lines, so stbi__malloc() must be returning a null pointer. stbi__malloc()’s code is straightforward: it invokes STBI_MALLOC(), a macro that simply invokes malloc(), the standard C memory allocation function:

Examining source code for stbi_malloc()

Now here’s something interesting: stbi__malloc() accepts a size_t, which is passed into malloc(). A size_t represents an unsigned integer. From the previous function, we saw that the value being passed into stbi__malloc() was the multiplication of two integers:

g->history = (stbi_uc *) stbi__malloc(g->w * g->h);

Let’s see what type g->w and g->h are:

Examining source code for the stbi__gif struct

They’re standard ints, which are signed integers. Two signed integers multiplied together might compute a negative value, if the two integers are large enough to result in a product larger than the signed integer maximum. Let’s see if that’s true here:

GDB showing an integer overflow

Sure enough, g->w is 65535 and g->h is 33023. These two numbers multiplied together would normally produce 2164162305, but since we’re dealing with signed integers limited to 32 bits in width, that value “wraps around” to produce a negative value. And, oh boy! Check out the value that’s passed to malloc() when stbi__malloc() interprets the result as a size_t: 18446744071578746625. Wow. That means this particular GIF has caused catimg to request 18.4 exabytes from the operating system. (That’s 18.4 billion gigabytes.) Considering the system I’m running this on has exactly 8 gigabytes of RAM, this allocation request will most certainly fail. Thus, malloc() returns NULL, revealing the root cause of this bug.

Integer Overflow Galore!

Something else interesting - if we go back and look at the g->out and g->background allocations, their size is computed similarly to g->history:

// src/stb_image.h line 6482
g->out = (stbi_uc *) stbi__malloc(4 * g->w * g->h);
g->background = (stbi_uc *) stbi__malloc(4 * g->w * g->h);
g->history = (stbi_uc *) stbi__malloc(g->w * g->h);

The one difference is the multipcation of 4. It just so happens that multiplying the negative product of g->w and g->h by four results in a negative value so large, that the final result wraps back around to be positive 66714628. For this reason, the allocations for g->out and g->background each request 66.7 megabytes, both of which succeed.

Wrapping Things Up

At this point we’re certain we’ve found a solid integer overflow bug. Before creating a bug report, let’s minimize the GIF using AFL++’s test case minimizer. This’ll make debugging easier for anyone that takes a crack at fixing the bug:

afl-tmin -i ./fuzz_run__0/default/crashes/id:000000* -o ./fuzz_run__0/crash0a.min ./catimg/bin/catimg @@

From here, I went ahead and submitted a bug report to the developer on GitHub. Posva, catimg’s developer, pointed out something I hadn’t realized: this code is copied from the STB Library. The bug has been patched in the library (see the current version of stb_image.h).