Fuzzing Tinybmp in Rust || From dumb to structure-aware guide

Introduction

In this blog post we will play around with some Rust code and fuzz the BMP header parsing methods within the TinyBMP Rust project. According to the project’s description:

A small BMP parser primarily for embedded, no-std environments but usable anywhere. This crate is primarily targeted at drawing BMP images to embedded_graphics DrawTargets, but can also be used to parse BMP files for other applications.

While I’ve been trying to learn Rust and understand a bit more about traits, I found this to be a perfect target as usually anything related to parsing might be prone to vulnerabilities. We will be starting off by reading the documentation of the project, setting up a simple (dumb) fuzzer and then move on to more interesting topic such as creating a structure-aware fuzzer. Hence if you haven’t done any fuzzing in Rust and looking for a beginner tutorial then hopefully this blog is for you! We will also be utilising cargo-fuzz , a cargo subcommand which uses libFuzzer (and needs LLVM sanitizer support). Before we move on, make sure to install it as per project instructions so you can follow along. As such, I will be using Kali 64bit for the rest of this tutorial.

Please note all of the discovered issues/bugs here have been reported already to the project owners and have been fixed!

This blog would also0 have not be possible without Addison’s (@addisoncrump_vr) help, which he provided guidance as well as the harness for the smart/structured-aware section which we will analyse in this blog!

Setting up the project

First things first, in order to be able to run cargo-fuzz you need to install the nightly version of rust (or switch to it):

$ rustup default nightly-x86_64-unknown-linux-gnu
info: using existing install for 'nightly-x86_64-unknown-linux-gnu'
info: default toolchain set to 'nightly-x86_64-unknown-linux-gnu'

  nightly-x86_64-unknown-linux-gnu unchanged - rustc 1.67.0-nightly (73c9eaf21 2022-11-07)
                                                                                          
$ rustup show                                    
Default host: x86_64-unknown-linux-gnu
rustup home:  /home/kali/.rustup

installed toolchains
--------------------

stable-x86_64-unknown-linux-gnu
nightly-2022-08-16-x86_64-unknown-linux-gnu
nightly-x86_64-unknown-linux-gnu (default)

Let’s start by cloning the repo and reverting it prior the patches that have been added.

$ git clone https://github.com/embedded-graphics/tinybmp
Cloning into 'tinybmp'...
remote: Enumerating objects: 338, done.
remote: Counting objects: 100% (206/206), done.
remote: Compressing objects: 100% (142/142), done.
remote: Total 338 (delta 121), reused 94 (delta 59), pack-reused 132
Receiving objects: 100% (338/338), 175.67 KiB | 3.03 MiB/s, done.
Resolving deltas: 100% (162/162), done.

Let’s limit the commit entries to 10:

$ git log -n 10
commit 8b4c2ced92a9ff16b5c534ca11c9877ff5d31a8d (HEAD -> master, tag: v0.4.0, origin/master, origin/HEAD)
Author: James Waples <james@wapl.es>
Date:   Fri Sep 30 19:13:03 2022 +0100

    (cargo-release) version 0.4.0

commit 955135693326b133dd6a333b97b4510f8060d4fe
Author: James Waples <james@wapl.es>
Date:   Fri Sep 30 19:12:03 2022 +0100

    Fix warning in release config

commit 132967379a1d16f2741f8dffdf423a21b4f34114
Author: James Waples <james@wapl.es>
Date:   Fri Sep 30 19:10:51 2022 +0100

    Add pixel getter examples (#37)
    
    * Add pixel getter examples
    
    * Apply suggestions from code review
    
    Co-authored-by: Ralf Fuest <mail@rfuest.de>

commit c64e35ae19c8a7175aebcbc9a008d331f72b4d20
Author: Ralf Fuest <mail@rfuest.de>
Date:   Fri Sep 30 17:11:52 2022 +0200

    Check for integer overflows in RawBmp::parse (#32)
    
    * Check for integer overflows in RawBmp::parse
    
    * Don't allow images with zero width or height
    
    * Check for negative width and add tests
    
    * Reformat code

    ... snip ...

commit 815e9f99aa5ce053b4985572ea145f39758e78a5
Author: James Waples <james@wapl.es>
Date:   Mon Apr 18 20:41:08 2022 +0100

    (cargo-release) version 0.3.3

We’re interested in reverting it to the version 0.3.3 so let’s do that:

┌──(kali㉿kali)-[~/Desktop/tinybmp]
└─$ git checkout -b old-state 815e9f99aa5ce053b4985572ea145f39758e78a5    
Switched to a new branch 'old-state'

┌──(kali㉿kali)-[~/Desktop/tinybmp]
└─$ git log                                                           
commit 815e9f99aa5ce053b4985572ea145f39758e78a5 (HEAD -> old-state)
Author: James Waples <james@wapl.es>
Date:   Mon Apr 18 20:41:08 2022 +0100

    (cargo-release) version 0.3.3

Creating a dumb fuzzer

Now that we’ve setup the project it’s time to experiment and play around with the docs. Navigating through the project we can see the following sample code:

If you are coming from a winafl/AFL background naturally you’ll probably think that somehow you’ll need to figure out a way to provide a file input, mutate it and then pass the fuzzed file to the target. However, cargo-fuzz/LLVM works slightly different… remember its API is defined as:

// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  DoSomethingInterestingWithMyAPI(Data, Size);
  return 0;  // Values other than 0 and -1 are reserved for future use.
}

Let’s compare that to cargo’s fuzz tutorial:

#![no_main]
#[macro_use] extern crate libfuzzer_sys;
extern crate url;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        let _ = url::Url::parse(s);
    }
});

So the API is pretty much similar, in fact we only need to privide a closure with the parameter data which is going to be what cargo-fuzz will mutate.

Let’s init a new campaign, create a new project and name it dumb:

┌──(kali㉿kali)-[~/Desktop/tinybmp]
└─$ cargo fuzz init
┌──(kali㉿kali)-[~/Desktop/tinybmp]
└─$ cargo fuzz add dumb
┌──(kali㉿kali)-[~/Desktop/tinybmp]
└─$ cat fuzz/fuzz_targets/dumb.rs    
#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    // fuzzed code goes here
});

Cargo-fuzz has created some boilerplate code for us, let’s modify it similar to the provided example:

#![no_main]
use libfuzzer_sys::fuzz_target;
use embedded_graphics::{pixelcolor::Rgb888, prelude::*};
use tinybmp::Bmp;

fuzz_target!(|data: &[u8]| {
    // Note: The color type is specified explicitly to match the format used by the BMP image.
    let bmp = Bmp::<Rgb888>::from_slice(data).unwrap();
}); 

That looks like it might work, let’s try to run it:

$ cargo fuzz run dumb                       
    Updating crates.io index
  Downloaded libfuzzer-sys v0.4.5
  Downloaded libc v0.2.137
  Downloaded arbitrary v1.2.0
  Downloaded 3 crates (763.8 KB) in 0.37s
    -- snip --
