haruspex/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/0xdea/haruspex/master/.img/logo.png")]
3
4use std::fs;
5use std::fs::File;
6use std::io::{BufWriter, Write};
7use std::path::Path;
8use std::sync::atomic::{AtomicUsize, Ordering};
9
10use anyhow::Context;
11use idalib::IDAError;
12use idalib::decompiler::HexRaysErrorCode;
13use idalib::func::{Function, FunctionFlags};
14use idalib::idb::IDB;
15use thiserror::Error;
16
17/// Reserved characters in filenames
18#[cfg(unix)]
19const RESERVED_CHARS: &[char] = &['.', '/'];
20#[cfg(windows)]
21const RESERVED_CHARS: &[char] = &['.', '/', '<', '>', ':', '"', '\\', '|', '?', '*'];
22
23/// Maximum length of filenames
24const MAX_FILENAME_LEN: usize = 64;
25
26/// Number of decompiled functions
27static COUNTER: AtomicUsize = AtomicUsize::new(0);
28
29/// Haruspex error type
30#[derive(Error, Debug)]
31pub enum HaruspexError {
32    /// Failure in decompiling the function
33    #[error(transparent)]
34    DecompileFailed(#[from] IDAError),
35    /// Failure in writing to the output file
36    #[error(transparent)]
37    FileWriteFailed(#[from] std::io::Error),
38}
39
40/// Extract pseudocode of functions in the binary file at `filepath` and save it in `filepath.dec`.
41///
42/// ## Errors
43///
44/// Returns how many functions were decompiled, or a generic error in case something goes wrong.
45pub fn run(filepath: &Path) -> anyhow::Result<usize> {
46    // Open the target binary and run auto-analysis
47    println!("[*] Analyzing binary file `{}`", filepath.display());
48    let idb = IDB::open(filepath)
49        .with_context(|| format!("Failed to analyze binary file `{}`", filepath.display()))?;
50    println!("[+] Successfully analyzed binary file");
51    println!();
52
53    // Print binary file information
54    println!("[-] Processor: {}", idb.processor().long_name());
55    println!("[-] Compiler: {:?}", idb.meta().cc_id());
56    println!("[-] File type: {:?}", idb.meta().filetype());
57    println!();
58
59    // Ensure Hex-Rays decompiler is available
60    anyhow::ensure!(idb.decompiler_available(), "Decompiler is not available");
61
62    // Create a new output directory, returning an error if it already exists, and it's not empty
63    let dirpath = filepath.with_extension("dec");
64    println!("[*] Preparing output directory `{}`", dirpath.display());
65    if dirpath.exists() {
66        fs::remove_dir(&dirpath).map_err(|_| anyhow::anyhow!("Output directory already exists"))?;
67    }
68    fs::create_dir_all(&dirpath)
69        .with_context(|| format!("Failed to create directory `{}`", dirpath.display()))?;
70    println!("[+] Output directory is ready");
71
72    // Extract pseudocode of functions
73    println!();
74    println!("[*] Extracting pseudocode of functions...");
75    println!();
76    for (_id, f) in idb.functions() {
77        // Skip the function if it has the `thunk` attribute
78        if f.flags().contains(FunctionFlags::THUNK) {
79            continue;
80        }
81
82        // Decompile function and write pseudocode to the output file
83        let func_name = f.name().unwrap_or_else(|| "<no name>".into());
84        let output_file = format!(
85            "{}@{:X}",
86            func_name
87                .replace(RESERVED_CHARS, "_")
88                .chars()
89                .take(MAX_FILENAME_LEN)
90                .collect::<String>(),
91            f.start_address()
92        );
93        let output_path = dirpath.join(output_file).with_extension("c");
94
95        match decompile_to_file(&idb, &f, &output_path) {
96            // Print the output path in case of successful function decompilation
97            Ok(()) => println!("{func_name} -> `{}`", output_path.display()),
98
99            // Return an error if Hex-Rays decompiler license is not available
100            Err(HaruspexError::DecompileFailed(IDAError::HexRays(e)))
101                if e.code() == HexRaysErrorCode::License =>
102            {
103                return Err(e.into());
104            }
105
106            // Ignore other IDA errors
107            Err(HaruspexError::DecompileFailed(_)) => continue,
108
109            // Return any other error
110            Err(e) => return Err(e.into()),
111        }
112
113        COUNTER.fetch_add(1, Ordering::Relaxed);
114    }
115
116    // Remove the output directory and return an error in case no functions were decompiled
117    if COUNTER.load(Ordering::Relaxed) == 0 {
118        fs::remove_dir(&dirpath)
119            .with_context(|| format!("Failed to remove directory `{}`", dirpath.display()))?;
120        anyhow::bail!("No functions were decompiled, check your input file");
121    }
122
123    println!();
124    println!(
125        "[+] Decompiled {COUNTER:?} functions into `{}`",
126        dirpath.display()
127    );
128    println!("[+] Done processing binary file `{}`", filepath.display());
129    Ok(COUNTER.load(Ordering::Relaxed))
130}
131
132/// Decompile [`Function`] `func` in [`IDB`] `idb` and save its pseudocode to the output file at `filepath`.
133///
134/// ## Errors
135///
136/// Returns the appropriate [`HaruspexError`] in case something goes wrong.
137///
138/// ## Examples
139///
140/// Basic usage:
141/// ```
142/// # fn main() -> anyhow::Result<()> {
143/// # let base_dir = std::path::Path::new("./tests/data");
144/// let input_file = base_dir.join("ls");
145/// let output_file = base_dir.join("ls-main.c");
146///
147/// let idb = idalib::idb::IDB::open(&input_file)?;
148/// let (_, func) = idb
149///     .functions()
150///     .find(|(_, f)| f.name().unwrap() == "main")
151///     .unwrap();
152///
153/// haruspex::decompile_to_file(&idb, &func, &output_file)?;
154/// # std::fs::remove_file(output_file)?;
155/// # Ok(())
156/// # }
157/// ```
158///
159pub fn decompile_to_file(
160    idb: &IDB,
161    func: &Function,
162    filepath: impl AsRef<Path>,
163) -> Result<(), HaruspexError> {
164    // Decompile function
165    let decomp = idb.decompile(func)?;
166    let source = decomp.pseudocode();
167
168    // Write pseudocode to output file
169    // Note: for easier testing, we could use a generic function together with `std::io::Cursor`
170    let mut writer = BufWriter::new(File::create(&filepath)?);
171    writer.write_all(source.as_bytes())?;
172    writer.flush()?;
173
174    Ok(())
175}