Skip to content

expert

Mayhem and Test Drivers

In this lesson we'll walk you through how a test driver works to test specific functions of an overall program!

Beware the Nomenclature

Users familiar with fuzzing may use the term "fuzzing harness" instead. To be clear, when we say "test driver" this is the same as a "fuzzing harness".


Estimated Time: 10 minutes

By the end of this lesson, you will be able to:

  1. Understand why a test driver is important.
  2. Define what a test driver is.
  3. Walk through an example test driver.
  4. Dissect the Heartbleed vulnerability.

Why Create a Test Driver?

Oftentimes it's necessary to create a more focused fuzzing operation by writing a test driver, or a small program that calls functions of interest. For example, if you had a large web server application, but only wanted to test the associated HTTP request parser, you can create a test driver to fuzz just the HTTP request parser rather than the entire web server application.

In addition, if your application does not meet the Mayhem Program Compatibility criteria, you can also write a test driver to make it compatible with Mayhem. For example, you can write a test driver that mocks out specialized hardware, turning an application that is otherwise unfuzzable (and difficult to test in general!) into one that can you analyze.

Note

This section assumes you have already gone through previous tutorials and have basic familiarity with Mayhem and the Mayhem CLI.

Mayhem and Test Drivers

A test driver creates a new entrypoint to the binary and can be written around any section of code to be more generic, or targeted, depending on the user's needs. Therefore, if you can identify an interesting function, you can vector Mayhem to focus its analysis on that specific region of code.

By creating a test driver, Mayhem will only focus on that part of the code, reducing time to find bugs.

Some examples of interesting functions may include:

  • User exposed code paths.
  • A critical procedure.
  • Historically buggy code from a developers perspective.

Test drivers can also help skip over initialization or blocking functions. For example, a target may require a username and password login to first authenticate before executing any interesting functions. A test driver can help skip over any initialization or blocking function that may exist.

Understanding an Example Test Driver

Let's now walk through how to an example test driver application works.

1
2
3
4
5
6
7
8
int SwordHarness(uint8_t *Data, size_t Size)
{
  char cmd = Data[0];
  uint8_t *nuke_data = Data + 1;
  if(cmd == LAUNCH_NUKE || cmd == CONFIGURE_NUKE)
    process_nuke_cmd(cmd, nuke_data);
  return 0;
}

In this example test driver, a SwordHarness function reads input (such as from a file) and calls the function of interest: process_nuke_cmd. Here we assume that the function parameter *Data is the input file provided by the fuzzer, and Size is the length of the input.

Note

The test driver must initialize any buffers of invariants needed to call the function.

The SwordHarness function uses the first byte of *Data to fill in the cmd variable and use the remaining bytes for nuke_data. In the case of the cmd set to LAUNCH_NUKE or CONFIGURE_NUKE, the process_nuke_cmd will use the cmd and nuke_data variables. Because this is a test driver, we can expect that the input will be mutated accordingly to maximize code coverage.

A Real World Use Case

In 2014, two teams of security researchers began independently fuzzing the OpenSSL library and found what was called the Heartbleed Vulnerability, a serious flaw in the OpenSSL encryption software that powered a lot of secure communications on the web.

Tip

Check out our blog for more information on the Heartbleed Vulnerability

In the next example, we'll go over a test driver that reproduces the Heartbleed vulnerability. Similar to our previous example test driver, the *Data and Size parameters represent the fuzzing inputs to the HeartBleed function.

Here we can see that the test driver sets up the openssl context required for most of the library's functionality and then performs a handshake with the data provided into the test driver. This is indicated on lines 4 and 13, respectively.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void HeartBleed(const uint8_t *Data, size_t Size)
{
  //initialization
  SSL_CTX *sctx = Init();
  SSL *server = SSL_new(sctx);
  BIO *sinbio = BIO_new(BIO_s_mem());
  BIO *soutbio = BIO_new(BIO_s_mem());

  //send one message
  SSL_set_bio(server, sinbio, soutbio);
  SSL_set_accept_state(server);
  BIO_write(sinbio, Data, Size);
  SSL_do_handshake(server);
  SSL_free(server);
  SSL_CTX_free(sctx);
}