Compiling tinybmp-fuzz v0.0.0 (/home/kali/Desktop/tinybmp/fuzz)
error[E0433]: failed to resolve: use of undeclared crate or module `embedded_graphics`
 --> fuzz_targets/dumb.rs:3:5
  |
3 | use embedded_graphics::{pixelcolor::Rgb888, prelude::*};
  |     ^^^^^^^^^^^^^^^^^ use of undeclared crate or module `embedded_graphics`

error[E0432]: unresolved import `embedded_graphics::pixelcolor::Rgb888`
 --> fuzz_targets/dumb.rs:3:25
  |
3 | use embedded_graphics::{pixelcolor::Rgb888, prelude::*};
  |                         ^^^^^^^^^^^^^^^^^^

Some errors have detailed explanations: E0432, E0433.
For more information about an error, try `rustc --explain E0432`.
error: could not compile `tinybmp-fuzz` due to 2 previous errors
Error: failed to build fuzz script: "cargo" "build" "--manifest-path" "/home/kali/Desktop/tinybmp/fuzz/Cargo.toml" "--target" "x86_64-unknown-linux-gnu" "--release" "--bin" "dumb"

That command failed, let’s add that crate to Cargo.toml and run it again:

[dependencies]
libfuzzer-sys = "0.4"
embedded-graphics = "0.7.1"
└─$ cargo fuzz run dumb          
    Updating crates.io index
  Downloaded libfuzzer-sys v0.4.5
  Downloaded libc v0.2.137
  Downloaded arbitrary v1.2.0
  Downloaded 3 crates (763.8 KB) in 0.27s
   Compiling libc v0.2.137
   Compiling autocfg v1.1.0
   Compiling az v1.2.1
   Compiling byteorder v1.4.3
   Compiling num-traits v0.2.15
   Compiling micromath v1.1.1
   Compiling jobserver v0.1.25
   Compiling arbitrary v1.2.0
   Compiling cc v1.0.73
   Compiling libfuzzer-sys v0.4.5
   Compiling float-cmp v0.8.0
   Compiling embedded-graphics-core v0.3.3
   Compiling embedded-graphics v0.7.1
   Compiling once_cell v1.15.0
   Compiling tinybmp v0.3.3 (/home/kali/Desktop/tinybmp)
   Compiling tinybmp-fuzz v0.0.0 (/home/kali/Desktop/tinybmp/fuzz)
   ....
 --> fuzz_targets/dumb.rs:3:45
  |
3 | use embedded_graphics::{pixelcolor::Rgb888, prelude::*};
  |                                             ^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: unused variable: `bmp`
 --> fuzz_targets/dumb.rs:8:9
  |
8 |     let bmp = Bmp::<Rgb888>::from_slice(data).unwrap();
  |         ^^^ help: if this is intentional, prefix it with an underscore: `_bmp`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: `tinybmp-fuzz` (bin "dumb") generated 2 warnings
    Finished release [optimized + debuginfo] target(s) in 0.90s
warning: unused import: `prelude::*`
 --> fuzz_targets/dumb.rs:3:45
  |
3 | use embedded_graphics::{pixelcolor::Rgb888, prelude::*};
  |                                             ^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: unused variable: `bmp`
 --> fuzz_targets/dumb.rs:8:9
  |
8 |     let bmp = Bmp::<Rgb888>::from_slice(data).unwrap();
  |         ^^^ help: if this is intentional, prefix it with an underscore: `_bmp`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: `tinybmp-fuzz` (bin "dumb") generated 2 warnings
    Finished release [optimized + debuginfo] target(s) in 0.01s
     Running `fuzz/target/x86_64-unknown-linux-gnu/release/dumb -artifact_prefix=/home/kali/Desktop/tinybmp/fuzz/artifacts/dumb/ /home/kali/Desktop/tinybmp/fuzz/corpus/dumb`
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 485875195
INFO: Loaded 1 modules   (11576 inline 8-bit counters): 11576 [0x558fe7ad9c10, 0x558fe7adc948), 
INFO: Loaded 1 PC tables (11576 PCs): 11576 [0x558fe7adc948,0x558fe7b09cc8), 
INFO:       24 files found in /home/kali/Desktop/tinybmp/fuzz/corpus/dumb
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: UnexpectedEndOfFile', fuzz_targets/dumb.rs:8:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
==6168== ERROR: libFuzzer: deadly signal
    #0 0x558fe787bfe1 in __sanitizer_print_stack_trace /rustc/llvm/src/llvm-project/compiler-rt/lib/asan/asan_stack.cpp:87:3
    #1 0x558fe79593ca in fuzzer::PrintStackTrace() /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerUtil.cpp:210:38
    #2 0x558fe7922f15 in fuzzer::Fuzzer::CrashCallback() /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:233:18
    #3 0x558fe7922f15 in fuzzer::Fuzzer::CrashCallback() /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:228:6
    #4 0x7f828083da9f  (/lib/x86_64-linux-gnu/libc.so.6+0x3da9f) (BuildId: 71a7c7b97bc0b3e349a3d8640252655552082bf5)
    #5 0x7f828088957b in __pthread_kill_implementation nptl/./nptl/pthread_kill.c:43:17
    #6 0x7f828083da01 in gsignal signal/../sysdeps/posix/raise.c:26:13
    #7 0x7f8280828468 in abort stdlib/./stdlib/abort.c:79:7
    #8 0x558fe79b4606 in std::sys::unix::abort_internal::h0897be309fb2baa9 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/sys/unix/mod.rs:293:14
    #9 0x558fe77ee216 in std::process::abort::hc3bb2bc8d10060de /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/process.rs:2119:5
    #10 0x558fe791d2b3 in libfuzzer_sys::initialize::_$u7b$$u7b$closure$u7d$$u7d$::hfbc7451104c5815a /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/src/lib.rs:91:9
    #11 0x558fe79a960c in std::panicking::rust_panic_with_hook::h76fd95765866f4c4 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:702:17
    #12 0x558fe79a9466 in std::panicking::begin_panic_handler::_$u7b$$u7b$closure$u7d$$u7d$::h60f8b3f90bb7fd93 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:588:13
    #13 0x558fe79a667b in std::sys_common::backtrace::__rust_end_short_backtrace::h483cd9e52367386f /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/sys_common/backtrace.rs:138:18
    #14 0x558fe79a9181 in rust_begin_unwind /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:584:5
    #15 0x558fe77ef5a2 in core::panicking::panic_fmt::h44b4db96b14cf253 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/core/src/panicking.rs:142:14
    #16 0x558fe77ef6f2 in core::result::unwrap_failed::hc10b6a87ebce4f5d /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/core/src/result.rs:1814:5
    #17 0x558fe78b5d41 in core::result::Result$LT$T$C$E$GT$::unwrap::h7ffe31d9ddee89b2 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/core/src/result.rs:1107:23
    #18 0x558fe78b5d41 in dumb::_::run::h93074574f0ae19de /home/kali/Desktop/tinybmp/fuzz/fuzz_targets/dumb.rs:8:15
    #19 0x558fe78b51d8 in rust_fuzzer_test_input /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/src/lib.rs:224:17
    #20 0x558fe791864f in libfuzzer_sys::test_input_wrap::_$u7b$$u7b$closure$u7d$$u7d$::h6b2b51dabd6d08ff /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/src/lib.rs:61:9
    #21 0x558fe791864f in std::panicking::try::do_call::h2b6b2fecd8b0ff94 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:492:40
    #22 0x558fe791d4e7 in __rust_try libfuzzer_sys.4efe2baf-cgu.0
    #23 0x558fe791c868 in std::panicking::try::hf3b7ef749ee396d0 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:456:19
    #24 0x558fe791c868 in std::panic::catch_unwind::hef97c053185a011b /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panic.rs:137:14
    #25 0x558fe791c868 in LLVMFuzzerTestOneInput /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/src/lib.rs:59:22
    #26 0x558fe7923449 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:612:15
    #27 0x558fe792bcf2 in fuzzer::Fuzzer::ReadAndExecuteSeedCorpora(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile> >&) /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:805:18
    #28 0x558fe792c2e7 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile> >&) /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:865:28
    #29 0x558fe7942004 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerDriver.cpp:912:10
    #30 0x558fe77ef9d2 in main /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerMain.cpp:20:30
    #31 0x7f8280829209 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #32 0x7f82808292bb in __libc_start_main csu/../csu/libc-start.c:389:3
    #33 0x558fe77efb10 in _start (/home/kali/Desktop/tinybmp/fuzz/target/x86_64-unknown-linux-gnu/release/dumb+0x72b10) (BuildId: efe97053d3a33c625fcf3451ca94dfe7e9bed6b7)

