I decided to give a descent test to the LIEF project. Executable parsers are not a new thing but that one picked my curiosity (just like most Quarkslab projects) because it also provides dead simple instrumentation functions. To top it up, LIEF is easy to use and well documented, which is becoming a rare perk in the circus of infosec tools.
By reading some blog posts about LIEF, I came across a new feature : easily adding arbitrary functions to an ELF export table. I highly recommend to dig through this post if you haven’t done so already.
When I was done reading, I realized one of the many good applications to this feature would be fuzzing. But why not use AFL you may ask? Well, AFL is an awesome (awesome awesome) tool, but it fuzzes the whole binary by providing some local mutated input. This has 2 disadvantages for precise, targeted function fuzzing:
- performance: in default mode (i.e. non persistent), AFL spawns and runs the entire binary, which obviously adds the process creation/deletion time, along with all the code before reaching the function(s) we’re aiming;
- modularity: it is not easy to fuzz network service parsing mechanism with it. I know of already existing attempts to fix this, but I find them too hacky and poorly scalable.
On the other side we have LLVM’s own LibFuzzer, which is an awesome (awesome awesome) library to fuzz, well… libraries. And fortunately, not everything is a library (sshd, httpd)
And that’s exactly where LIEF kicks in… How about using LIEF to export one (or many) functions from the ELF binary we target, into a shared object, and then use LibFuzzer to fuzz it! On top of that, we can also use the compilers sanitizers to track invalid memory access! But would that even work?
It turns out it did, big time and after successfully playing on simple PoCs, I realized this technique was relevant to dig into, so I chose to put it to practice by trying to find real vulnerabilities.
Concrete example: finding CVE-2018-6789
What better way to illustrate this technique than with a concrete example: earlier this week, mehqq_ released a great blog post about CVE-2018-6789 detailing the exploit steps for an off-by-one vulnerability she discovered in Exim. The issue was fixed in cf3cd306062a08969c41a1cdd32c6855f1abecf1 and given the CVE 2018-6789.
Exim is a MTA which once compiled is a standalone binary. So AFL would be of little help (network service), but it is a perfect practice case for LIEF + LibFuzzer.
We must compile Exim as PIE (usually done with setting
-fPIC in CFLAGS and
LDFLAGS). But we also need the address sanitizer since without
them, off-by-one overflow in the heap may go unoticed.
Compiling the target with ASAN & PIE
Note: in some cases, the use of ASAN fails to create the config file required
for the compilation. So edit
$EXIM/src/scripts/Configure-config.h shell script
to avoid the premature ending:
The compilation will occur normally and once compiled we can use
checksec from pwntools on the binary and make
sure it’s PIE and ASAN compatible:
Exporting the targeted functions
From the write-up, the vulnerable function is
This function is not static and the binary not stripped, so we can spot it
So now we know that we want to export the function
b64decode at PIE offset
0xcb0bd. We can use the following simple script to export the functions using
We also need to export
store_reset_3() which is used to free the structures.
Write a LibFuzzer loader to invoke the targeted function
First we need a handle to the library:
And reconstruct the function
b64decode() based on its prototype:
b64decode() can now be called:
That works! And we can thank only LIEF for that, by making the instrumention of arbitrary functions a child game.
Fuzz da planet!
We can now use this skeleton to build a LibFuzzer-based fuzzer around this:
Compile it, run it, and be amazed 😎 :
We’re running more than 1 million executions/second/core on the function
b64decode, not bad eh?
And in less than a 1 second, we get the heap overflow found by @mehqq_ , CVE-2018-6789:
Note: Earlier this week, I was notified by mehqq_ that this is OOB read is a different bug. I will post an update soon showcasing the actual bug instead. My bad for the confusion.
Although this technique is not as click-and-play like AFL since it requires a bit more work, it offers non-negligeable pros:
- excellent reliability, makes easy for fuzzing network services → focus on parsing functions (no network stack to handle etc.). perfect for can focus on specific points (packet parsing, message processing, etc.)
- crazy performance: no need to spawn the whole binary
- there is actually no need for the source code, we can use LibFuzzer on black-box binaries
- low hardware requirements allow to fuzz at very high rate even on weak hardware (and transform your RaspberryPis into a fuzzing cluster 😎)
But nothing ever being perfect, there are obviously also cons:
- need to code almost every fuzzer (so only for C/C++ coding people)
- specific edge cases you might need to consider (beware of memleaks!!)
- we must determine the function prototype. This is easy when the source code is open (FOSS projects), but black-box binaries may require some prior reversing. Tools like Binary Ninja Commercial License may also be of great help for automating this task.
All in all, it is a pretty neat approach made possible through 2 awesome tools. I do hope LIEF development keeps being active to bring us more goodies like this!
Thanks for reading 😁 !
Share this post: