fugue_idapro/
lib.rs

1//! Fugue importer glue for IDA Pro.
2//!
3//! Example use:
4//! ```rust,ignore
5//! use fugue::db::DatabaseImporter;
6//! use fugue::ir::LanguageDB;
7//!
8//! let ldb = LanguageDB::from_directory_with("path/to/processors", true)?;
9//! let mut dbi = DatabaseImporter::new("/bin/ls");
10//!
11//! dbi.register_backend(IDA::new()?);
12//!
13//! let db = dbi.import(&ldb)?;
14//! ```
15
16use std::env;
17use std::path::{Path, PathBuf};
18use std::process;
19
20use fugue_db::Error as ExportError;
21use fugue_db::backend::{Backend, Imported};
22
23use tempfile::tempdir;
24use which::{which, which_in};
25use url::Url;
26
27use thiserror::Error;
28
29#[derive(Debug, Error)]
30pub enum Error {
31    #[error("IDA Pro is not available as a backend")]
32    NotAvailable,
33    #[error("invalid path to IDA Pro: {0}")]
34    InvalidPath(which::Error),
35    #[error("error launching IDA Pro: {0}")]
36    Launch(std::io::Error),
37    #[error("IDA Pro reported I/O error")]
38    InputOutput,
39    #[error("IDA Pro reported error on import")]
40    Import,
41    #[error("IDA Pro reported unsupported file type")]
42    Unsupported,
43    #[error("IDA Pro reported error when attempting to rebase")]
44    Rebase,
45    #[error("IDA Pro encountered a generic failure")]
46    Failure,
47    #[error("could not create temporary directory to store exported database: {0}")]
48    TempDirectory(#[source] std::io::Error),
49    #[error("`{0}` is not a supported URL scheme")]
50    UnsupportedScheme(String),
51}
52
53impl From<Error> for ExportError {
54    fn from(e: Error) -> Self {
55        ExportError::importer_error("ida-pro", e)
56    }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub struct IDA {
61    ida_path: Option<PathBuf>,
62    fdb_path: Option<PathBuf>,
63    overwrite: bool,
64    wine: bool,
65}
66
67impl Default for IDA {
68    fn default() -> Self {
69        Self {
70            ida_path: None,
71            fdb_path: None,
72            overwrite: false,
73            wine: false,
74        }
75    }
76}
77
78impl IDA {
79    fn find_ida<F: Fn(&str) -> Result<PathBuf, Error>>(f: F) -> Result<(PathBuf, bool), Error> {
80        if let Ok(local) = f("idat64").or_else(|_| f("ida64")) {
81            Ok((local, false))
82        } else {
83            f("idat64.exe").or_else(|_| f("ida64.exe")).map(|path| (path, true))
84        }
85    }
86
87    pub fn new() -> Result<Self, Error> {
88        if let Ok(root_dir) = env::var("IDA_INSTALL_DIR") {
89            if let Ok(v) = Self::from_path(root_dir) {
90                return Ok(v);
91            }
92        }
93
94        if let Ok((ida_path, wine)) = Self::find_ida(|p| which(p).map_err(Error::InvalidPath)) {
95            Ok(Self { ida_path: Some(ida_path), wine, ..Default::default() })
96        } else {
97            Err(Error::NotAvailable)
98        }
99    }
100
101    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
102        let root_dir = path.as_ref();
103        let (ida_path, wine) =
104            Self::find_ida(|p| which_in(p, Some(root_dir), ".").map_err(Error::InvalidPath))?;
105
106        Ok(Self { ida_path: Some(ida_path), wine, ..Default::default() })
107    }
108
109    pub fn export_path<P: AsRef<Path>>(mut self, path: P, overwrite: bool) -> Self {
110        self.fdb_path = Some(path.as_ref().to_owned());
111        self.overwrite = overwrite;
112        self
113    }
114}
115
116impl Backend for IDA {
117    type Error = Error;
118
119    fn name(&self) -> &'static str {
120        "fugue-idapro"
121    }
122
123    fn is_available(&self) -> bool {
124        self.ida_path.is_some()
125    }
126
127    fn is_preferred_for(&self, path: &Url) -> Option<bool> {
128        if path.scheme() != "file" {
129            return None
130        }
131
132        if let Ok(path) = path.to_file_path() {
133            path.extension()
134                .map(|ext| Some(ext == "i64" || ext == "idb"))
135                .unwrap_or(Some(false))
136        } else {
137            None
138        }
139    }
140
141    fn import(&self, program: &Url) -> Result<Imported, Self::Error> {
142        if program.scheme() != "file" {
143            return Err(Error::UnsupportedScheme(program.scheme().to_owned()))
144        }
145
146        let program = program.to_file_path()
147            .map_err(|_| Error::UnsupportedScheme(program.scheme().to_owned()))?;
148
149        let ida_path = self.ida_path.as_ref().ok_or_else(|| Error::NotAvailable)?;
150
151        let load_existing = program.exists()
152            && program
153                .extension()
154                .map(|e| e == "i64" || e == "idb")
155                .unwrap_or(false);
156
157        let force_32bit =
158            load_existing && program.extension().map(|e| e == "idb").unwrap_or(false);
159
160        let mut cmd = if !self.wine {
161            if force_32bit {
162                process::Command::new(format!(
163                    "{}",
164                    ida_path.with_file_name("idat").display()
165                ))
166            } else {
167                process::Command::new(format!("{}", ida_path.display()))
168            }
169        } else {
170            let mut process = process::Command::new("wine");
171            if force_32bit {
172                process.arg(format!(
173                    "{}",
174                    ida_path.with_file_name("idat.exe").display()
175                ));
176            } else {
177                process.arg(format!("{}", ida_path.display()));
178            }
179            process
180        };
181
182        cmd.arg("-A");
183
184        let mut tmp = tempdir()
185            .map_err(Error::TempDirectory)?
186            .into_path();
187
188        let output = if let Some(ref fdb_path) = self.fdb_path {
189            fdb_path.to_owned()
190        } else {
191            tmp.join("fugue-temp-export.fdb")
192        };
193
194        let opts = vec![
195            format!("-OFugueOutput:{}", output.display()),
196            format!("-OFugueForceOverwrite:{}", self.overwrite),
197        ];
198
199        /*
200        if let Some(rebase) = rebase {
201            if rebase_relative > 0 {
202                opts.push(format!("-OFugueRebase:+{:#x}", rebase));
203            } else if rebase_relative < 0 {
204                opts.push(format!("-OFugueRebase:-{:#x}", rebase));
205            } else {
206                opts.push(format!("-OFugueRebase:{:#x}", rebase));
207            }
208        }
209        */
210
211        if load_existing {
212            cmd.args(&opts);
213            cmd.arg(&format!("{}", program.display()));
214        } else {
215            tmp.push("fugue-import-tmp.ida");
216            cmd.arg(&format!("-o{}", tmp.display()));
217            cmd.args(&opts);
218            cmd.arg(&format!("{}", program.display()));
219        }
220
221        match cmd
222            .output()
223            .map_err(Error::Launch)
224            .map(|output| output.status.code())?
225        {
226            Some(100) => Ok(Imported::File(output)),
227            Some(101) => Err(Error::InputOutput)?,
228            Some(102) => Err(Error::Import)?,
229            Some(103) => Err(Error::Unsupported)?,
230            Some(104) => Err(Error::Rebase)?,
231            _ => Err(Error::Failure)?,
232        }
233    }
234}