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