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:
Catimg is written in C. Let’s fuzz it 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
.
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:
.jpg
).png
).bmp
).gif
)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
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:
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.
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.
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.
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:
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:
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:
They’re standard int
s, 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:
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.
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.
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
).