Implementing a custom stack minimizer in libfuzzer
Following from part 1 (https://medium.com/@ditt0/rode0day-fuzzing-challenge-part-1-6de5f5367f2d), in this post we will try to implement a stack minimizer in libfuzzer to detect unique crashes!
The plan is as such:
- Linking with our custom libfuzzer
2. Hook the crash handler and read the stack when it crashes
3. Throw away if its not unique
##1. Linking with our custom libfuzzer
Libfuzzer is open source. Its possible to compile it yourself.
Edit the below files:
FuzzerMain.cpp
// FuzzerMain.cpp
#include "FuzzerDefs.h"
#include "FuzzerPlatform.h"
extern "C" {
// This function should be defined by the user.
int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size);
} // extern "C"
ATTRIBUTE_INTERFACE int main(int argc, char **argv) {
printf("Calling this main!\n");
printf("Libfuzzer hello world!\n");
return fuzzer::FuzzerDriver(&argc, &argv, LLVMFuzzerTestOneInput);
}
```
`file-pre.c` exposes `LLVMFuzzerTestOneInput`
```c
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
const char* file = buf_to_file(data, size);
if (file == NULL) {
exit(EXIT_FAILURE);
}
// ./file -m ~/Desktop/magic.mgc /bin/bash
char *args[4];
args[0] = "./file";
args[1] = "-m"; // First argument to main
args[2] = "/home/user/Desktop/magic.mgc"; // Terminating the argument list
args[3] = file; // Terminating the argument list
main2(4, args);
if (delete_file(file) != 0) {
exit(EXIT_FAILURE);
}
return 0;
}
file-pre.c (harness)
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
const char* file = buf_to_file(data, size);
if (file == NULL) {
exit(EXIT_FAILURE);
}
// ./file -m ~/Desktop/magic.mgc /bin/bash
char *args[4];
args[0] = "./file";
args[1] = "-m"; // First argument to main
args[2] = "/home/user/Desktop/magic.mgc"; // Terminating the argument list
args[3] = file; // Terminating the argument list
main2(4, args);
if (delete_file(file) != 0) {
exit(EXIT_FAILURE);
}
return 0;
}
Makefile:
clang++ -g $(CFLAGS) $^ use_mutator.cc -lz ./fuzzer/libFuzzer.a
##2 & 3. Hooking the crash handler
So what we can do is to write a hook at `WriteUnitToFileWithPrefix` (https://github.com/rust-fuzz/libfuzzer-sys/blob/35ce7d7177c254b9c798aec171dfe76877d1a20f/libfuzzer/FuzzerLoop.cpp#L578)
This function writes the crashing input onto disk. Before writing, we can check if the crashing stack trace is similar to what we have already. If it is similar then we do not write the crash. This will allow us to find unique crashes.
However, despite not saving the crash, Libfuzzer still terminates. This can be avoided by running libfuzzer in the fork mode.
Add the following code into your custom libfuzzer for detecting the stack:
int isDuplicate(const char *filename, const char *concatenated) {
// Open the file for reading
FILE *file = fopen(filename, "r");
if (file == NULL) {
perror("Error opening file");
return -1; // Return -1 to indicate error
}
char buffer[4096]; // Buffer to read each line from the file
// Read each line from the file
while (fgets(buffer, sizeof(buffer), file) != NULL) {
// Compare the read line with the concatenated string
if (strncmp(buffer, concatenated, strlen(concatenated)) == 0) {
fclose(file);
return 1; // Return 1 if a duplicate is found
}
}
// Close the file
fclose(file);
// No duplicate found
return 0;
}
void logConcatenatedToFile(const char *filename, const char *concatenated) {
// Open the file for writing
FILE *file = fopen(filename, "a");
if (file == NULL) {
perror("Error opening file");
return;
}
// Write the concatenated string to the file
if (fprintf(file, "%s\n", concatenated) < 0) {
perror("Error writing to file");
fclose(file);
return;
}
// Close the file
fclose(file);
}
void Fuzzer::WriteUnitToFileWithPrefix(const Unit &U, const char *Prefix) {
Printf("Hi hi hook here better 2\n");
void *buffer[0x200];
int nptrs = backtrace(buffer, 0x200);
Printf("DeathCallback\n");
Printf("DeathCallback: nptrs %d\n", nptrs);
char **strings;
strings = backtrace_symbols(buffer, nptrs);
int duplicate = 0;
size_t total_length = 0;
for (int j = 0; j < nptrs; j++) {
total_length += strlen(strings[j]);
}
char *concatenated = (char*)malloc(total_length + 1); // +1 for null terminator
if (concatenated == NULL) {
return;
}
memset(concatenated, 0, total_length+1);
// concatenated[0] = '\0'; // Ensure the string is null-terminated
for (int j = 0; j < nptrs; j++) {
strcat(concatenated, strings[j]);
}
Printf("concatenated: %s\n", concatenated);
// logConcatenatedToFile("output.txt", concatenated);
int result = isDuplicate("output.txt", concatenated);
if (result == 0){
Printf("no duplicate\n");
logConcatenatedToFile("output.txt", concatenated);
Printf("Log to file!\n");
}
else{
Printf("duplicate found!");
return;
}
for (int j = 0; j < nptrs; j++){
Printf("%s\n", strings[j]);
if (strstr(strings[j], "0x814d665") != NULL){
duplicate = 1;
}
if (strstr(strings[j], "0x8144b81") != NULL){
duplicate = 1;
}
if (strstr(strings[j], "0x812abb0") != NULL){
duplicate = 1;
}
}
if (duplicate == 1){
Printf("Found duplicate\n");
return;
}
Printf("DeathCallback\n");
if (!Options.SaveArtifacts)
return;
std::string Path = Options.ArtifactPrefix + Prefix + Hash(U);
if (!Options.ExactArtifactPath.empty())
Path = Options.ExactArtifactPath; // Overrides ArtifactPrefix.
WriteToFile(U, Path);
Printf("artifact_prefix='%s'; Test unit written to %s\n",
Options.ArtifactPrefix.c_str(), Path.c_str());
if (U.size() <= kMaxUnitSizeToPrint)
Printf("Base64: %s\n", Base64(U).c_str());
}
Now we get unique crashes!
user@ubuntu:~/Desktop/20.05/source/fileB7_orig/src$ ./file ../../fileB7/src/crash-2da6f45ca5db982d188b1e50a767c8e9204dc36c
=================================================================
==2723698==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5900650 at pc 0x0812abb1 bp 0xffffcc78 sp 0xffffcc70
READ of size 4 at 0xf5900650 thread T0
#0 0x812abb0 in looks_ascii /home/user/Desktop/20.05/source/fileB7_orig/src/encoding-pre.c:3762:13
#1 0x812a2af in file_encoding /home/user/Desktop/20.05/source/fileB7_orig/src/encoding-pre.c:3701:5
#2 0x8131355 in file_buffer /home/user/Desktop/20.05/source/fileB7_orig/src/funcs-pre.c:4608:16
#3 0x8138998 in file_or_fd /home/user/Desktop/20.05/source/fileB7_orig/src/magic-pre.c:5258:6
#4 0x8138c57 in magic_file /home/user/Desktop/20.05/source/fileB7_orig/src/magic-pre.c:5118:9
#5 0x812de93 in process /home/user/Desktop/20.05/source/fileB7_orig/src/file-pre.c:5810:9
#6 0x812c234 in main /home/user/Desktop/20.05/source/fileB7_orig/src/file-pre.c:5654:9
#7 0xf7c68ed4 in __libc_start_main /build/glibc-EexDTL/glibc-2.31/csu/../csu/libc-start.c:308:16
#8 0x80635f5 in _start (/home/user/Desktop/20.05/source/fileB7_orig/src/file+0x80635f5)
0xf5900653 is located 0 bytes to the right of 3-byte region [0xf5900650,0xf5900653)
allocated by thread T0 here:
#0 0x80daf88 in calloc (/home/user/Desktop/20.05/source/fileB7_orig/src/file+0x80daf88)
#1 0x8129dde in file_encoding /home/user/Desktop/20.05/source/fileB7_orig/src/encoding-pre.c:3650:33
#2 0x8131355 in file_buffer /home/user/Desktop/20.05/source/fileB7_orig/src/funcs-pre.c:4608:16
#3 0x8138998 in file_or_fd /home/user/Desktop/20.05/source/fileB7_orig/src/magic-pre.c:5258:6
#4 0x8138c57 in magic_file /home/user/Desktop/20.05/source/fileB7_orig/src/magic-pre.c:5118:9
...
user@ubuntu:~/Desktop/20.05/source/fileB7_orig/src$ ./file ../../fileB7/src/crash-29e2dcfbb16f63bb0254df7585a15bb6fb5e927d
=================================================================
==2724484==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5900610 at pc 0x0812abb1 bp 0xffffcad8 sp 0xffffcad0
READ of size 4 at 0xf5900610 thread T0
#0 0x812abb0 in looks_ascii /home/user/Desktop/20.05/source/fileB7_orig/src/encoding-pre.c:3762:13
#1 0x812a2af in file_encoding /home/user/Desktop/20.05/source/fileB7_orig/src/encoding-pre.c:3701:5
#2 0x811ae19 in file_ascmagic /home/user/Desktop/20.05/source/fileB7_orig/src/ascmagic-pre.c:4840:6 // <----This is different from above!
#3 0x813196a in file_buffer /home/user/Desktop/20.05/source/fileB7_orig/src/funcs-pre.c:4709:7
#4 0x8138998 in file_or_fd /home/user/Desktop/20.05/source/fileB7_orig/src/magic-pre.c:5258:6
#5 0x8138c57 in magic_file /home/user/Desktop/20.05/source/fileB7_orig/src/magic-pre.c:5118:9
#6 0x812de93 in process /home/user/Desktop/20.05/source/fileB7_orig/src/file-pre.c:5810:9
#7 0x812c234 in main /home/user/Desktop/20.05/source/fileB7_orig/src/file-pre.c:5654:9
#8 0xf7c68ed4 in __libc_start_main /build/glibc-EexDTL/glibc-2.31/csu/../csu/libc-start.c:308:16
#9 0x80635f5 in _start (/home/user/Desktop/20.05/source/fileB7_orig/src/file+0x80635f5)
...
user@ubuntu:~/Desktop/20.05/source/fileB7_orig/src$ ./file ../../fileB7/src/crash-c38a3553d7ef8872d6ad6302469614ddd043cfc8
AddressSanitizer:DEADLYSIGNAL
=================================================================
==2729982==ERROR: AddressSanitizer: SEGV on unknown address 0xa2957b18 (pc 0x08133151 bp 0xffffcc88 sp 0xffffcad0 T0)
==2729982==The signal is caused by a READ memory access.
#0 0x8133151 in file_check_mem /home/user/Desktop/20.05/source/fileB7_orig/src/funcs-pre.c:4875:12
#1 0x8147647 in match /home/user/Desktop/20.05/source/fileB7_orig/src/softmagic-pre.c:3791:6
#2 0x814731b in file_softmagic /home/user/Desktop/20.05/source/fileB7_orig/src/softmagic-pre.c:3748:13
#3 0x8131881 in file_buffer /home/user/Desktop/20.05/source/fileB7_orig/src/funcs-pre.c:4687:7
#4 0x8138998 in file_or_fd /home/user/Desktop/20.05/source/fileB7_orig/src/magic-pre.c:5258:6
#5 0x8138c57 in magic_file /home/user/Desktop/20.05/source/fileB7_orig/src/magic-pre.c:5118:9
#6 0x812de93 in process /home/user/Desktop/20.05/source/fileB7_orig/src/file-pre.c:5810:9
#7 0x812c234 in main /home/user/Desktop/20.05/source/fileB7_orig/src/file-pre.c:5654:9
#8 0xf7c68ed4 in __libc_start_main /build/glibc-EexDTL/glibc-2.31/csu/../csu/libc-start.c:308:16
#9 0x80635f5 in _start (/home/user/Desktop/20.05/source/fileB7_orig/src/file+0x80635f5)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/user/Desktop/20.05/source/fileB7_orig/src/funcs-pre.c:4875:12 in file_check_mem
==2729982==ABORTINGba
Despite so, the speed is real slow!!
In Part 1, I went with the approach of finding crash -> patching it. Immediately after patching, I can easily find the next crash. However, with this approach, it now takes a long time to find another unique crash, while the crash count is increasing crazily fast.
This is likely because libfuzzer is not able to find a new path and just keep hitting the buggy code. Hence, I feel that patching away shallow bugs is still the way to go!!