What does optimization mean?

Optimization is a process to make an existing system faster, more efficient than it currently is. The ways to achieve this optimization depends on what system we’re talking about. In this post, we’ll go through some simple yet effective strategies to make fuzzing with AFL more optimized. We’d also take a look at AFL’s QEMU mode, which allows us to fuzz binaries which we do not have the source for.

Note: This article builds on top of the last blog I wrote, where we talked about how to get started with fuzzing applications with American Fuzzy Lop, or AFL for short. So, if you haven’t read it yet, I’d suggest you go through it once before you continue. You can find it here.

Why optimize at all?

In the last post, we briefly talked about why should one try and optimize fuzzing as much as possible, irrespective of how smart AFL is on its own. It’s because we want to reach further faster, which means we want to give AFL as much of a head-start as we can so it can find bugs sooner.

There are various tools, which AFL comes pre-loaded with, built-in for you to make fuzzing as effortless as possible. We’ll go over them that one can (and should) use to cover the bare minimum optimization while fuzzing an application.

Optimization Strategies

Parallel Fuzzing

Let’s start off with one of the most obvious ways to optimize, fuzzing the target application simultaneously with multiple fuzzers working in parallel. Now, if you feel that you don’t want to do extra work and manage multiple fuzzers, their findings, etc. fear not, AFL’s got your back. AFL comes with support for parallel fuzzing right out-of-the-box without the need of any additional configurations or installations. It works such that there’s a master fuzzer and all other fuzzers are slave fuzzers. The command also remains almost identical except for an additional flag that specifies the type of the fuzzer, master or slave.

But before we begin to run multiple fuzzers, we need to know how many can we run, and its directly dependent on your machine’s processor. To find out how many free cores are available in your machine, AFL has a utility called afl-gotcpu which tells the number of CPU cores that are free and hence, can be used to run fuzzers. It generates an output similar to the one below:

root@kali:~# afl-gotcpu
afl-gotcpu 2.52b by <lcamtuf@google.com>
[*] Measuring per-core preemption rate (this will take 1.00 sec)...
    Core #0: AVAILABLE

    --- snipped ---

>>> PASS: You can run more processes on 1 core. <<<

As per the machine I’m using, there’s only one core free to run a fuzzer so I can run one more fuzzer (assuming I’ve got some running already). But for the sake of this article we’ll assume that we had 4 free cores and hence, we can run a total of 4 fuzzers.

To start AFL in parallel, there’s just one flag that gets appended to the command that’s used to start the fuzzing. The flag is either -M, to define that the fuzzer is the master fuzzer, or -S, to define that the fuzzer is a slave fuzzer. Both of these flags are followed by a name given to that fuzzer. One can use a utility, like screen or tmux, to run parallel fuzzers in the same terminal session. To run screen, the command would be:

root@kali:~# screen -dmS <name-for-screen> <command-to-run>

Note: to read the output of a particular fuzzer, you can use screen -rd <fuzzer name> to attach yourself to a particular screen and then use ctrl+a d to detach from the current screen and return back to the terminal.

Here’s an example of how I’d run 4 fuzzers, 1 master and 3 slaves, in parallel with screen:

root@kali:~# screen -dmS fuzzer1 /bin/bash -c "afl-fuzz -i in -o out -M fuzzer1 -- ./fuzzgoat @@"
root@kali:~# screen -dmS fuzzer2 /bin/bash -c "afl-fuzz -i in -o out -S fuzzer2 -- ./fuzzgoat @@"
root@kali:~# screen -dmS fuzzer3 /bin/bash -c "afl-fuzz -i in -o out -S fuzzer3 -- ./fuzzgoat @@"
root@kali:~# screen -dmS fuzzer4 /bin/bash -c "afl-fuzz -i in -o out -S fuzzer4 -- ./fuzzgoat @@"

Note: All fuzzers are passed the same input and out folders. Also, there can only be one master fuzzer.

Now, we’ve got multiple fuzzers running together, keeping track of consolidated progress of all of them can be done through afl-whatsup, another utility in AFL which gives out a report with various details. The command expects to be pointed towards the output folder made to store each fuzzer’s findings.

root@kali:~/fuzzgoat# afl-whatsup out
status check tool for afl-fuzz by <lcamtuf@google.com>
Individual fuzzers
==================
>>> fuzzer1 (0 days, 0 hrs) <<<
  cycle 1, lifetime speed 1340 execs/sec, path 26/160 (16%)
  pending 37/150, coverage 0.54%, crash count 11 (!)
>>> fuzzer2 (0 days, 0 hrs) <<<
  cycle 1, lifetime speed 909 execs/sec, path 200/212 (94%)
  pending 6/157, coverage 0.58%, crash count 7 (!)
>>> fuzzer3 (0 days, 0 hrs) <<<
  cycle 1, lifetime speed 0 execs/sec, path 0/1 (0%)
  pending 1/1, coverage 0.13%, no crashes yet
