//! Generate a fresh, unique signature at a known address — the tool's product output. //! //! Walk instructions from the function entry, emit their bytes, but wildcard the //! position-dependent ones (RIP-relative displacements and near-branch targets) so the //! signature survives relocation. Stop as soon as the accumulated pattern matches exactly //! once in the binary. Pure integer/byte work; the only float-free dependency is the decoder. use crate::elf::CodeImage; use crate::sig::Pattern; use iced_x86::{ConstantOffsets, Decoder, DecoderOptions, Instruction, OpKind}; /// Call `emit(k, masked)` for each byte `k` of a just-decoded instruction, where `masked` is true for /// the position-dependent bytes — a RIP-relative displacement or a near-branch target — that move on /// relocation and so must be wildcarded (in a signature) or normalized away (in a cross-build digest). /// The single source of truth for that masking, shared by `make_sig` and `normalized_digest` so the /// signature the tool ships and the digest it compares builds with can never drift apart. fn for_each_byte( instr: &Instruction, co: &ConstantOffsets, ilen: usize, mut emit: impl FnMut(usize, bool), ) { let rip_rel = instr.is_ip_rel_memory_operand(); let branch = (0..instr.op_count()).any(|i| { matches!( instr.op_kind(i), OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64 ) }); for k in 0..ilen { let in_disp = rip_rel && co.has_displacement() && k >= co.displacement_offset() && k < co.displacement_offset() + co.displacement_size(); let in_imm = branch && co.has_immediate() && k >= co.immediate_offset() && k < co.immediate_offset() + co.immediate_size(); emit(k, in_disp || in_imm); } } /// Build a unique signature for the function at `vaddr`, or `None` if it can't be made unique /// within `max_bytes`. pub fn make_sig(img: &CodeImage, vaddr: u64, max_bytes: usize) -> Option { let code = img.code_at(vaddr)?; let mut decoder = Decoder::with_ip(64, code, vaddr, DecoderOptions::NONE); let mut instr = Instruction::default(); let mut tokens: Vec = Vec::new(); let mut off = 0usize; while decoder.can_decode() && off < max_bytes { decoder.decode_out(&mut instr); let ilen = instr.len(); if ilen == 0 || off + ilen > code.len() { break; } let co = decoder.get_constant_offsets(&instr); for_each_byte(&instr, &co, ilen, |k, masked| { tokens.push(if masked { "?".into() } else { format!("{:02X}", code[off + k]) }); }); off += ilen; if let Ok(pat) = Pattern::parse(&tokens.join(" ")) && img.find(&pat).len() == 1 { return Some(tokens.join(" ")); } } None } /// FNV-1a 64-bit digest of the function `[start, end)`, with the position-dependent bytes masked to a /// constant (the same masking `make_sig` wildcards). The digest is therefore recompilation-shift- /// invariant: a function whose body is unchanged but whose call/jump targets moved with the surrounding /// layout digests IDENTICALLY across builds, while a genuine opcode/operand edit changes it. This is the /// per-function identity `classify-change` compares two builds by. `None` if nothing decodes. pub fn normalized_digest(img: &CodeImage, start: u64, end: u64) -> Option { let code = img.code_at(start)?; let span = (end.saturating_sub(start) as usize).min(code.len()); digest_code(&code[..span], start) } /// The shared core of `normalized_digest`: FNV-1a over a raw code slice with the position-dependent /// bytes masked. Split out from the `CodeImage` wrapper so it can be unit-tested on hand-assembled /// bytes. `None` if nothing decodes. fn digest_code(code: &[u8], ip: u64) -> Option { if code.is_empty() { return None; } let mut decoder = Decoder::with_ip(64, code, ip, DecoderOptions::NONE); let mut instr = Instruction::default(); let mut h: u64 = 0xcbf29ce484222325; // FNV-1a offset basis let mut off = 0usize; let mut decoded = false; while decoder.can_decode() && off < code.len() { decoder.decode_out(&mut instr); let ilen = instr.len(); if ilen == 0 || off + ilen > code.len() { break; } let co = decoder.get_constant_offsets(&instr); for_each_byte(&instr, &co, ilen, |k, masked| { // A masked byte hashes as a fixed 0 regardless of its build-specific value; kept in place // (not skipped) so instruction length still participates in the digest. let byte = if masked { 0 } else { code[off + k] }; h = (h ^ byte as u64).wrapping_mul(0x100000001b3); }); off += ilen; decoded = true; } decoded.then_some(h) } #[cfg(test)] mod tests { use super::digest_code; #[test] fn masks_call_target_shift_invariant() { // Two builds of the same function whose only difference is where its `call rel32` lands (the // callee moved with the layout) must digest IDENTICALLY — the shift-invariance the whole // command rests on. `E8 xx xx xx xx` = call; the 4 immediate bytes differ, nothing else. let a = digest_code(&[0xE8, 0x11, 0x22, 0x33, 0x44, 0xC3], 0x1000); // call +0x44332211 ; ret let b = digest_code(&[0xE8, 0x55, 0x66, 0x77, 0x00, 0xC3], 0x1000); // call +0x00776655 ; ret assert_eq!(a, b); assert!(a.is_some()); } #[test] fn masks_rip_relative_displacement() { // `lea rax, [rip+disp]` — the displacement moves every build; masking it makes the two equal. let a = digest_code(&[0x48, 0x8D, 0x05, 0x11, 0x22, 0x33, 0x44, 0xC3], 0x1000); let b = digest_code(&[0x48, 0x8D, 0x05, 0xAA, 0xBB, 0xCC, 0x00, 0xC3], 0x1000); assert_eq!(a, b); } #[test] fn distinguishes_opcode_change() { // A real body edit (add vs sub) must change the digest — the masking must NOT wash it out. let add = digest_code(&[0x48, 0x01, 0xD8, 0xC3], 0x1000); // add rax, rbx ; ret let sub = digest_code(&[0x48, 0x29, 0xD8, 0xC3], 0x1000); // sub rax, rbx ; ret assert_ne!(add, sub); } #[test] fn distinguishes_non_branch_immediate() { // A plain immediate (`mov eax, IMM`) is NOT masked — it's part of the function's identity, so a // changed constant is a real change, unlike a relocated branch/RIP displacement. let a = digest_code(&[0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3], 0x1000); // mov eax, 1 ; ret let b = digest_code(&[0xB8, 0x02, 0x00, 0x00, 0x00, 0xC3], 0x1000); // mov eax, 2 ; ret assert_ne!(a, b); } #[test] fn empty_is_none() { assert_eq!(digest_code(&[], 0x1000), None); } }