use std::collections::HashMap;
use crate::page::PAGE_SIZE;
use crate::store::PageStore;
pub const SECTOR_SIZE: usize = 512;
pub const SECTORS_PER_PAGE: usize = PAGE_SIZE / SECTOR_SIZE;
const TRAILER_START: usize = 0xFF0;
const LEARN_PURITY: f64 = 1.0;
const PURE_AP_TYPES: [u8; 4] = [0x40, 0x43, 0x48, 0x4D];
#[inline]
fn ap_base(pn: u64, si: usize, bv: u8) -> u8 {
let p16 = (pn % 16) as u8;
let offset = p16 / 2 * 4; bv.wrapping_add(pn as u8)
.wrapping_add(si as u8)
.wrapping_sub(offset)
}
fn dominant_step(sec: &[u8]) -> u8 {
let mut hist = [0u32; 256];
for w in sec.windows(2) {
let diff = w[1].wrapping_sub(w[0]);
hist[diff as usize] += 1;
}
hist.iter()
.enumerate()
.max_by_key(|&(_, &v)| v)
.map(|(i, _)| i as u8)
.unwrap_or(0)
}
fn recover_base(sec: &[u8], step: u8) -> u8 {
let mut hist = [0u32; 256];
for (i, &b) in sec.iter().enumerate() {
let candidate = b.wrapping_sub((i as u8).wrapping_mul(step));
hist[candidate as usize] += 1;
}
hist.iter()
.enumerate()
.max_by_key(|&(_, &v)| v)
.map(|(i, _)| i as u8)
.unwrap_or(0)
}
fn sector_purity(sec: &[u8], base: u8, step: u8) -> f64 {
let hits = sec
.iter()
.enumerate()
.filter(|&(i, &b)| b == base.wrapping_add((i as u8).wrapping_mul(step)))
.count();
hits as f64 / sec.len() as f64
}
fn recover_step_peak(sec: &[u8], base: u8) -> (u8, usize) {
let mut best_step = 0u8;
let mut best_count = 0usize;
for step in 0u8..=255 {
let mut hist = [0u16; 256];
for (i, &b) in sec.iter().enumerate() {
let plain = b
.wrapping_sub(base)
.wrapping_sub((i as u8).wrapping_mul(step));
hist[plain as usize] += 1;
}
let peak = hist.iter().copied().max().unwrap_or(0) as usize;
if peak > best_count {
best_count = peak;
best_step = step;
}
}
(best_step, best_count)
}
fn apply_stream(sec: &[u8], base: u8, step: u8, out: &mut [u8]) {
for (i, (&stored, plain)) in sec.iter().zip(out.iter_mut()).enumerate() {
*plain = stored
.wrapping_sub(base)
.wrapping_sub((i as u8).wrapping_mul(step));
}
}
fn score_bv_candidates(bytes: &[u8], pn: u64, bv_score: &mut [i64; 256]) {
let p16 = (pn % 16) as u8;
let bias = p16 / 2 * 4;
for si in 0..7usize {
let off = si * SECTOR_SIZE;
let sec = &bytes[off..off + SECTOR_SIZE];
let offset = (pn as u8).wrapping_add(si as u8).wrapping_sub(bias);
let mut step_bv_max = [0u16; 256];
for step in 0u8..=255 {
let mut hist = [0u16; 256];
for (i, &b) in sec.iter().enumerate() {
let m = b
.wrapping_sub(offset)
.wrapping_sub((i as u8).wrapping_mul(step));
hist[m as usize] += 1;
}
for (bv, &h) in hist.iter().enumerate() {
if h > step_bv_max[bv] {
step_bv_max[bv] = h;
}
}
}
for (bv, &m) in step_bv_max.iter().enumerate() {
bv_score[bv] += m as i64;
}
}
}
fn learn_bv_for_page(bytes: &[u8], pn: u64, ptype: u8) -> Option<u8> {
if !PURE_AP_TYPES.contains(&ptype) {
return None;
}
let p16 = (pn % 16) as u8;
let mut votes: HashMap<u8, u32> = HashMap::new();
for si in 0..SECTORS_PER_PAGE {
let off = si * SECTOR_SIZE;
let data_end = if si + 1 == SECTORS_PER_PAGE {
TRAILER_START
} else {
off + SECTOR_SIZE
};
let sec = &bytes[off..data_end];
let step = dominant_step(sec);
let base = recover_base(sec, step);
if sector_purity(sec, base, step) < LEARN_PURITY {
continue;
}
let bv = base
.wrapping_sub(pn as u8)
.wrapping_sub(si as u8)
.wrapping_add(p16 / 2 * 4);
*votes.entry(bv).or_default() += 1;
}
votes.into_iter().max_by_key(|&(_, v)| v).map(|(bv, _)| bv)
}
#[derive(Debug, Clone, Default)]
pub struct ApModel {
bv_map: HashMap<u64, u8>,
bv0: u8,
}
impl ApModel {
pub fn learn(store: &PageStore) -> Self {
let mut votes: HashMap<u64, HashMap<u8, u32>> = HashMap::new();
for page in store.pages() {
let ptype = page.trailer().page_type_raw;
if !PURE_AP_TYPES.contains(&ptype) {
continue;
}
let pn = page.index();
let bi = pn / 16;
let p16 = (pn % 16) as u8;
let bytes = page.bytes();
for si in 0..SECTORS_PER_PAGE {
let off = si * SECTOR_SIZE;
let data_end = if si + 1 == SECTORS_PER_PAGE {
TRAILER_START
} else {
off + SECTOR_SIZE
};
let sec = &bytes[off..data_end];
let step = dominant_step(sec);
let base = recover_base(sec, step);
if sector_purity(sec, base, step) < LEARN_PURITY {
continue;
}
let bv = base
.wrapping_sub(pn as u8)
.wrapping_sub(si as u8)
.wrapping_add(p16 / 2 * 4);
*votes.entry(bi).or_default().entry(bv).or_default() += 1;
}
}
let mut bv_map: HashMap<u64, u8> = HashMap::new();
for (bi, ctr) in &votes {
if let Some((&bv, _)) = ctr.iter().max_by_key(|&(_, &v)| v) {
bv_map.insert(*bi, bv);
}
}
let bv0 = bv_map
.get(&0)
.copied()
.unwrap_or_else(|| bv_map.values().copied().next().unwrap_or(0));
ApModel { bv_map, bv0 }
}
#[inline]
pub fn bv_at(&self, bi: u64) -> u8 {
self.bv_map.get(&bi).copied().unwrap_or(self.bv0)
}
pub fn deobfuscate(&self, raw: &[u8], pn: u64) -> Vec<u8> {
self.deobfuscate_with_bv(raw, pn, self.bv_at(pn / 16))
}
pub fn learned_block_count(&self) -> usize {
self.bv_map.len()
}
pub fn recover_bv_for_block(&self, store: &PageStore, bi: u64) -> u8 {
let page_start = bi * 16;
let page_end = (page_start + 16).min(store.page_count());
if page_start >= store.page_count() {
return self.bv0;
}
let sample_end = page_end.min(page_start + 3);
let mut bv_score = [0i64; 256];
for pn in page_start..sample_end {
if let Ok(page) = store.page(pn) {
score_bv_candidates(page.bytes(), pn, &mut bv_score);
}
}
bv_score
.iter()
.enumerate()
.max_by_key(|&(_, &v)| v)
.map(|(i, _)| i as u8)
.unwrap_or(self.bv0)
}
pub fn recover_bv_for_page(&self, store: &PageStore, pn: u64) -> u8 {
let mut bv_score = [0i64; 256];
match store.page(pn) {
Ok(page) => score_bv_candidates(page.bytes(), pn, &mut bv_score),
Err(_) => return self.bv0,
}
bv_score
.iter()
.enumerate()
.max_by_key(|&(_, &v)| v)
.map(|(i, _)| i as u8)
.unwrap_or(self.bv0)
}
pub fn deobfuscate_with_store(&self, raw: &[u8], pn: u64, store: &PageStore) -> Vec<u8> {
let ptype = raw[TRAILER_START + 2];
let bv = learn_bv_for_page(raw, pn, ptype)
.unwrap_or_else(|| self.recover_bv_for_page(store, pn));
self.deobfuscate_with_bv(raw, pn, bv)
}
pub fn deobfuscate_with_bv(&self, raw: &[u8], pn: u64, bv: u8) -> Vec<u8> {
assert!(raw.len() >= PAGE_SIZE, "page buffer too small");
let mut out = vec![0u8; PAGE_SIZE];
for si in 0..SECTORS_PER_PAGE {
let off = si * SECTOR_SIZE;
let data_end = if si + 1 == SECTORS_PER_PAGE {
TRAILER_START
} else {
off + SECTOR_SIZE
};
let sec = &raw[off..data_end];
let base = ap_base(pn, si, bv);
let (step, _) = recover_step_peak(sec, base);
apply_stream(sec, base, step, &mut out[off..data_end]);
}
out[TRAILER_START..PAGE_SIZE].copy_from_slice(&raw[TRAILER_START..PAGE_SIZE]);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ap_sector(base: u8, step: u8) -> [u8; SECTOR_SIZE] {
let mut sec = [0u8; SECTOR_SIZE];
for (i, b) in sec.iter_mut().enumerate() {
*b = base.wrapping_add((i as u8).wrapping_mul(step));
}
sec
}
#[test]
fn dominant_step_pure_ap() {
let sec = make_ap_sector(0x63, 3);
assert_eq!(dominant_step(&sec), 3);
}
#[test]
fn recover_base_pure_ap() {
let sec = make_ap_sector(0x71, 7);
let step = dominant_step(&sec);
assert_eq!(step, 7);
assert_eq!(recover_base(&sec, step), 0x71);
}
#[test]
fn sector_purity_perfect() {
let sec = make_ap_sector(0x40, 11);
let step = dominant_step(&sec);
let base = recover_base(&sec, step);
assert!(sector_purity(&sec, base, step) > 0.99);
}
#[test]
fn roundtrip_zero_plaintext() {
let base: u8 = 0x55;
let step: u8 = 19;
let stored: Vec<u8> = (0..SECTOR_SIZE)
.map(|i| base.wrapping_add((i as u8).wrapping_mul(step)))
.collect();
let (recovered_step, _) = recover_step_peak(&stored, base);
let mut plain = vec![0u8; SECTOR_SIZE];
apply_stream(&stored, base, recovered_step, &mut plain);
assert!(plain.iter().all(|&b| b == 0));
}
#[test]
fn ap_base_formula() {
assert_eq!(ap_base(10, 2, 0xC0), 0xB8);
assert_eq!(ap_base(0, 0, 0x00), 0x00);
assert_eq!(ap_base(16, 3, 0x10), 0x23);
}
fn encode_page(pn: u64, ptype: u8, bv: u8, step: u8, plaintext: &[u8]) -> Vec<u8> {
assert_eq!(plaintext.len(), TRAILER_START);
let mut out = vec![0u8; PAGE_SIZE];
for si in 0..SECTORS_PER_PAGE {
let off = si * SECTOR_SIZE;
let data_end = if si + 1 == SECTORS_PER_PAGE {
TRAILER_START
} else {
off + SECTOR_SIZE
};
let base = ap_base(pn, si, bv);
for (i, &p) in plaintext[off..data_end].iter().enumerate() {
out[off + i] = base
.wrapping_add((i as u8).wrapping_mul(step))
.wrapping_add(p);
}
}
out[TRAILER_START + 2] = ptype;
out
}
#[test]
fn deobfuscate_with_store_uses_per_page_bv_not_block_bv() {
let zero_plain = vec![0u8; TRAILER_START];
let page0 = encode_page(0, b'@', 10, 3, &zero_plain);
let page1 = encode_page(1, b'E', 200, 7, &zero_plain);
let mut bytes = Vec::new();
bytes.extend_from_slice(&page0);
bytes.extend_from_slice(&page1);
let store = PageStore::from_bytes(bytes).unwrap();
let model = ApModel::learn(&store);
assert_eq!(
model.bv_at(0),
10,
"block bv should learn from the pure-AP page"
);
let page1_raw = store.page(1).unwrap().bytes();
let plain = model.deobfuscate_with_store(page1_raw, 1, &store);
assert!(
plain[..TRAILER_START].iter().all(|&b| b == 0),
"per-page recovery should find page 1's true bv (200), not the block's (10)"
);
}
#[test]
fn recover_bv_for_page_matches_direct_learn() {
let zero_plain = vec![0u8; TRAILER_START];
let page = encode_page(0, b'H', 77, 13, &zero_plain);
let store = PageStore::from_bytes(page.clone()).unwrap();
assert_eq!(learn_bv_for_page(&page, 0, b'H'), Some(77));
let model = ApModel::default();
assert_eq!(model.recover_bv_for_page(&store, 0), 77);
}
}