Windows gdiplus (#789)

* Initial steps

* Harness code cleanup

* don't panic on linux in order not to break the CI

* formatting once again

* restored cfg unix to unbreak linux build
This commit is contained in:
expend20 2022-09-18 15:33:25 +02:00 committed by GitHub
parent 577f0be832
commit eebc412fb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 597 additions and 2 deletions

5
fuzzers/frida_gdiplus/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
corpus_discovered
*.exp
*.lib
*.obj

View File

@ -0,0 +1,42 @@
[package]
name = "frida_gdiplus"
version = "0.7.0"
authors = ["Richard Johnson <richinseattle@gmail.com>"]
edition = "2021"
[features]
default = ["std"]
std = []
[profile.release]
lto = true
codegen-units = 1
opt-level = 3
debug = true
[build-dependencies]
cc = { version = "1.0", features = ["parallel"] }
num_cpus = "1.0"
which = "4.1"
xz = "0.1.0"
flate2 = "1.0.22"
tar = "0.4.37"
reqwest = { version = "0.11.4", features = ["blocking"] }
[dependencies]
libafl = { path = "../../libafl/", features = [ "std", "llmp_compression", "llmp_bind_public", "frida_cli" ] } #, "llmp_small_maps", "llmp_debug"]}
capstone = "0.11.0"
frida-gum = { version = "0.7.1", features = [ "auto-download", "event-sink", "invocation-listener"] }
libafl_frida = { path = "../../libafl_frida", features = ["cmplog"] }
libafl_targets = { path = "../../libafl_targets", features = ["sancov_cmplog"] }
lazy_static = "1.4.0"
libc = "0.2"
libloading = "0.7"
num-traits = "0.2"
rangemap = "0.1"
clap = { version = "3.2", features = ["derive"] }
serde = "1.0"
mimalloc = { version = "*", default-features = false }
backtrace = "0.3"
color-backtrace = "0.5"

View File

@ -0,0 +1,9 @@
## Build
To build this example, run `cargo build --release` in this folder.
Then compile the harness `cl.exe /LD harness.cc /link /dll gdiplus.lib ole32.lib`
## Run
To run the example `target\release\frida_gdiplus.exe -H harness.dll -i corpus -o output`

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

View File

@ -0,0 +1,51 @@
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <vector>
#include <iostream>
#include <windows.h>
#include <gdiplus.h>
using namespace std;
using namespace Gdiplus;
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
extern "C" __declspec(dllexport) int LLVMFuzzerTestOneInput(const uint8_t *data,
size_t size) {
static DWORD init = 0;
if (!init) {
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
init = 1;
}
HGLOBAL m_hBuffer = ::GlobalAlloc(GMEM_MOVEABLE, size);
if (m_hBuffer) {
void *pBuffer = ::GlobalLock(m_hBuffer);
if (pBuffer) {
CopyMemory(pBuffer, data, size);
IStream *pStream = NULL;
if (::CreateStreamOnHGlobal(m_hBuffer, FALSE, &pStream) == S_OK) {
Gdiplus::Bitmap *m_pBitmap = Gdiplus::Bitmap::FromStream(pStream);
pStream->Release();
if (m_pBitmap) {
if (m_pBitmap->GetLastStatus() == Gdiplus::Ok) return true;
delete m_pBitmap;
m_pBitmap = NULL;
}
}
::GlobalUnlock(m_hBuffer);
}
::GlobalFree(m_hBuffer);
m_hBuffer = NULL;
}
// GdiplusShutdown(gdiplusToken);
return 0;
}

View File

@ -0,0 +1,481 @@
//! A libfuzzer-like fuzzer with llmp-multithreading support and restarts
//! The example harness is built for gdiplus.
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
use std::path::PathBuf;
use frida_gum::Gum;
use libafl::{
bolts::{
cli::{parse_args, FuzzerOptions},
current_nanos,
launcher::Launcher,
rands::StdRand,
shmem::{ShMemProvider, StdShMemProvider},
tuples::{tuple_list, Merge},
AsSlice,
},
corpus::{ondisk::OnDiskMetadataFormat, CachedOnDiskCorpus, Corpus, OnDiskCorpus},
events::{llmp::LlmpRestartingEventManager, EventConfig},
executors::{inprocess::InProcessExecutor, ExitKind, ShadowExecutor},
feedback_and_fast, feedback_or, feedback_or_fast,
feedbacks::{ConstFeedback, CrashFeedback, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
fuzzer::{Fuzzer, StdFuzzer},
inputs::{BytesInput, HasTargetBytes},
monitors::MultiMonitor,
mutators::{
scheduled::{havoc_mutations, tokens_mutations, StdScheduledMutator},
token_mutations::{I2SRandReplace, Tokens},
},
observers::{HitcountsMapObserver, StdMapObserver, TimeObserver},
schedulers::{IndexesLenTimeMinimizerScheduler, QueueScheduler},
stages::{ShadowTracingStage, StdMutationalStage},
state::{HasCorpus, HasMetadata, StdState},
Error,
};
#[cfg(unix)]
use libafl_frida::asan::asan_rt::AsanRuntime;
#[cfg(unix)]
use libafl_frida::asan::errors::{AsanErrorsFeedback, AsanErrorsObserver, ASAN_ERRORS};
use libafl_frida::{
cmplog_rt::CmpLogRuntime,
coverage_rt::{CoverageRuntime, MAP_SIZE},
executor::FridaInProcessExecutor,
helper::FridaInstrumentationHelper,
};
use libafl_targets::cmplog::{CmpLogObserver, CMPLOG_MAP};
/// The main fn, usually parsing parameters, and starting the fuzzer
pub fn main() {
color_backtrace::install();
let options = parse_args();
unsafe {
match fuzz(options) {
Ok(()) | Err(Error::ShuttingDown) => println!("\nFinished fuzzing. Good bye."),
Err(e) => panic!("Error during fuzzing: {:?}", e),
}
}
}
/// The actual fuzzer
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
unsafe fn fuzz(options: FuzzerOptions) -> Result<(), Error> {
// 'While the stats are state, they are usually used in the broker - which is likely never restarted
let monitor = MultiMonitor::new(|s| println!("{}", s));
let shmem_provider = StdShMemProvider::new()?;
let mut run_client = |state: Option<_>,
mgr: LlmpRestartingEventManager<_, _, _, _>,
core_id| {
// The restarting state will spawn the same process again as child, then restarted it each time it crashes.
// println!("{:?}", mgr.mgr_id());
let lib = libloading::Library::new(options.clone().harness.unwrap()).unwrap();
let target_func: libloading::Symbol<
unsafe extern "C" fn(data: *const u8, size: usize) -> i32,
> = lib.get(options.harness_function.as_bytes()).unwrap();
let mut frida_harness = |input: &BytesInput| {
let target = input.target_bytes();
let buf = target.as_slice();
(target_func)(buf.as_ptr(), buf.len());
ExitKind::Ok
};
if options.asan && options.asan_cores.contains(core_id) {
(|state: Option<_>, mut mgr: LlmpRestartingEventManager<_, _, _, _>, _core_id| {
let gum = Gum::obtain();
let coverage = CoverageRuntime::new();
#[cfg(unix)]
let asan = AsanRuntime::new(options.clone());
#[cfg(unix)]
let mut frida_helper =
FridaInstrumentationHelper::new(&gum, &options, tuple_list!(coverage, asan));
#[cfg(windows)]
let mut frida_helper =
FridaInstrumentationHelper::new(&gum, &options, tuple_list!(coverage));
// Create an observation channel using the coverage map
let edges_observer = HitcountsMapObserver::new(StdMapObserver::new_from_ptr(
"edges",
frida_helper.map_ptr_mut().unwrap(),
MAP_SIZE,
));
// Create an observation channel to keep track of the execution time
let time_observer = TimeObserver::new("time");
// Feedback to rate the interestingness of an input
// This one is composed by two Feedbacks in OR
let mut feedback = feedback_or!(
// New maximization map feedback linked to the edges observer and the feedback state
MaxMapFeedback::new_tracking(&edges_observer, true, false),
// Time feedback, this one does not need a feedback state
TimeFeedback::new_with_observer(&time_observer)
);
// Feedbacks to recognize an input as solution
#[cfg(unix)]
let mut objective = feedback_or_fast!(
CrashFeedback::new(),
TimeoutFeedback::new(),
// true enables the AsanErrorFeedback
feedback_and_fast!(ConstFeedback::from(true), AsanErrorsFeedback::new())
);
#[cfg(windows)]
let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
// If not restarting, create a State from scratch
let mut state = state.unwrap_or_else(|| {
StdState::new(
// RNG
StdRand::with_seed(current_nanos()),
// Corpus that will be evolved, we keep it in memory for performance
CachedOnDiskCorpus::new(PathBuf::from("./corpus_discovered"), 64).unwrap(),
// Corpus in which we store solutions (crashes in this example),
// on disk so the user can get them after stopping the fuzzer
OnDiskCorpus::new_save_meta(
options.output.to_path_buf(),
Some(OnDiskMetadataFormat::JsonPretty),
)
.unwrap(),
&mut feedback,
&mut objective,
)
.unwrap()
});
println!("We're a client, let's fuzz :)");
// Create a PNG dictionary if not existing
if state.metadata().get::<Tokens>().is_none() {
state.add_metadata(Tokens::from([
vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG header
b"IHDR".to_vec(),
b"IDAT".to_vec(),
b"PLTE".to_vec(),
b"IEND".to_vec(),
]));
}
// Setup a basic mutator with a mutational stage
let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations()));
// A minimization+queue policy to get testcasess from the corpus
let scheduler = IndexesLenTimeMinimizerScheduler::new(QueueScheduler::new());
// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
#[cfg(unix)]
let observers = tuple_list!(
edges_observer,
time_observer,
AsanErrorsObserver::new(&ASAN_ERRORS)
);
#[cfg(windows)]
let observers = tuple_list!(edges_observer, time_observer);
// Create the executor for an in-process function with just one observer for edge coverage
let mut executor = FridaInProcessExecutor::new(
&gum,
InProcessExecutor::new(
&mut frida_harness,
observers,
&mut fuzzer,
&mut state,
&mut mgr,
)?,
&mut frida_helper,
);
// In case the corpus is empty (on first run), reset
if state.corpus().count() < 1 {
state
.load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &options.input)
.unwrap_or_else(|_| {
panic!("Failed to load initial corpus at {:?}", &options.input)
});
println!("We imported {} inputs from disk.", state.corpus().count());
}
let mut stages = tuple_list!(StdMutationalStage::new(mutator));
fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?;
Ok(())
})(state, mgr, core_id)
} else if options.cmplog && options.cmplog_cores.contains(core_id) {
(|state: Option<_>, mut mgr: LlmpRestartingEventManager<_, _, _, _>, _core_id| {
let gum = Gum::obtain();
let coverage = CoverageRuntime::new();
let cmplog = CmpLogRuntime::new();
let mut frida_helper =
FridaInstrumentationHelper::new(&gum, &options, tuple_list!(coverage, cmplog));
// Create an observation channel using the coverage map
let edges_observer = HitcountsMapObserver::new(StdMapObserver::new_from_ptr(
"edges",
frida_helper.map_ptr_mut().unwrap(),
MAP_SIZE,
));
// Create an observation channel to keep track of the execution time
let time_observer = TimeObserver::new("time");
// Feedback to rate the interestingness of an input
// This one is composed by two Feedbacks in OR
let mut feedback = feedback_or!(
// New maximization map feedback linked to the edges observer and the feedback state
MaxMapFeedback::new_tracking(&edges_observer, true, false),
// Time feedback, this one does not need a feedback state
TimeFeedback::new_with_observer(&time_observer)
);
#[cfg(unix)]
let mut objective = feedback_or_fast!(
CrashFeedback::new(),
TimeoutFeedback::new(),
feedback_and_fast!(ConstFeedback::from(false), AsanErrorsFeedback::new())
);
#[cfg(windows)]
let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
// If not restarting, create a State from scratch
let mut state = state.unwrap_or_else(|| {
StdState::new(
// RNG
StdRand::with_seed(current_nanos()),
// Corpus that will be evolved, we keep it in memory for performance
CachedOnDiskCorpus::new(PathBuf::from("./corpus_discovered"), 64).unwrap(),
// Corpus in which we store solutions (crashes in this example),
// on disk so the user can get them after stopping the fuzzer
OnDiskCorpus::new_save_meta(
options.output.to_path_buf(),
Some(OnDiskMetadataFormat::JsonPretty),
)
.unwrap(),
&mut feedback,
&mut objective,
)
.unwrap()
});
println!("We're a client, let's fuzz :)");
// Create a PNG dictionary if not existing
if state.metadata().get::<Tokens>().is_none() {
state.add_metadata(Tokens::from([
vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG header
b"IHDR".to_vec(),
b"IDAT".to_vec(),
b"PLTE".to_vec(),
b"IEND".to_vec(),
]));
}
// Setup a basic mutator with a mutational stage
let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations()));
// A minimization+queue policy to get testcasess from the corpus
let scheduler = IndexesLenTimeMinimizerScheduler::new(QueueScheduler::new());
// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
#[cfg(unix)]
let observers = tuple_list!(
edges_observer,
time_observer,
AsanErrorsObserver::new(&ASAN_ERRORS)
);
#[cfg(windows)]
let observers = tuple_list!(edges_observer, time_observer,);
// Create the executor for an in-process function with just one observer for edge coverage
let mut executor = FridaInProcessExecutor::new(
&gum,
InProcessExecutor::new(
&mut frida_harness,
observers,
&mut fuzzer,
&mut state,
&mut mgr,
)?,
&mut frida_helper,
);
// In case the corpus is empty (on first run), reset
if state.corpus().count() < 1 {
state
.load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &options.input)
.unwrap_or_else(|_| {
panic!("Failed to load initial corpus at {:?}", &options.input)
});
println!("We imported {} inputs from disk.", state.corpus().count());
}
// Create an observation channel using cmplog map
let cmplog_observer = CmpLogObserver::new("cmplog", &mut CMPLOG_MAP, true);
let mut executor = ShadowExecutor::new(executor, tuple_list!(cmplog_observer));
let tracing = ShadowTracingStage::new(&mut executor);
// Setup a randomic Input2State stage
let i2s = StdMutationalStage::new(StdScheduledMutator::new(tuple_list!(
I2SRandReplace::new()
)));
// Setup a basic mutator
let mutational = StdMutationalStage::new(mutator);
// The order of the stages matter!
let mut stages = tuple_list!(tracing, i2s, mutational);
fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?;
Ok(())
})(state, mgr, core_id)
} else {
(|state: Option<_>, mut mgr: LlmpRestartingEventManager<_, _, _, _>, _core_id| {
let gum = Gum::obtain();
let coverage = CoverageRuntime::new();
let mut frida_helper =
FridaInstrumentationHelper::new(&gum, &options, tuple_list!(coverage));
// Create an observation channel using the coverage map
let edges_observer = HitcountsMapObserver::new(StdMapObserver::new_from_ptr(
"edges",
frida_helper.map_ptr_mut().unwrap(),
MAP_SIZE,
));
// Create an observation channel to keep track of the execution time
let time_observer = TimeObserver::new("time");
// Feedback to rate the interestingness of an input
// This one is composed by two Feedbacks in OR
let mut feedback = feedback_or!(
// New maximization map feedback linked to the edges observer and the feedback state
MaxMapFeedback::new_tracking(&edges_observer, true, false),
// Time feedback, this one does not need a feedback state
TimeFeedback::new_with_observer(&time_observer)
);
#[cfg(unix)]
let mut objective = feedback_or_fast!(
CrashFeedback::new(),
TimeoutFeedback::new(),
feedback_and_fast!(ConstFeedback::from(false), AsanErrorsFeedback::new())
);
#[cfg(windows)]
let mut objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
// If not restarting, create a State from scratch
let mut state = state.unwrap_or_else(|| {
StdState::new(
// RNG
StdRand::with_seed(current_nanos()),
// Corpus that will be evolved, we keep it in memory for performance
CachedOnDiskCorpus::new(PathBuf::from("./corpus_discovered"), 64).unwrap(),
// Corpus in which we store solutions (crashes in this example),
// on disk so the user can get them after stopping the fuzzer
OnDiskCorpus::new_save_meta(
options.output.to_path_buf(),
Some(OnDiskMetadataFormat::JsonPretty),
)
.unwrap(),
&mut feedback,
&mut objective,
)
.unwrap()
});
println!("We're a client, let's fuzz :)");
// Create a PNG dictionary if not existing
if state.metadata().get::<Tokens>().is_none() {
state.add_metadata(Tokens::from([
vec![137, 80, 78, 71, 13, 10, 26, 10], // PNG header
b"IHDR".to_vec(),
b"IDAT".to_vec(),
b"PLTE".to_vec(),
b"IEND".to_vec(),
]));
}
// Setup a basic mutator with a mutational stage
let mutator = StdScheduledMutator::new(havoc_mutations().merge(tokens_mutations()));
// A minimization+queue policy to get testcasess from the corpus
let scheduler = IndexesLenTimeMinimizerScheduler::new(QueueScheduler::new());
// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
#[cfg(unix)]
let observers = tuple_list!(
edges_observer,
time_observer,
AsanErrorsObserver::new(&ASAN_ERRORS)
);
#[cfg(windows)]
let observers = tuple_list!(edges_observer, time_observer,);
// Create the executor for an in-process function with just one observer for edge coverage
let mut executor = FridaInProcessExecutor::new(
&gum,
InProcessExecutor::new(
&mut frida_harness,
observers,
&mut fuzzer,
&mut state,
&mut mgr,
)?,
&mut frida_helper,
);
// In case the corpus is empty (on first run), reset
if state.corpus().count() < 1 {
state
.load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &options.input)
.unwrap_or_else(|_| {
panic!("Failed to load initial corpus at {:?}", &options.input)
});
println!("We imported {} inputs from disk.", state.corpus().count());
}
let mut stages = tuple_list!(StdMutationalStage::new(mutator));
fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?;
Ok(())
})(state, mgr, core_id)
}
};
Launcher::builder()
.configuration(EventConfig::AlwaysUnique)
.shmem_provider(shmem_provider)
.monitor(monitor)
.run_client(&mut run_client)
.cores(&options.cores)
.broker_port(options.broker_port)
.stdout_file(Some(&options.stdout))
.remote_broker_addr(options.remote_broker_addr)
.build()
.launch()
}

