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.

By the way, there is another interesting approach to this problem, but more on that later

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

bytestypeNameDescription
4uint32magic‘A1B2C3D4’ means the endianness is correct
2uint16vmajormajor number of the file format
2uint16vminorminor number of the file format
4int32thiszonecorrection time in seconds from UTC to local time (0)
4uint32sigfigsaccuracy of time stamps in the capture (0)
4uint32snaplenmax length of captured packed (65535)
4uint32networktype of data link (1 = ethernet)

Frame

bytestypeNameDescription
4uint32ts_sectimestamp seconds
4uint32ts_usectimestamp microseconds
4uint32incl_lennumber of octets of packet saved in file
4uint32orig_lenactual length of packet
incl_lenuint32datadata

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_splicethat 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *