Harnessing the Application

Note that as written, our application will be running the certificate verification with uninitialized allocated memory. We want to run it instead using our fuzzer input, so we need to add harnessing. We've already #include-ed our harness header file and loaded the TSFFS module in our simulation, so we're halfway there.

Adding Harness Code

In our Tutorial.c file, we'll add a few lines of code so that our main function looks like this (the rest of the code can stay the same):

EFI_STATUS
EFIAPI
UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) {
  UINTN MaxInputSize = 0x1000;
  UINTN InputSize = MaxInputSize;
  UINT8 *Input = (UINT8 *)AllocatePages(EFI_SIZE_TO_PAGES(MaxInputSize));

  if (!Input) {
    return EFI_OUT_OF_RESOURCES;
  }

  HARNESS_START(Input, &InputSize);

  Print(L"Input: %p Size: %d\n", Input, InputSize);
  UINT8 *Cert = Input;
  UINTN CertSize = InputSize / 2;
  UINT8 *CACert = (Input + CertSize);
  UINTN CACertSize = CertSize;

  Print(L"Certificate:\n");
  hexdump(Cert, CertSize);
  Print(L"CA Certificate:\n");
  hexdump(CACert, CACertSize);

  BOOLEAN Status = X509VerifyCert(Cert, CertSize, CACert, CACertSize);

  if (Status) {
    HARNESS_ASSERT();
  } else {
    HARNESS_STOP();
  }

  if (Input) {
    FreePages(Input, EFI_SIZE_TO_PAGES(MaxInputSize));
  }

  return EFI_SUCCESS;
}

First, we invoke HARNESS_START with two arguments:

  • The pointer to our buffer -- this is where the fuzzer will write each testcase
  • The pointer to our maximum input size (aka, the size of the buffer). The fuzzer records the initial value and will truncate testcases to it so it does not cause buffer overflows, and will write the actual size of the input here each iteration so we know how much data the fuzzer has given us.

Then, we let the function we are testing run normally. If a CPU exception happens, the fuzzer will pick it up and treat the input as a "solution" that triggers a configured exceptional condition.

Finally, we check the status of certificate verification. If validation was successful, we HARNESS_ASSERT because we really do not expect this to happen, and we want to know if it does happen. This type of assertion can be used for any condition that you want to fuzz for in your code. If the status is a certificate verification failure, we HARNESS_STOP, which just tells the fuzzer we completed our test under normal conditions and we should run again.

Re-compile the application by running the build script.

Obtain a Corpus

The fuzzer will take input from the corpus directory in the project directory, so we'll create that directory and add some sample certificate files in DER format as our input corpus.

mkdir corpus
curl -L -o corpus/0 https://github.com/dvyukov/go-fuzz-corpus/raw/master/x509/certificate/corpus/0
curl -L -o corpus/1 https://github.com/dvyukov/go-fuzz-corpus/raw/master/x509/certificate/corpus/1
curl -L -o corpus/2 https://github.com/dvyukov/go-fuzz-corpus/raw/master/x509/certificate/corpus/2
curl -L -o corpus/3 https://github.com/dvyukov/go-fuzz-corpus/raw/master/x509/certificate/corpus/3

Configuring the Fuzzer

Even though we loaded the fuzzer module, it didn't run previously because we did not instantiate and configure it. Let's do that now. At the top of your run.simics script, we'll add each of the following lines.

First, we need to create an actual tsffs object to instantiate the fuzzer.

load-module tsffs # You should already have this
init-tsffs

Next, we'll set the log level to maximum for demonstration purposes:

tsffs.log-level 4

Then, we'll set the fuzzer to start and stop on the magic harnesses we just compiled into our UEFI application. This is the default, so these calls can be skipped in real usage unless you want to change the defaults, they are just provided here for completeness.

@tsffs.start_on_harness = True
@tsffs.stop_on_harness = True

We'll set up our "solutions" which are all the exceptional conditions that we want to fuzz for. In our case, these are timeouts (we'll set the timeout to 3 seconds) to detect hangs, and CPU exceptions. we'll enable exceptions 13 for general protection fault and 14 for page faults to detect out of bounds reads and writes.

@tsffs.timeout = 3.0
@tsffs.exceptions = [13, 14]

We'll tell the fuzzer where to take its corpus and save its solutions. The fuzzer will take its corpus from the corpus directory and save solutions to the solutions directory in the project by default, so this call can be skipped in real usage unless you want to change the defaults.

@tsffs.corpus_directory = SIM_lookup_file("%simics%/corpus")
@tsffs.solutions_directory = SIM_lookup_file("%simics%/solutions")

We'll also delete the following code from the run.simics script:

script-branch {
  bp.time.wait-for seconds = 30
  quit 0
}

Since we'll be fuzzing, we don't want to exit!