#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/0xdea/haruspex/master/.img/logo.png")]
use std::fs;
use std::fs::File;
use std::io::{BufWriter, Write as _};
use std::path::Path;
use anyhow::Context as _;
use idalib::IDAError;
use idalib::decompiler::HexRaysErrorCode;
use idalib::func::{Function, FunctionFlags};
use idalib::idb::IDB;
use thiserror::Error;
#[cfg(unix)]
const RESERVED_CHARS: &[char] = &['.', '/'];
#[cfg(windows)]
const RESERVED_CHARS: &[char] = &['.', '/', '<', '>', ':', '"', '\\', '|', '?', '*'];
const MAX_FILENAME_LEN: usize = 64;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum HaruspexError {
#[error(transparent)]
DecompileFailed(#[from] IDAError),
#[error(transparent)]
FileWriteFailed(#[from] std::io::Error),
}
pub fn run(filepath: &Path) -> anyhow::Result<usize> {
println!("[*] Analyzing binary file `{}`", filepath.display());
let idb = IDB::open(filepath)
.with_context(|| format!("Failed to analyze binary file `{}`", filepath.display()))?;
println!("[+] Successfully analyzed binary file");
println!();
println!("[-] Processor: {}", idb.processor().long_name());
println!("[-] Compiler: {:?}", idb.meta().cc_id());
println!("[-] File type: {:?}", idb.meta().filetype());
println!();
anyhow::ensure!(idb.decompiler_available(), "Decompiler is not available");
let dirpath = filepath.with_extension("dec");
println!("[*] Preparing output directory `{}`", dirpath.display());
if dirpath.exists() {
fs::remove_dir(&dirpath).map_err(|_| anyhow::anyhow!("Output directory already exists"))?;
}
fs::create_dir_all(&dirpath)
.with_context(|| format!("Failed to create directory `{}`", dirpath.display()))?;
println!("[+] Output directory is ready");
let mut decompiled_count = 0;
println!();
println!("[*] Extracting pseudocode of functions...");
println!();
for (_id, f) in idb.functions() {
if f.flags().contains(FunctionFlags::THUNK) {
continue;
}
let func_name = f.name().unwrap_or_else(|| "[no name]".into());
let output_file = format!(
"{}@{:X}",
func_name
.replace(RESERVED_CHARS, "_")
.chars()
.take(MAX_FILENAME_LEN)
.collect::<String>(),
f.start_address()
);
let output_path = dirpath.join(output_file).with_extension("c");
match decompile_to_file(&idb, &f, &output_path) {
Ok(()) => {
println!("{func_name} -> `{}`", output_path.display());
decompiled_count += 1;
}
Err(HaruspexError::DecompileFailed(IDAError::HexRays(e)))
if e.code() == HexRaysErrorCode::License =>
{
return Err(e.into());
}
Err(HaruspexError::DecompileFailed(_)) => (),
Err(e) => return Err(e.into()),
}
}
if decompiled_count == 0 {
fs::remove_dir(&dirpath)
.with_context(|| format!("Failed to remove directory `{}`", dirpath.display()))?;
anyhow::bail!("No functions were decompiled, check your input file");
}
println!();
println!(
"[+] Decompiled {decompiled_count} functions into `{}`",
dirpath.display()
);
println!("[+] Done processing binary file `{}`", filepath.display());
Ok(decompiled_count)
}
pub fn decompile_to_file(
idb: &IDB,
func: &Function,
filepath: impl AsRef<Path>,
) -> Result<(), HaruspexError> {
let decomp = idb.decompile(func)?;
let source = decomp.pseudocode();
let mut writer = BufWriter::new(File::create(&filepath)?);
writer.write_all(source.as_bytes())?;
writer.flush()?;
Ok(())
}