ttdennis / fpicker

fpicker is a Frida-based fuzzing suite supporting various modes (including AFL++ in-process fuzzing)
MIT License
255 stars 25 forks source link
frida fuzzer in-process

fpicker

Fpicker logo

fpicker is a Frida-based fuzzing suite that offers a variety of fuzzing modes for in-process fuzzing, such as an AFL++ mode or a passive tracing mode. It should run on all platforms that are supported by Frida.

Some background information and the thoughts and ideas behind fpicker can be found in a blogpost I wrote.

Fpicker is based on previous efforts on ToothPicker, which was developed during my master thesis. Most of fpicker was developed during working hours at my employer (ERNW).

Requirements and Installation

Required for running fpicker:

Required only when running in AFL++ mode:

Building and Running

Fpicker can be built for macOS, iOS or Linux. The Makefile currently only supports building for iOS on macOS but it should be totally possible to build fpicker using an iOS toolchain on Linux.

Depending on the desired target run:

make fpicker-macos
make fpicker-ios
make fpicker-linux

to build fpicker.

Once fpicker is built, the fuzzing harness needs to be built next:

See the examples folder for different sample fuzzing cases. The general approach is as follows:

Now fpicker can start fuzzing. The exact command highly depends on the configuration and setup. In the following, a few example cases are given. These mostly correspond to the examples in the examples folder.

Creating a Fuzzing Harness

Each target requires its own fuzzing harness. The most important part of this harness is defining the entry function of Frida's Stalker, which effectively determines at which point the instrumentation is inserted. In the in-process mode this is simple. The function would usually be the one that is called on each fuzzing iteration. However, it could also be a different one.

A minimalist harness implementation (in command mode) could be this:

// Import the fuzzer base class
const Fuzzer = require("harness/fuzzer.js");

// The custom fuzzer needs to subclass the Fuzzer class to work properly
class TestFuzzer extends Fuzzer.Fuzzer {
    constructor() {
        // The constructor needs to specify the address of the targeted function and a NativeFunction
        // object that can later be called by the fuzzer.

        const FUZZ_FUNCTION_ADDR = Module.getExportByName(null, "FUZZ_FUNCTION");
        const FUZZ_FUNCTION = new NativeFunction(
            FUZZ_FUNCTION_ADDR,
            "void", ["pointer", "int64"], {
        });

        super("test", FUZZ_FUNCTION_ADDR, FUZZ_FUNCTION);
    }
}

const f = new TestFuzzer();
exports.fuzzer = f;

This harness configures the instrumentation to follow the function FUZZ_FUNCTION. The instrumentation will start when this function is entered and stops when the function returns. This function should be chosen carefully as it is expensive and the more (potentially unimportant) parts of the process are instrumented, the slower the fuzzer gets. Of course, this is a consideration between speed and intended coverage. Additionally, the fuzzer currently only supports functions that are only entered once during one fuzzing iteration, i.e., the function should not be called more than once during one fuzz case, otherwise the coverage information might become unreliable.

When the in-process mode is used, another function is required in the fuzzer script. The fuzz method. It will get called on each iteration. It will be called with two parameters, a pointer to a buffer and the length of the buffer. Our exemplary target function takes two parameters, a pointer to a buffer and its length. Thus, we can just pass the parameters were getting in the fuzz method.

fuzz(payload, len) {
    this.target_function(payload, parseInt(len));
}

In passive mode, a callback needs to be specified that processes the required data. The fuzzer expects to receive a payload buffer and its length. Depending on the target function that is fuzzed, this data needs to be extracted. In the following example, we again have a function that has two parameters: a pointer to a buffer and its length. The args parameter contains all potential parameters the target function receives, so the length parameter (which is the second one in our case) can be accessed with args[1]. We then read the buffer as Uint8Array and send it back to the fuzzer using the sendPassiveCorpus method.

passiveCallback(args) {
    const len = args[1];
    const data = new Uint8Array(Memory.readByteArray(args[0], parseInt(len)));

    // this encodes the data and sends it back to the fuzzer
    this.sendPassiveCorpus(data, len);
}

In case the target needs some sort of preparation before the fuzzer can start, fpicker provides a prepare method that is called during the initialization of the fuzzer. Preparation could be the establishment of state, e.g., by instantiating an object. Such a preparation function could look like the following:

prepare() {
  // the object can be attached to the fuzzer instance so that it can be used within the
  // fuzz() method later on.
  this.required_object = call_native_function_that_creates_object();
}

Modes and Configuration

pficker offers a large set of modes and configurations that are explained in the following. Most of these modes can be combined in different ways. At the end of this section is a table that shows which options can be combined and what their implementation status is.

Fuzzer Mode

Fpicker has three different fuzzing modes: AFL++ Mode, Standalone Active Mode and Standalone Passive Mode:

Input Mode

While fpicker is largely designed as an in-process fuzzer, it also supports fuzzing via an external command. For this fpicker offers two input modes.

Communication Mode

Communication mode determines how the injected harness communicates with the fuzzer. This largely depends on the target application. Frida offers an API to send and receive messages from the injected agent script. This type of communication is quite costly. One of the factors is that the transported message needs to be encoded in JSON. So sending binary data is straight-forward. Therefore, fpicker offers a second communicateion mode over shared memory. However, this only works if it is possible to establish shared memory between the fuzzer and the target application, which means that this mode cannot be used when the target is attached to the fuzzer host via USB. In CMD input mode, the communication mode only refers to how the coverage information is communicated back to the fuzzer, not how the payload is sent, as this is deferred to an external command.

Exec Mode

Exec mode can be either spawn or attach. This is pretty self-explanatory. fpicker can either attach to a runnning process or spawn a process. One thing that is a major difference between the two modes is that, should the attached target crash, fpicker will not try to respawn.

Standalone Mutator

In standalone mode fpicker offers three different input mutation strategies. Nicely put, input mutation certainly has lots of room for improvement.

USB devices

Using the -D usb device option, Frida will pick the first local USB device, e.g. an iPhone or Android phone.

Network devices

With option -D remote it is possible to fuzz a process running on a network device. For this, the remote device must be running frida-server. As a sample configuration, use SSH with port forwarding to bind the frida-server default listening port 27042 on the remote device to a socket on the local client.

ssh -N user@network.device -L 127.0.0.1:27042:127.0.0.1:27042

On an iPhone, one can also use iproxy to forward the port from a USB connection. This might be especially useful if running Frida on a non-standard port on a non-jailbroken device with the Frida gadget. When working with the Frida gadget, the only available process will have the name Gadget, regardless of the target app name.

iproxy 27042 27042

Then use frida-ps to validate the configuration by listing processes on the remote device:

frida-ps -R