NOTE: libFuzzer has rudimentary signal handlers.
      Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal
MS: 0 ; base unit: 0000000000000000000000000000000000000000


artifact_prefix='/home/kali/Desktop/tinybmp/fuzz/artifacts/dumb/'; Test unit written to /home/kali/Desktop/tinybmp/fuzz/artifacts/dumb/crash-da39a3ee5e6b4b0d3255bfef95601890afd80709
Base64: 

────────────────────────────────────────────────────────────────────────────────

Failing input:

	fuzz/artifacts/dumb/crash-da39a3ee5e6b4b0d3255bfef95601890afd80709

Output of `std::fmt::Debug`:

	[]

Reproduce with:

	cargo fuzz run dumb fuzz/artifacts/dumb/crash-da39a3ee5e6b4b0d3255bfef95601890afd80709

Minimize test case with:

	cargo fuzz tmin dumb fuzz/artifacts/dumb/crash-da39a3ee5e6b4b0d3255bfef95601890afd80709

────────────────────────────────────────────────────────────────────────────────

Error: Fuzz target exited with exit status: 77

That doesn’t look good. First things first notice the following two things:

Compiling tinybmp v0.3.3 (/home/kali/Desktop/tinybmp)

You need to make sure you’re compiling this version (the vulnerable one) and not latest one! Furthermore looking at the ASAN’s stack trace looks like the fuzzer is panicking and exiting and sure enough there are no refences to tinybmp code.. bummer. Let’s also see what’s the test case about:

$ hexdump -C fuzz/artifacts/dumb/crash-da39a3ee5e6b4b0d3255bfef95601890afd80709
$                                                                                  

Well that’s a bit sketchy, the test case is empty so something is not quite right here. We need to find another way or even better find another function that does something similar to the example and will allow us to target the same functionality. After spending some time reading the docs/examples and the APIs I came across this very interesting one, the RawBmp:

This struct can be used to access the image data in a BMP file at a lower level than with the Bmp struct. It doesn’t do automatic color conversion and doesn’t apply the color table, if it is present in the BMP file.

It even has a method which creates an image from a byte slice - that looks very promising! Let's update the code again:

#![no_main]
use libfuzzer_sys::fuzz_target;
use tinybmp::RawBmp;

fuzz_target!(|data: &[u8]| {
    let bmp = RawBmp::from_slice(data);
}); 

And run it one more time:

$ cargo fuzz run dumb                                                          
   Compiling tinybmp-fuzz v0.0.0 (/home/kali/Desktop/tinybmp/fuzz)
warning: unused variable: `bmp`
 --> fuzz_targets/dumb.rs:6:9
  |
6 |     let bmp = RawBmp::from_slice(data);
  |         ^^^ help: if this is intentional, prefix it with an underscore: `_bmp`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: `tinybmp-fuzz` (bin "dumb") generated 1 warning
    Finished release [optimized + debuginfo] target(s) in 0.85s
warning: unused variable: `bmp`
 --> fuzz_targets/dumb.rs:6:9
  |
