diff --git a/Cargo.toml b/Cargo.toml index f8d8e7b5b..66e7e806c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,3 +126,4 @@ alloy-primitives = "0.4.1" # logging tracing = "0.1" tracing-subscriber = "0.3" +colored = "2.0" diff --git a/src/evm/abi.rs b/src/evm/abi.rs index a4fb95e65..fb542a3b1 100644 --- a/src/evm/abi.rs +++ b/src/evm/abi.rs @@ -19,7 +19,10 @@ use revm_primitives::U256; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tracing::debug; -use super::types::checksum; +use super::{ + types::checksum, + utils::{colored_address, prettify_value}, +}; /// Definition of ABI types and their encoding, decoding, mutating methods use crate::evm::abi::ABILossyType::{TArray, TDynamic, TEmpty, TUnknown, T256}; use crate::{ @@ -140,6 +143,11 @@ pub trait ABI: CloneABI { fn get_concolic(&self) -> Vec>; /// Get the size of args fn get_size(&self) -> usize; + + /// Convert args to colored string + fn to_colored_string(&self) -> String { + self.to_string() + } } impl Default for Box { @@ -283,6 +291,14 @@ impl BoxedABI { pub fn set_bytes(&mut self, bytes: Vec) -> bool { self.b.set_bytes(bytes[4..].to_vec()) } + + pub fn to_colored_string(&self) -> String { + if self.function == [0; 4] { + self.to_string() + } else { + format!("{}{}", self.get_func_name(), self.b.to_colored_string()) + } + } } /// Randomly sample an args with any type with size `size` @@ -613,7 +629,10 @@ impl ABI for A256 { A256InnerType::Int => I256::from_hex_str(&vec_to_hex(&self.data)) .unwrap_or_default() .to_string(), - A256InnerType::Uint => U256::try_from_be_slice(&self.data).unwrap_or_default().to_string(), + A256InnerType::Uint => { + let value = U256::try_from_be_slice(&self.data).unwrap_or_default(); + prettify_value(value) + } A256InnerType::Bool => { if self.data == [0] { "false".to_string() @@ -632,6 +651,13 @@ impl ABI for A256 { } } + fn to_colored_string(&self) -> String { + match self.inner_type { + A256InnerType::Address => colored_address(&self.to_string()), + _ => self.to_string(), + } + } + fn get_concolic(&self) -> Vec> { let mut bytes = vec![Expr::const_byte(0u8); 32]; let data_len = self.data.len(); @@ -848,7 +874,14 @@ impl ABI for AArray { } fn to_string(&self) -> String { - format!("({})", self.data.iter().map(|x| x.b.deref().to_string()).join(",")) + format!("({})", self.data.iter().map(|x| x.b.deref().to_string()).join(", ")) + } + + fn to_colored_string(&self) -> String { + format!( + "({})", + self.data.iter().map(|x| x.b.deref().to_colored_string()).join(", ") + ) } fn as_any(&mut self) -> &mut dyn Any { diff --git a/src/evm/input.rs b/src/evm/input.rs index e6b67a9cb..f2eb6252c 100644 --- a/src/evm/input.rs +++ b/src/evm/input.rs @@ -1,6 +1,7 @@ use std::{cell::RefCell, fmt::Debug, ops::Deref, rc::Rc}; use bytes::Bytes; +use colored::{ColoredString, Colorize}; use libafl::{ inputs::Input, mutators::MutationResult, @@ -10,6 +11,7 @@ use libafl_bolts::{prelude::Rand, HasLen}; use revm_primitives::Env; use serde::{Deserialize, Deserializer, Serialize}; +use super::utils::{colored_address, colored_sender, prettify_value}; use crate::{ evm::{ abi::{AEmpty, AUnknown, BoxedABI}, @@ -292,83 +294,155 @@ impl ConciseEVMInput { #[allow(unused_variables)] #[cfg(feature = "flashloan_v2")] fn pretty_txn(&self) -> Option { - let liq: u8 = self.liquidation_percent; - #[cfg(not(feature = "debug"))] - let mut output = match self.data { - Some(ref d) => format!( - "{:?} => {:?} {} with {} ETH ({}), liq percent: {}", - self.caller, - self.contract, - d, - self.txn_value.unwrap_or(EVMU256::ZERO), - hex::encode(d.get_bytes()), - liq - ), + match self.data { + Some(ref d) => self.as_abi_call(d.to_colored_string()), None => match self.input_type { - EVMInputTy::ABI | EVMInputTy::ArbitraryCallBoundedAddr => format!( - "{:?} => {:?} with {} ETH, liq percent: {}", - self.caller, - self.contract, - self.txn_value.unwrap_or(EVMU256::ZERO), - liq - ), - EVMInputTy::Borrow => format!( - "{:?} borrow token {:?} with {} ETH, liq percent: {}", - self.caller, - self.contract, - self.txn_value.unwrap_or(EVMU256::ZERO), - liq - ), - EVMInputTy::Liquidate => "".to_string(), + EVMInputTy::ABI | EVMInputTy::ArbitraryCallBoundedAddr => self.as_transfer(), + EVMInputTy::Borrow => self.as_borrow(), + EVMInputTy::Liquidate => None, }, - }; + } #[cfg(feature = "debug")] - let mut output = format!( - "{:?} => {:?} with {:?} ETH, {}", - self.caller, - self.contract, - self.txn_value, - hex::encode(self.direct_data.clone()) - ); + self.as_transfer() + } + + #[cfg(not(feature = "flashloan_v2"))] + fn pretty_txn(&self) -> Option { + match self.data { + Some(ref d) => self.as_abi_call(d.to_colored_string()), + None => self.as_transfer(), + } + } + + #[allow(dead_code)] + #[inline] + fn as_abi_call(&self, call_str: String) -> Option { + let parts: Vec<&str> = call_str.splitn(2, '(').collect(); + if parts.len() < 2 && call_str.len() == 8 { + return self.as_fn_selector_call(); + } + + let mut fn_call = self.colored_fn_name(parts[0]).to_string(); + let value = self.txn_value.unwrap_or_default(); + if value != EVMU256::ZERO { + fn_call.push_str(&self.colored_value()); + } + + if parts.len() < 2 { + fn_call.push_str("()"); + } else { + fn_call.push_str(format!("({}", parts[1]).as_str()); + } - if !output.is_empty() && self.return_data.is_some() { - let return_data = hex::encode(self.return_data.as_ref().unwrap()); - output.push_str(format!(", return data: 0x{}", return_data).as_str()); + Some(format!("{}.{}", colored_address(&self.contract()), fn_call)) + } + + #[inline] + fn as_fn_selector_call(&self) -> Option { + let mut call = format!("{}.{}", colored_address(&self.contract()), self.colored_fn_name("call")); + let value = self.txn_value.unwrap_or_default(); + if value != EVMU256::ZERO { + call.push_str(&self.colored_value()); } - if output.is_empty() { - None + if self.fn_args().is_empty() { + call.push_str(format!("({})", self.fn_selector().purple()).as_str()); } else { - Some(output) + call.push_str( + format!( + "({}({}, {}))", + self.colored_fn_name("abi.encodeWithSelector"), + self.fn_selector().purple(), + self.fn_args() + ) + .as_str(), + ); } + + Some(call) + } + + #[inline] + fn as_transfer(&self) -> Option { + Some(format!( + "{}.{}{}()", + colored_address(&self.contract()), + self.colored_fn_name("call"), + self.colored_value() + )) + } + + #[allow(dead_code)] + #[inline] + fn as_borrow(&self) -> Option { + Some(format!( + "{}.{}{}(0, path:(WETH → {}), address(this), block.timestamp);", + colored_address("Router"), + self.colored_fn_name("swapExactETHForTokens"), + self.colored_value(), + colored_address(&self.contract()) + )) + } + + #[cfg(feature = "flashloan_v2")] + #[inline] + fn append_liquidation(&self, indent: String, call: String) -> String { + if self.liquidation_percent == 0 { + return call; + } + + let liq_call = format!( + "{}.{}(100% Balance, 0, path:({} → WETH), address(this), block.timestamp);", + colored_address("Router"), + self.colored_fn_name("swapExactTokensForETH"), + colored_address(&self.contract()) + ); + + let mut liq = indent.clone(); + liq.push_str(format!("├─ [{}] {}", self.layer + 1, liq_call).as_str()); + + [call, liq].join("\n") } #[cfg(not(feature = "flashloan_v2"))] - fn pretty_txn(&self) -> Option { - let mut output = match self.data { - Some(ref d) => format!( - "{:?} => {:?} {} with {} ETH ({})", - self.caller, - self.contract, - d.to_string(), - self.txn_value.unwrap_or(EVMU256::ZERO), - hex::encode(d.get_bytes()) - ), - None => format!( - "{:?} => {:?} transfer {} ETH", - self.caller, - self.contract, - self.txn_value.unwrap_or(EVMU256::ZERO), - ), - }; + #[inline] + fn append_liquidation(&self, _indent: String, call: String) -> String { + call + } - if self.return_data.is_some() { - let return_data = hex::encode(self.return_data.as_ref().unwrap()); - output.push_str(format!(", return data: 0x{}", return_data).as_str()); + #[inline] + fn colored_value(&self) -> String { + let value = self.txn_value.unwrap_or_default(); + format!("{{value: {}}}", prettify_value(value).truecolor(0x99, 0x00, 0xcc)) + } + + #[inline] + fn colored_fn_name(&self, fn_name: &str) -> ColoredString { + fn_name.truecolor(0xff, 0x7b, 0x72) + } + + #[inline] + fn pretty_return(&self, ret: &[u8]) -> String { + if ret.len() != 32 { + return format!("0x{}", hex::encode(ret)); } - Some(output) + + // Try to encode it as an address + if ret.len() == 32 && ret[..12] == [0; 12] && (ret[12] != 0 || ret[13] != 0) { + let addr = EVMAddress::from_slice(&ret[12..]); + return colored_address(&checksum(&addr)); + } + + // Remove leading zeros + let res = match hex::encode(ret).trim_start_matches('0') { + "" => "00".to_string(), + v if v.len() % 2 != 0 => format!("0{}", v), + v => v.to_string(), + }; + + format!("0x{}", res) } } @@ -384,7 +458,7 @@ impl SolutionTx for ConciseEVMInput { #[cfg(not(feature = "debug"))] fn fn_signature(&self) -> String { match self.data { - Some(ref d) => d.get_func_signature().unwrap_or("".to_string()), + Some(ref d) => d.get_func_signature().unwrap_or_default(), None => "".to_string(), } } @@ -412,7 +486,7 @@ impl SolutionTx for ConciseEVMInput { } fn value(&self) -> String { - self.txn_value.unwrap_or(EVMU256::ZERO).to_string() + self.txn_value.unwrap_or_default().to_string() } #[cfg(feature = "flashloan_v2")] @@ -662,7 +736,7 @@ impl EVMInput { S: State + HasCaller + HasRand + HasMetadata, { let vm_slots = input.get_state().get(&input.get_contract()).cloned(); - let input_by: [u8; 32] = input.get_txn_value().unwrap_or(EVMU256::ZERO).to_be_bytes(); + let input_by: [u8; 32] = input.get_txn_value().unwrap_or_default().to_be_bytes(); let mut input_vec = input_by.to_vec(); let mut wrapper = MutatorInput::new(&mut input_vec); let res = byte_mutator(state_, &mut wrapper, vm_slots); @@ -729,16 +803,68 @@ impl ConciseSerde for ConciseEVMInput { } fn serialize_string(&self) -> String { - let mut s = String::new(); + let mut indent = String::from(" "); + let mut tree_level = 1; for _ in 0..self.layer { - s.push_str("=="); + indent.push_str("│ │ "); + tree_level += 2; } - if self.layer > 0 { - s.push(' '); + + // Stepping with return + if self.step { + let res = format!("{}└─ ← ()", indent.clone()); + return self.append_liquidation(indent, res); + } + + let mut call = indent.clone(); + call.push_str(format!("├─ [{}] ", tree_level).as_str()); + call.push_str(self.pretty_txn().expect("Failed to pretty print txn").as_str()); + + // Control leak + if self.call_leak != u32::MAX { + let mut fallback = indent.clone(); + fallback.push_str( + format!( + "│ ├─ [{}] {}.fallback()", + tree_level + 1, + colored_sender(&self.sender()) + ) + .as_str(), + ); + call.push('\n'); + call.push_str(fallback.as_str()); } - s.push_str(self.pretty_txn().expect("Failed to pretty print txn").as_str()); - s + if self.return_data.is_some() { + let mut ret = indent.clone(); + let v = self.return_data.as_ref().unwrap(); + ret.push_str(format!("│ └─ ← {}", self.pretty_return(v)).as_str()); + call.push('\n'); + call.push_str(ret.as_str()); + } + + self.append_liquidation(indent, call) + } + + fn sender(&self) -> String { + checksum(&self.caller) + } + + fn indent(&self) -> String { + if self.layer == 0 { + return "".to_string(); + } + + let mut indent = String::from(" │ "); + for _ in 1..self.layer { + indent.push_str("│ │ "); + } + + indent + } + + fn is_step(&self) -> bool { + self.step } } diff --git a/src/evm/middlewares/cheatcode.rs b/src/evm/middlewares/cheatcode.rs index d841b89d7..64c333937 100644 --- a/src/evm/middlewares/cheatcode.rs +++ b/src/evm/middlewares/cheatcode.rs @@ -19,7 +19,7 @@ use libafl::{ }; use revm_interpreter::{analysis::to_analysed, opcode, BytecodeLocked, InstructionResult, Interpreter}; use revm_primitives::{Bytecode, Env, SpecId, B160, U256}; -use tracing::{debug, error}; +use tracing::{debug, error, warn}; use super::middleware::{Middleware, MiddlewareType}; use crate::{ @@ -295,7 +295,10 @@ where VmCalls::expectCallMinGas_0(args) => self.expect_call_mingas0(&mut host.expected_calls, args), VmCalls::expectCallMinGas_1(args) => self.expect_call_mingas1(&mut host.expected_calls, args), VmCalls::addr(args) => self.addr(args), - _ => None, + _ => { + warn!("[cheatcode] unknown VmCall: {:?}", vm_call); + None + } }; debug!("[cheatcode] VmCall result: {:?}", res); diff --git a/src/evm/mod.rs b/src/evm/mod.rs index 9a395110d..38ca0fabb 100644 --- a/src/evm/mod.rs +++ b/src/evm/mod.rs @@ -23,6 +23,7 @@ pub mod solution; pub mod srcmap; pub mod types; pub mod uniswap; +pub mod utils; pub mod vm; use std::{ diff --git a/src/evm/utils.rs b/src/evm/utils.rs new file mode 100644 index 000000000..9e645c88f --- /dev/null +++ b/src/evm/utils.rs @@ -0,0 +1,100 @@ +use colored::Colorize; +use revm_primitives::U256; + +use crate::input::ConciseSerde; + +pub fn colored_address(addr: &str) -> String { + let (r, g, b) = get_rgb_by_address(addr); + addr.truecolor(r, g, b).to_string() +} + +// The `[Sender]` and the address should be the same color. +pub fn colored_sender(sender: &str) -> String { + let (r, g, b) = get_rgb_by_address(sender); + format!("[Sender] {}", sender).truecolor(r, g, b).to_string() +} + +pub fn prettify_value(value: U256) -> String { + if value > U256::from(10).pow(U256::from(15)) { + let one_eth = U256::from(10).pow(U256::from(18)); + let integer = value / one_eth; + let decimal: String = (value % one_eth).to_string().chars().take(4).collect(); + + format!("{}.{} Ether", integer, decimal) + } else { + value.to_string() + } +} + +pub fn prettify_concise_inputs(inputs: &[CI]) -> String { + let mut res = String::new(); + let mut sender = String::new(); + + /* + * The rules for replacing the last `├─` with `└─`: + * 1. the indentation has reduced + * 2. the sender has changed in the same layer + * 3. the last input + */ + let mut prev_indent_len = 0; + let mut pending: Option = None; + + for input in inputs { + // Indentation has reduced. + if input.indent().len() < prev_indent_len { + push_last_input(&mut res, pending.take()); + } + + // Sender has changed + if sender != input.sender() && !input.is_step() { + if input.indent().len() == prev_indent_len { + push_last_input(&mut res, pending.take()); + } + + sender = input.sender().clone(); + res.push_str(format!("{}{}\n", input.indent(), colored_sender(&sender)).as_str()); + } + + if let Some(s) = pending.take() { + res.push_str(format!("{}\n", s).as_str()); + } + pending = Some(input.serialize_string()); + prev_indent_len = input.indent().len(); + } + + push_last_input(&mut res, pending); + res +} + +fn get_rgb_by_address(addr: &str) -> (u8, u8, u8) { + let default = vec![0x00, 0x76, 0xff]; + // 8 is the length of `0x` + 3 bytes + let mut rgb = if addr.len() < 8 { + default.clone() + } else { + hex::decode(&addr[addr.len() - 6..]).unwrap_or(default.clone()) + }; + // ignore black and white + if rgb[..] == [0x00, 0x00, 0x00] || rgb[..] == [0xff, 0xff, 0xff] { + rgb = default; + } + + (rgb[0], rgb[1], rgb[2]) +} + +fn push_last_input(res: &mut String, input: Option) { + if input.is_none() { + return; + } + let s = input.unwrap(); + if s.contains("└─") { + res.push_str(format!("{}\n", s).as_str()); + return; + } + + let mut parts: Vec<&str> = s.split("├─").collect(); + if let Some(last) = parts.pop() { + let input = format!("{}└─{}\n", parts.join("├─"), last); + res.push_str(input.as_str()); + } +} diff --git a/src/fuzzer.rs b/src/fuzzer.rs index 874f9d2ae..80b01530e 100644 --- a/src/fuzzer.rs +++ b/src/fuzzer.rs @@ -42,19 +42,13 @@ use serde::{de::DeserializeOwned, Serialize}; use tracing::info; use crate::{ - evm::host::JMP_MAP, + evm::{host::JMP_MAP, solution, utils::prettify_concise_inputs}, generic_vm::{vm_executor::MAP_SIZE, vm_state::VMStateT}, - input::{ConciseSerde, SolutionTx}, + input::{ConciseSerde, SolutionTx, VMInputT}, minimizer::SequentialMinimizer, oracle::BugMetadata, scheduler::HasReportCorpus, - state::HasExecutionResult, -}; -/// Implements fuzzing logic for ItyFuzz -use crate::{ - evm::solution, - input::VMInputT, - state::{HasCurrentInputIdx, HasInfantStateState, HasItyState, InfantStateState}, + state::{HasCurrentInputIdx, HasExecutionResult, HasInfantStateState, HasItyState, InfantStateState}, }; pub static mut RUN_FOREVER: bool = false; @@ -299,7 +293,7 @@ macro_rules! dump_file { let txn_text_replayable = tx_trace.to_file_str($state); let data = format!( - "Reverted? {} \n Txn: {}", + "Reverted? {} \n Txn:\n{}", $state.get_execution_result().reverted, txn_text ); @@ -553,7 +547,7 @@ where &mut self.objective, corpus_idx.into(), ); - let txn_text = minimized.iter().map(|ci| ci.serialize_string()).join("\n"); + let txn_text = prettify_concise_inputs(&minimized); let txn_json = minimized .iter() .map(|ci| String::from_utf8(ci.serialize_concise()).expect("utf-8 failed")) diff --git a/src/input.rs b/src/input.rs index 3e9fdf8fb..4a89bacc1 100644 --- a/src/input.rs +++ b/src/input.rs @@ -98,6 +98,17 @@ pub trait ConciseSerde { fn serialize_concise(&self) -> Vec; fn deserialize_concise(data: &[u8]) -> Self; fn serialize_string(&self) -> String; + + fn sender(&self) -> String { + String::from("") + } + // Get the indentation of the input + fn indent(&self) -> String { + String::from("") + } + fn is_step(&self) -> bool { + false + } } /// SolutionTx for generating a test file. diff --git a/src/tracer.rs b/src/tracer.rs index 0c4e33237..ad75dac74 100644 --- a/src/tracer.rs +++ b/src/tracer.rs @@ -6,7 +6,12 @@ use std::fmt::Debug; use libafl::{corpus::Corpus, prelude::HasCorpus}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use crate::{generic_vm::vm_state::VMStateT, input::ConciseSerde, state::HasInfantStateState}; +use crate::{ + evm::utils::prettify_concise_inputs, + generic_vm::vm_state::VMStateT, + input::ConciseSerde, + state::HasInfantStateState, +}; /// Represent a trace of transactions with starting VMState ID (from_idx). /// If VMState ID is None, it means that the trace is from the initial state. @@ -50,31 +55,12 @@ where Addr: Debug + Serialize + DeserializeOwned + Clone, Loc: Debug + Serialize + DeserializeOwned + Clone, { - // If from_idx is None, it means that the trace is from the initial state - if self.from_idx.is_none() { - return String::from("Begin\n"); - } - let current_idx = self.from_idx.unwrap(); - let corpus_item = state.get_infant_state_state().corpus().get(current_idx.into()); - // This happens when full_trace feature is not enabled, the corpus item may be - // discarded - if corpus_item.is_err() { - return String::from("Corpus returning error\n"); - } - let testcase = corpus_item.unwrap().clone().into_inner(); - let testcase_input = testcase.input(); - if testcase_input.is_none() { + let inputs = self.get_concise_inputs(state); + if inputs.is_empty() { return String::from("[REDACTED]\n"); } - // Try to reconstruct transactions leading to the current VMState recursively - let mut s = Self::to_string(&testcase_input.as_ref().unwrap().trace.clone(), state); - - // Dump the current transaction - for concise_input in &self.transactions { - s.push_str(format!("{}\n", concise_input.serialize_string()).as_str()); - } - s + prettify_concise_inputs(&inputs) } /// Serialize the trace so that it can be replayed by using --replay-file