UAFuzz: Binary-level Directed Fuzzing for Use-After-Free Vulnerabilities
Directed Greybox Fuzzing (DGF) like AFLGo aims to perform stress testing on pre-selected potentially vulnerable target locations, with applications to different security contexts: (1) bug reproduction, (2) patch testing or (3) static analysis report verification. There are recently more research work that improved directed fuzzing's effectiveness and efficiency (see awesome-directed-fuzzing).
We propose UAFuzz which is a directed fuzzer dedicated to Use-After-Free (UAF) bugs at binary-level by carefully tuning the key components of directed fuzzing to meet specific characteristics of this bug class. UAF bugs appear when a heap element is used after having been freed. Detecting UAF bugs is hard: (1) complexity because a Proof-of-Concept (PoC) input needs to trigger a sequence of three events – alloc, free and use – on the same memory location, spanning multiple functions of the tested program and (2) silence with no segmentation fault.
Overall, UAFuzz has the similar workflow as directed fuzzers with our modifications highlighted in orange along the whole fuzzing process, as shown in the following figure. As we focus on (1) bug reproduction and (2) patch testing applications, it is more likely we have (mostly) complete stack traces of all memory-related UAF events. Unlike existing general directed approaches where targets could be selected independently, we take into account the relationship among targets (e.g., the ordering which is essential for UAFs) to improve the directedness. First, the static precomputation of UAFuzz is fast at binary level. Second, we introduce new ordering-aware input metrics to guide the fuzzer towards targets at runtime. Finally, we triage only potential inputs covering all targets in the expected trace and pre-filter for free inputs that are less likely to trigger the bug.
More details in our paper at RAID'20 and our talk at Black Hat USA'20. Thanks also to Sébastien Bardin, Matthieu Lemerre, Prof. Roland Groz and especially Richard Bonichon (@rbonichon) for his help on Ocaml.
Our tested environment is Ubuntu 16.04 64-bit. ~~~bash
sudo apt update sudo apt update sudo apt install ocaml ocaml-native-compilers camlp4-extra opam emacs llvm-6.0-dev pkg-config protobuf-compiler libgmp-dev libzmq3-dev cmake valgrind opam init opam switch 4.05.0 opam depext conf-m4.1 opam install merlin ocp-indent caml-mode tuareg menhir ocamlgraph ocamlfind piqi zmq.5.0.0 zarith llvm.6.0.0 eval
opam config env
sudo python -m pip install networkx pydot sudo apt install graphviz
wget https://cpan.metacpan.org/authors/id/S/SH/SHLOMIF/Graph-Easy-0.76.tar.gz tar xzf Graph-Easy-0.76.tar.gz cd Graph-Easy-0.76 perl Makefile.PL; make test; sudo make install export GRAPHEASYPATH=/usr/local/bin/graph-easy
git clone https://github.com/strongcourage/uafuzz.git
export IDAPATH = /path/to/ida-6.9/idaq export GRAPHEASYPATH=/path/to/graph-easy cd uafuzz; export UAFUZZPATH=
./binsec/src/binsec -ida-help ./binsec/src/binsec -uafuzz-help ~~~
Our fuzzer is built upon AFL v2.52b in QEMU mode for fuzzing and BINSEC for lightweight static analysis (see uafuzz/README.md). We currently use IDA Pro v6.9 to extract control flow graphs (CFGs) and call graph of the tested binary (see ida/README.md). ~~~ uafuzz ├── binsec/src │ └── ida: a plugin to import and process IDA's CFGs and call graph │ └── uafuzz: fuzzing code │ │ └── afl-2.52b: core fuzzing built on top of AFL-QEMU │ │ └── uafuzz_*.ml(i): a plugin to compute static information and communicate with AFL-QEMU └── scripts: some scripts for building and bug triaging ~~~
We first consider a simple UAF bug. Both AFL-QEMU and even directed fuzzer AFLGo with targets at source-level can't detect this bug within 6 hours, while UAFuzz can detect it within minutes with the help of a Valgrind's UAF report. ~~~bash/
$UAFUZZ_PATH/tests/example.sh aflqemu 360
$UAFUZZ_PATH/tests/example.sh aflgo 360
$UAFUZZPATH/tests/example.sh uafuzz 360 $UAFUZZPATH/tests/example/example.valgrind ~~~
For real-world programs, we use the UAF Fuzzing Benchmark for our evaluations. ~~~bash
git clone https://github.com/strongcourage/uafbench.git cd uafbench; export UAFBENCH_PATH=
We show in details how to run UAFuzz for bug reproduction application of CVE-2018-20623 of readelf (Binutils). The stack traces of this UAF bug obtained by Valgrind are as follows: ~~~ // stack trace for the bad Use ==5358== Invalid read of size 1 ==5358== at 0x40A9393: vfprintf (vfprintf.c:1632) ==5358== by 0x40A9680: bufferedvfprintf (vfprintf.c:2320) ==5358== by 0x40A72E0: vfprintf (vfprintf.c:1293)  ==5358== by 0x80AB881: error (elfcomm.c:43)  ==5358== by 0x8086217: processarchive (readelf.c:19409)  ==5358== by 0x80868EA: process_file (readelf.c:19588)  ==5358== by 0x8086B01: main (readelf.c:19664)
// stack trace for the Free ==5358== Address 0x4221dc0 is 0 bytes inside a block of size 80 free'd ==5358== at 0x402D358: free (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
 ==5358== by 0x8086647: processarchive (readelf.c:19524)  ==5358== by 0x80868EA: processfile (readelf.c:19588)  ==5358== by 0x8086B01: main (readelf.c:19664)