Writing Quality Test Drivers

There are several best practices you should consider when writing a test driver in order to improve your test driver performance and maximize the efficiency of the analysis.

  1. Ensure determinism: Avoid modifying global state on each iteration.
  2. Avoid things that cause delays: Mock databases, network connections, or other external components.
  3. Isolate code under test: Only run the code you want to test or necessary for interesting paths.
  4. Avoid reading/writing from disk: Slows down analysis and reduces determinism.
  5. Check that your test driver makes progress quickly and continually: Monitor the progress of your test driver to ensure that there are no pain points causing performance slowdowns.

Below is a visual comparison for code coverage of an example target application. Here we see that when comparing a test driver written without best practices compared to a test driver written with best practices, the latter test driver is more performant and can therefore achieve more code coverage in regards to the underlying target application.

bad-harness.png

*Harness written without best practices.*

good-harness.png

*Harness written with best practices.*

✏️ Summary and Recap

In this lesson, you learned why you should create a test driver, how Mayhem uses test drivers to fuzz specific functionality of an overall taret application, and the best practices to keep in mind when writing your own test driver.


I learned how to...

1. Understand why a test driver is important.
  • Oftentimes it's necessary to create a more focused fuzzing operation by writing a test driver, or a small program that calls functions of interest. For example, if you had a large web server application, but only wanted to test the associated HTTP request parser, you can create a test driver to fuzz just the HTTP request parser rather than the entire web server application.
  • If your application does not meet the Mayhem Program Compatibility criteria, you can also write a test driver to make it compatible with Mayhem. For example, you can write a test driver that mocks out specialized hardware, turning an application that is otherwise unfuzzable (and difficult to test in general!) into one that can you analyze.
2. Define what a test driver is.
  • A test driver creates a new entrypoint to the binary and can be written around any section of code to be more generic, or targeted, depending on the user's needs. Therefore, if you can identify an interesting function, you can vector Mayhem to focus its analysis on that specific region of code.
3. Walk through an example test driver.
  • In this example test driver, a SwordHarness function reads input (such as from a file) and calls the function of interest: process_nuke_cmd. Here we assume that the function parameter *Data is the input file provided by the fuzzer, and Size is the length of the input.
  • The SwordHarness function uses the first byte of *Data to fill in the cmd variable and use the remaining bytes for nuke_data. In the case of the cmd set to LAUNCH_NUKE or CONFIGURE_NUKE, the process_nuke_cmd will use the cmd and nuke_data variables. Because this is a test driver, we can expect that the input will be mutated accordingly to maximize code coverage.

    1
    2
    3
    4
    5
    6
    7
    8
    int SwordHarness(uint8_t *Data, size_t Size)
    {
        char cmd = Data[0];
        uint8_t *nuke_data = Data + 1;
        if(cmd == LAUNCH_NUKE || cmd == CONFIGURE_NUKE)
            process_nuke_cmd(cmd, nuke_data);
        return 0;
    }
    
4. Dissect the Heartbleed vulnerability.
  • In 2014, two teams of security researchers began independently fuzzing the OpenSSL library and found what was called the Heartbleed Vulnerability, a serious flaw in the OpenSSL encryption software that powered a lot of secure communications on the web.
  • Here we can see that the test driver sets up the openssl context required for most of the library's functionality and then performs a handshake with the data provided into the test driver. This is indicated on lines 4 and 13, respectively.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    void HeartBleed(const uint8_t *Data, size_t Size)
    {
        //initialization
        SSL_CTX *sctx = Init();
        SSL *server = SSL_new(sctx);
        BIO *sinbio = BIO_new(BIO_s_mem());
        BIO *soutbio = BIO_new(BIO_s_mem());
    
        //send one message
        SSL_set_bio(server, sinbio, soutbio);
        SSL_set_accept_state(server);
        BIO_write(sinbio, Data, Size);
        SSL_do_handshake(server);
        SSL_free(server);
        SSL_CTX_free(sctx);
    }