#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/0xdea/rhabdomancer/master/.img/logo.png")]
use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf};
use std::{env, mem};
use anyhow::Context as _;
use config::{Config, ConfigError, File};
use idalib::bookmarks::BookmarkIndex;
use idalib::ffi::BADADDR;
use idalib::func::{Function, FunctionId};
use idalib::idb::IDB;
use idalib::xref::{XRef, XRefQuery};
use idalib::{Address, IDAError};
pub const PREFIX: &str = "[BAD ";
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
enum Priority {
High = 0,
Medium,
Low,
}
impl Priority {
const fn code(self) -> u8 {
self as u8
}
fn tag_prefix(self) -> String {
format!("{PREFIX}{}]", self.code())
}
fn description(self, func_name: &str) -> String {
format!("{} {}", self.tag_prefix(), func_name)
}
}
#[derive(serde::Deserialize)]
struct KnownBadFunctions {
high: HashSet<String>,
medium: HashSet<String>,
low: HashSet<String>,
}
impl KnownBadFunctions {
fn load() -> Result<Self, ConfigError> {
let path = env::var_os("RHABDOMANCER_CONFIG").map_or_else(
|| Path::new(env!("CARGO_MANIFEST_DIR")).join("conf/rhabdomancer.toml"),
PathBuf::from,
);
println!("[*] Using configuration file `{}`", path.display());
let mut this = Config::builder()
.add_source(File::from(path))
.build()?
.try_deserialize::<Self>()?;
this.normalize_sets();
Ok(this)
}
fn check_function(&self, func: &Function) -> Option<Priority> {
let func_name = func.name()?;
let func_name = normalize_name(&func_name);
if self.high.contains(func_name) {
return Some(Priority::High);
}
if self.medium.contains(func_name) {
return Some(Priority::Medium);
}
if self.low.contains(func_name) {
return Some(Priority::Low);
}
None
}
fn normalize_sets(&mut self) {
self.high = mem::take(&mut self.high)
.into_iter()
.map(|s| normalize_name(&s).to_owned())
.collect();
self.medium = mem::take(&mut self.medium)
.into_iter()
.map(|s| normalize_name(&s).to_owned())
.collect();
self.low = mem::take(&mut self.low)
.into_iter()
.map(|s| normalize_name(&s).to_owned())
.collect();
}
}
struct BadFunctions<'a> {
high: BTreeMap<FunctionId, Function<'a>>,
medium: BTreeMap<FunctionId, Function<'a>>,
low: BTreeMap<FunctionId, Function<'a>>,
marked: BookmarkIndex,
}
impl<'a> BadFunctions<'a> {
fn find_all(idb: &'a IDB, bad: &KnownBadFunctions) -> Self {
let mut found = Self {
high: BTreeMap::new(),
medium: BTreeMap::new(),
low: BTreeMap::new(),
marked: 0,
};
for (id, f) in idb.functions() {
if let Some(p) = bad.check_function(&f) {
found.insert_function(id, f, p);
}
}
found
}
fn insert_function(&mut self, id: FunctionId, func: Function<'a>, priority: Priority) {
match priority {
Priority::High => {
self.high.insert(id, func);
}
Priority::Medium => {
self.medium.insert(id, func);
}
Priority::Low => {
self.low.insert(id, func);
}
}
}
fn locate_calls(&mut self, idb: &'a IDB) -> anyhow::Result<BookmarkIndex> {
let mut marked = 0;
for f in self.high.values() {
Self::mark_calls(idb, f, Priority::High, &mut marked)?;
}
for f in self.medium.values() {
Self::mark_calls(idb, f, Priority::Medium, &mut marked)?;
}
for f in self.low.values() {
Self::mark_calls(idb, f, Priority::Low, &mut marked)?;
}
self.marked = marked;
Ok(self.marked)
}
fn mark_calls(
idb: &IDB,
func: &Function,
priority: Priority,
marked: &mut BookmarkIndex,
) -> Result<(), IDAError> {
let Some(func_name) = func.name() else {
return Err(IDAError::ffi_with("empty function name"));
};
let desc = priority.description(normalize_name(&func_name));
if is_in_plt(idb, func.start_address()) {
println!("\n{desc} (thunk)");
} else {
println!("\n{desc}");
}
idb.first_xref_to(func.start_address(), XRefQuery::ALL)
.map_or(Ok(()), |cur| Self::traverse_xrefs(idb, &cur, &desc, marked))
}
fn traverse_xrefs(
idb: &IDB,
xref: &XRef,
desc: &str,
marked: &mut BookmarkIndex,
) -> Result<(), IDAError> {
if is_in_plt(idb, xref.from()) {
idb.first_xref_to(
idb.function_at(xref.from())
.map_or_else(|| BADADDR.into(), |func| func.start_address()),
XRefQuery::ALL,
)
.map_or(Ok(()), |thunk| {
Self::traverse_xrefs(idb, &thunk, desc, marked)
})?;
} else if xref.is_code() {
let caller = idb.function_at(xref.from()).map_or_else(
|| "[unknown]".into(),
|func| func.name().unwrap_or_else(|| "[no name]".into()),
);
println!("{:#X} in {}", xref.from(), caller);
if !idb
.bookmarks()
.get_description(xref.from())
.unwrap_or_default()
.contains(PREFIX)
{
idb.bookmarks().mark(xref.from(), desc)?;
*marked += 1;
}
if !idb
.get_cmt(xref.from())
.unwrap_or_default()
.contains(PREFIX)
{
idb.append_cmt(xref.from(), desc)?;
}
}
xref.next_to().map_or(Ok(()), |next| {
Self::traverse_xrefs(idb, &next, desc, marked)
})
}
}
pub fn run(filepath: &Path) -> anyhow::Result<BookmarkIndex> {
println!("[*] Loading known bad API function names");
let known_bad =
KnownBadFunctions::load().context("Failed to load known bad API function names")?;
println!("[*] Analyzing binary file `{}`", filepath.display());
let idb = IDB::open_with(filepath, true, true)
.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!();
println!("[*] Finding bad API function calls...");
let marked = BadFunctions::find_all(&idb, &known_bad)
.locate_calls(&idb)
.context("Failed to find bad API function calls")?;
println!();
println!("[+] Marked {marked} new call locations");
println!("[+] Done processing binary file `{}`", filepath.display());
Ok(marked)
}
fn is_in_plt(idb: &IDB, addr: Address) -> bool {
idb.segment_at(addr)
.is_some_and(|segm| segm.name().unwrap_or_default().starts_with(".plt"))
}
fn normalize_name(name: &str) -> &str {
name.trim_start_matches(['.', '_'])
}