use goblin::Object;
#[derive(Debug, Clone)]
pub struct XorVtableDispatch {
pub call_site_va: u64,
pub trampoline_slot: u64,
pub data_slots: Vec<u64>,
}
pub fn scan_region(
code: &[u8],
base_va: u64,
trampoline_slots: &std::collections::HashSet<u64>,
) -> Vec<XorVtableDispatch> {
let mut hits = Vec::new();
let mut off = 0;
while off + 6 <= code.len() {
if code[off] == 0xff && code[off + 1] == 0x15 {
let d32 =
i32::from_le_bytes([code[off + 2], code[off + 3], code[off + 4], code[off + 5]]);
let next_rip = base_va.wrapping_add((off + 6) as u64);
let target_slot = next_rip.wrapping_add(d32 as i64 as u64);
if trampoline_slots.contains(&target_slot) {
let lb_start = off.saturating_sub(80);
let window = &code[lb_start..off];
let window_va = base_va + lb_start as u64;
if shape_matches(window) {
let slots = collect_rip_slots(window, window_va);
hits.push(XorVtableDispatch {
call_site_va: base_va + off as u64,
trampoline_slot: target_slot,
data_slots: slots,
});
}
}
off += 6;
continue;
}
off += 1;
}
hits
}
fn shape_matches(window: &[u8]) -> bool {
let mut indexed_load = false;
let mut xor_count = 0;
let mut rip_load_count = 0;
let mut k = 0;
while k + 3 <= window.len() {
if k + 4 <= window.len() && window[k] == 0x48 && window[k + 1] == 0x8b {
let modrm = window[k + 2];
if modrm & 0xc0 == 0 && modrm & 0x07 == 0x04 {
let sib = window[k + 3];
let scale = (sib >> 6) & 0x3;
if scale == 3 {
indexed_load = true;
}
}
if modrm & 0xc0 == 0 && modrm & 0x07 == 0x05 && k + 7 <= window.len() {
rip_load_count += 1;
k += 7;
continue;
}
}
if window[k] == 0x48 && (window[k + 1] == 0x31 || window[k + 1] == 0x33) {
let modrm = window[k + 2];
if modrm & 0xc0 == 0xc0 {
xor_count += 1;
}
}
k += 1;
}
indexed_load && xor_count >= 2 && rip_load_count >= 2
}
fn collect_rip_slots(window: &[u8], window_va: u64) -> Vec<u64> {
let mut slots = Vec::new();
let mut k = 0;
while k + 7 <= window.len() {
if window[k] == 0x48 && window[k + 1] == 0x8b {
let modrm = window[k + 2];
if modrm & 0xc0 == 0 && modrm & 0x07 == 0x05 {
let d32 = i32::from_le_bytes([
window[k + 3],
window[k + 4],
window[k + 5],
window[k + 6],
]);
let next_rip = window_va.wrapping_add((k + 7) as u64);
let slot = next_rip.wrapping_add(d32 as i64 as u64);
slots.push(slot);
k += 7;
continue;
}
}
k += 1;
}
slots
}
pub fn scan(
obj: &Object<'_>,
data: &[u8],
trampoline_slots: &std::collections::HashSet<u64>,
) -> Vec<XorVtableDispatch> {
match obj {
Object::PE(pe) => {
let mut hits = Vec::new();
const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000;
for sec in &pe.sections {
if sec.characteristics & IMAGE_SCN_MEM_EXECUTE == 0 {
continue;
}
let raddr = sec.pointer_to_raw_data as usize;
let rsize = sec.size_of_raw_data as usize;
if raddr + rsize > data.len() {
continue;
}
let base_va = pe.image_base as u64 + sec.virtual_address as u64;
hits.extend(scan_region(
&data[raddr..raddr + rsize],
base_va,
trampoline_slots,
));
}
hits
}
_ => Vec::new(),
}
}
pub fn iat_slots_for_trampolines(
obj: &Object<'_>,
data: &[u8],
trampolines: &[u64],
) -> std::collections::HashSet<u64> {
let mut out = std::collections::HashSet::new();
if trampolines.is_empty() {
return out;
}
let trampoline_set: std::collections::HashSet<u64> = trampolines.iter().copied().collect();
if let Object::PE(pe) = obj {
for sec in &pe.sections {
const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000;
if sec.characteristics & IMAGE_SCN_MEM_EXECUTE != 0 {
continue;
}
let raddr = sec.pointer_to_raw_data as usize;
let rsize = sec.size_of_raw_data as usize;
if raddr + rsize > data.len() {
continue;
}
let base_va = pe.image_base as u64 + sec.virtual_address as u64;
let bytes = &data[raddr..raddr + rsize];
let mut k = 0;
while k + 8 <= bytes.len() {
let v = u64::from_le_bytes([
bytes[k],
bytes[k + 1],
bytes[k + 2],
bytes[k + 3],
bytes[k + 4],
bytes[k + 5],
bytes[k + 6],
bytes[k + 7],
]);
if trampoline_set.contains(&v) {
out.insert(base_va + k as u64);
}
k += 8;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_matches_dispatcher_skeleton() {
let mut window = vec![];
window.extend_from_slice(&[0x48, 0x8b, 0x0d, 0x10, 0, 0, 0]);
window.extend_from_slice(&[0x48, 0x8b, 0x05, 0x20, 0, 0, 0]);
window.extend_from_slice(&[0x48, 0x33, 0xc8]);
window.extend_from_slice(&[0x48, 0x8b, 0x0c, 0xd1]);
window.extend_from_slice(&[0x48, 0x33, 0xc1]);
assert!(shape_matches(&window));
}
#[test]
fn shape_rejects_quiet_code() {
let window = vec![0x90; 80];
assert!(!shape_matches(&window));
}
#[test]
fn collects_rip_slots() {
let mut window = vec![];
window.extend_from_slice(&[0x48, 0x8b, 0x0d, 0x10, 0, 0, 0]); window.extend_from_slice(&[0x48, 0x8b, 0x05, 0x20, 0, 0, 0]); let slots = collect_rip_slots(&window, 0x1000);
assert_eq!(slots.len(), 2);
assert_eq!(slots[0], 0x1017);
assert_eq!(slots[1], 0x102e);
}
}