From 98407247b8bb5c38d7e2713fa822123f72e25d4b Mon Sep 17 00:00:00 2001 From: plotchy <98172525+plotchy@users.noreply.github.com> Date: Sat, 18 May 2024 12:48:09 -0400 Subject: [PATCH] Refactor parameters that adjust fuzzer behavior (#477) * refactor: move parameter constants to const.rs * fix: below is exclusive. change to 256 to fill all u8 range * fix: add in RANDOM_ADDRESS_CHOICE const * chore: fmt --- src/const.rs | 76 +++++++++++++++++++++++++++++++++ src/evm/abi.rs | 15 +++---- src/evm/corpus_initializer.rs | 3 +- src/evm/mutator.rs | 79 +++++++++++++++++++++++++---------- src/evm/scheduler.rs | 14 ++++--- src/feedback.rs | 18 ++++---- src/fuzzer.rs | 8 +++- src/mutation_utils.rs | 16 ++++--- src/scheduler.rs | 20 ++++----- src/state.rs | 8 +--- 10 files changed, 187 insertions(+), 70 deletions(-) diff --git a/src/const.rs b/src/const.rs index ec3144c4b..43d831bfc 100644 --- a/src/const.rs +++ b/src/const.rs @@ -1 +1,77 @@ pub const DEBUG_PRINT_PERCENT: usize = 8000; + +pub const INFANT_STATE_INITIAL_VOTES: usize = 3; // used in fuzzer.rs when infant is added to states +pub const CORPUS_INITIAL_VOTES: usize = 3; // used in fuzzer.rs when infant is added to states +/// used in libafl scheduled mutator (this is at max 1 << MAX_STACK_POW == +/// 2^MAX_STACK_POW) +pub const MAX_STACK_POW: usize = 7; +pub const MAX_HAVOC_ATTEMPTS: usize = 10; +pub const KNOWN_STATE_MAX_SIZE: usize = 1000; +pub const KNOWN_STATE_SKIP_SIZE: usize = 500; + +// src/fuzzer.rs +/// The maximum number of inputs (or VMState) to keep in the corpus before +/// pruning +pub const DROP_THRESHOLD: usize = 500; +/// The number of inputs (or VMState) to prune each time the corpus is pruned +pub const PRUNE_AMT: usize = 250; +/// If inputs (or VMState) has not been visited this many times, it will be +/// ignored during pruning +pub const VISIT_IGNORE_THRESHOLD: usize = 2; + +// src/state.rs +/// Amount of accounts and contracts that can be caller during fuzzing. +/// We will generate random addresses for these accounts and contracts. +pub const ACCOUNT_AMT: u8 = 2; +/// Amount of accounts and contracts that can be caller during fuzzing. +/// We will generate random addresses for these accounts and contracts. +pub const CONTRACT_AMT: u8 = 2; +/// Maximum size of the input data +pub const MAX_INPUT_SIZE: usize = 20; + +// src/abi.rs +/// Sample will be used to generate a random value with max value +pub const SAMPLE_MAX: u64 = 100; +/// Maximum value of the mutate choice. Related to [SAMPLE_MAX] +pub const MUTATE_CHOICE_MAX: u64 = 80; +/// Maximum value of the expand choice. Related to [SAMPLE_MAX] +pub const EXPAND_CHOICE_MAX: u64 = 90; +/// Maximum value of the random address choice. Related to [SAMPLE_MAX] +pub const RANDOM_ADDRESS_CHOICE: u64 = 90; + +// src/evm/corpus_initializer.rs +/// If there are more than 1/UNKNOWN_SIGS_DIVISOR unknown sigs, we will +/// decompile with heimdall +pub const UNKNOWN_SIGS_DIVISOR: usize = 30; + +// src/evm/mutator.rs +/// Sample will be used to generate a random value with max value +pub const MUTATOR_SAMPLE_MAX: u64 = 100; +/// Related to [MUTATOR_SAMPLE_MAX] +pub const EXPLOIT_PRESET_CHOICE: u64 = 20; +/// Related to [MUTATOR_SAMPLE_MAX] +pub const ABI_MUTATE_CHOICE: u64 = 96; +/// Related to [MUTATOR_SAMPLE_MAX] +pub const HAVOC_CHOICE: u64 = 60; +/// Maximum number of iterations to try to find a valid havoc mutation +pub const HAVOC_MAX_ITERS: u64 = 10; +/// Related to [MUTATOR_SAMPLE_MAX] +pub const MUTATE_CALLER_CHOICE: u64 = 20; +/// Related to [MUTATOR_SAMPLE_MAX] +pub const TURN_TO_STEP_CHOICE: u64 = 60; +/// Related to [MUTATOR_SAMPLE_MAX] +pub const RANDOMNESS_CHOICE: u64 = 33; +/// Related to [MUTATOR_SAMPLE_MAX] +pub const LIQUIDATE_CHOICE: u64 = 5; +/// Related to [MUTATOR_SAMPLE_MAX] +pub const LIQ_PERCENT_CHOICE: u64 = 80; +pub const LIQ_PERCENT: u64 = 10; +/// Related to [MUTATOR_SAMPLE_MAX] and [LIQUIDATE_CHOICE] +pub const RANDOMNESS_CHOICE_2: u64 = 6; +/// Maximum number of retries to try to find a valid mutation +pub const MUTATION_RETRIES: usize = 20; + +// src/evm/scheduler.rs +pub const POWER_MULTIPLIER: f64 = 32.0; +pub const MAX_POWER: f64 = 3200.0; +pub const MIN_POWER: f64 = 32.0; diff --git a/src/evm/abi.rs b/src/evm/abi.rs index c380aea2e..2fb8df9a1 100644 --- a/src/evm/abi.rs +++ b/src/evm/abi.rs @@ -33,6 +33,7 @@ use crate::{ generic_vm::vm_state::VMStateT, input::ConciseSerde, mutation_utils::{byte_mutator, byte_mutator_with_expansion}, + r#const::{EXPAND_CHOICE_MAX, MUTATE_CHOICE_MAX, RANDOM_ADDRESS_CHOICE, SAMPLE_MAX}, state::{HasCaller, HasItyState}, }; @@ -344,7 +345,7 @@ where match state.rand_mut().below(100) % 4 { // dynamic 0 => BoxedABI::new(Box::new(ADynamic { - data: vec![state.rand_mut().below(255) as u8; vec_size], + data: vec![state.rand_mut().below(256) as u8; vec_size], multiplier: 32, })), // tuple @@ -412,7 +413,7 @@ impl BoxedABI { return MutationResult::Skipped; } if a256.is_address { - if state.rand_mut().below(100) < 90 { + if state.rand_mut().below(SAMPLE_MAX) < RANDOM_ADDRESS_CHOICE { a256.data = state.get_rand_address().0.to_vec(); } else { a256.data = [0; 20].to_vec(); @@ -438,13 +439,13 @@ impl BoxedABI { return MutationResult::Skipped; } if aarray.dynamic_size { - match state.rand_mut().below(100) { - 0..=80 => { + match state.rand_mut().below(SAMPLE_MAX) { + 0..=MUTATE_CHOICE_MAX => { let index: usize = state.rand_mut().next() as usize % data_len; let result = aarray.data[index].mutate_with_vm_slots(state, vm_slots); return result; } - 81..=90 => { + MUTATE_CHOICE_MAX..=EXPAND_CHOICE_MAX => { // increase size if state.max_size() <= aarray.data.len() { return MutationResult::Skipped; @@ -453,7 +454,7 @@ impl BoxedABI { aarray.data.push(aarray.data[0].clone()); } } - 91..=100 => { + EXPAND_CHOICE_MAX..=SAMPLE_MAX => { // decrease size if aarray.data.is_empty() { return MutationResult::Skipped; @@ -478,7 +479,7 @@ impl BoxedABI { a_unknown.concrete = BoxedABI::new(Box::new(AEmpty {})); return MutationResult::Skipped; } - if (state.rand_mut().below(100)) < 80 { + if (state.rand_mut().below(SAMPLE_MAX)) < MUTATE_CHOICE_MAX { a_unknown.concrete.mutate_with_vm_slots(state, vm_slots) } else { a_unknown.concrete = sample_abi(state, a_unknown.size); diff --git a/src/evm/corpus_initializer.rs b/src/evm/corpus_initializer.rs index 7f7e0427c..fd1e56881 100644 --- a/src/evm/corpus_initializer.rs +++ b/src/evm/corpus_initializer.rs @@ -54,6 +54,7 @@ use crate::{ fuzzer::REPLAY, generic_vm::vm_executor::GenericVM, input::ConciseSerde, + r#const::UNKNOWN_SIGS_DIVISOR, state::HasCaller, state_input::StagedVMState, }; @@ -310,7 +311,7 @@ where } } - if unknown_sigs >= sigs.len() / 30 { + if unknown_sigs >= sigs.len() / UNKNOWN_SIGS_DIVISOR { info!("Too many unknown function signature for {:?}, we are going to decompile this contract using Heimdall", contract.name); let abis = fetch_abi_heimdall(contract_code) .iter() diff --git a/src/evm/mutator.rs b/src/evm/mutator.rs index 874f76284..c52e091dd 100644 --- a/src/evm/mutator.rs +++ b/src/evm/mutator.rs @@ -24,6 +24,21 @@ use crate::{ }, generic_vm::vm_state::VMStateT, input::{ConciseSerde, VMInputT}, + r#const::{ + ABI_MUTATE_CHOICE, + EXPLOIT_PRESET_CHOICE, + HAVOC_CHOICE, + HAVOC_MAX_ITERS, + LIQUIDATE_CHOICE, + LIQ_PERCENT, + LIQ_PERCENT_CHOICE, + MUTATE_CALLER_CHOICE, + MUTATION_RETRIES, + MUTATOR_SAMPLE_MAX, + RANDOMNESS_CHOICE, + RANDOMNESS_CHOICE_2, + TURN_TO_STEP_CHOICE, + }, state::{HasCaller, HasItyState, HasPresets, InfantStateState}, }; @@ -149,15 +164,21 @@ where } Constraint::Contract(target) => { let rand_int = state.rand_mut().next(); - let always_none = state.rand_mut().next() % 30 == 0; + let always_none = state.rand_mut().below(MUTATOR_SAMPLE_MAX); let abis = state .metadata_map() .get::() .expect("ABIAddressToInstanceMap not found"); let abi = match abis.map.get(&target) { Some(abi) => { - if !abi.is_empty() && !always_none { - Some((*abi)[rand_int as usize % abi.len()].clone()) + if !abi.is_empty() { + match always_none { + 0..=ABI_MUTATE_CHOICE => { + // we return a random abi + Some((*abi)[rand_int as usize % abi.len()].clone()) + } + _ => None, + } } else { None } @@ -216,7 +237,7 @@ where } // use exploit template - if state.has_preset() && state.rand_mut().below(100) < 20 { + if state.has_preset() && state.rand_mut().below(MUTATOR_SAMPLE_MAX) < EXPLOIT_PRESET_CHOICE { // if flashloan_v2, we don't mutate if it's a borrow if input.get_input_type() != Borrow { match state.get_next_call() { @@ -237,11 +258,13 @@ where // abi.b.get_size()).unwrap_or(0) / 32 + 1; if amount_of_args > 6 { // amount_of_args = 6; // } - let should_havoc = state.rand_mut().below(100) < 60; // (amount_of_args * 10) as u64; + let should_havoc = state.rand_mut().below(MUTATOR_SAMPLE_MAX) < HAVOC_CHOICE; // determine how many times we should mutate the input let havoc_times = if should_havoc { - state.rand_mut().below(10) + 1 + state.rand_mut().below(HAVOC_MAX_ITERS) + 1 // (amount_of_args * + // HAVOC_MAX_ITERS) as + // u64; } else { 1 }; @@ -249,7 +272,7 @@ where let mut mutated = false; { - if !input.is_step() && state.rand_mut().below(100) < 20_u64 { + if !input.is_step() && state.rand_mut().below(MUTATOR_SAMPLE_MAX) < MUTATE_CALLER_CHOICE { let old_idx = input.get_state_idx(); let (idx, new_state) = state.get_infant_state(&mut self.infant_scheduler).unwrap(); if idx != old_idx { @@ -266,7 +289,7 @@ where if input.get_staged_state().state.has_post_execution() && !input.is_step() && - state.rand_mut().below(100) < 60_u64 + state.rand_mut().below(MUTATOR_SAMPLE_MAX) < TURN_TO_STEP_CHOICE { macro_rules! turn_to_step { () => { @@ -292,12 +315,18 @@ where // if the input is a step input (resume execution from a control leak) // we should not mutate the VM state, but only mutate the bytes if input.is_step() { - let res = match state.rand_mut().below(100) { - 0..=5 => { + let res = match state.rand_mut().below(MUTATOR_SAMPLE_MAX) { + 0..=LIQUIDATE_CHOICE => { // only when there are more than one liquidation path, we attempt to liquidate if unsafe { CAN_LIQUIDATE } { let prev_percent = input.get_liquidation_percent(); - input.set_liquidation_percent(if state.rand_mut().below(100) < 80 { 10 } else { 0 } as u8); + input.set_liquidation_percent(if state.rand_mut().below(MUTATOR_SAMPLE_MAX) < + LIQ_PERCENT_CHOICE + { + LIQ_PERCENT + } else { + 0 + } as u8); if prev_percent != input.get_liquidation_percent() { MutationResult::Mutated } else { @@ -316,9 +345,9 @@ where // if the input is to borrow token, we should mutate the randomness // (use to select the paths to buy token), VM state, and bytes if input.get_input_type() == Borrow { - let rand_u8 = state.rand_mut().below(255) as u8; - return match state.rand_mut().below(3) { - 0 => { + let rand_u8 = state.rand_mut().below(256) as u8; + return match state.rand_mut().below(MUTATOR_SAMPLE_MAX) { + 0..=RANDOMNESS_CHOICE => { // mutate the randomness input.set_randomness(vec![rand_u8; 1]); MutationResult::Mutated @@ -330,18 +359,22 @@ where // mutate the bytes or VM state or liquidation percent (percentage of token to // liquidate) by default - match state.rand_mut().below(100) { - 6..=10 => { + match state.rand_mut().below(MUTATOR_SAMPLE_MAX) { + 0..=LIQUIDATE_CHOICE => { let prev_percent = input.get_liquidation_percent(); - input.set_liquidation_percent(if state.rand_mut().below(100) < 80 { 10 } else { 0 } as u8); + input.set_liquidation_percent(if state.rand_mut().below(MUTATOR_SAMPLE_MAX) < LIQ_PERCENT_CHOICE { + LIQ_PERCENT + } else { + 0 + } as u8); if prev_percent != input.get_liquidation_percent() { MutationResult::Mutated } else { MutationResult::Skipped } } - 11 => { - let rand_u8 = state.rand_mut().below(255) as u8; + LIQUIDATE_CHOICE..=RANDOMNESS_CHOICE_2 => { + let rand_u8 = state.rand_mut().below(256) as u8; input.set_randomness(vec![rand_u8; 1]); MutationResult::Mutated } @@ -356,10 +389,10 @@ where }; let mut tries = 0; - // try to mutate the input for [`havoc_times`] times with 20 retries if - // the input is not mutated - while res != MutationResult::Mutated && tries < 20 { - for _ in 0..havoc_times { + // try to mutate the input for [`havoc_times`] times with MUTATION_RETRIES + // retries if the input is not mutated + while res != MutationResult::Mutated && tries < MUTATION_RETRIES { + for i in 0..havoc_times { if mutator() == MutationResult::Mutated { res = MutationResult::Mutated; } diff --git a/src/evm/scheduler.rs b/src/evm/scheduler.rs index 3c252249e..4f1f15672 100644 --- a/src/evm/scheduler.rs +++ b/src/evm/scheduler.rs @@ -27,6 +27,7 @@ use crate::{ }, input::VMInputT, power_sched::{PowerMutationalStageWithId, TestcaseScoreWithId}, + r#const::{MAX_POWER, MIN_POWER, POWER_MULTIPLIER}, }; /// The status of the branch, whether it is covered on true, false or both @@ -394,14 +395,15 @@ where meta.testcase_to_uncovered_branches.get(&idx).unwrap_or(&0).to_owned() + 1 }; - let mut power = uncov_branch as f64 * 32.0; - - if power >= 3200.0 { - power = 3200.0; + let mut power = uncov_branch as f64 * POWER_MULTIPLIER; + // we score based on how a test case uncovered branches. 100 is cap, 1 is always + // min + if power >= MAX_POWER { + power = MAX_POWER; } - if power <= 32.0 { - power = 32.0; + if power <= MIN_POWER { + power = MIN_POWER; } Ok(power) diff --git a/src/feedback.rs b/src/feedback.rs index 490ee5f51..5bb041352 100644 --- a/src/feedback.rs +++ b/src/feedback.rs @@ -29,13 +29,11 @@ use crate::{ generic_vm::vm_state::VMStateT, input::{ConciseSerde, VMInputT}, oracle::{BugMetadata, Oracle, OracleCtx, Producer}, + r#const::{INFANT_STATE_INITIAL_VOTES, KNOWN_STATE_MAX_SIZE, KNOWN_STATE_SKIP_SIZE}, scheduler::HasVote, state::{HasExecutionResult, HasInfantStateState, InfantStateState}, }; -const KNOWN_STATE_MAX_SIZE: usize = 1000; -const KNOWN_STATE_SKIP_SIZE: usize = 500; - /// OracleFeedback is a wrapper around a set of oracles and producers. /// It executes the producers and then oracles after each successful execution. /// If any of the oracle returns true, then it returns true and report a @@ -541,16 +539,22 @@ where // if the current distance is smaller than the min_map, vote for the state if cmp_interesting { debug!("Voted for {} because of CMP", input.get_state_idx()); - self.scheduler - .vote(state.get_infant_state_state(), input.get_state_idx(), 3); + self.scheduler.vote( + state.get_infant_state_state(), + input.get_state_idx(), + INFANT_STATE_INITIAL_VOTES, + ); } // if coverage has increased, vote for the state if cov_interesting { debug!("Voted for {} because of COV", input.get_state_idx()); - self.scheduler - .vote(state.get_infant_state_state(), input.get_state_idx(), 3); + self.scheduler.vote( + state.get_infant_state_state(), + input.get_state_idx(), + INFANT_STATE_INITIAL_VOTES, + ); } if self.vm.deref().borrow_mut().state_changed() || diff --git a/src/fuzzer.rs b/src/fuzzer.rs index f6d57d12c..5f53c58f8 100644 --- a/src/fuzzer.rs +++ b/src/fuzzer.rs @@ -47,6 +47,7 @@ use crate::{ input::{ConciseSerde, SolutionTx, VMInputT}, minimizer::SequentialMinimizer, oracle::BugMetadata, + r#const::INFANT_STATE_INITIAL_VOTES, scheduler::HasReportCorpus, state::{HasCurrentInputIdx, HasExecutionResult, HasInfantStateState, HasItyState, InfantStateState}, }; @@ -444,8 +445,11 @@ where .infant_result_feedback .is_interesting(state, manager, &input, observers, &exitkind)? { - self.infant_scheduler - .sponsor_state(state.get_infant_state_state(), state_idx, 3) + self.infant_scheduler.sponsor_state( + state.get_infant_state_state(), + state_idx, + INFANT_STATE_INITIAL_VOTES, + ) } } diff --git a/src/mutation_utils.rs b/src/mutation_utils.rs index 7bdeeab0a..e76bb4400 100644 --- a/src/mutation_utils.rs +++ b/src/mutation_utils.rs @@ -35,7 +35,7 @@ use libafl::{ use libafl_bolts::{impl_serdeany, prelude::Rand, tuples::tuple_list, Named}; use serde::{Deserialize, Serialize}; -use crate::evm::types::EVMU256; +use crate::{evm::types::EVMU256, r#const::MAX_STACK_POW}; /// Constants in the contracts /// @@ -249,10 +249,13 @@ where ); if let Some(vm_slots) = vm_slots { - let mut mutator = StdScheduledMutator::new((VMStateHintedMutator::new(&vm_slots), mutations)); + let mut mutator = StdScheduledMutator::with_max_stack_pow( + (VMStateHintedMutator::new(&vm_slots), mutations), + MAX_STACK_POW as u64, + ); mutator.mutate(state, input, 0).unwrap() } else { - let mut mutator = StdScheduledMutator::new(mutations); + let mut mutator = StdScheduledMutator::with_max_stack_pow(mutations, MAX_STACK_POW as u64); mutator.mutate(state, input, 0).unwrap() } } @@ -282,10 +285,13 @@ where ); if let Some(vm_slots) = vm_slots { - let mut mutator = StdScheduledMutator::new((VMStateHintedMutator::new(&vm_slots), mutations)); + let mut mutator = StdScheduledMutator::with_max_stack_pow( + (VMStateHintedMutator::new(&vm_slots), mutations), + MAX_STACK_POW as u64, + ); mutator.mutate(state, input, 0).unwrap() } else { - let mut mutator = StdScheduledMutator::new(mutations); + let mut mutator = StdScheduledMutator::with_max_stack_pow(mutations, MAX_STACK_POW as u64); mutator.mutate(state, input, 0).unwrap() } } diff --git a/src/scheduler.rs b/src/scheduler.rs index 6bedf011f..a40ce58b3 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -15,7 +15,10 @@ use rand::random; use serde::{Deserialize, Serialize}; use tracing::{debug, info}; -use crate::state::HasParent; +use crate::{ + r#const::{CORPUS_INITIAL_VOTES, DROP_THRESHOLD, PRUNE_AMT, VISIT_IGNORE_THRESHOLD}, + state::HasParent, +}; /// A trait providing functions necessary for voting mechanisms pub trait HasVote @@ -25,15 +28,6 @@ where fn vote(&self, state: &mut S, idx: usize, amount: usize); } -/// The maximum number of inputs (or VMState) to keep in the corpus before -/// pruning -pub const DROP_THRESHOLD: usize = 500; -/// The number of inputs (or VMState) to prune each time the corpus is pruned -pub const PRUNE_AMT: usize = 250; -/// If inputs (or VMState) has not been visited this many times, it will be -/// ignored during pruning -pub const VISIT_IGNORE_THRESHOLD: usize = 2; - /// A scheduler that drops inputs (or VMState) based on a voting mechanism #[derive(Debug, Clone)] pub struct SortedDroppingScheduler { @@ -172,7 +166,7 @@ where S: HasCorpus + HasRand + HasMetadata + HasParent, { fn report_corpus(&self, state: &mut S, state_idx: usize) { - self.vote(state, state_idx, 3); + self.vote(state, state_idx, CORPUS_INITIAL_VOTES); let data = state.metadata_map_mut().get_mut::().unwrap(); #[cfg(feature = "full_trace")] @@ -215,9 +209,9 @@ where { let parent_idx = state.get_parent_idx(); let data = state.metadata_map_mut().get_mut::().unwrap(); - data.votes_and_visits.insert(idx, (3, 1)); + data.votes_and_visits.insert(idx, (CORPUS_INITIAL_VOTES, 1)); data.visits_total += 1; - data.votes_total += 3; + data.votes_total += CORPUS_INITIAL_VOTES; data.sorted_votes.push(idx); #[cfg(feature = "full_trace")] diff --git a/src/state.rs b/src/state.rs index 88b545eb8..0a6555f21 100644 --- a/src/state.rs +++ b/src/state.rs @@ -29,14 +29,10 @@ use crate::{ evm::{abi::BoxedABI, presets::ExploitTemplate, types::EVMAddress}, generic_vm::{vm_executor::ExecutionResult, vm_state::VMStateT}, input::{ConciseSerde, VMInputT}, + r#const::{ACCOUNT_AMT, CONTRACT_AMT, MAX_INPUT_SIZE}, state_input::StagedVMState, }; -/// Amount of accounts and contracts that can be caller during fuzzing. -/// We will generate random addresses for these accounts and contracts. -pub const ACCOUNT_AMT: u8 = 2; -pub const CONTRACT_AMT: u8 = 2; - /// Trait providing state functions needed by ItyFuzz pub trait HasItyState where @@ -285,7 +281,7 @@ where callers_pool: Vec::new(), addresses_pool: Vec::new(), rand_generator: RomuDuoJrRand::with_seed(seed), - max_size: 20, + max_size: MAX_INPUT_SIZE, hash_to_address: Default::default(), last_report_time: None, phantom: Default::default(),