use crate::error::{ProFuzzError, ProFuzzResult};
use crate::output::Output;
use crate::traits::{Corpus, HealthCheck, Mutable, ResetHandler, Transport};
use crate::triage::utils::{hamming_distance, mark_differences};
use crate::types::Crash;
use sha1::{Digest, Sha1};
use std::time::Duration;
use tokio::time::sleep;
pub struct DynamicTriage<T, H, R>
where
T: Transport,
H: HealthCheck,
R: ResetHandler,
{
transport: T,
healthcheck: H,
resethandler: R,
}
impl<T, H, R> DynamicTriage<T, H, R>
where
T: Transport,
H: HealthCheck,
R: ResetHandler,
{
pub fn new(transport: T, healthcheck: H, resethandler: R) -> Self {
Self {
transport,
healthcheck,
resethandler,
}
}
pub async fn triage_from_output_dir<M: Mutable + Corpus + Clone>(
&mut self,
output: &Output,
) -> ProFuzzResult<()> {
let crash_file = output.get_crash_file();
let content = std::fs::read_to_string(crash_file)?;
let crashes: Vec<Crash> = serde_json::from_str(&content)?;
self.triage::<M>(crashes, output).await?;
Ok(())
}
pub(crate) async fn triage<M>(
&mut self,
crashes: Vec<Crash>,
output: &Output,
) -> ProFuzzResult<()>
where
M: Mutable + Corpus + Clone,
{
let len = crashes.len();
for (i, crash) in crashes.into_iter().enumerate() {
println!("Triage {i}/{len} [y/n]? ");
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
let input = input.trim();
if input == "y" {
tracing::info!("Starting with detecting the cause.");
if !self.find_the_crash_cause::<M>(crash, output).await? {
tracing::error!("Could not detect the crash :/");
}
}
}
Ok(())
}
async fn find_the_crash_cause<M>(
&mut self,
mut crash: Crash,
output: &Output,
) -> ProFuzzResult<bool>
where
M: Mutable + Corpus + Clone,
{
tracing::info!("Starting with a healthcheck.");
if self
.send_and_detect_crash::<M>(&crash.buffer, output, &crash.last_send_buffers)
.await?
{
return Ok(true);
}
tracing::info!("Running full test of all buffers.");
let mut similar_corpuses = vec![];
let total_len = crash.last_send_buffers.len();
crash.last_send_buffers.reverse();
for (i, crash_buffer) in crash.last_send_buffers.into_iter().enumerate() {
print!("\r Testing {i}/{total_len}");
if self
.send_and_detect_crash::<M>(&crash_buffer, output, &similar_corpuses)
.await?
{
tracing::info!("Identified the corpus that crashed the target.");
return Ok(true);
}
similar_corpuses.push(crash_buffer);
}
Ok(false)
}
async fn send_and_detect_crash<M>(
&mut self,
buffer: &[u8],
output: &Output,
similar_corpuses: &[Vec<u8>],
) -> ProFuzzResult<bool>
where
M: Mutable + Corpus + Clone,
{
loop {
let is_ok = self.healthcheck.is_ok().await;
if is_ok.is_ok_and(|x| x) {
break;
}
sleep(Duration::from_millis(1000)).await;
}
let _ = self.transport.connect().await;
if let Err(err) = self.transport.write(buffer).await {
tracing::error!("COULD NOT write: {err}");
}
let mut tmp = [0; 2000];
let _ = self.transport.read(&mut tmp).await;
let _ = self.transport.close().await;
let mut crashed = false;
loop {
let is_ok = self.healthcheck.is_ok().await;
if !crashed && (is_ok.is_err() || is_ok.as_ref().is_ok_and(|x| !x)) {
crashed = true;
tracing::info!("TARGET crashed");
let Some(base) = M::from_bytes(buffer.to_vec()) else {
return Err(ProFuzzError::Custom {
err_msg: "Could not create a structured representation of the crash"
.to_string(),
});
};
let mut min_hamming = usize::MAX;
let mut most_equal = &similar_corpuses[0];
for non_crash in similar_corpuses {
if let Some(hamming) = hamming_distance(non_crash, buffer) {
if hamming == 0 {
continue;
}
if hamming < min_hamming {
most_equal = non_crash;
min_hamming = hamming;
}
}
}
if let Some(most_equal) = M::from_bytes(most_equal.to_owned()) {
let marked = mark_differences(&base.show(), &most_equal.show());
println!("{marked}");
} else {
println!("{}", base.show());
}
{
let mut hash = Sha1::new();
hash.update(buffer);
let hash = hash.finalize();
let file = output.get_crash_folder().join(hex::encode(hash));
if std::fs::write(&file, buffer).is_err() {
tracing::error!("Could not store crash in {}", file.display());
} else {
tracing::info!("Crash stored in {}", file.display());
}
}
self.resethandler.reset().await?;
}
if is_ok.is_ok_and(|x| x) {
break;
}
sleep(Duration::from_millis(1000)).await;
}
Ok(crashed)
}
}