#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/0xdea/augur/master/.img/logo.png")]
use std::fs;
use std::ops::Deref;
use std::path::Path;
use anyhow::Context as _;
use haruspex::{
HaruspexError, decompile_to_file, output_path_for_function, prepare_output_dir,
sanitize_filename,
};
use idalib::decompiler::HexRaysErrorCode;
use idalib::func::{Function, FunctionFlags};
use idalib::idb::IDB;
use idalib::xref::{XRef, XRefQuery};
use idalib::{Address, IDAError};
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct IDAString(String);
impl IDAString {
fn traverse_xrefs(
&self,
idb: &IDB,
first_xref: XRef,
addr: Address,
dirpath: &Path,
string_uses_count: &mut usize,
) -> Result<(), HaruspexError> {
let string_name = self.filter_printable_chars();
let dirpath_sub = dirpath.join(format!("_{addr:X}_{}_", sanitize_filename(&string_name)));
let mut current = Some(first_xref);
while let Some(xref) = current {
let from = xref.from();
if let Some(f) = idb.function_at(from) {
if !f.flags().contains(FunctionFlags::THUNK) {
dump_function_pseudocode(idb, &f, from, &dirpath_sub)?;
*string_uses_count += 1;
}
} else {
println!("{from:#X} in [unknown]");
}
current = xref.next_to();
}
Ok(())
}
fn filter_printable_chars(&self) -> String {
self.chars()
.filter(|c| c.is_ascii_graphic() || *c == ' ')
.collect()
}
}
impl AsRef<str> for IDAString {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Deref for IDAString {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<String> for IDAString {
fn from(value: String) -> Self {
Self(value)
}
}
pub fn run(filepath: &Path) -> anyhow::Result<usize> {
println!("[*] Analyzing binary file `{}`", filepath.display());
let idb = IDB::open(filepath)
.with_context(|| format!("Failed to analyze binary file `{}`", filepath.display()))?;
println!("[+] Successfully analyzed binary file");
println!();
println!("[-] Processor: {}", idb.processor().long_name());
println!("[-] Compiler: {:?}", idb.meta().cc_id());
println!("[-] File type: {:?}", idb.meta().filetype());
println!();
anyhow::ensure!(idb.decompiler_available(), "Decompiler is not available");
let dirpath = filepath.with_extension("str");
prepare_output_dir(&dirpath)?;
let mut string_uses_count = 0;
println!();
println!("[*] Finding cross-references to strings...");
let strings = idb.strings();
for i in 0..strings.len() {
let string = IDAString::from(
strings
.get_by_index(i)
.context("Failed to get string content")?,
);
let addr = strings
.get_address_by_index(i)
.context("Failed to get string address")?;
println!("\n{addr:#X} {:?} ", string.as_ref());
idb.first_xref_to(addr, XRefQuery::ALL)
.map_or(Ok::<(), HaruspexError>(()), |xref| {
match string.traverse_xrefs(&idb, xref, addr, &dirpath, &mut string_uses_count) {
Err(HaruspexError::DecompileFailed(IDAError::HexRays(e)))
if e.code() == HexRaysErrorCode::License =>
{
fs::remove_dir_all(&dirpath)?;
Err(IDAError::HexRays(e).into())
}
Err(HaruspexError::DecompileFailed(_)) | Ok(()) => Ok(()),
Err(e) => Err(e),
}
})?;
}
if string_uses_count == 0 {
fs::remove_dir_all(&dirpath)
.with_context(|| format!("Failed to remove directory `{}`", dirpath.display()))?;
anyhow::bail!("No string uses were found, check your input file");
}
println!();
println!(
"[+] Found {string_uses_count} string uses in functions, decompiled into `{}`",
dirpath.display()
);
println!("[+] Done processing binary file `{}`", filepath.display());
Ok(string_uses_count)
}
fn dump_function_pseudocode(
idb: &IDB,
func: &Function,
from: Address,
dirpath: &Path,
) -> Result<(), HaruspexError> {
let func_name = func.name().unwrap_or_else(|| "[no name]".into());
let output_path = output_path_for_function(func, dirpath);
fs::create_dir_all(dirpath)?;
decompile_to_file(idb, func, &output_path)?;
println!("{from:#X} in {func_name} -> `{}`", output_path.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter_keeps_ascii_graphic() {
let s = IDAString::from("hello!@#$%^&*()".to_owned());
assert_eq!(s.filter_printable_chars(), "hello!@#$%^&*()");
}
#[test]
fn filter_keeps_space() {
let s = IDAString::from("hello world".to_owned());
assert_eq!(s.filter_printable_chars(), "hello world");
}
#[test]
fn filter_strips_control_chars() {
let s = IDAString::from("hel\x00lo\x01\x1f".to_owned());
assert_eq!(s.filter_printable_chars(), "hello");
}
#[test]
fn filter_strips_nul_bytes() {
let s = IDAString::from("foo\x00bar".to_owned());
assert_eq!(s.filter_printable_chars(), "foobar");
}
#[test]
fn filter_strips_non_ascii() {
let s = IDAString::from("caf\u{00e9}".to_owned());
assert_eq!(s.filter_printable_chars(), "caf");
}
#[test]
fn filter_empty_string() {
let s = IDAString::from(String::new());
assert_eq!(s.filter_printable_chars(), "");
}
#[test]
fn filter_all_non_printable() {
let s = IDAString::from("\x00\x01\x02\x03".to_owned());
assert_eq!(s.filter_printable_chars(), "");
}
}