mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
120 lines
3.5 KiB
Rust
120 lines
3.5 KiB
Rust
// Bytecode interpreter for the custom-decryptor stages. Those stages are tiny
|
||
// instruction programs embedded in the decrypted buffer; we compile each
|
||
// program down to a Vec<Op> and interpret it.
|
||
|
||
#[derive(Clone, Copy)]
|
||
pub enum Op {
|
||
Add(u8),
|
||
Sub(u8),
|
||
Xor(u8),
|
||
Rol(u32),
|
||
Ror(u32),
|
||
Inc,
|
||
Dec,
|
||
}
|
||
|
||
pub fn apply(ops: &[Op], mut x: u8) -> u8 {
|
||
for &op in ops {
|
||
x = match op {
|
||
Op::Add(n) => x.wrapping_add(n),
|
||
Op::Sub(n) => x.wrapping_sub(n),
|
||
Op::Xor(n) => x ^ n,
|
||
Op::Rol(n) => x.rotate_left(n & 7),
|
||
Op::Ror(n) => x.rotate_right(n & 7),
|
||
Op::Inc => x.wrapping_add(1),
|
||
Op::Dec => x.wrapping_sub(1),
|
||
};
|
||
}
|
||
x
|
||
}
|
||
|
||
/// A precomputed 256-entry byte→byte translation table for a fixed op list.
|
||
///
|
||
/// `apply` is a pure function of a single byte, but the hot decrypt paths run it
|
||
/// over multi-megabyte regions. Building the full table once and translating
|
||
/// each byte with a single lookup turns an O(region × ops) walk into O(region) —
|
||
/// a large constant-factor win on those paths.
|
||
pub struct OpsLut {
|
||
t: [u8; 256],
|
||
}
|
||
|
||
impl OpsLut {
|
||
pub fn new(ops: &[Op]) -> Self {
|
||
let mut t = [0u8; 256];
|
||
let mut i = 0;
|
||
while i < 256 {
|
||
t[i] = apply(ops, i as u8);
|
||
i += 1;
|
||
}
|
||
Self { t }
|
||
}
|
||
|
||
/// Translate `d[off .. off + n]` in place through the table.
|
||
#[inline]
|
||
pub fn map_region(&self, d: &mut [u8], off: usize, n: usize) {
|
||
for b in &mut d[off..off + n] {
|
||
*b = self.t[*b as usize];
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn generate(data: &[u8], offset: u32) -> Option<Vec<Op>> {
|
||
// Bounds-checked cursor: a corrupt `data_offset` (bad decrypt_data6 / the
|
||
// alignment fallback) must yield `None`, not an out-of-bounds panic — the
|
||
// panic path would surface as a misleading `UnpackError::InternalPanic` instead
|
||
// of the precise `BytecodeGenerationFailed`, and any future caller without a
|
||
// `catch_unwind` wrapper would abort outright.
|
||
let mut pos = offset as usize;
|
||
let mut next = move || {
|
||
let b = data.get(pos).copied()?;
|
||
pos += 1;
|
||
Some(b)
|
||
};
|
||
let mut ops = Vec::new();
|
||
loop {
|
||
match next()? {
|
||
4 => ops.push(Op::Add(next()?)),
|
||
44 => ops.push(Op::Sub(next()?)),
|
||
52 => ops.push(Op::Xor(next()?)),
|
||
144 => {} // nop
|
||
192 => {
|
||
let mb = next()?;
|
||
let rm = mb & 7;
|
||
let reg = (mb >> 3) & 7;
|
||
let mod_ = (mb >> 6) & 3;
|
||
if mod_ != 3 || rm != 0 {
|
||
return None;
|
||
}
|
||
let imm = next()? as u32;
|
||
match reg {
|
||
0 => ops.push(Op::Rol(imm)),
|
||
1 => ops.push(Op::Ror(imm)),
|
||
_ => {
|
||
return None;
|
||
}
|
||
}
|
||
}
|
||
254 => {
|
||
let mb = next()?;
|
||
let rm = mb & 7;
|
||
let reg = (mb >> 3) & 7;
|
||
let mod_ = (mb >> 6) & 3;
|
||
if mod_ != 3 || rm != 0 {
|
||
return None;
|
||
}
|
||
match reg {
|
||
0 => ops.push(Op::Inc),
|
||
1 => ops.push(Op::Dec),
|
||
_ => {
|
||
return None;
|
||
}
|
||
}
|
||
}
|
||
195 => return Some(ops),
|
||
_ => {
|
||
return None;
|
||
}
|
||
}
|
||
}
|
||
}
|