use std::convert::TryInto;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScopeRecord {
pub begin_va: u64,
pub end_va: u64,
pub handler_va: u64,
pub jump_target_va: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SehRecord {
pub func_begin: u64,
pub func_end: u64,
pub handler: Option<u64>,
pub scope_table: Option<u64>,
pub version: u8,
pub flags: u8,
}
impl SehRecord {
pub fn has_ehandler(&self) -> bool {
(self.flags & 0x1) != 0
}
pub fn has_uhandler(&self) -> bool {
(self.flags & 0x2) != 0
}
pub fn has_chaininfo(&self) -> bool {
(self.flags & 0x4) != 0
}
}
pub fn parse_pe64_seh(image_data: &[u8]) -> Vec<SehRecord> {
let obj = match goblin::Object::parse(image_data) {
Ok(o) => o,
Err(_) => return vec![],
};
let pe = match obj {
goblin::Object::PE(pe) => pe,
_ => return vec![],
};
if !pe.is_64 {
return vec![];
}
let base = pe.image_base as u64;
let pdata = pe.sections.iter().find(|s| {
std::str::from_utf8(&s.name)
.unwrap_or("")
.trim_end_matches('\0')
== ".pdata"
});
let Some(pdata) = pdata else {
return vec![];
};
let pd_fo = pdata.pointer_to_raw_data as usize;
let pd_sz = pdata.virtual_size.min(pdata.size_of_raw_data) as usize;
if pd_fo + pd_sz > image_data.len() || pd_sz < 12 {
return vec![];
}
let rva_to_fo = |rva: u32| -> Option<usize> {
let rva = rva as u64;
for sec in &pe.sections {
let va = sec.virtual_address as u64;
let vsz = sec.virtual_size as u64;
if rva >= va && rva < va + vsz {
return Some(sec.pointer_to_raw_data as usize + (rva - va) as usize);
}
}
None
};
let read_u32 = |fo: usize| -> Option<u32> {
if fo + 4 > image_data.len() {
return None;
}
Some(u32::from_le_bytes(image_data[fo..fo + 4].try_into().ok()?))
};
fn resolve_unwind(
mut unwind_rva: u32,
image: &[u8],
rva_to_fo: &dyn Fn(u32) -> Option<usize>,
depth: u32,
) -> Option<UnwindSummary> {
if depth > 8 {
return None;
}
if (unwind_rva & 1) != 0 {
let rf_fo = rva_to_fo(unwind_rva & !1)?;
if rf_fo + 12 > image.len() {
return None;
}
let next_unwind = u32::from_le_bytes(image[rf_fo + 8..rf_fo + 12].try_into().ok()?);
unwind_rva = next_unwind;
}
let fo = rva_to_fo(unwind_rva)?;
if fo + 4 > image.len() {
return None;
}
let hdr0 = image[fo];
let count_of_codes = image[fo + 2] as usize;
let version = hdr0 & 0x07;
let flags = (hdr0 >> 3) & 0x1f;
if version == 0 || version > 2 {
return None;
}
let codes_bytes = ((count_of_codes + 1) & !1) * 2;
let trailer_fo = fo + 4 + codes_bytes;
if flags & 0x4 != 0 {
if trailer_fo + 12 > image.len() {
return None;
}
let next_unwind =
u32::from_le_bytes(image[trailer_fo + 8..trailer_fo + 12].try_into().ok()?);
return resolve_unwind(next_unwind, image, rva_to_fo, depth + 1);
}
if flags & 0x3 != 0 {
if trailer_fo + 4 > image.len() {
return None;
}
let handler_rva =
u32::from_le_bytes(image[trailer_fo..trailer_fo + 4].try_into().ok()?);
let scope_rva = (trailer_fo + 4) as u32; return Some(UnwindSummary {
version,
flags,
handler_rva: Some(handler_rva),
scope_table_rva: Some(unwind_rva + 4 + codes_bytes as u32 + 4),
});
}
Some(UnwindSummary {
version,
flags,
handler_rva: None,
scope_table_rva: None,
})
}
let mut out = Vec::new();
let mut off = 0;
while off + 12 <= pd_sz {
let Some(begin) = read_u32(pd_fo + off) else {
break;
};
let Some(end) = read_u32(pd_fo + off + 4) else {
break;
};
let Some(uwd) = read_u32(pd_fo + off + 8) else {
break;
};
off += 12;
if begin == 0 && end == 0 && uwd == 0 {
break;
}
let Some(summary) = resolve_unwind(uwd, image_data, &rva_to_fo, 0) else {
out.push(SehRecord {
func_begin: base + begin as u64,
func_end: base + end as u64,
handler: None,
scope_table: None,
version: 1,
flags: 0,
});
continue;
};
out.push(SehRecord {
func_begin: base + begin as u64,
func_end: base + end as u64,
handler: summary.handler_rva.map(|r| base + r as u64),
scope_table: summary.scope_table_rva.map(|r| base + r as u64),
version: summary.version,
flags: summary.flags,
});
}
out
}
pub fn read_scope_table(image: &[u8], scope_table_va: u64) -> Vec<ScopeRecord> {
let obj = match goblin::Object::parse(image) {
Ok(o) => o,
_ => return vec![],
};
let pe = match obj {
goblin::Object::PE(p) => p,
_ => return vec![],
};
if !pe.is_64 {
return vec![];
}
let base = pe.image_base as u64;
let va_to_fo = |va: u64| -> Option<usize> {
for sec in &pe.sections {
let sva = base + sec.virtual_address as u64;
let vsz = sec.virtual_size as u64;
if va >= sva && va < sva + vsz {
return Some(sec.pointer_to_raw_data as usize + (va - sva) as usize);
}
}
None
};
let Some(fo) = va_to_fo(scope_table_va) else {
return vec![];
};
if fo + 4 > image.len() {
return vec![];
}
let count = u32::from_le_bytes(image[fo..fo + 4].try_into().unwrap_or([0; 4]));
if count == 0 || count > 1024 {
return vec![];
}
let need = 4usize + count as usize * 16;
if fo + need > image.len() {
return vec![];
}
let mut out = Vec::with_capacity(count as usize);
for i in 0..count as usize {
let r = fo + 4 + i * 16;
let begin = u32::from_le_bytes(image[r..r + 4].try_into().unwrap()) as u64;
let end = u32::from_le_bytes(image[r + 4..r + 8].try_into().unwrap()) as u64;
let handler = u32::from_le_bytes(image[r + 8..r + 12].try_into().unwrap()) as u64;
let jump = u32::from_le_bytes(image[r + 12..r + 16].try_into().unwrap()) as u64;
if end <= begin {
return vec![];
}
out.push(ScopeRecord {
begin_va: base + begin,
end_va: base + end,
handler_va: if handler <= 1 {
handler
} else {
base + handler
},
jump_target_va: if jump == 0 { 0 } else { base + jump },
});
}
out
}
pub fn scope_table_addresses(image: &[u8]) -> Vec<u64> {
let records = parse_pe64_seh(image);
let mut scope_by_fn: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
for r in &records {
if let Some(st) = r.scope_table {
scope_by_fn.insert(r.func_begin, st);
}
}
let mut visited_st: std::collections::HashSet<u64> = std::collections::HashSet::new();
let mut out: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
let mut queue: std::collections::VecDeque<(u64, u32)> = std::collections::VecDeque::new();
for r in &records {
if let Some(st) = r.scope_table {
queue.push_back((st, 0));
}
}
while let Some((st_va, depth)) = queue.pop_front() {
if depth > 8 {
continue;
}
if !visited_st.insert(st_va) {
continue;
}
for sr in read_scope_table(image, st_va) {
if sr.handler_va > 1 {
out.insert(sr.handler_va);
if let Some(&next_st) = scope_by_fn.get(&sr.handler_va) {
queue.push_back((next_st, depth + 1));
}
}
if sr.jump_target_va != 0 {
out.insert(sr.jump_target_va);
if let Some(&next_st) = scope_by_fn.get(&sr.jump_target_va) {
queue.push_back((next_st, depth + 1));
}
}
}
}
out.into_iter().collect()
}
pub fn handler_addresses(records: &[SehRecord]) -> Vec<u64> {
let mut set: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
for r in records {
if let Some(h) = r.handler {
set.insert(h);
}
}
set.into_iter().collect()
}
#[derive(Debug, Clone, Copy)]
struct UnwindSummary {
version: u8,
flags: u8,
handler_rva: Option<u32>,
scope_table_rva: Option<u32>,
}
#[derive(Debug, Clone, Default)]
pub struct HandlerAnalysis {
pub redirects_rip: bool,
pub skips_rip: bool,
pub mutates_context: bool,
pub reads_exception_info: bool,
pub calls_wpm: bool,
pub calls_vprotect: bool,
pub uses_rep_movs: bool,
pub resumption_va: Option<u64>,
pub iat_calls: Vec<String>,
pub insn_count: usize,
pub registers_runtime_tables: bool,
pub reads_dispatcher_context: bool,
pub exc_code_triggers: Vec<u32>,
}
impl HandlerAnalysis {
pub fn is_smc_candidate(&self) -> bool {
self.redirects_rip
|| self.skips_rip
|| self.calls_wpm
|| self.calls_vprotect
|| self.uses_rep_movs
|| self.registers_runtime_tables
}
}
pub fn analyse_all_handlers(image: &[u8]) -> std::collections::BTreeMap<u64, HandlerAnalysis> {
let records = parse_pe64_seh(image);
let mut out = std::collections::BTreeMap::new();
let handlers = handler_addresses(&records);
for h in handlers {
if let Some(a) = analyse_handler(image, h) {
out.insert(h, a);
}
}
out
}
pub fn analyse_handler(image: &[u8], handler_va: u64) -> Option<HandlerAnalysis> {
use iced_x86::{Decoder, DecoderOptions, Mnemonic, OpKind, Register};
let obj = match goblin::Object::parse(image) {
Ok(o) => o,
_ => return None,
};
let pe = match obj {
goblin::Object::PE(p) => p,
_ => return None,
};
if !pe.is_64 {
return None;
}
let base = pe.image_base as u64;
let mut handler_fo: Option<usize> = None;
for sec in &pe.sections {
let va = base + sec.virtual_address as u64;
let vsz = sec.virtual_size as u64;
if handler_va >= va && handler_va < va + vsz {
let delta = (handler_va - va) as usize;
handler_fo = Some(sec.pointer_to_raw_data as usize + delta);
break;
}
}
let fo = handler_fo?;
let max_len = 4096usize.min(image.len().saturating_sub(fo));
if max_len == 0 {
return None;
}
let mut iat_name: std::collections::HashMap<u64, String> = std::collections::HashMap::new();
if let Some(imports) = pe.imports.iter().next().map(|_| &pe.imports) {
for imp in imports.iter() {
let slot = base + imp.offset as u64;
iat_name.insert(slot, imp.name.to_string());
}
}
let bytes = &image[fo..fo + max_len];
let mut dec = Decoder::with_ip(64, bytes, handler_va, DecoderOptions::NONE);
let mut insn = iced_x86::Instruction::default();
let mut a = HandlerAnalysis::default();
let mut imm64_of: [Option<u64>; 16] = [None; 16];
let mut tag_of: [u8; 16] = [0; 16];
let mut exc_trig_set: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
let reg_idx = |r: Register| -> Option<usize> {
match r {
Register::RAX => Some(0),
Register::RCX => Some(1),
Register::RDX => Some(2),
Register::RBX => Some(3),
Register::RSP => Some(4),
Register::RBP => Some(5),
Register::RSI => Some(6),
Register::RDI => Some(7),
Register::R8 => Some(8),
Register::R9 => Some(9),
Register::R10 => Some(10),
Register::R11 => Some(11),
Register::R12 => Some(12),
Register::R13 => Some(13),
Register::R14 => Some(14),
Register::R15 => Some(15),
_ => None,
}
};
tag_of[1] = 1;
while dec.can_decode() && a.insn_count < 1024 {
dec.decode_out(&mut insn);
a.insn_count += 1;
let op = insn.mnemonic();
match op {
Mnemonic::Ret | Mnemonic::Ud2 | Mnemonic::Int3 => break,
_ => {}
}
if op == Mnemonic::Mov
&& insn.op_count() == 2
&& insn.op_kind(0) == OpKind::Register
&& insn.op_kind(1) == OpKind::Immediate64
{
if let Some(i) = reg_idx(insn.op_register(0)) {
imm64_of[i] = Some(insn.immediate64());
}
} else if op == Mnemonic::Mov && insn.op_count() == 2 && insn.op_kind(0) == OpKind::Register
{
if let Some(i) = reg_idx(insn.op_register(0)) {
imm64_of[i] = None;
}
}
if op == Mnemonic::Mov
&& insn.op_count() == 2
&& insn.op_kind(0) == OpKind::Register
&& insn.op_kind(1) == OpKind::Memory
&& insn.memory_index() == Register::None
{
let base = insn.memory_base();
let base_tag = reg_idx(base).map(|i| tag_of[i]).unwrap_or(0);
let dst_idx = reg_idx(insn.op_register(0));
if base_tag == 1 {
let disp = insn.memory_displacement64();
if let Some(di) = dst_idx {
tag_of[di] = match disp {
0x00 => 2, 0x08 => 3, _ => 0,
};
if tag_of[di] != 0 {
a.reads_exception_info = true;
}
}
} else if let Some(di) = dst_idx {
tag_of[di] = 0;
}
} else if op == Mnemonic::Mov
&& insn.op_count() == 2
&& insn.op_kind(0) == OpKind::Register
&& insn.op_kind(1) == OpKind::Register
{
let di = reg_idx(insn.op_register(0));
let si = reg_idx(insn.op_register(1));
if let (Some(di), Some(si)) = (di, si) {
tag_of[di] = tag_of[si];
}
}
if op == Mnemonic::Cmp && insn.op_count() == 2 {
match (insn.op_kind(0), insn.op_kind(1)) {
(OpKind::Register, _) => {
if let Some(ri) = reg_idx(insn.op_register(0)) {
if tag_of[ri] == 2 {
let imm = match insn.op_kind(1) {
OpKind::Immediate8
| OpKind::Immediate8to16
| OpKind::Immediate8to32
| OpKind::Immediate8to64 => {
Some(insn.immediate8() as i8 as i32 as u32)
}
OpKind::Immediate16 => Some(insn.immediate16() as u32),
OpKind::Immediate32 | OpKind::Immediate32to64 => {
Some(insn.immediate32())
}
_ => None,
};
if let Some(v) = imm {
exc_trig_set.insert(v);
}
}
}
}
(OpKind::Memory, _) => {
if insn.memory_index() == Register::None {
let base = insn.memory_base();
let base_tag = reg_idx(base).map(|i| tag_of[i]).unwrap_or(0);
if base_tag == 1 && insn.memory_displacement64() == 0 {
let imm = match insn.op_kind(1) {
OpKind::Immediate8
| OpKind::Immediate8to16
| OpKind::Immediate8to32
| OpKind::Immediate8to64 => {
Some(insn.immediate8() as i8 as i32 as u32)
}
OpKind::Immediate16 => Some(insn.immediate16() as u32),
OpKind::Immediate32 | OpKind::Immediate32to64 => {
Some(insn.immediate32())
}
_ => None,
};
if let Some(v) = imm {
exc_trig_set.insert(v);
a.reads_exception_info = true;
}
}
}
}
_ => {}
}
}
let is_mem_write_to_r8 = insn.op_count() >= 1
&& insn.op_kind(0) == OpKind::Memory
&& insn.memory_base() == Register::R8;
let is_mem_write_to_rcx = insn.op_count() >= 1
&& insn.op_kind(0) == OpKind::Memory
&& insn.memory_base() == Register::RCX;
if is_mem_write_to_r8 {
let disp = insn.memory_displacement64();
if disp == 0xF8 {
match op {
Mnemonic::Mov => {
a.redirects_rip = true;
if insn.op_kind(1) == OpKind::Register {
if let Some(i) = reg_idx(insn.op_register(1)) {
a.resumption_va = imm64_of[i];
}
} else if insn.op_kind(1) == OpKind::Immediate32to64 {
a.resumption_va = Some(insn.immediate64());
}
}
Mnemonic::Add | Mnemonic::Sub | Mnemonic::Inc | Mnemonic::Dec => {
a.skips_rip = true;
}
_ => {}
}
} else {
a.mutates_context = true;
}
} else if is_mem_write_to_rcx {
let disp = insn.memory_displacement64();
if disp >= 0x20 {
a.reads_exception_info = true; }
}
for i in 0..insn.op_count() {
if insn.op_kind(i) == OpKind::Memory && insn.memory_base() == Register::RCX && i > 0
{
let disp = insn.memory_displacement64();
if disp == 0x08 || (0x20..=0x30).contains(&disp) {
a.reads_exception_info = true;
}
}
if insn.op_kind(i) == OpKind::Memory && insn.memory_base() == Register::R9 && i > 0 {
let disp = insn.memory_displacement64();
if disp == 0x00 || disp == 0x20 {
a.reads_dispatcher_context = true;
}
}
}
if insn.has_rep_prefix()
&& matches!(op, Mnemonic::Movsb | Mnemonic::Movsd | Mnemonic::Movsq)
{
a.uses_rep_movs = true;
}
if op == Mnemonic::Call
&& insn.op_count() == 1
&& insn.op_kind(0) == OpKind::Memory
&& insn.is_ip_rel_memory_operand()
{
let target = insn.ip_rel_memory_address();
if let Some(name) = iat_name.get(&target) {
if a.iat_calls.len() < 16 && !a.iat_calls.contains(name) {
a.iat_calls.push(name.clone());
}
match name.as_str() {
"WriteProcessMemory" | "NtWriteVirtualMemory" | "ZwWriteVirtualMemory" => {
a.calls_wpm = true
}
"VirtualProtect"
| "VirtualProtectEx"
| "NtProtectVirtualMemory"
| "ZwProtectVirtualMemory" => a.calls_vprotect = true,
"RtlAddFunctionTable"
| "RtlInstallFunctionTableCallback"
| "RtlDeleteFunctionTable" => a.registers_runtime_tables = true,
_ => {}
}
}
}
}
a.exc_code_triggers = exc_trig_set.into_iter().collect();
Some(a)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImagePatch {
pub target_va: u64,
pub bytes: Vec<u8>,
pub handler_va: u64,
}
fn enumerate_jump_table(pe: &goblin::pe::PE, image: &[u8], table_va: u64, scale: u64) -> Vec<u64> {
let base = pe.image_base as u64;
let mut text: Vec<(u64, u64)> = Vec::new();
for sec in &pe.sections {
if (sec.characteristics & 0x20000000) != 0 {
let va = base + sec.virtual_address as u64;
text.push((va, va + sec.virtual_size as u64));
}
}
let in_text = |va: u64| text.iter().any(|(lo, hi)| va >= *lo && va < *hi);
let va_to_fo = |va: u64| -> Option<usize> {
for sec in &pe.sections {
let sva = base + sec.virtual_address as u64;
let vsz = sec.virtual_size as u64;
if va >= sva && va < sva + vsz {
return Some(sec.pointer_to_raw_data as usize + (va - sva) as usize);
}
}
None
};
const MAX_ENTRIES: u64 = 256;
let mut out = Vec::new();
let stride = if scale == 0 { 8 } else { scale };
if stride == 8 {
for i in 0..MAX_ENTRIES {
let entry_va = table_va.wrapping_add(i * stride);
let Some(fo) = va_to_fo(entry_va) else {
break;
};
if fo + 8 > image.len() {
break;
}
let v = u64::from_le_bytes(image[fo..fo + 8].try_into().unwrap());
if v == 0 || !in_text(v) {
break;
}
out.push(v);
}
if !out.is_empty() {
return out;
}
}
for i in 0..MAX_ENTRIES {
let entry_va = table_va.wrapping_add(i * 4);
let Some(fo) = va_to_fo(entry_va) else {
break;
};
if fo + 4 > image.len() {
break;
}
let off = i32::from_le_bytes(image[fo..fo + 4].try_into().unwrap()) as i64;
let target = (table_va as i64).wrapping_add(off) as u64;
if target == 0 || !in_text(target) {
break;
}
out.push(target);
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RegVal {
Top,
Imm(u64),
Addr(u64),
ExcRecordPtr,
ExcCode,
ExcAddress,
}
impl RegVal {
fn as_concrete(self) -> Option<u64> {
match self {
RegVal::Imm(v) | RegVal::Addr(v) => Some(v),
_ => None,
}
}
}
fn walk_helper_linear(
pe: &goblin::pe::PE,
image: &[u8],
entry_va: u64,
handler_va: u64,
regs: &mut [RegVal; 16],
patches: &mut Vec<ImagePatch>,
patch_set: &mut std::collections::HashSet<(u64, Vec<u8>)>,
call_depth: u32,
budget: usize,
) {
use iced_x86::{Decoder, DecoderOptions, Mnemonic, OpKind, Register};
if call_depth >= 3 || budget == 0 {
return;
}
let base = pe.image_base as u64;
let mut fo: Option<usize> = None;
for sec in &pe.sections {
let va = base + sec.virtual_address as u64;
let vsz = sec.virtual_size as u64;
if entry_va >= va && entry_va < va + vsz {
fo = Some(sec.pointer_to_raw_data as usize + (entry_va - va) as usize);
break;
}
}
let Some(start) = fo else {
return;
};
let span = (budget * 16).min(image.len().saturating_sub(start));
if span == 0 {
return;
}
let va_to_fo = |va: u64| -> Option<usize> {
for sec in &pe.sections {
let sva = base + sec.virtual_address as u64;
let vsz = sec.virtual_size as u64;
if va >= sva && va < sva + vsz {
return Some(sec.pointer_to_raw_data as usize + (va - sva) as usize);
}
}
None
};
let read_bytes = |va: u64, len: usize| -> Option<Vec<u8>> {
let o = va_to_fo(va)?;
if o + len > image.len() {
return None;
}
Some(image[o..o + len].to_vec())
};
let reg_idx = |r: Register| -> Option<usize> {
match r {
Register::RAX => Some(0),
Register::RCX => Some(1),
Register::RDX => Some(2),
Register::RBX => Some(3),
Register::RSP => Some(4),
Register::RBP => Some(5),
Register::RSI => Some(6),
Register::RDI => Some(7),
Register::R8 => Some(8),
Register::R9 => Some(9),
Register::R10 => Some(10),
Register::R11 => Some(11),
Register::R12 => Some(12),
Register::R13 => Some(13),
Register::R14 => Some(14),
Register::R15 => Some(15),
_ => None,
}
};
let mut dec = Decoder::with_ip(
64,
&image[start..start + span],
entry_va,
DecoderOptions::NONE,
);
let mut insn = iced_x86::Instruction::default();
let mut count = 0usize;
while dec.can_decode() && count < budget {
dec.decode_out(&mut insn);
count += 1;
let op = insn.mnemonic();
if matches!(op, Mnemonic::Ret | Mnemonic::Ud2 | Mnemonic::Int3) {
return;
}
if op == Mnemonic::Mov && insn.op_count() == 2 && insn.op_kind(0) == OpKind::Register {
let Some(di) = reg_idx(insn.op_register(0)) else {
continue;
};
regs[di] = match insn.op_kind(1) {
OpKind::Immediate64 => RegVal::Imm(insn.immediate64()),
OpKind::Immediate32to64 => RegVal::Imm(insn.immediate64()),
OpKind::Register => reg_idx(insn.op_register(1))
.map(|i| regs[i])
.unwrap_or(RegVal::Top),
_ => RegVal::Top,
};
continue;
}
if op == Mnemonic::Mov && insn.op_count() == 2 && insn.op_kind(0) == OpKind::Memory {
let base_reg = insn.memory_base();
if matches!(base_reg, Register::R8 | Register::RCX) {
continue;
}
let tva = if insn.is_ip_rel_memory_operand() {
Some(insn.ip_rel_memory_address())
} else if base_reg != Register::None && insn.memory_index() == Register::None {
reg_idx(base_reg)
.and_then(|i| regs[i].as_concrete())
.map(|v| v.wrapping_add(insn.memory_displacement64()))
} else {
None
};
let Some(tva) = tva else {
continue;
};
let width = insn.memory_size().size();
if width == 0 || width > 8 {
continue;
}
let v: Option<u64> = match insn.op_kind(1) {
OpKind::Immediate8 | OpKind::Immediate8to64 => Some(insn.immediate8() as u64),
OpKind::Immediate32 | OpKind::Immediate32to64 => Some(insn.immediate32() as u64),
OpKind::Immediate64 => Some(insn.immediate64()),
OpKind::Register => {
let full = insn.op_register(1).full_register();
reg_idx(full).and_then(|i| regs[i].as_concrete())
}
_ => None,
};
if let Some(v) = v {
let mut buf = v.to_le_bytes().to_vec();
buf.truncate(width);
if patch_set.insert((tva, buf.clone())) {
patches.push(ImagePatch {
target_va: tva,
bytes: buf,
handler_va,
});
}
}
continue;
}
if insn.has_rep_prefix()
&& matches!(op, Mnemonic::Movsb | Mnemonic::Movsd | Mnemonic::Movsq)
{
let rdi = regs[reg_idx(Register::RDI).unwrap()].as_concrete();
let rsi = regs[reg_idx(Register::RSI).unwrap()].as_concrete();
let rcx = regs[reg_idx(Register::RCX).unwrap()].as_concrete();
if let (Some(dst), Some(src), Some(cnt)) = (rdi, rsi, rcx) {
let unit = match op {
Mnemonic::Movsb => 1,
Mnemonic::Movsd => 4,
Mnemonic::Movsq => 8,
_ => 1,
};
let total = (cnt as usize).saturating_mul(unit);
if total > 0 && total <= 0x10000 {
if let Some(body) = read_bytes(src, total) {
if patch_set.insert((dst, body.clone())) {
patches.push(ImagePatch {
target_va: dst,
bytes: body,
handler_va,
});
}
}
}
}
regs[reg_idx(Register::RCX).unwrap()] = RegVal::Top;
regs[reg_idx(Register::RDI).unwrap()] = RegVal::Top;
regs[reg_idx(Register::RSI).unwrap()] = RegVal::Top;
continue;
}
if op == Mnemonic::Call && insn.op_count() == 1 && insn.op_kind(0) == OpKind::NearBranch64 {
walk_helper_linear(
pe,
image,
insn.near_branch64(),
handler_va,
regs,
patches,
patch_set,
call_depth + 1,
budget.saturating_sub(count),
);
for r in [
Register::RAX,
Register::RCX,
Register::RDX,
Register::R8,
Register::R9,
Register::R10,
Register::R11,
] {
if let Some(i) = reg_idx(r) {
regs[i] = RegVal::Top;
}
}
continue;
}
if op == Mnemonic::Jmp && insn.op_count() == 1 && insn.op_kind(0) == OpKind::NearBranch64 {
walk_helper_linear(
pe,
image,
insn.near_branch64(),
handler_va,
regs,
patches,
patch_set,
call_depth + 1,
budget.saturating_sub(count),
);
return;
}
if insn.op_count() >= 1 && insn.op_kind(0) == OpKind::Register {
if let Some(di) = reg_idx(insn.op_register(0)) {
regs[di] = RegVal::Top;
}
}
}
}
pub fn extract_handler_patches(image: &[u8], handler_va: u64) -> Vec<ImagePatch> {
use iced_x86::{Decoder, DecoderOptions, FlowControl, Mnemonic, OpKind, Register};
let obj = match goblin::Object::parse(image) {
Ok(o) => o,
_ => return vec![],
};
let pe = match obj {
goblin::Object::PE(p) => p,
_ => return vec![],
};
if !pe.is_64 {
return vec![];
}
let base = pe.image_base as u64;
let mut handler_fo: Option<usize> = None;
for sec in &pe.sections {
let va = base + sec.virtual_address as u64;
let vsz = sec.virtual_size as u64;
if handler_va >= va && handler_va < va + vsz {
handler_fo = Some(sec.pointer_to_raw_data as usize + (handler_va - va) as usize);
break;
}
}
let Some(fo) = handler_fo else {
return vec![];
};
let max_len = 8192usize.min(image.len().saturating_sub(fo));
if max_len == 0 {
return vec![];
}
let bytes_slice = &image[fo..fo + max_len];
let va_to_fo = |va: u64| -> Option<usize> {
for sec in &pe.sections {
let sva = base + sec.virtual_address as u64;
let vsz = sec.virtual_size as u64;
if va >= sva && va < sva + vsz {
return Some(sec.pointer_to_raw_data as usize + (va - sva) as usize);
}
}
None
};
let read_u64 = |va: u64| -> Option<u64> {
let o = va_to_fo(va)?;
if o + 8 > image.len() {
return None;
}
Some(u64::from_le_bytes(image[o..o + 8].try_into().ok()?))
};
let read_bytes = |va: u64, len: usize| -> Option<Vec<u8>> {
let o = va_to_fo(va)?;
if o + len > image.len() {
return None;
}
Some(image[o..o + len].to_vec())
};
let reg_idx = |r: Register| -> Option<usize> {
match r {
Register::RAX => Some(0),
Register::RCX => Some(1),
Register::RDX => Some(2),
Register::RBX => Some(3),
Register::RSP => Some(4),
Register::RBP => Some(5),
Register::RSI => Some(6),
Register::RDI => Some(7),
Register::R8 => Some(8),
Register::R9 => Some(9),
Register::R10 => Some(10),
Register::R11 => Some(11),
Register::R12 => Some(12),
Register::R13 => Some(13),
Register::R14 => Some(14),
Register::R15 => Some(15),
_ => None,
}
};
let merge = |a: [RegVal; 16], b: [RegVal; 16]| -> [RegVal; 16] {
let mut out = [RegVal::Top; 16];
for i in 0..16 {
out[i] = match (a[i], b[i]) {
(x, y) if x == y => x,
_ => RegVal::Top,
};
}
out
};
let mut visited: std::collections::HashMap<u64, [RegVal; 16]> =
std::collections::HashMap::new();
let mut worklist: Vec<(u64, [RegVal; 16])> = Vec::new();
worklist.push((handler_va, [RegVal::Top; 16]));
let mut patches: Vec<ImagePatch> = Vec::new();
let mut patch_set: std::collections::HashSet<(u64, Vec<u8>)> = std::collections::HashSet::new();
let mut total_icount = 0usize;
let icount_cap = 8192usize;
'outer: while let Some((pc_start, mut regs)) = worklist.pop() {
if let Some(prev) = visited.get(&pc_start) {
let merged = merge(*prev, regs);
if merged == *prev {
continue;
}
visited.insert(pc_start, merged);
regs = merged;
} else {
visited.insert(pc_start, regs);
}
let Some(start_off) = (|| -> Option<usize> {
if pc_start < handler_va {
return None;
}
let delta = (pc_start - handler_va) as usize;
if delta >= bytes_slice.len() {
return None;
}
Some(delta)
})() else {
continue;
};
let mut dec = Decoder::with_ip(
64,
&bytes_slice[start_off..],
pc_start,
DecoderOptions::NONE,
);
let mut insn = iced_x86::Instruction::default();
while dec.can_decode() {
if total_icount >= icount_cap {
break 'outer;
}
dec.decode_out(&mut insn);
total_icount += 1;
let op = insn.mnemonic();
if matches!(op, Mnemonic::Ret | Mnemonic::Ud2 | Mnemonic::Int3) {
break;
}
match insn.flow_control() {
FlowControl::UnconditionalBranch => {
if insn.op_count() == 1 && insn.op_kind(0) == OpKind::NearBranch64 {
worklist.push((insn.near_branch64(), regs));
}
break;
}
FlowControl::ConditionalBranch => {
if insn.op_count() == 1 && insn.op_kind(0) == OpKind::NearBranch64 {
worklist.push((insn.near_branch64(), regs));
}
}
FlowControl::IndirectBranch => {
let mut resolved = Vec::<u64>::new();
if insn.op_count() == 1 {
match insn.op_kind(0) {
OpKind::Register => {
if let Some(ri) = reg_idx(insn.op_register(0)) {
if let Some(t) = regs[ri].as_concrete() {
resolved.push(t);
}
}
}
OpKind::Memory => {
if insn.is_ip_rel_memory_operand() {
let ptr_va = insn.ip_rel_memory_address();
if let Some(t) = read_u64(ptr_va) {
resolved.push(t);
}
} else if insn.memory_base() != Register::None {
let base = insn.memory_base();
let base_va = reg_idx(base)
.and_then(|i| regs[i].as_concrete())
.map(|v| v.wrapping_add(insn.memory_displacement64()));
if let Some(table_va) = base_va {
resolved.extend(enumerate_jump_table(
&pe,
image,
table_va,
insn.memory_index_scale() as u64,
));
}
}
}
_ => {}
}
}
for t in resolved {
worklist.push((t, regs));
}
break;
}
_ => {}
}
if op == Mnemonic::Mov && insn.op_count() == 2 && insn.op_kind(0) == OpKind::Register {
let dst = insn.op_register(0);
let Some(di) = reg_idx(dst) else {
continue;
};
match insn.op_kind(1) {
OpKind::Immediate64 => regs[di] = RegVal::Imm(insn.immediate64()),
OpKind::Immediate32to64 => regs[di] = RegVal::Imm(insn.immediate64()),
OpKind::Immediate32 => regs[di] = RegVal::Imm(insn.immediate32() as u64),
OpKind::Register => {
if let Some(si) = reg_idx(insn.op_register(1)) {
regs[di] = regs[si];
} else {
regs[di] = RegVal::Top;
}
}
OpKind::Memory => {
if insn.is_ip_rel_memory_operand() {
let a = insn.ip_rel_memory_address();
if let Some(v) = read_u64(a) {
regs[di] = RegVal::Imm(v);
} else {
regs[di] = RegVal::Top;
}
} else {
regs[di] = RegVal::Top;
}
}
_ => regs[di] = RegVal::Top,
}
continue;
}
if op == Mnemonic::Lea
&& insn.op_count() == 2
&& insn.op_kind(0) == OpKind::Register
&& insn.op_kind(1) == OpKind::Memory
{
let dst = insn.op_register(0);
if let Some(di) = reg_idx(dst) {
if insn.is_ip_rel_memory_operand() {
regs[di] = RegVal::Addr(insn.ip_rel_memory_address());
} else if insn.memory_base() != Register::None
&& insn.memory_index() == Register::None
{
if let Some(si) = reg_idx(insn.memory_base()) {
if let Some(v) = regs[si].as_concrete() {
regs[di] =
RegVal::Addr(v.wrapping_add(insn.memory_displacement64()));
continue;
}
}
regs[di] = RegVal::Top;
} else {
regs[di] = RegVal::Top;
}
}
continue;
}
if op == Mnemonic::Mov && insn.op_count() == 2 && insn.op_kind(0) == OpKind::Memory {
let target_va = if insn.is_ip_rel_memory_operand() {
Some(insn.ip_rel_memory_address())
} else if insn.memory_base() != Register::None
&& insn.memory_index() == Register::None
{
let base_reg = insn.memory_base();
if matches!(base_reg, Register::R8 | Register::RCX) {
None
} else if let Some(bi) = reg_idx(base_reg) {
regs[bi]
.as_concrete()
.map(|v| v.wrapping_add(insn.memory_displacement64()))
} else {
None
}
} else {
None
};
let Some(tva) = target_va else {
continue;
};
let width = insn.memory_size().size();
if width == 0 || width > 8 {
continue;
}
let value: Option<u64> = match insn.op_kind(1) {
OpKind::Immediate8
| OpKind::Immediate8to16
| OpKind::Immediate8to32
| OpKind::Immediate8to64 => Some(insn.immediate8() as i8 as i64 as u64),
OpKind::Immediate16 => Some(insn.immediate16() as u64),
OpKind::Immediate32 | OpKind::Immediate32to64 => {
Some(insn.immediate32() as u64)
}
OpKind::Immediate64 => Some(insn.immediate64()),
OpKind::Register => {
let full = insn.op_register(1).full_register();
reg_idx(full).and_then(|si| regs[si].as_concrete())
}
_ => None,
};
let Some(v) = value else {
continue;
};
let mut buf = v.to_le_bytes().to_vec();
buf.truncate(width);
if patch_set.insert((tva, buf.clone())) {
patches.push(ImagePatch {
target_va: tva,
bytes: buf,
handler_va,
});
}
continue;
}
if op == Mnemonic::Call
&& insn.op_count() == 1
&& insn.op_kind(0) == OpKind::NearBranch64
{
let callee = insn.near_branch64();
let in_text = pe.sections.iter().any(|sec| {
if (sec.characteristics & 0x20000000) == 0 {
return false;
}
let va = base + sec.virtual_address as u64;
callee >= va && callee < va + sec.virtual_size as u64
});
if in_text {
walk_helper_linear(
&pe,
image,
callee,
handler_va,
&mut regs,
&mut patches,
&mut patch_set,
0,
128,
);
}
for r in [
Register::RAX,
Register::RCX,
Register::RDX,
Register::R8,
Register::R9,
Register::R10,
Register::R11,
] {
if let Some(i) = reg_idx(r) {
regs[i] = RegVal::Top;
}
}
continue;
}
if op == Mnemonic::Call
&& insn.op_count() == 1
&& insn.op_kind(0) == OpKind::Memory
&& insn.is_ip_rel_memory_operand()
{
let tgt = insn.ip_rel_memory_address();
let name = pe
.imports
.iter()
.find(|imp| base + imp.offset as u64 == tgt)
.map(|imp| imp.name.to_string())
.unwrap_or_default();
if matches!(
name.as_str(),
"WriteProcessMemory" | "NtWriteVirtualMemory" | "ZwWriteVirtualMemory"
) {
let dst = regs[reg_idx(Register::RDX).unwrap()].as_concrete();
let src = regs[reg_idx(Register::R8).unwrap()].as_concrete();
let cnt = regs[reg_idx(Register::R9).unwrap()].as_concrete();
if let (Some(d), Some(s), Some(c)) = (dst, src, cnt) {
if c > 0 && c <= 0x100000 {
if let Some(body) = read_bytes(s, c as usize) {
if patch_set.insert((d, body.clone())) {
patches.push(ImagePatch {
target_va: d,
bytes: body,
handler_va,
});
}
}
}
}
}
for r in [
Register::RAX,
Register::RCX,
Register::RDX,
Register::R8,
Register::R9,
Register::R10,
Register::R11,
] {
if let Some(i) = reg_idx(r) {
regs[i] = RegVal::Top;
}
}
continue;
}
if insn.has_rep_prefix()
&& matches!(op, Mnemonic::Movsb | Mnemonic::Movsq | Mnemonic::Movsd)
{
let rdi = regs[reg_idx(Register::RDI).unwrap()].as_concrete();
let rsi = regs[reg_idx(Register::RSI).unwrap()].as_concrete();
let rcx = regs[reg_idx(Register::RCX).unwrap()].as_concrete();
if let (Some(dst), Some(src), Some(cnt)) = (rdi, rsi, rcx) {
let unit = match op {
Mnemonic::Movsb => 1,
Mnemonic::Movsd => 4,
Mnemonic::Movsq => 8,
_ => 1,
};
let total = (cnt as usize).saturating_mul(unit);
if total > 0 && total <= 0x10000 {
if let Some(body) = read_bytes(src, total) {
if patch_set.insert((dst, body.clone())) {
patches.push(ImagePatch {
target_va: dst,
bytes: body,
handler_va,
});
}
}
}
}
regs[reg_idx(Register::RCX).unwrap()] = RegVal::Top;
regs[reg_idx(Register::RDI).unwrap()] = RegVal::Top;
regs[reg_idx(Register::RSI).unwrap()] = RegVal::Top;
continue;
}
if insn.op_count() >= 1 && insn.op_kind(0) == OpKind::Register {
if let Some(di) = reg_idx(insn.op_register(0)) {
regs[di] = RegVal::Top;
}
}
} }
patches
}
pub fn extract_all_patches(image: &[u8]) -> Vec<ImagePatch> {
let records = parse_pe64_seh(image);
let mut seen: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
let mut out: Vec<ImagePatch> = Vec::new();
for r in &records {
if let Some(h) = r.handler {
if seen.insert(h) {
out.extend(extract_handler_patches(image, h));
}
}
}
out
}
pub fn tls_callback_addresses(image: &[u8]) -> Vec<u64> {
let obj = match goblin::Object::parse(image) {
Ok(o) => o,
_ => return vec![],
};
let pe = match obj {
goblin::Object::PE(pe) => pe,
_ => return vec![],
};
if !pe.is_64 {
return vec![];
}
let base = pe.image_base as u64;
let oh = match pe.header.optional_header {
Some(o) => o,
None => return vec![],
};
let (tls_va, tls_sz) = match oh.data_directories.get_tls_table() {
Some(d) if d.virtual_address != 0 && d.size >= 0x28 => (d.virtual_address, d.size),
_ => return vec![],
};
let _ = tls_sz;
let rva_to_fo = |rva: u64| -> Option<usize> {
for sec in &pe.sections {
let va = sec.virtual_address as u64;
let sz = sec.virtual_size.max(sec.size_of_raw_data) as u64;
if rva >= va && rva < va + sz {
return Some(sec.pointer_to_raw_data as usize + (rva - va) as usize);
}
}
None
};
let tls_fo = match rva_to_fo(tls_va as u64) {
Some(fo) if fo + 0x28 <= image.len() => fo,
_ => return vec![],
};
let callbacks_va = u64::from_le_bytes(image[tls_fo + 0x18..tls_fo + 0x20].try_into().unwrap());
if callbacks_va == 0 || callbacks_va < base {
return vec![];
}
let callbacks_rva = callbacks_va - base;
let mut cb_fo = match rva_to_fo(callbacks_rva) {
Some(f) => f,
None => return vec![],
};
let mut out: Vec<u64> = Vec::new();
for _ in 0..64 {
if cb_fo + 8 > image.len() {
break;
}
let va = u64::from_le_bytes(image[cb_fo..cb_fo + 8].try_into().unwrap());
if va == 0 {
break;
}
out.push(va);
cb_fo += 8;
}
out
}
pub fn extract_patches_at_candidates(image: &[u8], candidate_vas: &[u64]) -> Vec<ImagePatch> {
let mut seen: std::collections::BTreeSet<(u64, Vec<u8>)> = std::collections::BTreeSet::new();
let mut out: Vec<ImagePatch> = Vec::new();
for &va in candidate_vas {
for p in extract_handler_patches(image, va) {
let key = (p.target_va, p.bytes.clone());
if seen.insert(key) {
out.push(p);
}
}
}
out
}
pub fn extract_all_patches_extended(image: &[u8], extra: &[u64]) -> Vec<ImagePatch> {
let mut vas: Vec<u64> = Vec::new();
vas.extend(handler_addresses(&parse_pe64_seh(image)));
vas.extend(tls_callback_addresses(image));
vas.extend_from_slice(extra);
vas.sort();
vas.dedup();
extract_patches_at_candidates(image, &vas)
}
#[derive(Debug, Clone)]
pub struct FixpointResult {
pub image: Vec<u8>,
pub patches: Vec<ImagePatch>,
pub newly_discovered_fns: Vec<u64>,
pub iterations: usize,
pub converged: bool,
}
pub fn smc_fixpoint<F>(image: &[u8], max_iters: usize, mut discover_fn: F) -> FixpointResult
where
F: FnMut(&[u8]) -> Vec<u64>,
{
let mut working = image.to_vec();
let mut applied: Vec<ImagePatch> = Vec::new();
let mut applied_set: std::collections::HashSet<(u64, Vec<u8>)> =
std::collections::HashSet::new();
let mut discovered: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
let baseline: std::collections::BTreeSet<u64> = discover_fn(&working).into_iter().collect();
let mut iter = 0usize;
let converged = loop {
if iter >= max_iters {
break false;
}
iter += 1;
let fresh = extract_all_patches_extended(&working, &[]);
let mut new_this_round: Vec<ImagePatch> = Vec::new();
for p in fresh {
let key = (p.target_va, p.bytes.clone());
if applied_set.insert(key) {
new_this_round.push(p);
}
}
let applied_count = if new_this_round.is_empty() {
0
} else {
let n = apply_patches(&mut working, &new_this_round);
applied.extend(new_this_round.into_iter());
n
};
let now: std::collections::BTreeSet<u64> = discover_fn(&working).into_iter().collect();
let prev_size = discovered.len();
for va in now.difference(&baseline) {
discovered.insert(*va);
}
let discovered_grew = discovered.len() > prev_size;
if applied_count == 0 && !discovered_grew {
break true;
}
};
FixpointResult {
image: working,
patches: applied,
newly_discovered_fns: discovered.into_iter().collect(),
iterations: iter,
converged,
}
}
pub fn smc_fixpoint_seh_only(image: &[u8], max_iters: usize) -> FixpointResult {
smc_fixpoint(image, max_iters, |img| {
let mut v = handler_addresses(&parse_pe64_seh(img));
v.extend(scope_table_addresses(img));
v.sort_unstable();
v.dedup();
v
})
}
pub fn apply_patches(image: &mut [u8], patches: &[ImagePatch]) -> usize {
let obj = match goblin::Object::parse(image) {
Ok(o) => o,
_ => return 0,
};
let pe = match obj {
goblin::Object::PE(p) => p,
_ => return 0,
};
if !pe.is_64 {
return 0;
}
let base = pe.image_base as u64;
let secs: Vec<(u64, u64, usize, usize)> = pe
.sections
.iter()
.map(|s| {
(
base + s.virtual_address as u64,
s.virtual_size as u64,
s.pointer_to_raw_data as usize,
s.size_of_raw_data as usize,
)
})
.collect();
drop(pe);
let mut n = 0usize;
for p in patches {
let Some((va, vsz, fo, fsz)) = secs
.iter()
.find(|(va, vsz, _, _)| p.target_va >= *va && p.target_va < va + vsz)
.copied()
else {
continue;
};
let in_section = (p.target_va - va) as usize;
if in_section >= fsz {
continue;
}
let end = in_section + p.bytes.len();
if end > fsz || fo + end > image.len() {
continue;
}
image[fo + in_section..fo + end].copy_from_slice(&p.bytes);
n += 1;
let _ = vsz; }
n
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crackmev3_pdata_parses() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
eprintln!("skipping: {:?} not staged", fixture);
return;
}
let bytes = std::fs::read(&fixture).unwrap();
let recs = parse_pe64_seh(&bytes);
assert!(!recs.is_empty(), "expected at least one .pdata record");
for r in &recs {
assert!(
r.func_begin >= 0x180000000 && r.func_begin < 0x181000000,
"func_begin {:#x} out of range",
r.func_begin
);
assert!(
r.func_end > r.func_begin,
"end {:#x} <= begin {:#x}",
r.func_end,
r.func_begin
);
}
}
#[test]
fn crackmev3_handler_analysis_detects_rtl_unwind() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
eprintln!("skipping: crackmev3.pyd not staged");
return;
}
let bytes = std::fs::read(&fixture).unwrap();
let analyses = analyse_all_handlers(&bytes);
assert!(
!analyses.is_empty(),
"expected at least one handler analysed"
);
let h = analyses
.get(&0x180019ca0)
.expect("handler 0x180019ca0 missing");
assert!(
h.insn_count > 50,
"expected > 50 instructions in personality handler, got {}",
h.insn_count
);
assert!(
h.iat_calls.iter().any(|s| s == "RtlUnwindEx"),
"expected RtlUnwindEx IAT call, got: {:?}",
h.iat_calls
);
for (va, a) in &analyses {
assert!(
!a.is_smc_candidate(),
"handler {:#x} wrongly flagged as SMC: {:?}",
va,
a
);
}
}
#[test]
fn crackmev3_fixpoint_converges_with_zero_patches() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
eprintln!("skipping: crackmev3.pyd not staged");
return;
}
let bytes = std::fs::read(&fixture).unwrap();
let r = smc_fixpoint_seh_only(&bytes, 16);
assert!(r.converged, "expected convergence on a no-SMC fixture");
assert_eq!(r.patches.len(), 0);
assert_eq!(r.newly_discovered_fns.len(), 0);
assert_eq!(r.iterations, 1);
assert_eq!(r.image, bytes, "image must be unchanged");
}
#[test]
fn fixpoint_stops_at_max_iters() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
eprintln!("skipping: crackmev3.pyd not staged");
return;
}
let bytes = std::fs::read(&fixture).unwrap();
let mut counter = 0u64;
let r = smc_fixpoint(&bytes, 4, |_| {
counter = counter.wrapping_add(1);
vec![0x180_0000_0000 + counter]
});
assert!(
!r.converged,
"expected non-convergence under adversarial oracle"
);
assert_eq!(r.iterations, 4);
assert!(r.newly_discovered_fns.len() >= 4);
}
#[test]
fn msvc_cxx_handler_detects_msc_exception_code() {
let path = std::path::Path::new("/tmp/clang-ar/clang-apply-replacements.exe");
if !path.exists() {
eprintln!("skipping: {:?} not staged", path);
return;
}
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(_) => return,
};
let analyses = analyse_all_handlers(&bytes);
let found = analyses
.values()
.any(|a| a.exc_code_triggers.contains(&0xe06d7363));
assert!(
found,
"expected at least one handler to case-split on MSVC C++ exception code 0xe06d7363"
);
}
#[test]
fn crackmev3_no_runtime_handler_registration_or_dispctx() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
return;
}
let bytes = std::fs::read(&fixture).unwrap();
let analyses = analyse_all_handlers(&bytes);
for (va, a) in &analyses {
assert!(
!a.registers_runtime_tables,
"handler {:#x} wrongly flagged registers_runtime_tables",
va
);
assert!(
!a.reads_dispatcher_context,
"handler {:#x} wrongly flagged reads_dispatcher_context",
va
);
}
}
#[test]
fn crackmev3_scope_tables_parse() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
eprintln!("skipping: crackmev3.pyd not staged");
return;
}
let bytes = std::fs::read(&fixture).unwrap();
let extra = scope_table_addresses(&bytes);
assert!(
!extra.is_empty(),
"expected at least one scope-table-derived address"
);
for a in &extra {
assert!(
*a >= 0x180000000 && *a < 0x181000000,
"scope address {:#x} out of image range",
a
);
}
}
#[test]
fn crackmev3_no_patches_from_handlers() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
eprintln!("skipping: crackmev3.pyd not staged");
return;
}
let bytes = std::fs::read(&fixture).unwrap();
let patches = extract_all_patches(&bytes);
assert!(
patches.is_empty(),
"expected zero patches on crackmev3 v4, got {} (first: {:?})",
patches.len(),
patches.first()
);
}
fn abstract_patches_from(bytes: &[u8]) -> Vec<ImagePatch> {
use iced_x86::{Decoder, DecoderOptions, Mnemonic, OpKind, Register};
let mut regs: [RegVal; 16] = [RegVal::Top; 16];
let mut patches: Vec<ImagePatch> = Vec::new();
let reg_idx = |r: Register| -> Option<usize> {
match r {
Register::RAX => Some(0),
Register::RCX => Some(1),
Register::RDX => Some(2),
Register::RBX => Some(3),
Register::RSP => Some(4),
Register::RBP => Some(5),
Register::RSI => Some(6),
Register::RDI => Some(7),
Register::R8 => Some(8),
Register::R9 => Some(9),
Register::R10 => Some(10),
Register::R11 => Some(11),
Register::R12 => Some(12),
Register::R13 => Some(13),
Register::R14 => Some(14),
Register::R15 => Some(15),
_ => None,
}
};
let mut dec = Decoder::with_ip(64, bytes, 0x1000, DecoderOptions::NONE);
let mut insn = iced_x86::Instruction::default();
while dec.can_decode() {
dec.decode_out(&mut insn);
let op = insn.mnemonic();
if matches!(op, Mnemonic::Ret | Mnemonic::Ud2 | Mnemonic::Int3) {
break;
}
if op == Mnemonic::Mov && insn.op_count() == 2 && insn.op_kind(0) == OpKind::Register {
let Some(di) = reg_idx(insn.op_register(0)) else {
continue;
};
regs[di] = match insn.op_kind(1) {
OpKind::Immediate64 => RegVal::Imm(insn.immediate64()),
OpKind::Immediate32to64 => RegVal::Imm(insn.immediate64()),
OpKind::Register => reg_idx(insn.op_register(1))
.map(|i| regs[i])
.unwrap_or(RegVal::Top),
_ => RegVal::Top,
};
}
if op == Mnemonic::Mov && insn.op_count() == 2 && insn.op_kind(0) == OpKind::Memory {
let base = insn.memory_base();
if !matches!(base, Register::R8 | Register::RCX) {
let tva = reg_idx(base)
.and_then(|i| regs[i].as_concrete())
.map(|v| v.wrapping_add(insn.memory_displacement64()));
if let Some(tva) = tva {
let width = insn.memory_size().size();
let v: Option<u64> = match insn.op_kind(1) {
OpKind::Immediate8 | OpKind::Immediate8to64 => {
Some(insn.immediate8() as u64)
}
OpKind::Immediate32 | OpKind::Immediate32to64 => {
Some(insn.immediate32() as u64)
}
OpKind::Immediate64 => Some(insn.immediate64()),
OpKind::Register => {
reg_idx(insn.op_register(1)).and_then(|i| regs[i].as_concrete())
}
_ => None,
};
if let Some(v) = v {
let mut buf = v.to_le_bytes().to_vec();
buf.truncate(width);
patches.push(ImagePatch {
target_va: tva,
bytes: buf,
handler_va: 0,
});
}
}
}
}
}
patches
}
#[test]
fn abstract_interpreter_recovers_two_byte_patches() {
let bytes = [
0x48, 0xB8, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0x40, 0x10, 0x90, 0xC6, 0x40, 0x11, 0x90, 0xC3, ];
let patches = abstract_patches_from(&bytes);
assert_eq!(patches.len(), 2);
assert_eq!(patches[0].target_va, 0x00400010);
assert_eq!(patches[0].bytes, vec![0x90]);
assert_eq!(patches[1].target_va, 0x00400011);
assert_eq!(patches[1].bytes, vec![0x90]);
}
#[test]
fn call_follow_captures_helper_patch() {
use iced_x86::{Decoder, DecoderOptions, Mnemonic, OpKind, Register};
let mut bytes = vec![0u8; 0x40];
let m = [0x48u8, 0xB9, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00];
bytes[0..m.len()].copy_from_slice(&m);
let m = [0x48u8, 0xBA, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
bytes[0x0A..0x0A + m.len()].copy_from_slice(&m);
bytes[0x14] = 0xE8;
let rel: i32 = 0x0020 - (0x14 + 5);
bytes[0x15..0x19].copy_from_slice(&rel.to_le_bytes());
bytes[0x19] = 0xC3;
bytes[0x20] = 0x88;
bytes[0x21] = 0x11;
bytes[0x22] = 0xC3;
fn reg_idx(r: Register) -> Option<usize> {
match r {
Register::RAX => Some(0),
Register::RCX => Some(1),
Register::RDX => Some(2),
Register::RBX => Some(3),
Register::RSP => Some(4),
Register::RBP => Some(5),
Register::RSI => Some(6),
Register::RDI => Some(7),
Register::R8 => Some(8),
Register::R9 => Some(9),
Register::R10 => Some(10),
Register::R11 => Some(11),
Register::R12 => Some(12),
Register::R13 => Some(13),
Register::R14 => Some(14),
Register::R15 => Some(15),
_ => None,
}
}
fn walk(
bytes: &[u8],
start_va: u64,
entry_off: usize,
regs: &mut [RegVal; 16],
patches: &mut Vec<ImagePatch>,
patch_set: &mut std::collections::HashSet<(u64, Vec<u8>)>,
depth: u32,
) {
if depth >= 3 {
return;
}
let mut dec = Decoder::with_ip(
64,
&bytes[entry_off..],
start_va + entry_off as u64,
DecoderOptions::NONE,
);
let mut insn = iced_x86::Instruction::default();
while dec.can_decode() {
dec.decode_out(&mut insn);
let op = insn.mnemonic();
if matches!(op, Mnemonic::Ret | Mnemonic::Ud2 | Mnemonic::Int3) {
return;
}
if op == Mnemonic::Mov
&& insn.op_count() == 2
&& insn.op_kind(0) == OpKind::Register
{
let Some(di) = reg_idx(insn.op_register(0)) else {
continue;
};
regs[di] = match insn.op_kind(1) {
OpKind::Immediate64 => RegVal::Imm(insn.immediate64()),
OpKind::Immediate32to64 => RegVal::Imm(insn.immediate64()),
_ => RegVal::Top,
};
continue;
}
if op == Mnemonic::Mov && insn.op_count() == 2 && insn.op_kind(0) == OpKind::Memory
{
let base = insn.memory_base();
if !matches!(base, Register::R8) {
let tva = reg_idx(base)
.and_then(|i| regs[i].as_concrete())
.map(|v| v.wrapping_add(insn.memory_displacement64()));
if let Some(tva) = tva {
let v: Option<u64> = match insn.op_kind(1) {
OpKind::Register => {
let full = insn.op_register(1).full_register();
reg_idx(full).and_then(|i| regs[i].as_concrete())
}
_ => None,
};
if let Some(v) = v {
let buf = vec![(v & 0xff) as u8];
if patch_set.insert((tva, buf.clone())) {
patches.push(ImagePatch {
target_va: tva,
bytes: buf,
handler_va: 0,
});
}
}
}
}
continue;
}
if op == Mnemonic::Call
&& insn.op_count() == 1
&& insn.op_kind(0) == OpKind::NearBranch64
{
let cb = insn.near_branch64() - start_va;
walk(
bytes,
start_va,
cb as usize,
regs,
patches,
patch_set,
depth + 1,
);
continue;
}
}
}
let mut regs: [RegVal; 16] = [RegVal::Top; 16];
let mut patches: Vec<ImagePatch> = Vec::new();
let mut pset: std::collections::HashSet<(u64, Vec<u8>)> = std::collections::HashSet::new();
walk(&bytes, 0x1000, 0, &mut regs, &mut patches, &mut pset, 0);
assert_eq!(
patches.len(),
1,
"expected one patch from helper, got: {:?}",
patches
);
assert_eq!(patches[0].target_va, 0x500000);
assert_eq!(patches[0].bytes, vec![0xAB]);
}
#[test]
fn indirect_branch_resolver_smokes_on_msvc_binaries() {
for candidate in [
"/tmp/clang-ar/clang-apply-replacements.exe",
"/Users/shane/repos/qbridge/node_modules/7zip-bin/win/x64/7za.exe",
] {
let p = std::path::Path::new(candidate);
if !p.exists() {
continue;
}
let bytes = match std::fs::read(p) {
Ok(b) => b,
Err(_) => continue,
};
let t0 = std::time::Instant::now();
let patches = extract_all_patches(&bytes);
let elapsed = t0.elapsed();
assert!(
elapsed.as_secs() < 30,
"extract_all_patches on {:?} took {:?} — jump-table resolver may be unbounded",
p.file_name(),
elapsed
);
let _ = patches;
}
}
#[test]
fn control_flow_interp_explores_both_branches() {
let bytes: [u8; 0x20] = [
0x48, 0xB8, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x81, 0x39, 0x03, 0x00, 0x00, 0x80,
0x74, 0x06,
0xC6, 0x40, 0x10, 0x90, 0xC3, 0x90, 0xC6, 0x40, 0x20, 0xCC, 0xC3, 0x90, 0x90, 0x90,
];
let patches = cf_abstract_patches_from(&bytes);
assert_eq!(
patches.len(),
2,
"both branches should yield a patch: {:?}",
patches
);
let tvas: std::collections::BTreeSet<u64> = patches.iter().map(|p| p.target_va).collect();
assert_eq!(tvas, vec![0x00400010, 0x00400020].into_iter().collect());
}
fn cf_abstract_patches_from(bytes: &[u8]) -> Vec<ImagePatch> {
use iced_x86::{Decoder, DecoderOptions, FlowControl, Mnemonic, OpKind, Register};
fn merge(a: [RegVal; 16], b: [RegVal; 16]) -> [RegVal; 16] {
let mut out = [RegVal::Top; 16];
for i in 0..16 {
out[i] = if a[i] == b[i] { a[i] } else { RegVal::Top };
}
out
}
let reg_idx = |r: Register| -> Option<usize> {
match r {
Register::RAX => Some(0),
Register::RCX => Some(1),
Register::RDX => Some(2),
Register::RBX => Some(3),
Register::RSP => Some(4),
Register::RBP => Some(5),
Register::RSI => Some(6),
Register::RDI => Some(7),
Register::R8 => Some(8),
Register::R9 => Some(9),
Register::R10 => Some(10),
Register::R11 => Some(11),
Register::R12 => Some(12),
Register::R13 => Some(13),
Register::R14 => Some(14),
Register::R15 => Some(15),
_ => None,
}
};
let start_va = 0x1000u64;
let mut worklist: Vec<(u64, [RegVal; 16])> = vec![(start_va, [RegVal::Top; 16])];
let mut visited: std::collections::HashMap<u64, [RegVal; 16]> =
std::collections::HashMap::new();
let mut patches: Vec<ImagePatch> = Vec::new();
let mut patch_set: std::collections::HashSet<(u64, Vec<u8>)> =
std::collections::HashSet::new();
while let Some((pc, mut regs)) = worklist.pop() {
if let Some(prev) = visited.get(&pc) {
let m = merge(*prev, regs);
if m == *prev {
continue;
}
visited.insert(pc, m);
regs = m;
} else {
visited.insert(pc, regs);
}
let off = (pc - start_va) as usize;
if off >= bytes.len() {
continue;
}
let mut dec = Decoder::with_ip(64, &bytes[off..], pc, DecoderOptions::NONE);
let mut insn = iced_x86::Instruction::default();
while dec.can_decode() {
dec.decode_out(&mut insn);
let op = insn.mnemonic();
if matches!(op, Mnemonic::Ret | Mnemonic::Ud2 | Mnemonic::Int3) {
break;
}
match insn.flow_control() {
FlowControl::UnconditionalBranch => {
if insn.op_count() == 1 && insn.op_kind(0) == OpKind::NearBranch64 {
worklist.push((insn.near_branch64(), regs));
}
break;
}
FlowControl::ConditionalBranch => {
if insn.op_count() == 1 && insn.op_kind(0) == OpKind::NearBranch64 {
worklist.push((insn.near_branch64(), regs));
}
}
FlowControl::IndirectBranch => break,
_ => {}
}
if op == Mnemonic::Mov
&& insn.op_count() == 2
&& insn.op_kind(0) == OpKind::Register
{
let Some(di) = reg_idx(insn.op_register(0)) else {
continue;
};
regs[di] = match insn.op_kind(1) {
OpKind::Immediate64 => RegVal::Imm(insn.immediate64()),
OpKind::Immediate32to64 => RegVal::Imm(insn.immediate64()),
OpKind::Register => reg_idx(insn.op_register(1))
.map(|i| regs[i])
.unwrap_or(RegVal::Top),
_ => RegVal::Top,
};
}
if op == Mnemonic::Mov && insn.op_count() == 2 && insn.op_kind(0) == OpKind::Memory
{
let base = insn.memory_base();
if !matches!(base, Register::R8 | Register::RCX) {
let tva = reg_idx(base)
.and_then(|i| regs[i].as_concrete())
.map(|v| v.wrapping_add(insn.memory_displacement64()));
if let Some(tva) = tva {
let width = insn.memory_size().size();
let v: Option<u64> = match insn.op_kind(1) {
OpKind::Immediate8 | OpKind::Immediate8to64 => {
Some(insn.immediate8() as u64)
}
OpKind::Immediate32 | OpKind::Immediate32to64 => {
Some(insn.immediate32() as u64)
}
OpKind::Immediate64 => Some(insn.immediate64()),
OpKind::Register => {
reg_idx(insn.op_register(1)).and_then(|i| regs[i].as_concrete())
}
_ => None,
};
if let Some(v) = v {
let mut buf = v.to_le_bytes().to_vec();
buf.truncate(width);
if patch_set.insert((tva, buf.clone())) {
patches.push(ImagePatch {
target_va: tva,
bytes: buf,
handler_va: 0,
});
}
}
}
}
}
}
}
patches
}
#[test]
fn apply_patches_oob_is_noop() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
eprintln!("skipping: crackmev3.pyd not staged");
return;
}
let mut bytes = std::fs::read(&fixture).unwrap();
let bogus = vec![
ImagePatch {
target_va: 0xDEADBEEF_00000000,
bytes: vec![0u8; 4],
handler_va: 0,
},
ImagePatch {
target_va: 0,
bytes: vec![0u8; 4],
handler_va: 0,
},
];
let n = apply_patches(&mut bytes, &bogus);
assert_eq!(n, 0, "out-of-bounds patches should be ignored");
}
#[test]
fn handler_addresses_deduped() {
let rs = vec![
SehRecord {
func_begin: 0x1000,
func_end: 0x1100,
handler: Some(0x2000),
scope_table: None,
version: 1,
flags: 1,
},
SehRecord {
func_begin: 0x1200,
func_end: 0x1300,
handler: Some(0x2000),
scope_table: None,
version: 1,
flags: 1,
},
SehRecord {
func_begin: 0x1400,
func_end: 0x1500,
handler: None,
scope_table: None,
version: 1,
flags: 0,
},
SehRecord {
func_begin: 0x1600,
func_end: 0x1700,
handler: Some(0x3000),
scope_table: None,
version: 1,
flags: 1,
},
];
let addrs = handler_addresses(&rs);
assert_eq!(addrs, vec![0x2000, 0x3000]);
}
#[test]
fn tls_callback_addresses_empty_on_non_tls_binary() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
eprintln!("skipping: {:?} not staged", fixture);
return;
}
let bytes = std::fs::read(&fixture).unwrap();
let cbs = tls_callback_addresses(&bytes);
assert!(
cbs.len() <= 4,
"unexpectedly many TLS callbacks: {}",
cbs.len()
);
}
#[test]
fn extract_patches_at_candidates_dedups() {
let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let fixture = manifest
.parent()
.unwrap()
.join("test-harness/fixtures/crackmev3.pyd");
if !fixture.exists() {
return;
}
let bytes = std::fs::read(&fixture).unwrap();
let via_seh = extract_all_patches(&bytes);
let via_general = extract_all_patches_extended(&bytes, &[]);
assert!(
via_general.len() >= via_seh.len(),
"general patch set smaller than SEH-only: {} < {}",
via_general.len(),
via_seh.len()
);
}
}