Greybox Fuzzing by the example of AFLSmart
Probably everyone heard about the cool AFL fuzzer.
Many use it as the main fuzzer to search for vulnerabilities and errors.
The AFL fork recently appeared, AFLSmartwhich has an interesting development of the idea. According to the documentation, it can mutate data according to a pre-prepared model, while AFL uses random low-level operations. However, there are several pitfalls. We talk about AFLSmart and understand what is under his hood.
In fact, this fork appeared several years ago, but a more or less clear description was published only in August 2019 in an article Smart graybox fuzzing. Published data show that this fuzzer can increase the speed of searching for bugs. For example, the authors found 9 bugs in FFmpeg in a week. We decided to try out AFLSmart in one of our projects.
Unfortunately, in the information security industry there is still no clearly established definition of what Gray-Box Fuzzing is. Some people understand it as the fuzzing that has feedback from the target for preparing test data to improve coverage (this honggfuzz, AFL and all its modifications). Others understand the approach in which the fuzzer at the start already has some information about the structure of the input data and in the process of its work complies with this description (not counting on the randomness of random transformations) so as not to get stuck and better move deep into the code (our current patient is AFLSmart) . We are in the second category. AFLSmart is not the only representative of the Gray-Box approach, and if you are interested in this topic, we also recommend that you pay attention to Superion and Nautilus.
Let’s get back to our project. The object of the study was the not very well-known network library. Now we don’t even remember what took more time — to study the library or to understand AFLSmart. We wanted to get the result no worse than that of the authors of the above article, so we paid close attention to fuzzer.
The problem was that the fuzzer did not behave in a predictable way and did not generate the data that we expected from it. Looking ahead a bit, we note that there is a flaw in AFLSmart that can be encountered when creating a more demanding data model than in the examples of the authors of the article. It’s amazing that a year after the public release, no one came across this before us.
A bit about our expectations. After reading a white paper about AFLSmart, it seems that he can adhere to the data structure and mutate the data itself so as to significantly increase coverage. That is, it can take into account where it is located, as well as what and how it can and cannot be mutated. This is very important, because many file formats and network packets contain various magic words, checksums, etc., if the value is incorrect, further processing of the file or packet does not occur at all, that is, there is no progress in covering the new code. We expected that just with the help of AFLSmart we can solve the emu problem by describing the format we know for it.
T-Fuzz: fuzzing by program transformation
The idea is to transform not only the input data, but also the program itself during fuzzing, in order to increase coverage by removing the “hard” checks.
In short:
1) We show that fuzzing can more effectively find bugs by transforming the target program,
instead of resorting to heavy weight program analysis techniques.
2) We present a set of techniques that enable fuzzing to mutate both inputs and the programs, including techniques for
(i) automatic detection of sanity checks in the target program,
(ii) program transformation to remove the detected sanity checks,
(iii) reproducing bugs in the original program by filtering false positives that only crash in the transformed program.
So, AFLSmart is a bunch of two fuzzers: AFL and Peach. If you came across Peach, then you know that for work it requires a model according to which data will be generated (since this is a generation fuzzer). You can simply describe the data format in the selected application, but you can also give instructions on what can be mutated and what cannot. For example, you cannot touch magic words if you are sure that they are definitely needed.
As it turned out, Peach is needed here only to break the input test cases into pieces (chunks) according to the model. It’s just a parser, and mutators added to AFL are involved in the generation, and there was a mistake in them. You will see that although it was easy to fix it, we had to sort through all the nuances along the way to be sure that this was the only problematic place.
For clarity, we take pcap
file, it has the following format:
Pcapheader
bytes | type | Name | Description |
---|---|---|---|
4 | uint32 | magic | ‘A1B2C3D4’ means the endianness is correct |
2 | uint16 | vmajor | major number of the file format |
2 | uint16 | vminor | minor number of the file format |
4 | int32 | thiszone | correction time in seconds from UTC to local time (0) |
4 | uint32 | sigfigs | accuracy of time stamps in the capture (0) |
4 | uint32 | snaplen | max length of captured packed (65535) |
4 | uint32 | network | type of data link (1 = ethernet) |
Frame
bytes | type | Name | Description |
---|---|---|---|
4 | uint32 | ts_sec | timestamp seconds |
4 | uint32 | ts_usec | timestamp microseconds |
4 | uint32 | incl_len | number of octets of packet saved in file |
4 | uint32 | orig_len | actual length of packet |
incl_len | uint32 | data | data |
pcap
files, of course, have other fields in Frame
(Ethernet Header, IPv4, UDP), but we will not describe them in detail and, to facilitate the task, we hide in the field data
.
The corresponding model for Peach will look like this:
Now let’s see what new mutators working with this model were added to the original AFL and what, in fact, was the problem.
High Level Mutators
In fact, mutations are made in three simple ways.
Chunk removal
Seed s
Is a valid file whose mutator will remove the chunk c
with coordinates c.start
, c.end
. Coordinates are offsets from the beginning of the file. For example, pcap
file comes constant first A1B2C3D4
, hence, c.start
= 0, c.end
= 3.
Adding Chunk
Fazzer selects an arbitrary chunk c2
from one file and inserts it right behind c1
in another file. It is important here that both chunks have parents of the same type. This mutator won’t add a chunk timestamp
to constant A1B2C3D4
in case of pcap
file. But he can add version_major
, because both the constant and version_major
refer to PCAP Packet Header
.
Changing Chunk
This mutator just changes an arbitrary chunk c1
on c2
. It is taken into account that chunks can have different sizes.
It’s also worthwhile to figure out where these coordinates come from.
As already mentioned, Peach acts as a parser – it breaks the test case into chunks.
You can see how it works:
peach -1 -inputFilePath=valid_file -outputFilePath=valid_file.chunks model.xml
Generated valid_file.chunks
will contain the offsets of all chunks.
Here’s what it looks like with pcap
and by this file:
0,95,Pcap,Enabled
0,23,Pcap~PHeader,Enabled
0,3,Pcap~PHeader~magic,Disabled
4,5,Pcap~PHeader~vmajor,Enabled
6,7,Pcap~PHeader~vminor,Enabled
8,11,Pcap~PHeader~thiszone,Enabled
12,15,Pcap~PHeader~sigfigs,Enabled
16,19,Pcap~PHeader~snaplen,Enabled
20,23,Pcap~PHeader~network,Enabled
24,95,Pcap~PFrame,Enabled
24,95,Pcap~PFrame~PFrame,Enabled
24,27,Pcap~PFrame~PFrame~ts_sec,Enabled
28,31,Pcap~PFrame~PFrame~ts_usec,Enabled
32,35,Pcap~PFrame~PFrame~incl_len,Enabled
36,39,Pcap~PFrame~PFrame~orig_len,Enabled
40,95,Pcap~PFrame~PFrame~data,Enabled
Just offsets, attribute names and value mutable
.
That was the problem with this attribute.
Problem
Each chunk in the model may have an attribute. mutable=false
, which tells fuzzer not to touch its contents. This may be required if there is a magic word, like pcap
files, for example, – A1B2C3D4
.
However, AFLSmart stubbornly ignored this. He correctly parsed the value, but he subjected the data to mutations anyway. AFLSmart has a debug launch option -l
, it includes logging the results of high-level mutations, logs can then be viewed in the {out} / log folder.
I had to climb inside and figure out what was the matter. Everything turned out to be easier than ever.
The representation of the chunk in memory is as follows:
struct chunk {
unsigned long
id; /* The id of the chunk, which either equals its pointer value or, when
loaded from chunks file, equals to the hashcode of its chunk
identifer string casted to unsigned long. */
int type; /* The hashcode of the chunk type. */
int start_byte; /* The start byte, negative if unknown. */
int end_byte; /* The last byte, negative if unknown. */
char modifiable; /* The modifiable flag. */
struct chunk *next; /* The next sibling child. */
struct chunk *children; /* The children chunks linked list. */
};
We see the field modifiable
. It takes the value 0 if the chunk cannot be modified, and 1 if it can.
It is logical to assume that this field should be checked somewhere.
A quick search of the sources led us to the conclusion that it is not checked anywhere. Although there are two functions where this can be done.
Function get_chunk_to_delete
gives a random chunk for deletion, but checks only its coordinates:
struct chunk *get_chunk_to_delete(struct chunk **chunks_array, u32 total_chunks,
u32 *del_from, u32 *del_len) {
struct chunk *chunk_to_delete = NULL;
u8 i;
*del_from = 0;
*del_len = 0;
for (i = 0; i < 3; ++i) {
int start_byte;
u32 chunk_id = UR(total_chunks);
chunk_to_delete = chunks_array[chunk_id];
start_byte = chunk_to_delete->start_byte;
if (start_byte >= 0 &&
chunk_to_delete->end_byte >= start_byte) {
*del_from = start_byte;
*del_len = chunk_to_delete->end_byte - start_byte + 1;
break;
}
}
Same thing with function get_target_to_splice
that issues a chunk to be replaced:
struct chunk *get_target_to_splice(struct chunk **chunks_array,
u32 total_chunks, int *target_start_byte,
u32 *target_len, u32 *type) {
struct chunk *target_chunk = NULL;
u8 i;
*target_start_byte = 0;
*target_len = 0;
*type = 0;
for (i = 0; i < 3; ++i) {
u32 chunk_id = UR(total_chunks);
target_chunk = chunks_array[chunk_id];
*target_start_byte = target_chunk->start_byte;
if (*target_start_byte >= 0 &&
target_chunk->end_byte >= *target_start_byte) {
*target_len = target_chunk->end_byte - *target_start_byte + 1;
*type = target_chunk->type;
break;
}
}
return target_chunk;
}
In general, the matter was solved by adding field checks modifiable
in these functions and pull request to the AFLSmart repository. The authors have made changes.
So, now you can see how AFLSmart works and what modes it has.
AFLSmart Modes
AFLSmart has two modes of operation, and the choice of mode depends on how much you need native AFL mutators (low-level).
In default mode, higher-level operators work first. Moreover, if the result of their work does not lead to an increase in coverage, a low-level splicing mutator will be applied. He takes the original case and the random case, then changes part of the contents of the original (the second case acts as a source). Fazzer further looks at how much this will improve the coverage situation.
In the second mode, the fuzzer first changes the case through the high-level mutators, and then the result scrolls through the random low-level ones. As you understand, low-level mutators no longer take into account the format and can easily spoil fields that cannot be touched. It is necessary to understand and take this into account. This mode is called stacking and is enabled by the -h option.
By the way, the cool option -e
: it will substitute the extension
into the current case. The original AFL writes the next test case to the file {out} /. Cur_input, but there are applications that check the extension and wait for input, for example, {out} /. Cur_input.png | wav | avi, etc. If there is no proper extension, the file will simply be discarded. Therefore, developers have added such a feature.
Total
After eliminating the defect, fuzzing with AFLSmart still didn’t take off. The library we are studying starts several threads, and AFLSmart starts to go crazy, because the same test case can detect different paths, and the standard recommendations did not work for us. This is the mechanism of the original AFL, it was originally created for file parsers, and not for working with network packets of an asynchronous server.
See for yourself, authors demonstratethey found 42 zero-day vulnerabilities in several popular applications. These are all parsers that AFLSmart can handle out of the box.
This restriction can be overcome if you finish clang
module afl-llvm-pass.so and force him to instrument only the code responsible for parsing packets. Also in AFL and AFLSmart you can load your bitmap and make it ignore paths in other threads. But more about that next time.