Lately, I have been interested in fuzzing and had been spending time learning about libfuzzer and reading the code. Fuzzing sounds scary but libfuzzer is actually pretty easy to read. Next, I will love to read AFL code. I also wanted to practice fuzzing CTF problems and slowly moving on to real world targets.
Recently, while browsing twitter and (https://twitter.com/MurmusCTF), I happened to chance about rode0day!
It is a fuzzing competition! This is really unique, I dont really see it in the CTF scene. Unfortunately, this competition seems like it has been discontinued.
For this blog, I will be using libfuzzer to work on the Rode0day-20.05 set of challenges.
So here we go!
#1 Setup of application
sudo apt-get install zlib1g-dev
sudo apt-get install zlib1g-dev:i386 # because its x86
After installing these libraries, we can quickly build the file binary.
Next, we try to compile with asan. Sweet, also pretty easy!
The Makefile looks like that now.
Note the following changes:
all: file
top_srcdir = ..
HDR = $(top_srcdir)/src/magic.h.in
CC=clang
CFLAGS += -m32 -g -DHAVE_CONFIG_H -I. -I.. -gdwarf-2 -O1 -fsanitize=fuzzer-no-link,address # note this change
...
# note this change
file : magic.h $(LIBOBJ)
$(CC) -g $(CFLAGS) $^ -lz
mv a.out file
#2 Reading the code (just a little)
Before we can fuzz, we need to understand the code a little to find the starting point. The binary works as such:
./file {target_file}
The main function starts in file-pre.c
. file-pre.c
is the injected vulnerable code.
Basically, it does the following:
- Parses the cmdline arguments
- From the cmdline, we can specify to load a custom magic file
- Finally load and process our
specified_file
Thats enough to get us started in fuzzing. For someone familiar with fuzzing, one can notice that our end fuzzer will looks pretty standard where a file is taken in through stdin
. This is the standard AFL fuzzing template!
#3 Writing a harness
Libfuzzer harness usually looks like that:
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
FuzzTarget(Data, Size); // your fuzz function
return 0;
}
The function that is being fuzzed takes in a Data
and Size
argument.
Thankfully, there have been security researchers who tried to convert libfuzzer harness to AFL harness and have written the helper functions to write the `Data` to disc.
To convert the standard libfuzzer harness to take in a file instead, we can do something like this:
// https://github.com/google/security-research-pocs/blob/649b6ed74c842f533d15410f13d94aada96375ef/autofuzz/alembic_fuzzer.cc#L299
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
// create a tmp file
const char* file = buf_to_file(data, size);
...
FuzzTarget(file); // your fuzz function
// clean up
if (delete_file(file) != 0) {
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
As a result, our final harness looks like such:
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"; // can omit the following option
args[2] = "/home/user/Desktop/magic.mgc";
args[3] = file;
main2(4, args);
if (delete_file(file) != 0) {
exit(EXIT_FAILURE);
}
return 0;
}
Hitting very short file (no magic)
error message
Because libfuzzer is running in process, we need to reset the state after every execution. This global `optind` have to be reseted after every execution.
optind = 3;
if (optind == argc) {
if (!didsomefiles)
usage();
}
And finally we are good to go!
#4 The exciting part — Running the fuzzer!
After starting the fuzzer, we immediately hit a crash.
user@ubuntu:~/Desktop/20.05_original/source/fileB7/src$ ./file /home/user/Desktop/20.05/source/fileB7/src/crash-ae8444de02705346dae4f4c67d0c710b833c14e1
=================================================================
==2499959==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5900630 at pc 0x08133952 bp 0xffed8748 sp 0xffed8740
READ of size 4 at 0xf5900630 thread T0
#0 0x8133951 in looks_ascii /home/user/Desktop/20.05_original/source/fileB7/src/encoding-pre.c:3762:13
#1 0x8132f7d in file_encoding /home/user/Desktop/20.05_original/source/fileB7/src/encoding-pre.c:3701:5
#2 0x811f4d7 in file_ascmagic /home/user/Desktop/20.05_original/source/fileB7/src/ascmagic-pre.c:4840:6
#3 0x813c980 in file_buffer /home/user/Desktop/20.05_original/source/fileB7/src/funcs-pre.c:4709:7
#4 0x8145936 in file_or_fd /home/user/Desktop/20.05_original/source/fileB7/src/magic-pre.c:5258:6
#5 0x8145cb0 in magic_file /home/user/Desktop/20.05_original/source/fileB7/src/magic-pre.c:5118:9
#6 0x8137dff in process /home/user/Desktop/20.05_original/source/fileB7/src/file-pre.c:5810:9
#7 0x8136665 in main /home/user/Desktop/20.05_original/source/fileB7/src/file-pre.c:5654:9
#8 0xf7c1ded4 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_original/source/fileB7/src/file+0x80635f5)
0xf5900633 is located 0 bytes to the right of 3-byte region [0xf5900630,0xf5900633)
allocated by thread T0 here:
#0 0x80daf88 in calloc (/home/user/Desktop/20.05_original/source/fileB7/src/file+0x80daf88)
#1 0x813297b in file_encoding /home/user/Desktop/20.05_original/source/fileB7/src/encoding-pre.c:3650:33
#2 0x811f4d7 in file_ascmagic /home/user/Desktop/20.05_original/source/fileB7/src/ascmagic-pre.c:4840:6
#3 0x813c980 in file_buffer /home/user/Desktop/20.05_original/source/fileB7/src/funcs-pre.c:4709:7
#4 0x8145936 in file_or_fd /home/user/Desktop/20.05_original/source/fileB7/src/magic-pre.c:5258:6
#5 0x8145cb0 in magic_file /home/user/Desktop/20.05_original/source/fileB7/src/magic-pre.c:5118:9
#6 0x8137dff in process /home/user/Desktop/20.05_original/source/fileB7/src/file-pre.c:5810:9
#7 0x8136665 in main /home/user/Desktop/20.05_original/source/fileB7/src/file-pre.c:5654:9
#8 0xf7c1ded4 in __libc_start_main /build/glibc-EexDTL/glibc-2.31/csu/../csu/libc-start.c:308:16
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/user/Desktop/20.05_original/source/fileB7/src/encoding-pre.c:3762:13 in looks_ascii
Shadow bytes around the buggy address:
0x3eb20070: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb20080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb20090: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb200a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb200b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x3eb200c0: fa fa fa fa fa fa[03]fa fa fa 00 04 fa fa fd fa
0x3eb200d0: fa fa 00 00 fa fa 01 fa fa fa 01 fa fa fa fd fd
0x3eb200e0: fa fa fd fd fa fa 00 04 fa fa 00 04 fa fa 00 00
0x3eb200f0: fa fa 00 00 fa fa fd fa fa fa fa fa fa fa fa fa
0x3eb20100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb20110: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==2499959==ABORTING
We have a problem, we are always hitting the shallow bug!
There are 2 options:
1) patch the bug
2) for libfuzzer to check the stack and discard if it has the same stack trace
Option 2 currently seems not feasible for libfuzzer without modifying it. Other fuzzers like hongfuzz and AFL seems like they are able to tackle the problem of identifying unique bugs. So we will be moving along option 1.
The root cause of the bug:
static int
looks_ascii(const unsigned char *buf, size_t nbytes, unichar *ubuf,
size_t *ulen)
{
size_t i={0};
*ulen = 0;
for (i = 0; i < nbytes; i++) {
if (buf) {
// when buf is less than 2 bytes,
// *const unsigned int reads 4 bytes in buf and reads OOB
lava_set(1, *(const unsigned int *)buf);
}
...
}
}
To patch it, we just do the following:
if (buf) {
lava_set(1, *(const unsigned char *)buf);
}
After patching:
Now we get to hit more bugs! Almost immediately again!
=================================================================
==2506408==ERROR: AddressSanitizer: SEGV on unknown address 0xa2768818 (pc 0x081c0a68 bp 0xff928ce8 sp 0xff928cc0 T0)
==2506408==The signal is caused by a READ memory access.
#0 0x81c0a68 in file_check_mem /home/user/Desktop/20.05/source/fileB7/src/funcs-pre.c:4875:21
#1 0x81dac98 in match /home/user/Desktop/20.05/source/fileB7/src/softmagic-pre.c:3791:6
#2 0x81da903 in file_softmagic /home/user/Desktop/20.05/source/fileB7/src/softmagic-pre.c:3748:13
#3 0x81bef83 in file_buffer /home/user/Desktop/20.05/source/fileB7/src/funcs-pre.c:4687:7
#4 0x81c80b6 in file_or_fd /home/user/Desktop/20.05/source/fileB7/src/magic-pre.c:5258:6
#5 0x81c8430 in magic_file /home/user/Desktop/20.05/source/fileB7/src/magic-pre.c:5118:9
#6 0x81b9f6f in process /home/user/Desktop/20.05/source/fileB7/src/file-pre.c:5919:9
#7 0x81b8835 in main2 /home/user/Desktop/20.05/source/fileB7/src/file-pre.c:5665:9
#8 0x81ba535 in LLVMFuzzerTestOneInput /home/user/Desktop/20.05/source/fileB7/src/file-pre.c:5763:6
#9 0x8099ccb in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned int) (/home/user/Desktop/20.05/source/fileB7/src/file+0x8099ccb)
#10 0x8099630 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned int, bool, fuzzer::InputInfo*, bool*) (/home/user/Desktop/20.05/source/fileB7/src/file+0x8099630)
#11 0x809abd8 in fuzzer::Fuzzer::MutateAndTestOne() (/home/user/Desktop/20.05/source/fileB7/src/file+0x809abd8)
#12 0x809b534 in fuzzer::Fuzzer::Loop(std::__Fuzzer::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) (/home/user/Desktop/20.05/source/fileB7/src/file+0x809b534)
#13 0x808cb58 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned int)) (/home/user/Desktop/20.05/source/fileB7/src/file+0x808cb58)
#14 0x80aebd7 in main (/home/user/Desktop/20.05/source/fileB7/src/file+0x80aebd7)
#15 0xf7a1aed4 in __libc_start_main /build/glibc-EexDTL/glibc-2.31/csu/../csu/libc-start.c:308:16
#16 0x8065805 in _start (/home/user/Desktop/20.05/source/fileB7/src/file+0x8065805)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/user/Desktop/20.05/source/fileB7/src/funcs-pre.c:4875:21 in file_check_mem
==2506408==ABORTING
MS: 4 InsertRepeatedBytes-ShuffleBytes-ChangeByte-CMP- DE: "V`TB"-; base unit: 9ff376d2e20edbface121dcfcd29a51deaf128e4
0x56,0x60,0x54,0x42,0x15,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x23,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x15,0x15,0x15,0x15,0x15,0x15,0x15,0xd,0x15,0x15,0x15,0x15,0x15,0x15,0x15,0x15,0xbd,
V`TB\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15\x15\x15\x15\x15\x15\x15\x0d\x15\x15\x15\x15\x15\x15\x15\x15\xbd
artifact_prefix='./'; Test unit written to ./crash-a1fbb3f5f76b5f22834dc77f44965fd0210c7609
Base64: VmBUQhUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAAAAAAAAAAAAAAAAAAAAAAAAAAFRUVFRUVFQ0VFRUVFRUVFb0=
After patching and running multiple times, I stopped with around 10+ unique crashes.
You can check out the crashes on my github repo if interested. crashes
#5 Now, lets fuzz the updated project!
Build command looks slightly different now.
./file -m ../magic/magic.mgc /bin/bash
export CC=clang
export CXX=clang++
autoreconf -f -i
cd ../
autoreconf -f -i
make distclean
./configure --disable-silent-rules
Didnt get any results here :(
#6 Conclusions
Comparing my results with other blogs like https://piffd0s.medium.com/fuzzing-for-known-vulnerabilities-with-road0day-lava-c95544ce23c5, it seems that libfuzzer is able to sieve out the magic bytes comparison and quickly get to new coverage and crashes. This could be due to the advancements in fuzzing technology in the recent years!
Altogether, I have like 10+ unique crashes with the fuzzer running less than a minute each time. Everytime it crashes on a place, i patch the bug.
#7 Future Improvements
The problem with libfuzzer is that too many crashes appear and we have to patch them one by one. Using hongfuzz instead, we can minimize bugs better and detect unique crashes. However, I would say this isn’t a big problem because in real world software, we wont get to see too many crashes at once (if it happens, that will be good lol).
Maybe I will leave this for the next fuzzing article! Sounds fun to make a libfuzzer stack minimizer!