use crate::BinaryInfo;
use std::collections::HashMap;
const VMT_SELFPTR_OFF_32: u64 = 0x4C;
const VMT_CLASSNAME_OFF_32: u64 = 0x2C;
const VMT_SELFPTR_OFF_64: u64 = 0x98;
const VMT_CLASSNAME_OFF_64: u64 = 0x58;
const MAX_VTABLE_METHODS: usize = 256;
const MAX_CLASSNAME_LEN: u8 = 100;
#[must_use]
pub fn parse(bi: &BinaryInfo) -> HashMap<u64, String> {
let mut out = HashMap::new();
let ptrsize = bi.bitness / 8;
if ptrsize != 4 && ptrsize != 8 {
return out;
}
let (selfptr_off, classname_off) = if ptrsize == 4 {
(VMT_SELFPTR_OFF_32, VMT_CLASSNAME_OFF_32)
} else {
(VMT_SELFPTR_OFF_64, VMT_CLASSNAME_OFF_64)
};
let selfptr_to_classname = selfptr_off - classname_off;
for sm in &bi.section_maps {
let Some(end) = sm.file_offset.checked_add(sm.file_size) else {
continue;
};
let Some(sec_bytes) = bi.raw_data.get(sm.file_offset..end) else {
continue;
};
let step = ptrsize as usize;
if sec_bytes.len() < step {
continue;
}
let mut i = 0usize;
while i + step <= sec_bytes.len() {
let value = read_ptr(&sec_bytes[i..], ptrsize);
let va_at_i = sm.va_start.saturating_add(i as u64);
if value != 0 && value == va_at_i.saturating_add(selfptr_off) {
let vtable_va = value;
let cname_pos_in_section = i.saturating_add(selfptr_to_classname as usize);
if cname_pos_in_section + step <= sec_bytes.len() {
let cname_ptr = read_ptr(&sec_bytes[cname_pos_in_section..], ptrsize);
if let Some(class_name) = read_pascal_short_string(bi, cname_ptr) {
for (m_va, m_idx) in walk_vtable(bi, vtable_va, ptrsize) {
out.entry(m_va)
.or_insert_with(|| format!("{class_name}::vmt_{m_idx}"));
}
}
}
}
i += step;
}
}
out
}
fn read_ptr(buf: &[u8], ptrsize: u32) -> u64 {
match ptrsize {
4 if buf.len() >= 4 => u32::from_le_bytes(buf[0..4].try_into().unwrap_or([0; 4])) as u64,
8 if buf.len() >= 8 => u64::from_le_bytes(buf[0..8].try_into().unwrap_or([0; 8])),
_ => 0,
}
}
fn read_pascal_short_string(bi: &BinaryInfo, va: u64) -> Option<String> {
if va == 0 {
return None;
}
let len_byte = bi.bytes_at(va, 1).ok()?[0];
if len_byte == 0 || len_byte > MAX_CLASSNAME_LEN {
return None;
}
let bytes = bi.bytes_at(va.checked_add(1)?, len_byte as u32).ok()?;
if !bytes
.iter()
.all(|b| b.is_ascii_graphic() || *b == b' ' || *b == b'_')
{
return None;
}
std::str::from_utf8(bytes).ok().map(str::to_owned)
}
fn walk_vtable(bi: &BinaryInfo, vtable_va: u64, ptrsize: u32) -> Vec<(u64, usize)> {
let mut out = Vec::new();
for idx in 0..MAX_VTABLE_METHODS {
let off = (idx as u64).saturating_mul(ptrsize as u64);
let Some(pos) = vtable_va.checked_add(off) else {
break;
};
let Ok(buf) = bi.bytes_at(pos, ptrsize) else {
break;
};
let mptr = read_ptr(buf, ptrsize);
if mptr == 0 {
break;
}
if !is_addr_in_image(bi, mptr) {
break;
}
out.push((mptr, idx));
}
out
}
fn is_addr_in_image(bi: &BinaryInfo, va: u64) -> bool {
bi.section_maps
.iter()
.any(|sm| va >= sm.va_start && va < sm.va_end)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FileArchitecture, FileFormat, SectionMap};
#[test]
fn detects_synthetic_32bit_vmt() {
let base = 0x400000u64;
let mut bytes = vec![0u8; 0x300];
bytes[0x100] = 4;
bytes[0x101..0x105].copy_from_slice(b"TFoo");
let vtable_va = base + 0x18C;
bytes[0x140..0x144].copy_from_slice(&(vtable_va as u32).to_le_bytes());
bytes[0x160..0x164].copy_from_slice(&((base + 0x100) as u32).to_le_bytes());
let method_va = base + 0x1F0;
bytes[0x18C..0x190].copy_from_slice(&(method_va as u32).to_le_bytes());
let bi = BinaryInfo {
file_format: FileFormat::PE,
file_architecture: FileArchitecture::I386,
base_addr: base,
raw_data: &bytes,
section_maps: vec![SectionMap {
va_start: base,
va_end: base + bytes.len() as u64,
file_offset: 0,
file_size: bytes.len(),
}],
binary_size: bytes.len() as u64,
bitness: 32,
code_areas: vec![],
component: String::new(),
family: String::new(),
file_path: String::new(),
is_library: false,
is_buffer: false,
sha256: String::new(),
entry_point: 0,
sections: vec![("test".to_string(), base, bytes.len())],
imports: vec![],
exports: vec![],
};
let result = parse(&bi);
assert!(
!result.is_empty(),
"expected at least one VMT method, got empty result"
);
assert!(
result.contains_key(&method_va),
"expected method VA 0x{method_va:x} in result, got {result:?}"
);
assert_eq!(result[&method_va], "TFoo::vmt_0");
}
#[test]
fn detects_synthetic_64bit_vmt() {
let base = 0x140000000u64;
let mut bytes = vec![0u8; 0x400];
bytes[0x100] = 4;
bytes[0x101..0x105].copy_from_slice(b"TBar");
let vtable_va = base + 0x240;
let selfptr_off_in_blob = 0x240 - 0x98;
bytes[selfptr_off_in_blob..selfptr_off_in_blob + 8]
.copy_from_slice(&vtable_va.to_le_bytes());
let cname_off_in_blob = 0x240 - 0x58;
bytes[cname_off_in_blob..cname_off_in_blob + 8]
.copy_from_slice(&(base + 0x100).to_le_bytes());
let method_va = base + 0x300;
bytes[0x240..0x248].copy_from_slice(&method_va.to_le_bytes());
let bi = BinaryInfo {
file_format: FileFormat::PE,
file_architecture: FileArchitecture::AMD64,
base_addr: base,
raw_data: &bytes,
section_maps: vec![SectionMap {
va_start: base,
va_end: base + bytes.len() as u64,
file_offset: 0,
file_size: bytes.len(),
}],
binary_size: bytes.len() as u64,
bitness: 64,
code_areas: vec![],
component: String::new(),
family: String::new(),
file_path: String::new(),
is_library: false,
is_buffer: false,
sha256: String::new(),
entry_point: 0,
sections: vec![("test".to_string(), base, bytes.len())],
imports: vec![],
exports: vec![],
};
let result = parse(&bi);
assert!(
!result.is_empty(),
"expected at least one VMT method (64-bit), got empty result"
);
assert!(
result.contains_key(&method_va),
"expected method VA 0x{method_va:x} in result, got {result:?}"
);
assert_eq!(result[&method_va], "TBar::vmt_0");
}
}