Home Blog Embedded Fuzzing Type-Aware Fuzzing with Security Benchmarks

Type-Aware Fuzzing with Security Benchmarks

Author: Arjen Rouvoet

Here’s the third blog post in our series about fuzzing in embedded systems. Click here to view other blog posts.

The attack area of embedded systems is large. The software must not only be secure in friendly operational conditions but also be resilient in a hostile environment where data may be compromised. Verifying that your software products are secure in those circumstances is hard. Despite the development of a variety of techniques to aid the verification, teams struggle with the integration of these techniques into their software development process. One of the reasons is the lack of good, integrated and supported tooling that takes off some of the burden. At Riscure, we have been working to address those struggles that we see time and time again in embedded software development teams. In this blog post, we discuss the problem of harnessing C code for fuzzing.

Fuzz Targets and their Types

There is a gap between the fuzzing engine and the fuzzing test target. Consider a simplified IOT device that receives messages from the network, deserializes them, and then dispatches an operation of the device based on the message contents:

typedef struct ClientMessage_t {
  enum MessageType type,
  char* header;
  char* content;
  // ... meta-data omitted
} *ClientMessage;

void dispatch(ClientMessage message);

ClientMessage read_next_message();

void handle_next() {
    ClientMessage incoming = read_next_message();
    dispatch(incoming);
    ClientMessage_free(incoming);
}

int main(void) {
    while(true) {
        handle_next();
    }
}

The application loop that processes messages assumes that the incoming data is tainted—i.e., that it can be controlled by an attacker. With fuzzing we can check that it sanitizes its inputs correctly and can handle nasty corner cases without crashing the device.

In principle we can fuzz the whole application, but in practice it is better to fuzz smaller components that are easier to validate. Here, the responsibilities of sanitizing the untrusted inputs are divided between the function that reads the incoming raw network packages into the ClientMessage struct, and the function that dispatches between various operations based on the message type. One important reason to fuzz the two components separately is that the function read_next_message reduces the size of the relevant input significantly. This means that if we fuzz dispatch separately, it will be much easier to reach a deeper coverage quickly.

Let us now consider fuzzing of the dispatch function in more detail.

Fuzzing with Well Structured Inputs

The function dispatch is a good fuzzing target: it assumes that the content of the message that it reads is tainted. Hence, it validates the content and should handle invalid data without crashing. Importantly, however, it presumes that the high-level structure of the message have been parsed by the code that receives the message. This division of responsibilities is reflected in the type struct ClientMessage_t.

When we fuzz a function like dispatch that delegates some responsibility to the caller, it is not useful to fuzz it inputs that violate the assumed contract, because for such inputs, the caller is to blame for a potential crash. Hence, we want to delimit the input space that the fuzzing engine explores. In this case, we want to delimit the inputs to valid ClientMessage struct instances.

Fuzzing engines like libfuzzer of AFL treat the target as a black box, however, and have no notion of structured inputs: inputs as just raw byte arrays from the engine’s perspective. This gap between the raw and the structured inputs becomes very visible in the fuzzing driver:

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // For meaningful fuzz results we want to call dispatch
    // with well structured client-messages, varying only the content
    // and not the shape of the message.
    // Yet, we only have a _raw_ data buffer here.
    dispatch(???);
}

 

The byte-level mutations of the engine are not directly informed by the meaning of the bytes and hence are not guaranteed to maintain the desired invariant that inputs remain well structured. The engine is indirectly informed by running the program with the generated input, which can result in the program rejecting the input, steering further input exploration. But this can be a relatively slow path, because many inputs can be rejected if the subset of valid inputs is sparse.

We need to bridge this gap between the low-level inputs provided by the engine and the structured inputs required by these fuzz targets. A technique to accomplish this is to treat the byte array coming from the engine as if it were a binary serialization of a well-structured input. The “bridge” then needs to parse the binary input. Coming back to our example, the fuzzing driver would look like this:

ClientMessage parse(const uint8_t *data, size_t size);

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    ClientMessage message = parse(data, size);
    if (!message) {
        return -1; // reject the input
    }
    dispatch(message);

    return 0;
}

 

We call such a fuzzing harness ‘type-aware’, because parse essentially uses the available static type information to navigate the input space of dispatch more efficiently.

Auto-Generated Type Providers

That is the theory. In practice, we face the problem of implementing the function parse. This is tricky, because unlike a normal binary deserializer, we cannot assume much about the binary input! To get the most out of our fuzzing campaign, we want to reject as few inputs as possible. Or, conversely, we want to translate as many byte arrays as possible to some well-structured input. At the same time, we want to avoid bias in the input generation, so that the engine can in principle still trigger every program behavior. This is extra tricky because the binary input has limited size. We have to spend the entropy in this input wisely.

In the upcoming Riscure True Code update 2023.2, we are releasing new extensive support for type-aware fuzzing benchmarks to automate most of the work of implementing this bridge between the fuzzing engine and your fuzz target. To use them, you simply declare the required function, as follows (1):

// This is a null-safe type filler/provider whose implementation
// will be generated by True Code.
void tc_fill_ClientMessage(struct ClientMessage *clientMessage, ProviderContext context);
// ... as well as its inverse for freeing any allocated data.
void tc_empty_ClientMessage(struct ClientMessage *clientMessage);

int driver(ProviderContext context) {
    ClientMessage message = malloc(sizeof(struct ClientMessage));
    dispatch(tc_fill_struct_ClientMessage(message, context));

    if (message) {
        tc_empty_struct_ClientMessage(message);
        free(message);
    }

    return 0;
}

 

The implementation of these type fillers and emptiers will be generated by True Code during the benchmark’s execution, so that they are transparently updated whenever you change the definitions of the types.

Because type providers are generated modularly in 1-to-1 relation with you type definitions it is also easy to refine their implementation in case the out-of-the-box behavior is not exactly what you need. For example, True Code generates the following type provider for enums:

enum MessageType tc_provide_enum_MessageType(ProviderContext context) {
    // This will MessageType values with any value between 0 and 255,
    // regardless if those are valid message types, potentially
    // revealing bugs for functions that have unwated behaviors for
    // invalid message types.
    return provide_uint_in_range(0, 255, context);
}

 

If you are only interested in the target’s behavior for valid message types, you can define your own implementation of this signature to override the default. As a consequence, all ClientMessage structs will then have a valid type field.

Conclusion

We looked at an approach to fuzz target functions that receive structured parameters akin to type-aware fuzzing and upcoming extensive automation to help you employ this method to real-world C codebases. This new automated removes the barrier to fuzzing many C target functions and fully integrates into normal C development workflows, as code generation moves in sync with your evolving source code.

Type-aware benchmarks will be the default when you create a new fuzzing benchmarks with True Code 2023.2. That is, the driver shown above will be generated automatically when you create a benchmark for the target function dispatch. This allows you to start fuzzing new targets with the virtual press of a button.

[1] Since this blog is a feature-preview, implementation details may differ in the actual release. Consult the documentation of your True Code release (>= 2023.2) for the exact API.

Share This