fugue_ghidra/
lib.rs

1use std::env;
2use std::ffi::OsString;
3use std::fs::File;
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6use std::process;
7
8use fugue_db::backend::{Backend, Imported};
9use fugue_db::Error as ExportError;
10
11use tempfile::tempdir;
12use thiserror::Error;
13use url::Url;
14use which::{which, which_in};
15
16#[derive(Debug, Error)]
17pub enum Error {
18    #[error("Ghidra is not available as a backend")]
19    NotAvailable,
20    #[error("invalid path to Ghidra: {0}")]
21    InvalidPath(#[from] which::Error),
22    #[error("invalid input path: {0}")]
23    InvalidInputPath(PathBuf),
24    #[error("error launching Ghidra exporter: {0}")]
25    Launch(#[from] std::io::Error),
26    #[error("Ghidra reported I/O error")]
27    InputOutput,
28    #[error("Ghidra reported error on import")]
29    Import,
30    #[error("Ghidra reported unsupported file type")]
31    Unsupported,
32    #[error("Ghidra encountered a generic failure")]
33    Failure,
34    #[error("could not fix ownership on project file: {0}")]
35    Ownership(#[source] std::io::Error),
36    #[error("could not create temporary directory for export: {0}")]
37    TempDirectory(#[source] std::io::Error),
38    #[error("`{0}` is not a supported URL scheme")]
39    UnsupportedScheme(String),
40}
41
42impl From<Error> for ExportError {
43    fn from(e: Error) -> Self {
44        Self::importer_error("ghidra", e)
45    }
46}
47
48#[derive(Debug)]
49pub struct GhidraProject {
50    project_root: PathBuf,
51    project_name: OsString,
52    project_path: PathBuf,
53    project_file: PathBuf,
54    previous_prp: Vec<u8>,
55}
56
57macro_rules! PRP_TEMPLATE {
58    () => {
59        r#"<?xml version="1.0" encoding="UTF-8"?>
60<FILE_INFO>
61    <BASIC_INFO>
62        <STATE NAME="OWNER" TYPE="string" VALUE="{}" />
63    </BASIC_INFO>
64</FILE_INFO>"#
65    };
66}
67
68impl GhidraProject {
69    fn toggle_ownership(&mut self, restore: bool) -> Result<(), Error> {
70        let prp_path = self.project_path.join("project.prp");
71        if !prp_path.exists() {
72            return Ok(());
73        }
74
75        let old_prp = {
76            let mut prp = File::open(&prp_path).map_err(Error::Ownership)?;
77            let mut buf = Vec::new();
78            prp.read_to_end(&mut buf).map_err(Error::Ownership)?;
79            buf
80        };
81
82        let mut prp = File::create(&prp_path).map_err(Error::Ownership)?;
83
84        if restore {
85            prp.write_all(&self.previous_prp)
86        } else {
87            self.previous_prp = old_prp;
88            write!(prp, PRP_TEMPLATE!(), whoami::username())
89        }
90        .map_err(Error::Ownership)?;
91
92        Ok(())
93    }
94
95    pub fn modify_ownership(&mut self) -> Result<(), Error> {
96        self.toggle_ownership(false)
97    }
98
99    pub fn restore_ownership(&mut self) -> Result<(), Error> {
100        self.toggle_ownership(true)
101    }
102
103    pub fn resolve<P: AsRef<Path>>(path: P) -> Option<Self> {
104        let path = path.as_ref();
105
106        // there needs to be a project file
107        if path.exists() {
108            return None;
109        }
110
111        let mut parent = path.parent();
112        while let Some(project_path) = parent {
113            let rep_path = project_path.with_extension("rep");
114            // first path that exists
115            if rep_path.exists() {
116                if project_path.exists() && rep_path != project_path {
117                    if !project_path
118                        .extension()
119                        .map(|ext| ext == "gpr")
120                        .unwrap_or(false)
121                    {
122                        // this can't be a real project
123                        return None;
124                    }
125                }
126
127                if project_path.extension().is_some() {
128                    if !project_path
129                        .extension()
130                        .map(|ext| ext == "gpr" || ext == "rep")
131                        .unwrap_or(false)
132                    {
133                        // this can't be a real project
134                        return None;
135                    }
136                }
137
138                let prp_path = rep_path.join("project.prp");
139                if !prp_path.exists() {
140                    // this can't be a real project if prp does not exist
141                    return None;
142                }
143
144                // Sanity checks passed
145                return Some(GhidraProject {
146                    project_root: rep_path.parent()?.to_owned(),
147                    project_name: rep_path.file_stem()?.to_owned(),
148                    project_path: rep_path,
149                    project_file: path.strip_prefix(project_path).ok()?.to_owned(),
150                    previous_prp: Vec::default(),
151                });
152            }
153
154            if project_path.exists() {
155                return None;
156            }
157            parent = project_path.parent();
158        }
159        None
160    }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
164pub struct Ghidra {
165    path: Option<PathBuf>,
166    fdb_path: Option<PathBuf>,
167    overwrite: bool,
168    fix_ownership: bool,
169}
170
171impl Default for Ghidra {
172    fn default() -> Self {
173        Self {
174            path: None,
175            fdb_path: None,
176            overwrite: false,
177            fix_ownership: false,
178        }
179    }
180}
181
182impl Ghidra {
183    fn find_ghidra<F: Fn(&str) -> Result<PathBuf, Error>>(f: F) -> Result<PathBuf, Error> {
184        if cfg!(target_os = "windows") {
185            f("ghidraRun.bat")
186        } else {
187            f("ghidraRun")
188        }
189    }
190
191    fn find_headless<F: Fn(&str) -> Result<PathBuf, Error>>(f: F) -> Result<PathBuf, Error> {
192        if cfg!(target_os = "windows") {
193            f("analyzeHeadless.bat")
194        } else {
195            f("analyzeHeadless")
196        }
197    }
198
199    pub fn new() -> Result<Self, ExportError> {
200        let root = if let Ok(root_dir) = env::var("GHIDRA_INSTALL_DIR") {
201            PathBuf::from(root_dir).join("support")
202        } else {
203            let base = Self::find_ghidra(|p| which(p).map_err(Error::InvalidPath))
204                .map_err(ExportError::from)?;
205            base.parent().unwrap().join("support")
206        };
207
208        let path =
209            Self::find_headless(|p| which_in(p, Some(&root), ".").map_err(Error::InvalidPath))
210                .map_err(ExportError::from)?;
211
212        Ok(Self {
213            path: Some(path),
214            ..Default::default()
215        })
216    }
217
218    pub fn export_path<P: AsRef<Path>>(mut self, path: P, overwrite: bool) -> Self {
219        self.fdb_path = Some(path.as_ref().to_owned());
220        self.overwrite = overwrite;
221        self
222    }
223
224    pub fn force_ownership(mut self, force: bool) -> Self {
225        self.fix_ownership = force;
226        self
227    }
228}
229
230impl Backend for Ghidra {
231    type Error = Error;
232
233    fn name(&self) -> &'static str {
234        "fugue-ghidra"
235    }
236
237    fn is_available(&self) -> bool {
238        self.path.is_some()
239    }
240
241    fn is_preferred_for(&self, path: &Url) -> Option<bool> {
242        if path.scheme() != "file" {
243            return None;
244        }
245
246        if let Ok(path) = path.to_file_path() {
247            Some(GhidraProject::resolve(&path).is_some())
248        } else {
249            None
250        }
251    }
252
253    fn import(&self, program: &Url) -> Result<Imported, Error> {
254        if program.scheme() != "file" {
255            return Err(Error::UnsupportedScheme(program.scheme().to_owned()));
256        }
257
258        let path = program
259            .to_file_path()
260            .map_err(|_| Error::UnsupportedScheme(program.scheme().to_owned()))?;
261
262        let headless = self.path.as_ref().ok_or_else(|| Error::NotAvailable)?;
263        let mut process = process::Command::new(headless);
264
265        // Check if the file is a ghidra project
266        let mut project = GhidraProject::resolve(&path);
267
268        if let Some(ref project) = project {
269            // existing
270
271            let location = project.project_root.clone();
272            let project_name = project.project_name.clone();
273            let project_file = project.project_file.clone();
274
275            process.arg(location);
276            process.arg(project_name);
277
278            process.arg("-process");
279            process.arg(project_file);
280        } else {
281            // Not a ghidra project, so we need to create one
282            let tmp = tempdir().map_err(Error::TempDirectory)?.into_path();
283            process.arg(tmp);
284            process.arg("fugue-temp-project");
285
286            process.arg("-import");
287            process.arg(path);
288
289            process.arg("-deleteProject");
290        }
291
292        process.arg("-preScript");
293        process.arg("FugueAnalysisOptions.java");
294
295        process.arg("-postScript");
296        process.arg("FugueExport.java");
297
298        process.arg(format!("FugueForceOverwrite:{}", self.overwrite));
299
300        let output = if let Some(ref fdb_path) = self.fdb_path {
301            process.arg(format!("FugueOutput:{}", fdb_path.display()));
302            Imported::File(fdb_path.to_owned())
303        } else {
304            let mut tmp = tempdir().map_err(Error::TempDirectory)?.into_path();
305            tmp.push("fugue-temp-export.fdb");
306            process.arg(format!("FugueOutput:{}", tmp.display()));
307            Imported::File(tmp)
308        };
309
310        // Fix ownership of ghidra project
311        if self.fix_ownership {
312            if let Some(ref mut project) = project {
313                project.modify_ownership()?;
314            }
315        }
316
317        let result = match process
318            .output()
319            .map_err(Error::Launch)
320            .map(|output| output.status.code())
321        {
322            Ok(Some(100)) => Ok(output),
323            Ok(Some(101)) => Err(Error::InputOutput),
324            Ok(Some(102)) => Err(Error::Import),
325            Ok(Some(103)) => Err(Error::Unsupported),
326            Ok(_) => Err(Error::Failure),
327            Err(e) => Err(e),
328        };
329
330        if self.fix_ownership {
331            if let Some(ref mut project) = project {
332                project.restore_ownership()?;
333            }
334        }
335
336        result
337    }
338}
339
340#[cfg(test)]
341mod test {
342    use super::*;
343
344    #[test]
345    fn test_clean_import() -> Result<(), Box<dyn std::error::Error>> {
346        let ghidra = Ghidra::new()?.export_path("/tmp/exported.fdb", true);
347        let url = Url::from_file_path(Path::new("./tests/true").canonicalize()?).unwrap();
348
349        let _ = ghidra.import(&url)?;
350
351        Ok(())
352    }
353}