// stack trace for the Alloc ==5358== Block was alloc'd at ==5358== at 0x402C17C: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
 ==5358== by 0x80AD97E: makequalifiedname (elfcomm.c:906)  ==5358== by 0x8086350: processarchive (readelf.c:19435)  ==5358== by 0x80868EA: processfile (readelf.c:19588)  ==5358== by 0x8086B01: main (readelf.c:19664) ~~~
The preprocessing script takes the tested binary in x86 and the Valgrind's stack traces as inputs, then generates the UAF bug trace which is a sequence of target locations in the format
(basic_block_address,function_name)like the following: ~~~ 0 -> 1 -> 2 -> 3, alloc -> 4, free -> 5 -> 6, use ~~~
We provide a fuzzing script's template with several input parameters, for example the fuzzer we want to run, the timeout in minutes and the predefined targets (e.g., extracted from the bug report). For the example above, we use the script CVE-2018-20623.sh and run UAFuzz as: ~~~bash
$UAFBENCHPATH/CVE-2018-20623.sh uafuzz 60 $UAFBENCHPATH/valgrind/CVE-2018-20623.valgrind ~~~
After the fuzzing timeout, UAFuzz can identify which inputs cover in sequence all target locations of the expected UAF bug trace (e.g., input name that ends with
',all'). Thus, UAFuzz only triages those kinds of inputs that are likely to trigger the desired bug by using existing profiling tools like Valgrind or AddressSanitizer.
We use CVE-2018-6952 of GNU Patch to illustrate the importance of producing different unique bug-triggering inputs to favor the repair process. There was a double free in GNU Patch which has been fixed by developers (commitan incomplete bug fix CVE-2019-20633 of the latest version 2.7.6 (commit
76e7758), with a slight difference of the bug trace. Overall, the process is similar to the bug reproduction application, except that some manual work could be required in identifying target UAF bug trace. We use PoC inputs of existing bugs and valid files in fuzzing-corpus as high quality seeds. ~~~bash
$UAFBENCHPATH/CVE-2019-20633.sh uafuzz 360 $UAFBENCHPATH/valgrind/CVE-2018-6952.valgrind ~~~
A possible hybrid approach is to combine UAFuzz with GUEB which is the only binary-level static analyzer written in Ocaml for UAF. However, GUEB produces many false positives and is currently not able to work properly with complex binaries. So we currently improve and integrate GUEB into BINSEC and then use targets extracted from GUEB's reports to guide UAFuzz. Stay tuned!