use crate::model::Binary;
use serde::Serialize;
use std::sync::OnceLock;
#[derive(Debug, Clone, Serialize)]
pub struct Hit {
pub name: String,
pub category: String, pub offset: u64,
pub section: Option<String>,
pub note: String,
}
pub fn scan(bin: &Binary, bytes: &[u8]) -> Vec<Hit> {
let mut hits = Vec::new();
for sig in fixed_signatures() {
let mut base = 0usize;
while let Some(pos) = find(&bytes[base..], &sig.needle) {
let off = (base + pos) as u64;
hits.push(Hit {
name: sig.name.to_string(),
category: sig.category.to_string(),
offset: off,
section: section_at(bin, off),
note: sig.note.to_string(),
});
base += pos + 1;
if hits.len() > 4096 {
break;
}
}
}
let mut base = 1usize;
while let Some(pos) = find(&bytes[base..], b"MZ") {
let off = base + pos;
if is_pe_at(bytes, off) {
hits.push(Hit {
name: "embedded PE".into(),
category: "embedded".into(),
offset: off as u64,
section: section_at(bin, off as u64),
note: "bundled or dropped PE image".into(),
});
}
base = off + 2;
if base >= bytes.len() {
break;
}
}
hits.sort_by_key(|h| h.offset);
hits
}
fn is_pe_at(bytes: &[u8], off: usize) -> bool {
if off + 0x40 > bytes.len() || &bytes[off..off + 2] != b"MZ" {
return false;
}
let e_lfanew = u32::from_le_bytes([
bytes[off + 0x3c],
bytes[off + 0x3d],
bytes[off + 0x3e],
bytes[off + 0x3f],
]) as usize;
let pe = off + e_lfanew;
(0x40..0x1000).contains(&e_lfanew) && pe + 4 <= bytes.len() && &bytes[pe..pe + 4] == b"PE\0\0"
}
fn section_at(bin: &Binary, off: u64) -> Option<String> {
bin.sections
.iter()
.find(|s| s.file_size > 0 && off >= s.file_off && off < s.file_off + s.file_size)
.map(|s| s.name.clone())
}
struct Sig {
name: &'static str,
category: &'static str,
note: &'static str,
needle: Vec<u8>,
}
fn fixed_signatures() -> &'static [Sig] {
static S: OnceLock<Vec<Sig>> = OnceLock::new();
S.get_or_init(|| {
let mut v: Vec<Sig> = Vec::new();
v.push(Sig {
name: "AES S-box",
category: "crypto",
note: "AES / Rijndael substitution table",
needle: aes_sbox().to_vec(),
});
v.push(Sig {
name: "AES inverse S-box",
category: "crypto",
note: "AES decryption table",
needle: aes_inv_sbox().to_vec(),
});
v.push(Sig {
name: "AES Te0 / T-table",
category: "crypto",
note: "AES round T-table (first entries)",
needle: vec![0xc6, 0x63, 0x63, 0xa5, 0xf8, 0x7c, 0x7c, 0x84],
});
push_words(
&mut v,
"SHA-256 init",
"hash",
"H0..H7 initial hash values",
&[
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
],
);
push_words(
&mut v,
"SHA-256 K",
"hash",
"round constants (first 8)",
&[
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5,
],
);
push_words(
&mut v,
"SHA-1 init",
"hash",
"H0..H4 initial hash values",
&[0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0],
);
push_words(
&mut v,
"MD5 T-table",
"hash",
"sin() round constants (first 4)",
&[0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee],
);
push_words_le(
&mut v,
"CRC32 table",
"hash",
"IEEE 802.3 CRC table (reversed poly)",
&[0x00000000, 0x77073096, 0xee0e612c, 0x990951ba],
);
v.push(Sig {
name: "Base64 alphabet",
category: "encoding",
note: "standard Base64 charset",
needle: b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".to_vec(),
});
v.push(Sig {
name: "Base64 URL-safe alphabet",
category: "encoding",
note: "URL-safe Base64 charset",
needle: b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".to_vec(),
});
v.push(sig("UPX!", "packer", "UPX packer marker", b"UPX!"));
v.push(sig("MPRESS", "packer", "MPRESS packer marker", b"MPRESS1"));
v.push(sig(
".NET metadata (BSJB)",
"embedded",
"CLR metadata header",
b"BSJB",
));
v.push(sig(
"PK zip",
"embedded",
"ZIP / JAR / APK / OOXML",
b"PK\x03\x04",
));
v.push(sig(
"7-Zip",
"embedded",
"7z archive",
b"7z\xbc\xaf\x27\x1c",
));
v.push(sig("gzip", "embedded", "gzip stream", b"\x1f\x8b\x08"));
v.push(sig("PNG", "embedded", "PNG image", b"\x89PNG\r\n\x1a\n"));
v.push(sig("PDF", "embedded", "PDF document", b"%PDF-"));
v
})
}
fn sig(name: &'static str, category: &'static str, note: &'static str, needle: &[u8]) -> Sig {
Sig {
name,
category,
note,
needle: needle.to_vec(),
}
}
fn push_words(
v: &mut Vec<Sig>,
name: &'static str,
cat: &'static str,
note: &'static str,
words: &[u32],
) {
let be: Vec<u8> = words.iter().flat_map(|w| w.to_be_bytes()).collect();
let le: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
v.push(Sig {
name,
category: cat,
note,
needle: be,
});
v.push(Sig {
name,
category: cat,
note,
needle: le,
});
}
fn push_words_le(
v: &mut Vec<Sig>,
name: &'static str,
cat: &'static str,
note: &'static str,
words: &[u32],
) {
let le: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
v.push(Sig {
name,
category: cat,
note,
needle: le,
});
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
fn aes_sbox() -> &'static [u8; 256] {
static SBOX: OnceLock<[u8; 256]> = OnceLock::new();
SBOX.get_or_init(|| {
let mut sbox = [0u8; 256];
let mut p: u8 = 1;
let mut q: u8 = 1;
loop {
p = p ^ (p << 1) ^ if p & 0x80 != 0 { 0x1b } else { 0 };
q ^= q << 1;
q ^= q << 2;
q ^= q << 4;
if q & 0x80 != 0 {
q ^= 0x09;
}
let xformed =
q ^ q.rotate_left(1) ^ q.rotate_left(2) ^ q.rotate_left(3) ^ q.rotate_left(4);
sbox[p as usize] = xformed ^ 0x63;
if p == 1 {
break;
}
}
sbox[0] = 0x63;
sbox
})
}
fn aes_inv_sbox() -> &'static [u8; 256] {
static INV: OnceLock<[u8; 256]> = OnceLock::new();
INV.get_or_init(|| {
let sbox = aes_sbox();
let mut inv = [0u8; 256];
for (i, &s) in sbox.iter().enumerate() {
inv[s as usize] = i as u8;
}
inv
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aes_sbox_is_correct() {
let s = aes_sbox();
assert_eq!(s[0x00], 0x63);
assert_eq!(s[0x01], 0x7c);
assert_eq!(s[0x53], 0xed);
assert_eq!(s[0xff], 0x16);
}
#[test]
fn inv_sbox_inverts() {
let s = aes_sbox();
let inv = aes_inv_sbox();
for i in 0..=255u8 {
assert_eq!(inv[s[i as usize] as usize], i);
}
}
#[test]
fn finds_base64_and_sbox() {
let bin =
crate::model::Binary::stub(crate::model::Format::Unknown, crate::model::Arch::Other);
let mut data = vec![0u8; 32];
data.extend_from_slice(aes_sbox());
data.extend_from_slice(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
let hits = scan(&bin, &data);
assert!(hits.iter().any(|h| h.name == "AES S-box"));
assert!(hits.iter().any(|h| h.name == "Base64 alphabet"));
}
}