6 |     let bmp = RawBmp::from_slice(data);
  |         ^^^ help: if this is intentional, prefix it with an underscore: `_bmp`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: `tinybmp-fuzz` (bin "dumb") generated 1 warning
    Finished release [optimized + debuginfo] target(s) in 0.01s
     Running `fuzz/target/x86_64-unknown-linux-gnu/release/dumb -artifact_prefix=/home/kali/Desktop/tinybmp/fuzz/artifacts/dumb/ /home/kali/Desktop/tinybmp/fuzz/corpus/dumb`
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3764774578
INFO: Loaded 1 modules   (11479 inline 8-bit counters): 11479 [0x56019247cb50, 0x56019247f827), 
INFO: Loaded 1 PC tables (11479 PCs): 11479 [0x56019247f828,0x5601924ac598), 
INFO:       34 files found in /home/kali/Desktop/tinybmp/fuzz/corpus/dumb
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: seed corpus: files: 34 min: 1b max: 72b total: 1382b rss: 37Mb
#35	INITED cov: 183 ft: 186 corp: 23/849b exec/s: 0 rss: 40Mb
thread '<unnamed>' panicked at 'attempt to negate with overflow', /home/kali/Desktop/tinybmp/src/header/dib_header.rs:109:52
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
==6871== ERROR: libFuzzer: deadly signal
    #0 0x56019221ffe1 in __sanitizer_print_stack_trace /rustc/llvm/src/llvm-project/compiler-rt/lib/asan/asan_stack.cpp:87:3
    #1 0x5601922fc3aa in fuzzer::PrintStackTrace() /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerUtil.cpp:210:38
    #2 0x5601922c5ef5 in fuzzer::Fuzzer::CrashCallback() /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:233:18
    #3 0x5601922c5ef5 in fuzzer::Fuzzer::CrashCallback() /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:228:6
    #4 0x7fb12503da9f  (/lib/x86_64-linux-gnu/libc.so.6+0x3da9f) (BuildId: 71a7c7b97bc0b3e349a3d8640252655552082bf5)
    #5 0x7fb12508957b in __pthread_kill_implementation nptl/./nptl/pthread_kill.c:43:17
    #6 0x7fb12503da01 in gsignal signal/../sysdeps/posix/raise.c:26:13
    #7 0x7fb125028468 in abort stdlib/./stdlib/abort.c:79:7
    #8 0x5601923575e6 in std::sys::unix::abort_internal::h0897be309fb2baa9 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/sys/unix/mod.rs:293:14
    #9 0x560192192216 in std::process::abort::hc3bb2bc8d10060de /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/process.rs:2119:5
    #10 0x5601922c0293 in libfuzzer_sys::initialize::_$u7b$$u7b$closure$u7d$$u7d$::hfbc7451104c5815a /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/src/lib.rs:91:9
    #11 0x56019234c5ec in std::panicking::rust_panic_with_hook::h76fd95765866f4c4 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:702:17
    #12 0x56019234c400 in std::panicking::begin_panic_handler::_$u7b$$u7b$closure$u7d$$u7d$::h60f8b3f90bb7fd93 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:586:13
    #13 0x56019234965b in std::sys_common::backtrace::__rust_end_short_backtrace::h483cd9e52367386f /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/sys_common/backtrace.rs:138:18
    #14 0x56019234c161 in rust_begin_unwind /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:584:5
    #15 0x5601921935a2 in core::panicking::panic_fmt::h44b4db96b14cf253 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/core/src/panicking.rs:142:14
    #16 0x56019219346c in core::panicking::panic::h1cd478110c27042a /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/core/src/panicking.rs:48:5
    #17 0x56019227f035 in core::num::_$LT$impl$u20$i32$GT$::abs::h3b4f60dd81fb0910 /home/kali/Desktop/tinybmp/src/header/dib_header.rs:109:52
    #18 0x56019227f035 in tinybmp::header::dib_header::DibHeader::parse::h11481022451b7bb4 /home/kali/Desktop/tinybmp/src/header/dib_header.rs:109:52
    #19 0x560192280f27 in tinybmp::header::Header::parse::h17662c086025b4a3 /home/kali/Desktop/tinybmp/src/header/mod.rs:119:35
    #20 0x56019228414d in tinybmp::raw_bmp::RawBmp::from_slice::h62745928fd20c17c /home/kali/Desktop/tinybmp/src/raw_bmp.rs:37:51
    #21 0x560192258d16 in dumb::_::run::h93074574f0ae19de /home/kali/Desktop/tinybmp/fuzz/fuzz_targets/dumb.rs:6:15
    #22 0x560192258378 in rust_fuzzer_test_input /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/src/lib.rs:224:17
    #23 0x5601922bb62f in libfuzzer_sys::test_input_wrap::_$u7b$$u7b$closure$u7d$$u7d$::h6b2b51dabd6d08ff /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/src/lib.rs:61:9
    #24 0x5601922bb62f in std::panicking::try::do_call::h2b6b2fecd8b0ff94 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:492:40
    #25 0x5601922c04c7 in __rust_try libfuzzer_sys.4efe2baf-cgu.0
    #26 0x5601922bf848 in std::panicking::try::hf3b7ef749ee396d0 /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panicking.rs:456:19
    #27 0x5601922bf848 in std::panic::catch_unwind::hef97c053185a011b /rustc/40336865fe7d4a01139a3336639c6971647e885c/library/std/src/panic.rs:137:14
    #28 0x5601922bf848 in LLVMFuzzerTestOneInput /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/src/lib.rs:59:22
    #29 0x5601922c6429 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:612:15
    #30 0x5601922cd85f in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:514:22
    #31 0x5601922ce6b2 in fuzzer::Fuzzer::MutateAndTestOne() /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:758:25
    #32 0x5601922cf437 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile> >&) /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerLoop.cpp:903:21
    #33 0x5601922e4fe4 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerDriver.cpp:912:10
    #34 0x5601921939d2 in main /home/kali/.cargo/registry/src/github.com-1ecc6299db9ec823/libfuzzer-sys-0.4.5/libfuzzer/FuzzerMain.cpp:20:30
    #35 0x7fb125029209 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #36 0x7fb1250292bb in __libc_start_main csu/../csu/libc-start.c:389:3
    #37 0x560192193b10 in _start (/home/kali/Desktop/tinybmp/fuzz/target/x86_64-unknown-linux-gnu/release/dumb+0x72b10) (BuildId: b139364c0b8f8dbd36bbe24a5dc1ea26395b8666)

NOTE: libFuzzer has rudimentary signal handlers.
      Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal
MS: 1 CMP- DE: "\000\000\000\200"-; base unit: 3f7608254f3fb2a41b60936bd3b177b690ab1143
0x42,0x4d,0x6e,0x6a,0x6a,0x28,0x10,0x0,0x0,0x4d,0x6a,0x6a,0x6a,0x6a,0x28,0x0,0x0,0x0,0xb0,0x95,0x95,0x95,0x0,0x0,0x0,0x80,0x0,0x0,0x8,0x0,0x0,0x0,0x0,0x0,0x6a,0x6a,0x6a,0x0,0x0,0x0,0x0,0x6a,0x6a,0x6a,0x2d,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x6a,0x7f,
BMnjj(\020\000\000Mjjjj(\000\000\000\260\225\225\225\000\000\000\200\000\000\010\000\000\000\000\000jjj\000\000\000\000jjj-\000\000\000\000\000\000\000j\177
artifact_prefix='/home/kali/Desktop/tinybmp/fuzz/artifacts/dumb/'; Test unit written to /home/kali/Desktop/tinybmp/fuzz/artifacts/dumb/crash-81481a4874f3dc166c900050e181757698fda554
Base64: Qk1uamooEAAATWpqamooAAAAsJWVlQAAAIAAAAgAAAAAAGpqagAAAABqamotAAAAAAAAAGp/

────────────────────────────────────────────────────────────────────────────────

Failing input:

	fuzz/artifacts/dumb/crash-81481a4874f3dc166c900050e181757698fda554

Output of `std::fmt::Debug`:

	[66, 77, 110, 106, 106, 40, 16, 0, 0, 77, 106, 106, 106, 106, 40, 0, 0, 0, 176, 149, 149, 149, 0, 0, 0, 128, 0, 0, 8, 0, 0, 0, 0, 0, 106, 106, 106, 0, 0, 0, 0, 106, 106, 106, 45, 0, 0, 0, 0, 0, 0, 0, 106, 127]

Reproduce with:

	cargo fuzz run dumb fuzz/artifacts/dumb/crash-81481a4874f3dc166c900050e181757698fda554

Minimize test case with:

	cargo fuzz tmin dumb fuzz/artifacts/dumb/crash-81481a4874f3dc166c900050e181757698fda554

────────────────────────────────────────────────────────────────────────────────

Error: Fuzz target exited with exit status: 77

Success! Literally within 3 seconds of running the fuzzer we get a thread '' panicked at 'attempt to negate with overflow', /home/kali/Desktop/tinybmp/src/header/dib_header.rs:109:52 </code>

panic issue! Also, notice the following stack traces:

Looks like indeed with our dirty harness dumb.rs line 6 we are hitting the parsing functionality we were aiming for. Let’s quickly verify the crasher:

$ hexdump -C fuzz/artifacts/dumb/crash-53bc3fdf92ab10167c288b0b52b5228361deab14
00000000  42 4d 6e 6a 68 28 00 04  00 4d 6a 6a 6a 4a 28 00  |BMnjh(...MjjjJ(.|
00000010  00 00 4d 38 00 00 00 00  00 80 00 00 18 00 00 00  |..M8............|
00000020  00 00 6a 6a 6a 34 00 00  00 00 1a 6a 01 00 00 00  |..jjj4.....j....|
00000030  00 00 04 6a 6a 6a 4a 28  00 00 00 4d 38 00 00 00  |...jjjJ(...M8...|
00000040  00 00 00 00 00 18 00 00  00 00 00 6a 6a 6a 34 00  |...........jjj4.|
00000050  00 00 00 1a 6a 01 00 00  00 00 00 04 6a 6a 6a 6a  |....j.......jjjj|
00000060  6a                                                |j|
00000061

Fantastic! Looking at the header.rs file:

the fuzzer was able to successfully create a new test case with this signature (notice the BM magic header 2 bytes) and find an issue. I’d also like to mention here that one of my issues was an interesting out of bounds read. For the detailed analysis please check the github issue here.

Excellent! We were literally able with a single line of code to unveil some bugs!

Coverage

Remember that it’s very essential to check coverage, so let’s do that. Before proceeding make sure to install the llvm-profdata for the rust toolchain. Let’s run the coverage command:

cargo fuzz coverage dumb

                                    -- snip --
Generating coverage data for "4bc861e1c2b691badd48e0533504794b2364d6ed"
warning: unused variable: `bmp`
 --> fuzz_targets/dumb.rs:6:9
  |
6 |     let bmp = RawBmp::from_slice(data);
  |         ^^^ help: if this is intentional, prefix it with an underscore: `_bmp`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: `tinybmp-fuzz` (bin "dumb") generated 1 warning (run `cargo fix --bin "dumb"` to apply 1 suggestion)
    Finished release [optimized + debuginfo] target(s) in 0.02s
     Running `target/x86_64-unknown-linux-gnu/release/dumb -artifact_prefix=/home/kali/Desktop/tinybmp/fuzz/artifacts/dumb/ /home/kali/Desktop/tinybmp/fuzz/corpus/dumb/4bc861e1c2b691badd48e0533504794b2364d6ed`
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 4142636592
INFO: Loaded 1 modules   (61528 inline 8-bit counters): 61528 [0x56533c7e85e0, 0x56533c7f7638), 
INFO: Loaded 1 PC tables (61528 PCs): 61528 [0x56533c7f7638,0x56533c8e7bb8), 
target/x86_64-unknown-linux-gnu/release/dumb: Running 1 inputs 1 time(s) each.
Running: /home/kali/Desktop/tinybmp/fuzz/corpus/dumb/4bc861e1c2b691badd48e0533504794b2364d6ed
Executed /home/kali/Desktop/tinybmp/fuzz/corpus/dumb/4bc861e1c2b691badd48e0533504794b2364d6ed in 0 ms
***
*** NOTE: fuzzing was not performed, you have only
***       executed the target code on a fixed set of inputs.
***
Merging raw coverage data...
Coverage data merged and saved in "/home/kali/Desktop/tinybmp/fuzz/coverage/dumb/coverage.profdata".

Ok, coverage data has been saved, let’s try to convert and view it:

$ cd coverage/dumb 
$ ls
coverage.profdata  raw

$ cargo cov -- show -Xdemangler=rustfilt ../../target/x86_64-unknown-linux-gnu/release/dumb \
    --format=html \
    -instr-profile=coverage.profdata  > index.html

Looking at the raw_bmp.rs reveals that lines 73-104 got never hit. Within the RawBmp trait implemention we can see that ParseError::InvalidImageDimensions got never hit, including all those function in the above image.

Patches verification and 2nd round of fuzzing

Let’s revert it back to the patched state:

C:\home\kali\Desktop\tinybmp> git checkout -b patched c64e35ae19c8a7175aebcbc9a008d331f72b4d20                                                                  
Switched to a new branch 'patched'
                                                                                   
C:\home\kali\Desktop\tinybmp> git log                                                         
commit c64e35ae19c8a7175aebcbc9a008d331f72b4d20 (HEAD -> patches)
Author: Ralf Fuest <mail@rfuest.de>
Date:   Fri Sep 30 17:11:52 2022 +0200

    Check for integer overflows in RawBmp::parse (#32)
    
    * Check for integer overflows in RawBmp::parse
    
    * Don't allow images with zero width or height
    
    * Check for negative width and add tests
    
    * Reformat code

and re-run the fuzzing campaign for five minutes..

cargo fuzz run dumb -- -max_total_time=300

Hmm! As you can see from the above image looks like the project mainteners have done a great job - they’ve added lots of verification and improved header parsing so that dumb fuzzing won’t find any low-hanging fruits..

Time for us to skill up and move to smart fuzzing!

Structured Aware Fuzzing

Now it’s time to invest some time a bit more and get a better understanding of the parsing mechanism. We will be using the harness provided here. Create a new project and paste the harness from the above link:

cargo fuzz add structured

Before starting make sure you add those extra depedencies to your main Cargo.toml:

[dependencies]
libfuzzer-sys = "0.4"
embedded-graphics = "0.7.1"
embedded-graphics-core = "0.3.3"
arbitrary = "1.2.0"
rand = "0.8.5"

Let’s try to break down and understand what this harness does.

Lines 4-16 define our modules. The most interesting one that we will be using is the arbitrary one which as per documentation:

This crate is primarily intended to be combined with a fuzzer like libFuzzer and cargo-fuzz or AFL, and to help you turn the raw, untyped byte buffers that they produce into well-typed, valid, structured values. This allows you to combine structure-aware test case generation with coverage-guided, mutation-based fuzzers.

We will be also importing a few other crates such as the Point and rand::rngs::StdRng because we need them for the harness.

4	use arbitrary::{Arbitrary, Unstructured};
5	use embedded_graphics_core::geometry::Point;
6	use libfuzzer_sys::fuzz_target;
7	use rand::rngs::StdRng;
8	use rand::{RngCore, SeedableRng};
9	use std::num::{NonZeroI8, NonZeroU8};
10	#[cfg(not(fuzzing))]
11	use std::{
12	    fs::{read_dir, File},
13	    io::Read,
14	};
15	use tinybmp::Bpp::*;
16	use tinybmp::{Bpp, RawBmp};
17	
18	#[allow(non_camel_case_types)]
19	#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]

Line 18 #[allow(non_camel_case_types)] disables the camel case warnings.

Lines 21-26 create a new enum type DibType that is required so we can initialise the header size. Notice also how on line 19 we are automatically implementing the #[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] traits for the DibType structure. If we don’t do that, the harness won’t compile:

20	#[repr(u8)]
21	enum DibType {
22	    DIB_INFO_HEADER_SIZE = 40,
23	    DIB_V3_HEADER_SIZE = 56,
24	    DIB_V4_HEADER_SIZE = 108,
25	    DIB_V5_HEADER_SIZE = 124,
26	}
27	

Moving on:

28	#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
29	#[repr(u8)]
30	enum CompressionMethod {
31	    Rgb = 0,
32	    Bitfields = 3,
33	}

Simillary to the previous struct we again implement the required traits and initialise the Rgb and Bitfields values. The following lines define a more interesting struct, a FuzzyBmp one.

35	#[derive(Debug, Clone)]
36	struct FuzzyBmp {
37	    dib_type: DibType,
38	    bpp: Bpp,
39	    compress: CompressionMethod,
40	    colours_used: u32,
41	    width: u32,
42	    height: i32,
43	    mask_red: u32,
44	    mask_green: u32,
45	    mask_blue: u32,
46	    mask_alpha: u32,
47	    // TODO compute at arb time
48	    colour_table: Box<[u8]>,
49	    image_data: Box<[u8]>,
50	}

If you’ve previously played with Rust you will immediately recognise the u32 and i32 which stands for unsigned and signed integers. In addition to those, we are using the Box and <u8> types so what are they?

It should be noted that some of these values are taken from the dib_header.rs code which will be used for creating a smart-ish BMP file:

There are a few variables that need our attention here. libFuzzer supports only the primitive variables (signed/unsigned integers such i32/u32) as well as chars. However in this struct we have defined some custom ones such as the DibType and Bpp. Later in this section we will see how we will implement the arbitrary trait in order for libfuzzer to understand these custom variables.

52	impl From<FuzzyBmp> for Vec<u8> {
53	    fn from(bmp: FuzzyBmp) -> Self {
54	        let mut res = Vec::new();
55	
56	        // --- HEADER ---
57	
58	        // header magic
59	        res.extend_from_slice(b"BM");
60	
61	        // size; calculated later
62	        let size_marker = res.len();
63	
64	        // reserved data
65	        res.extend_from_slice(&0u16.to_le_bytes());
66	        res.extend_from_slice(&0u16.to_le_bytes());
67	
68	        // image data offset; calculated later
69	        let data_ptr_marker = res.len();
70	

From Rust’s documentation:

From

The From trait allows for a type to define how to create itself from another type, hence providing a very simple mechanism for converting between several types. There are numerous implementations of this trait within the standard library for conversion of primitive and common types.

In short, on lines 52-53 we are implementing the Vec<u8> vector. Then on line 54, we declare a new mutable vector, and we slowly start filling the values for the BMP file format. Since this is a Vec<u8> we will be using Vec::extend_from_slice to append to the vector. Then on 59-69 we start crafting the header. Before moving on with the DIB header let’s panic on purpose the fuzzer and print so far the contents so we can verify we are on the right track:

println!("{:?}", res);
panic!("printing contents of res, exiting.");

and running the fuzzer yeilds the following:

Output of `std::fmt::Debug`:

	FuzzyBmp {
	    dib_type: DIB_INFO_HEADER_SIZE,
	    bpp: Bits16,
	    compress: Rgb,
	    colours_used: 0,
	    width: 4,
	    height: 126,
	    mask_red: 4294967295,
	    mask_green: 4286513151,
	    mask_blue: 4294902016,
	    mask_alpha: 65535,
	    colour_table: [],
	    image_data: [.....snip....]
  }

So far, so good. We’ve managed to correctly populate the right values for the image header. Let’s continue with the DIB header:

71	        // --- DIB Header ---
72	
73	        // length
74	        res.extend_from_slice(&(bmp.dib_type as u32).to_le_bytes());
75	
76	        // width, height
77	        res.extend_from_slice(&bmp.width.to_le_bytes());
78	        res.extend_from_slice(&bmp.height.to_le_bytes());
79	
80	        // color planes -- unused
81	        res.extend_from_slice(&0u16.to_le_bytes());
82	
83	        // bpp
84	        res.extend_from_slice(&(bmp.bpp.bits()).to_le_bytes());
85	
86	        // compression method
87	        res.extend_from_slice(&(bmp.compress as u32).to_le_bytes());
88	
89	        // image data len
90	        res.extend_from_slice(&u32::try_from(bmp.image_data.len()).unwrap().to_le_bytes());
91	
92	        // pels per meter x + y -- unused
93	        res.extend_from_slice(&0u32.to_le_bytes());
94	        res.extend_from_slice(&0u32.to_le_bytes());
95	
96	        // colours used
97	        res.extend_from_slice(&bmp.colours_used.to_le_bytes());
98	
99	        // colours important -- unused
100	        res.extend_from_slice(&0u32.to_le_bytes());
101	
102	        // channel masks, if we meet the described conditions
103	        if bmp.dib_type >= DibType::DIB_V3_HEADER_SIZE
104	            && bmp.compress == CompressionMethod::Bitfields
105	        {
106	            res.extend_from_slice(&bmp.mask_red.to_le_bytes());
107	            res.extend_from_slice(&bmp.mask_green.to_le_bytes());
108	            res.extend_from_slice(&bmp.mask_blue.to_le_bytes());
109	            res.extend_from_slice(&bmp.mask_alpha.to_le_bytes());
110	        }
111	
112	        // colour table
113	        res.extend_from_slice(&bmp.colour_table);
114	
115	        // insert data pointer marker, with corrections
116	        res.splice(
117	            data_ptr_marker..data_ptr_marker,
118	            u32::try_from(res.len() + 2 * core::mem::size_of::<u32>())
119	                .unwrap()
120	                .to_le_bytes(),
121	        );
122	
123	        // image data
124	        res.extend_from_slice(&bmp.image_data);
125	
126	        // insert total length, with corrections
127	        res.splice(
128	            size_marker..size_marker,
129	            u32::try_from(res.len() + core::mem::size_of::<u32>())
130	                .unwrap()
131	                .to_le_bytes(),
132	        );
133	
134	        res
135	    }
136	}

Now let’s move on to arbitrary trait implementation.

137	
138	impl<'a> Arbitrary<'a> for FuzzyBmp {
139	    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
140	        let dib_type = match u8::arbitrary(u)? % 4 {
141	            0 => DibType::DIB_INFO_HEADER_SIZE,
142	            1 => DibType::DIB_V3_HEADER_SIZE,
143	            2 => DibType::DIB_V4_HEADER_SIZE,
144	            3 => DibType::DIB_V5_HEADER_SIZE,
145	            _ => unreachable!(),
146	        };
147	        let bpp = match u8::arbitrary(u)? % 6 {
148	            0 => Bits1,
149	            1 => Bits4,
150	            2 => Bits8,
151	            3 => Bits16,
152	            4 => Bits24,
153	            5 => Bits32,
154	            _ => unreachable!(),
155	        };
156	        let compress = match bool::arbitrary(u)? {
157	            false => CompressionMethod::Rgb,
158	            true => CompressionMethod::Bitfields,
159	        };
160	
161	        // simply restrict it to 8 bits to keep it within reasonable bounds
162	        let width: u32 = NonZeroU8::arbitrary(u)?.get().into();
163	        let height: i32 = NonZeroI8::arbitrary(u)?.get().into();
164	
165	        let mask_red = u.arbitrary()?;
166	        let mask_green = u.arbitrary()?;
167	        let mask_blue = u.arbitrary()?;
168	        let mask_alpha = u.arbitrary()?;
169	
170	        let mut colours_used = u8::arbitrary(u)? as u32;
171	        // bias towards special case
172	        let colour_table_num_entries = if colours_used % 4 == 0 && bpp.bits() < 16 {
173	            colours_used = 0;
174	            (1 << bpp.bits()) as usize
175	        } else {
176	            colours_used as usize
177	        };
178	
179	        // image data doesn't actually matter; seed and fill
180	        let seed = u64::arbitrary(u)?;
181	        // rows must be a multiple of 4 bytes, so we do some wacky corrections here
182	        let baseline = width * height.unsigned_abs();
183	        let data_len = (match bpp {
184	            Bits1 => baseline / 8,
185	            Bits4 => baseline / 2,
186	            Bits8 => baseline,
187	            Bits16 => baseline * 2,
188	            Bits24 => baseline * 3,
189	            Bits32 => baseline * 4,
190	            _ => unreachable!(),
191	        } as u32
192	            + (width * 4 - 1))
193	            * (width * 4)
194	            / (width * 4);
195	        let mut rng = StdRng::seed_from_u64(seed);
196	        let mut image_data = vec![0u8; data_len as usize];
197	        rng.fill_bytes(image_data.as_mut_slice());
198	        let image_data = image_data.into_boxed_slice();
199	

Here we are using arbitrary’s Unstructured data, which as per documentation:

An Unstructured helps Arbitrary implementations interpret raw data (typically provided by a fuzzer) as a “DNA string” that describes how to construct the Arbitrary type. The goal is that a small change to the “DNA string” (the raw data wrapped by an Unstructured) results in a small change to the generated Arbitrary instance. This helps a fuzzer efficiently explore the Arbitrary’s input space. Unstructured is deterministic: given the same raw data, the same series of API calls will return the same results (modulo system resource constraints, like running out of memory). However, Unstructured does not guarantee anything beyond that: it makes not guarantee that it will yield bytes from the underlying data in any particular order. You shouldn’t generally need to use an Unstructured unless you are writing a custom Arbitrary implementation by hand, instead of deriving it. Mostly, you should just be passing it through to nested Arbitrary::arbitrary calls.

We start off with the DibType where one of the DIB_INFO_HEADER_SIZE, DIB_V3_HEADER_SIZE, DIB_V4_HEADER_SIZE, DIB_V5_HEADER_SIZE values are randomly selected. Then the same smart values are generated for the bpp structure.The compress returns just two values: either Rgb or Bitfields. On lines 164-173 we generate more smartish values that make sense for the parsing. Then on lines 185-197 we generate the data_len which was previously hardcoded. Continuing, on line 195 a new random generator is declared where it will be used to fill random data for the image_data variable.

Lines 200-208 will create a vector filled with random colour table values. We are using an iterator to chain the take() method and fill it only with colour_table_num_entries * 4

200	        let colour_table = u
201	            .arbitrary_iter()?
202	            .flatten()
203	            .take(colour_table_num_entries * 4)
204	            .collect::<Vec<_>>()
205	            .into_boxed_slice();
206	        if colour_table.len() != colour_table_num_entries * 4 {
207	            return Err(arbitrary::Error::NotEnoughData);
208	        }

I’ve added a few print methods and here are a few sample examples of the generated data:

println!("{:?}", colour_table);
println!("{:?}", colour_table.len());

Would yield:

[0, 255, 255, 0]
4
[11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 255, 255, 255, 255, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 255, 255]
44
[11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 255, 255, 255, 255, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 255, 255]
44
[]
0
[]
0
[]
0
[255, 61, 61, 61, 61, 61, 61, 61, 61, 61, 61, 255, 255, 255, 255, 219, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
64

Last bits of the arbitrary implementation we’ve got his Ok result since we need to return a result according to the function signature. Finally we implement the size_hint() function which Returns the bounds on the remaining length of the iterator.

209	
210	        Ok(Self {
211	            dib_type,
212	            bpp,
213	            compress,
214	            colours_used,
215	            width,
216	            height,
217	            mask_red,
218	            mask_green,
219	            mask_blue,
220	            mask_alpha,
221	            colour_table,
222	            image_data,
223	        })
224	    }
225	
226	    fn size_hint(_: usize) -> (usize, Option<usize>) {
227	        (25, Some(65536))
228	    }
229	}

If we run the fuzzer a couple of times we can see that indeed the smart values are properly generated. First run:

FuzzyBmp {
	    dib_type: DIB_V5_HEADER_SIZE,
	    bpp: Bits32,
	    compress: Rgb,
	    colours_used: 0,
	    width: 245,
	    height: -2,
	    mask_red: 4294967295,
	    mask_green: 16711701,
	    mask_blue: 436207360,
	    mask_alpha: 64512,
	    colour_table: [],
	    image_data: [
	        52,
          --snip--
	        181,
	    ],
	}

Second run:

FuzzyBmp {
	    dib_type: DIB_V4_HEADER_SIZE,
	    bpp: Bits16,
	    compress: Rgb,
	    colours_used: 0,
	    width: 86,
	    height: -65,
	    mask_red: 4294967295,
	    mask_green: 65535,
	    mask_blue: 402652928,
	    mask_alpha: 64767,
	    colour_table: [],
	    image_data: [
	        52,
	        175,
	        76,
          --snip--
	        149,
	        114,
	    ],
	}

We’re getting close finishing the harness. Let’s take a look at the last bits:

230	fn do_fuzz(bmp: FuzzyBmp) {
231	    let bmp = Vec::from(bmp);
232	    if let Ok(bmp) = RawBmp::from_slice(bmp.as_slice()) {
233	        bmp.pixels().for_each(|p| drop(p));
234	        for x in 0.. {
235	            if bmp.pixel(Point::new(x, 0)).is_none() {
236	                break;
237	            }
238	            for y in 1.. {
239	                if bmp.pixel(Point::new(x, y)).is_none() {
240	                    break;
241	                }
242	            }
243	        }
244	    }
245	}

On line 231 we create the new bmp Vector (derived from the FuzzyBmp structure) and we use again the familiar RawBmp::from_slice() method (which we used in our dumb fuzzer) but this time we also provide the smart bmp structure. Also notice how looking at the raw_bmp.rs source code the following snippet shows that the pixel() function expects a Point structure as parameter and that’s what we are doing on lines 234-240.

At this stage let’s print the contents of the FuzzyBmp vector again:

RawBmp { header: Header { file_size: 3233, 
                          image_data_start: 54, 
                          image_size: Size { width: 159, height: 8 }, 
                          bpp: Bits16, 
                          image_data_len: 3179, 
                          channel_masks: None, 
                          row_order: BottomUp }, 
                  color_type: Rgb555, 
                  color_table: None,                           
                  image_data: [226, 81, 8, 28, 132, 44, --snip--]
        }

This looks more complete, we’ve now calculated dynamically the file_size, as well as the image_data_len.

246	
247	fuzz_target!(|data: FuzzyBmp| {
248	    do_fuzz(data);
249	});

The above code snippet is the bit where cargo-fuzz uses to start mutating data, it calls the previously defined do_fuzz() function.

250	
251	#[cfg(not(fuzzing))]
252	extern "C" {
253	    fn rust_fuzzer_test_input(bytes: &[u8]) -> i32;
254	}
255	
256	#[cfg(not(fuzzing))]
257	fn main() {
258	    let dir = std::env::args().skip(1).next().unwrap();
259	    let mut content = Vec::new();
260	    for entry in read_dir(dir).unwrap() {
261	        let entry = entry.unwrap();
262	        if !entry.file_type().unwrap().is_file() {
263	            continue;
264	        }
265	        let mut file = File::open(entry.path()).unwrap();
266	        content.clear();
267	        content.reserve(file.metadata().unwrap().len() as usize);
268	        file.read_to_end(&mut content).unwrap();
269	        unsafe {
270	            rust_fuzzer_test_input(content.as_slice());
271	        }
272	    }
273	}
274	

Finally, these last bits will not be used within cargo-fuzz, if we compile the harness and run it we can see that expects a parameter as a dictionary and reads the contents of the (assuming we provided bmp) files. If we print the contents we can see the following:

[255, 64, 8, 253, 122, 200, 200, 200, 255, 255, 1, 0, 0, 0, 0, 0, 0, 4, 107, 0, 0, 0, 255, 255, 0, 0]
[255, 193, 1, 131, 75, 0, 87, 255, 191, 255, 255, 252, 1, 87, 255, 0, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 0, 0, 0, 126, 255, 0, 1, 255, 255, 255, 255, 0]
[255, 193, 1, 131, 87, 255, 191, 255, 63, 255, 252, 1, 255, 255, 0, 0, 0, 0, 0, 0, 9, 1, 0, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 255, 255]

We are done with our analysis, let’s try to kick in the fuzzer now:

$ cargo fuzz run structured -- -max_total_time=300                         
   Compiling tinybmp-fuzz v0.0.0 (/home/kali/Desktop/tinybmp/fuzz)
    Finished release [optimized + debuginfo] target(s) in 2.48s
    Finished release [optimized + debuginfo] target(s) in 0.03s
     Running `target/x86_64-unknown-linux-gnu/release/structured -artifact_prefix=/home/kali/Desktop/tinybmp/fuzz/artifacts/structured/ -max_total_time=300 /home/kali/Desktop/tinybmp/fuzz/corpus/structured`
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3975089662
INFO: Loaded 1 modules   (79206 inline 8-bit counters): 79206 [0x5600359c55d0, 0x5600359d8b36), 
INFO: Loaded 1 PC tables (79206 PCs): 79206 [0x5600359d8b38,0x560035b0e198), 
INFO:      416 files found in /home/kali/Desktop/tinybmp/fuzz/corpus/structured
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: seed corpus: files: 416 min: 25b max: 218b total: 18397b rss: 42Mb
#417	INITED cov: 664 ft: 1865 corp: 166/5683b exec/s: 0 rss: 81Mb
#1465	REDUCE cov: 664 ft: 1865 corp: 166/5681b lim: 195 exec/s: 1465 rss: 111Mb L: 37/181 MS: 3 ChangeByte-ChangeBinInt-EraseBytes-
#2436	REDUCE cov: 664 ft: 1865 corp: 166/5679b lim: 202 exec/s: 1218 rss: 138Mb L: 37/181 MS: 1 CrossOver-
#4096	pulse  cov: 664 ft: 1865 corp: 166/5679b lim: 216 exec/s: 1365 rss: 192Mb
                                          --snip--
#311445	REDUCE cov: 664 ft: 1868 corp: 168/5731b lim: 3157 exec/s: 1720 rss: 378Mb L: 37/181 MS: 5 CrossOver-ShuffleBytes-EraseBytes-InsertByte-ChangeBit-
#337091	REDUCE cov: 664 ft: 1868 corp: 168/5730b lim: 3410 exec/s: 1719 rss: 378Mb L: 37/181 MS: 1 EraseBytes-
#419743	REDUCE cov: 664 ft: 1868 corp: 168/5729b lim: 4096 exec/s: 1727 rss: 378Mb L: 37/181 MS: 2 ChangeBinInt-EraseBytes-
#441823	REDUCE cov: 664 ft: 1868 corp: 168/5727b lim: 4096 exec/s: 1725 rss: 382Mb L: 44/181 MS: 5 ChangeBinInt-EraseBytes-ChangeBit-ShuffleBytes-EraseBytes-
#445856	REDUCE cov: 664 ft: 1868 corp: 168/5726b lim: 4096 exec/s: 1728 rss: 382Mb L: 37/181 MS: 3 ChangeBinInt-CrossOver-EraseBytes-
#477133	REDUCE cov: 664 ft: 1868 corp: 168/5722b lim: 4096 exec/s: 1722 rss: 382Mb L: 40/181 MS: 2 ChangeBit-EraseBytes-
#479444	REDUCE cov: 664 ft: 1868 corp: 168/5721b lim: 4096 exec/s: 1724 rss: 382Mb L: 37/181 MS: 1 EraseBytes-
#495684	REDUCE cov: 664 ft: 1868 corp: 168/5718b lim: 4096 exec/s: 1721 rss: 382Mb L: 43/181 MS: 5 ChangeBit-CMP-ChangeBinInt-ShuffleBytes-EraseBytes- DE: "x\000"-
#519634	DONE   cov: 664 ft: 1868 corp: 168/5718b lim: 4096 exec/s: 1726 rss: 382Mb
###### Recommended dictionary. ######
"\001\000\000\000\000\000\000\004" # Uses: 44855
"x\000" # Uses: 1145
###### End of recommended dictionary. ######
Done 519634 runs in 301 second(s)

Unfortunately this improved harness didn’t yield any new bugs!

Coverage round 2

Let’s do this one more time running the smart-ish fuzzer:

cargo fuzz coverage structured

and after converting the data to HTML we get:

Fantastic! We did a lot of effort but as you can see this time we were able to hit all those functions (1.7k and 3.24 million times!) and get decent coverage.

Conclusion

We started with finding a fun target, created a dumb fuzzer and found some bugs with it. Then, we moved on with a smart-ish/structured aware approach and despite the fact were not able to uncover new bugs, we learnt how to mess around with arbitrary trait, and we dug a bit deeper to the internals of the project. Hope you enjoyed it and learnt something - I definitely did!

References

[1] Earn $200K by fuzzing for a weekend: Part 1
[2] Fuzzing with cargo-fuzz