>>> fuzzer4 (0 days, 0 hrs) <<<
  cycle 1, lifetime speed 0 execs/sec, path 0/1 (0%)
  pending 1/1, coverage 0.13%, no crashes yet
Summary stats
=============
       Fuzzers alive : 4
      Total run time : 0 days, 1 hours
         Total execs : 0 million
    Cumulative speed : 2249 execs/sec
       Pending paths : 45 faves, 309 total
  Pending per fuzzer : 11 faves, 77 total (on average)
       Crashes found : 18 locally unique

Isolating Code to be Fuzzed

For a lot of cases, that you might encounter while fuzzing applications, you’d see that your target is a library or a module in an application, and not the whole application itself. To fuzz this one module, running AFL on the whole application hardly seems efficient.

In such cases, you can isolate the code segment (the module, library) to be tested and run AFL just on it. This will not only make the fuzzing faster but also guarantee that all bugs found will be sourced from the target i.e. no false-positives.

Using Custom parameters

AFl comes along with some (nice) assumptions for certain values that it uses to identify crashes and hangs in an application. Timeout and Memory are two of them. Now, if you know certain traits of the target application you’re testing, you can customize how AFL detects crashes, hangs in the application by altering these values with custom ones which, in turn, makes the fuzzing more accurate.

To set a custom value, which defines when AFL should consider the application has hanged, can be defined explicitly (in milliseconds) with the -t flag in the AFL Fuzzing command structure. Similarly, we can define memory constraints with -m flag.

Here’s an example of using these commands with the two flags mentioned above:

afl-fuzz -i afl_in -o afl_out -t 30 -m 256 -- <instrumented binary> <options> @@

Optimizing the Test Corpus

In the last blog, we talked about creating a Test Corpus and why it’s a good idea to select good test cases. But it is fairly common to observe that there are similar test cases in our corpus. Now, if AFL was to go through the same or similar test cases, it’ll obviously cost us more time, so we’d make use of another utility that comes with AFL to tackle this issue. AFL has afl-cmin, which is used to identify test cases that seem similar to AFL in regard to increasing path coverage and remove them from the Corpus.

The command to use afl-cmin is as mentioned below:

root@kali:~/fuzzgoat# afl-cmin -i in -o out -- ./fuzzgoat @@
corpus minimization tool for afl-fuzz by <lcamtuf@google.com>
[*] Testing the target binary...
[+] OK, 82 tuples recorded.
[*] Obtaining traces for input files in 'in'...
    Processing file 1/1...
[*] Sorting trace sets (this may take a while)...
[+] Found 82 unique tuples across 1 files.
[*] Finding best candidates for each tuple...
    Processing file 1/1...
[*] Sorting candidate list (be patient)...
[*] Processing candidates and writing output files...
    Processing tuple 82/82...
[!] WARNING: All test cases had the same traces, check syntax!
[+] Narrowed down to 1 files, saved in 'out'.

Now, reducing the number of test cases is not enough. At times, the individual test cases might be bloated with unnecessary segments. But AFL has got us covered here as well. We use a similar command as before, just notice that we use afl-tmin this time:

afl-tmin -i afl_in -o afl_out -- ./fuzzgoat @@

Miscellaneous: Fuzzing Applications without Source

AFL comes with a QEMU mode which allows us to fuzz pre-built binaries (which are not instrumented by AFL). This is a useful tool as one might encounter scenarios where the source to the application isn’t available. Hence, to fuzz such applications we use AFL’s QEMU mode.

To use this mode, we need to install it separately. It also has a few dependencies. To install and enable QEMU support for AFL, follow the below instructions:

  • Installing Dependencies
  root@kali:~# apt install libtool-bin
  root@kali:~# apt install automake
  root@kali:~# apt install bison
  root@kali:~# apt install libglib2.0-dev
  root@kali:~# apt install qemu
  • Enabling/Installing QEMU support for AFL
  cd afl-2.52b/qemu_mode
  ./build_qemu_support.sh
  cd ..
  sudo make install

After we’ve set up everything, all we need to do is add a -Q flag in the usual command structure for AFL and point it to the pre-built binary as follows:

afl-fuzz -Q -i afl_in -o afl_out -- <non-instrumented binary> <options> @@

You should be greeted by the AFL’s familiar ASCII output screen. From here, the steps are the same as the ones we’d have for fuzzing an instrumented binary.

Conclusions

We went through some fairly basic methodologies and strategies that we can use to optimize the whole fuzzing process when working with AFL to test applications. There remain a plethora of other ways to still make AFL more efficient, but they go beyond the scope of a small blog.

We also looked at how can we fuzz applications with closed source i.e. the applications that we do not have access to the source for. It is another handy utility which helps us not having to stop just because we did not have the source of the application to instrument. This method, however, has some drawbacks and results, on various occasions, are not of the same quality as that with an instrumented binary. So, whenever possible, still try to get your hands on the source code.

References