View File

@ -0,0 +1,4 @@
mod fuzzer;
pub fn main() {
fuzzer::main();
}

View File

@ -80,6 +80,7 @@ pub const STATUS_INVALID_CRUNTIME_PARAMETER: i32 = 0xC0000417;
pub const STATUS_ASSERTION_FAILURE: i32 = 0xC0000420;
pub const STATUS_SXS_EARLY_DEACTIVATION: i32 = 0xC015000F;
pub const STATUS_SXS_INVALID_DEACTIVATION: i32 = 0xC0150010;
pub const STATUS_NOT_IMPLEMENTED: i32 = 0xC0000002;
#[derive(Debug, TryFromPrimitive, Clone, Copy)]
#[repr(i32)]
@ -131,6 +132,7 @@ pub enum ExceptionCode {
AssertionFailure = STATUS_ASSERTION_FAILURE,
SXSEarlyDeactivation = STATUS_SXS_EARLY_DEACTIVATION,
SXSInvalidDeactivation = STATUS_SXS_INVALID_DEACTIVATION,
NotImplemented = STATUS_NOT_IMPLEMENTED,
#[num_enum(default)]
Other,
}
@ -150,7 +152,6 @@ pub static CRASH_EXCEPTIONS: &[ExceptionCode] = &[
ExceptionCode::HeapCorruption,
ExceptionCode::StackBufferOverrun,
ExceptionCode::AssertionFailure,
ExceptionCode::Other,
];
impl PartialEq for ExceptionCode {
@ -213,6 +214,7 @@ impl Display for ExceptionCode {
ExceptionCode::AssertionFailure => write!(f, "STATUS_ASSERTION_FAILURE")?,
ExceptionCode::SXSEarlyDeactivation => write!(f, "STATUS_SXS_EARLY_DEACTIVATION")?,
ExceptionCode::SXSInvalidDeactivation => write!(f, "STATUS_SXS_INVALID_DEACTIVATION")?,
ExceptionCode::NotImplemented => write!(f, "STATUS_NOT_IMPLEMENTED")?,
ExceptionCode::Other => write!(f, "Other/User defined exception")?,
};
@ -220,7 +222,7 @@ impl Display for ExceptionCode {
}
}
pub static EXCEPTION_CODES_MAPPING: [ExceptionCode; 46] = [
pub static EXCEPTION_CODES_MAPPING: [ExceptionCode; 47] = [
ExceptionCode::AccessViolation,
ExceptionCode::ArrayBoundsExceeded,
ExceptionCode::Breakpoint,
@ -266,6 +268,7 @@ pub static EXCEPTION_CODES_MAPPING: [ExceptionCode; 46] = [
ExceptionCode::AssertionFailure,
ExceptionCode::SXSEarlyDeactivation,
ExceptionCode::SXSInvalidDeactivation,
ExceptionCode::NotImplemented,
ExceptionCode::Other,
];