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 as _};
7use std::path::Path;
8
9use anyhow::Context as _;
10use idalib::IDAError;
11use idalib::decompiler::HexRaysErrorCode;
12use idalib::func::{Function, FunctionFlags};
13use idalib::idb::IDB;
14use thiserror::Error;
15
16/// Reserved characters in filenames
17#[cfg(unix)]
18const RESERVED_CHARS: &[char] = &['.', '/'];
19#[cfg(windows)]
20const RESERVED_CHARS: &[char] = &['.', '/', '<', '>', ':', '"', '\\', '|', '?', '*'];
21
22/// Maximum length of filenames
23const MAX_FILENAME_LEN: usize = 64;
24
25/// Haruspex error type
26#[derive(Error, Debug)]
27#[non_exhaustive]
28pub enum HaruspexError {
29    /// Failure in decompiling the function
30    #[error(transparent)]
31    DecompileFailed(#[from] IDAError),
32    /// Failure in writing to the output file
33    #[error(transparent)]
34    FileWriteFailed(#[from] std::io::Error),
35}
36
37/// Extract pseudocode of functions in the binary file at `filepath` and save it in `filepath.dec`.
38///
39/// ## Errors
40///
41/// Returns how many functions were decompiled, or an error in case something goes wrong.
42pub fn run(filepath: &Path) -> anyhow::Result<usize> {
43    // Open the target binary and run auto-analysis
44    println!("[*] Analyzing binary file `{}`", filepath.display());
45    let idb = IDB::open(filepath)
46        .with_context(|| format!("Failed to analyze binary file `{}`", filepath.display()))?;
47    println!("[+] Successfully analyzed binary file");
48    println!();
49
50    // Print binary file information
51    println!("[-] Processor: {}", idb.processor().long_name());
52    println!("[-] Compiler: {:?}", idb.meta().cc_id());
53    println!("[-] File type: {:?}", idb.meta().filetype());
54    println!();
55
56    // Ensure Hex-Rays decompiler is available
57    anyhow::ensure!(idb.decompiler_available(), "Decompiler is not available");
58
59    // Create a new output directory, returning an error if it already exists, and it's not empty
60    let dirpath = filepath.with_extension("dec");
61    println!("[*] Preparing output directory `{}`", dirpath.display());
62    if dirpath.exists() {
63        fs::remove_dir(&dirpath).map_err(|_| anyhow::anyhow!("Output directory already exists"))?;
64    }
65    fs::create_dir_all(&dirpath)
66        .with_context(|| format!("Failed to create directory `{}`", dirpath.display()))?;
67    println!("[+] Output directory is ready");
68
69    let mut decompiled_count = 0;
70
71    // Extract pseudocode of functions
72    println!();
73    println!("[*] Extracting pseudocode of functions...");
74    println!();
75    for (_id, f) in idb.functions() {
76        // Skip the function if it has the `thunk` attribute
77        if f.flags().contains(FunctionFlags::THUNK) {
78            continue;
79        }
80
81        // Decompile function and write pseudocode to the output file
82        let func_name = f.name().unwrap_or_else(|| "[no name]".into());
83        let output_file = format!(
84            "{}@{:X}",
85            func_name
86                .replace(RESERVED_CHARS, "_")
87                .chars()
88                .take(MAX_FILENAME_LEN)
89                .collect::<String>(),
90            f.start_address()
91        );
92        let output_path = dirpath.join(output_file).with_extension("c");
93
94        match decompile_to_file(&idb, &f, &output_path) {
95            // Print the output path in case of successful function decompilation
96            Ok(()) => {
97                println!("{func_name} -> `{}`", output_path.display());
98                decompiled_count += 1;
99            }
100
101            // Return an error if Hex-Rays decompiler license is not available
102            Err(HaruspexError::DecompileFailed(IDAError::HexRays(e)))
103                if e.code() == HexRaysErrorCode::License =>
104            {
105                return Err(e.into());
106            }
107
108            // Ignore other IDA errors
109            Err(HaruspexError::DecompileFailed(_)) => (),
110
111            // Return any other error
112            Err(e) => return Err(e.into()),
113        }
114    }
115
116    // Remove the output directory and return an error in case no functions were decompiled
117    if decompiled_count == 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 {decompiled_count} functions into `{}`",
126        dirpath.display()
127    );
128    println!("[+] Done processing binary file `{}`", filepath.display());
129    Ok(decompiled_count)
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}