use crate::analysis::engine::{Analysis, Function, XrefKind};
use crate::analysis::strings::{self, Located};
use crate::db::Db;
use crate::model::Binary;
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "kind", content = "text", rename_all = "lowercase")]
pub enum Annot {
Note(String),
Symbol(String),
Local(String),
Text(String),
Hint(String),
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Line {
Label { addr: u64, text: String },
Insn {
addr: u64,
mnemonic: String,
operands: String,
annot: Option<Annot>,
target: Option<u64>,
},
Data { addr: u64, text: String },
}
impl Line {
pub fn addr(&self) -> u64 {
match self {
Line::Label { addr, .. } | Line::Insn { addr, .. } | Line::Data { addr, .. } => *addr,
}
}
pub fn target(&self) -> Option<u64> {
match self {
Line::Insn { target, .. } => *target,
_ => None,
}
}
}
pub fn function(
an: &Analysis,
f: &Function,
db: &Db,
base: u64,
strings: &BTreeMap<u64, Located>,
hints: Option<&BTreeMap<u64, String>>,
) -> Vec<Line> {
let mut out = Vec::new();
for (i, block) in f.blocks.iter().enumerate() {
if i > 0 {
out.push(Line::Label {
addr: block.start,
text: format!("loc_{:x}", block.start + an.display_base),
});
}
for ins in &block.insns {
let a = ins.addr + an.display_base;
let text = ins.text(an.bits, an.arch);
let (mnemonic, operands) = text
.split_once(' ')
.map(|(m, r)| (m.to_string(), r.to_string()))
.unwrap_or_else(|| (text.clone(), String::new()));
let mut annot = if let Some(n) = db.notes.get(&a.wrapping_sub(base)) {
Some(Annot::Note(n.clone()))
} else if let Some(name) = &ins.target_name {
Some(Annot::Symbol(name.to_string()))
} else if let Some(t) = ins.target {
let ta = t + an.display_base;
if an.find_function(t).is_some() {
Some(Annot::Symbol(an.label(t)))
} else {
Some(Annot::Local(format!("loc_{ta:x}")))
}
} else {
data_annot(an, strings, ins.addr)
};
if annot.is_none() {
annot = hints
.and_then(|m| m.get(&ins.addr))
.map(|s| Annot::Hint(s.clone()));
}
out.push(Line::Insn {
addr: ins.addr,
mnemonic,
operands,
annot,
target: ins.target,
});
}
}
out
}
fn data_annot(an: &Analysis, strings: &BTreeMap<u64, Located>, addr: u64) -> Option<Annot> {
for r in an.xrefs_from.get(&addr)? {
if r.kind != XrefKind::Data {
continue;
}
if let Some((start, s)) = strings.range(..=r.to).next_back() {
if *start <= r.to && r.to < start + s.len {
return Some(Annot::Text(s.text.clone()));
}
}
if let Some(name) = an.names.get(&r.to).or_else(|| an.imports.get(&r.to)) {
return Some(Annot::Symbol(name.clone()));
}
}
None
}
pub fn string_map(bin: &Binary, bytes: &[u8], base: u64) -> BTreeMap<u64, Located> {
let mut out = BTreeMap::new();
for s in strings::extract_located(bytes, 4) {
if let Some(va) = crate::analysis::engine::off_to_va(bin, base, s.off) {
out.insert(va, s);
}
}
out
}
pub fn data_view(bin: &Binary, base: u64, bytes: &[u8], addr: u64) -> Vec<Line> {
let Some(off) = crate::analysis::engine::va_to_off(bin, base, addr) else {
return vec![Line::Data {
addr,
text: format!("0x{addr:x} is not in any mapped section"),
}];
};
let mut out = Vec::new();
for (row, chunk) in bytes[off..].chunks(16).enumerate().take(8) {
let hex: String = chunk
.iter()
.enumerate()
.map(|(i, b)| {
let sep = if i == 15 {
""
} else if i == 7 {
" "
} else {
" "
};
format!("{b:02x}{sep}")
})
.collect();
let ascii: String = chunk
.iter()
.map(|&b| {
if (0x20..0x7f).contains(&b) {
b as char
} else {
'.'
}
})
.collect();
out.push(Line::Data {
addr: addr + (row as u64) * 16,
text: format!("{hex:<48} {ascii}"),
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::engine;
use crate::model::{Arch, Binary, Format, Section};
fn code_at(vaddr: u64, code: &[u8]) -> (Binary, Vec<u8>) {
let mut bin = Binary::stub(Format::Elf, Arch::X86_64);
bin.entry = vaddr;
bin.sections = vec![Section {
name: ".text".into(),
vaddr,
vsize: code.len() as u64,
file_off: vaddr,
file_size: code.len() as u64,
entropy: 0.0,
read: true,
write: false,
exec: true,
}];
let mut bytes = vec![0u8; vaddr as usize];
bytes.extend_from_slice(code);
(bin, bytes)
}
#[test]
fn a_reference_to_a_named_address_is_annotated_with_its_name() {
let code = [
0x48, 0x8b, 0x05, 0x00, 0x10, 0x00, 0x00, 0xc3, ];
let (mut bin, bytes) = code_at(0x1000, &code);
let global = 0x1007 + 0x1000;
bin.sections.push(Section {
name: ".data".into(),
vaddr: global,
vsize: 8,
file_off: global,
file_size: 8,
entropy: 0.0,
read: true,
write: true,
exec: false,
});
bin.symbols.push(crate::model::Symbol {
addr: global,
name: "GetLastError".into(),
kind: crate::model::SymKind::Import,
});
let mut bytes = bytes;
bytes.resize((global + 8) as usize, 0);
let an = engine::analyze(&bin, &bytes, 1000, &Db::default());
let f = an.find_function(0x1000).unwrap();
let lines = function(&an, f, &Db::default(), 0, &BTreeMap::new(), None);
assert!(
lines.iter().any(|l| matches!(
l,
Line::Insn { annot: Some(Annot::Symbol(n)), .. } if n == "GetLastError"
)),
"the import's name belongs on the line that reads its slot: {lines:?}"
);
}
#[test]
fn a_branch_target_gets_its_own_label_line() {
let code = [
0x31, 0xc0, 0x74, 0x02, 0x90, 0x90, 0xc3, ];
let (bin, bytes) = code_at(0x1000, &code);
let an = engine::analyze(&bin, &bytes, 1000, &Db::default());
let f = an.find_function(0x1000).unwrap();
let lines = function(
&an,
f,
&Db::default(),
0,
&string_map(&bin, &bytes, 0),
None,
);
assert!(
lines.iter().any(|l| matches!(l, Line::Label { .. })),
"a branch target should produce a label"
);
let mut addrs: Vec<u64> = lines
.iter()
.filter(|l| matches!(l, Line::Insn { .. }))
.map(Line::addr)
.collect();
let n = addrs.len();
addrs.sort_unstable();
addrs.dedup();
assert_eq!(n, addrs.len());
}
#[test]
fn a_field_hint_is_lowest_priority() {
let code = [0x8b, 0x40, 0x10, 0xc3]; let (bin, bytes) = code_at(0x1000, &code);
let an = engine::analyze(&bin, &bytes, 1000, &Db::default());
let f = an.find_function(0x1000).unwrap();
let strings = string_map(&bin, &bytes, 0);
let plain = function(&an, f, &Db::default(), 0, &strings, None);
assert!(!plain.iter().any(|l| matches!(
l,
Line::Insn {
annot: Some(Annot::Hint(_)),
..
}
)));
let mut hints = BTreeMap::new();
hints.insert(
0x1000u64,
"Parameters.DeviceIoControl.IoControlCode".to_string(),
);
let hinted = function(&an, f, &Db::default(), 0, &strings, Some(&hints));
assert!(hinted.iter().any(|l| {
matches!(
l,
Line::Insn { addr: 0x1000, annot: Some(Annot::Hint(t)), .. } if t == "Parameters.DeviceIoControl.IoControlCode"
)
}));
let mut db = Db::default();
db.set_note(0x1000, "hand written");
let noted = function(&an, f, &db, 0, &strings, Some(&hints));
assert!(noted.iter().any(|l| {
matches!(
l,
Line::Insn { addr: 0x1000, annot: Some(Annot::Note(n)), .. } if n == "hand written"
)
}));
}
#[test]
fn a_note_replaces_the_derived_annotation() {
let code = [0x74, 0x00, 0xc3]; let (bin, bytes) = code_at(0x1000, &code);
let an = engine::analyze(&bin, &bytes, 1000, &Db::default());
let f = an.find_function(0x1000).unwrap();
let strings = string_map(&bin, &bytes, 0);
let plain = function(&an, f, &Db::default(), 0, &strings, None);
assert!(matches!(
plain.iter().find(|l| l.addr() == 0x1000),
Some(Line::Insn {
annot: Some(Annot::Local(_)),
..
})
));
let mut db = Db::default();
db.set_note(0x1000, "bounds check");
let noted = function(&an, f, &db, 0, &strings, None);
assert_eq!(
noted.iter().find(|l| l.addr() == 0x1000).and_then(|l| {
match l {
Line::Insn { annot, .. } => annot.clone(),
_ => None,
}
}),
Some(Annot::Note("bounds check".into()))
);
}
#[test]
fn a_string_operand_is_annotated_with_the_literal() {
let mut bin = Binary::stub(Format::Elf, Arch::X86_64);
bin.entry = 0x1000;
bin.sections = vec![
Section {
name: ".text".into(),
vaddr: 0x1000,
vsize: 8,
file_off: 0,
file_size: 8,
entropy: 0.0,
read: true,
write: false,
exec: true,
},
Section {
name: ".rodata".into(),
vaddr: 0x101e,
vsize: 5,
file_off: 8,
file_size: 5,
entropy: 0.0,
read: true,
write: false,
exec: false,
},
];
let mut bytes = vec![0x48, 0x8d, 0x05, 0x17, 0x00, 0x00, 0x00, 0xc3];
bytes.extend_from_slice(b"hello");
let an = engine::analyze(&bin, &bytes, 1000, &Db::default());
let f = an.find_function(0x1000).unwrap();
let lines = function(
&an,
f,
&Db::default(),
0,
&string_map(&bin, &bytes, 0),
None,
);
let annot = lines
.iter()
.find(|l| l.addr() == 0x1000)
.and_then(|l| match l {
Line::Insn { annot, .. } => annot.clone(),
_ => None,
});
assert_eq!(annot, Some(Annot::Text("hello".into())));
}
#[test]
fn data_view_rows_start_at_the_requested_address() {
let (bin, bytes) = code_at(0x1000, &[0x90, 0xc3, 0x90, 0x90]);
let lines = data_view(&bin, 0, &bytes, 0x1000);
assert_eq!(lines[0].addr(), 0x1000);
assert!(matches!(lines[0], Line::Data { .. }));
}
}