Fuzzing libsignal-protocol-c with libfuzzer and OSS-Fuzz

I was looking for something productive to do with my time and saw that libsignal-protocol-c (hereinafter referred to as libsignal) had an open ticket from Google asking if they were interested in integrating the project with OSS-Fuzz. From poking around I saw that it was a pretty large (~30,000 LoC) C project with minimal unit tests and no fuzzing set up. And it is used by a number of different apps to implement end-to-end encrypted chat apps. So it seemed like a prime target for fuzzing.

Since it is a library, it doesn’t provide any CLI tool that could easily be integrated with AFL. So I chose to target libFuzzer. libFuzzer is LLVM’s coverage guided fuzzer that has recently become pretty popular in the fuzzing community. In order to integrate a program with libFuzzer you have to create a fuzz target, which is just a function that takes your random data and does something interesting with it.

libFuzzer provides the below example fuzz target:

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  if (size > 0 && data[0] == 'H')
    if (size > 1 && data[1] == 'I')
       if (size > 2 && data[2] == '!')
       __builtin_trap();
  return 0;
}

This program will crash if it is given an input that starts with HI!. In order to test libsignal with libFuzzer, we have to choose what we want to do with the random input from libFuzzer. In this case, attempting to decrypt it as if it was a message we received over the network seems like a good choice. Anything that uses libsignal likely exposes this in some way, so if we were to find a bug here it would be pretty critical.

In order to do so I started by creating fuzz_target.c:

extern int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  int result = 0;

  // ... Set up Bob as a libsignal user and create their keys ... 

  // Attempt to deserialize the input data
  pre_key_signal_message *incoming_message_bad = 0;
  result = pre_key_signal_message_deserialize(
      &incoming_message_bad, Data, Size, global_context);
  if (result != 0) {
    goto done;
  }

  // And if it deserialized okay, then decrypt it 
  signal_buffer *plaintext = 0;
  session_cipher_decrypt_pre_key_signal_message(
      bob_session_cipher, incoming_message_bad, 0, &plaintext);

  return 0;
}

Next, I just had to build this. libsignal uses CMake to handle their builds, so I just added a new executable target to the CMakeLists.txt:

find_package(Check 0.9.10 REQUIRED)
find_package(OpenSSL 1.0 REQUIRED)
set(LIBS 
	${CHECK_LDFLAGS}
	${OPENSSL_LIBRARIES}
	signal-protocol-c
)
add_executable(fuzzer "fuzz_target.c" "test_common_openssl.c")
target_link_libraries(fuzzer ${LIBS})
set_target_properties(fuzzer
	PROPERTIES COMPILE_FLAGS "-fsanitize=fuzzer-no-link,address"
)
set_target_properties(fuzzer
	PROPERTIES LINK_FLAGS "-fsanitize=fuzzer,address -z muldefs"
)

This will build an executable fuzzer that will run our fuzzer. So after running cmake and make, we get fuzzer and can run our fuzzer:

corp: 46/4780b lim: 254 exec/s: 0 rss: 40Mb L: 177/254 MS: 2 ChangeBit-ChangeBit-
#1024	pulse  cov: 185 ft: 494 corp: 46/4780b lim: 261 exec/s: 512 rss: 64Mb
#1872	REDUCE cov: 185 ft: 494 corp: 46/4779b lim: 269 exec/s: 468 rss: 92Mb L: 2/254 MS: 3 ChangeBit-CopyPart-EraseBytes-
#2048	pulse  cov: 185 ft: 494 corp: 46/4779b lim: 269 exec/s: 409 rss: 95Mb

Note that in the above I added a few flags to help the fuzzer learn how to explore the code base and trigger new paths. I also am running it with a corpus directory which I seeded with a couple valid serialized ciphertexts. I ran this on my own hardware for ~100 million runs and it didn’t find any crashes.

I sent in a PR to Signal to try to add it to their test suite (so it could get added to OSS-Fuzz!) but sadly that PR has languished since I opened it. But at least in my own fuzz testing, it seemed solid. ¯\(ツ)