163 lines
6.9 KiB
Rust
163 lines
6.9 KiB
Rust
//! 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<String> {
|
|
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<String> = 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<u64> {
|
|
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<u64> {
|
|
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);
|
|
}
|
|
}
|