4 Commits
Author SHA1 Message Date
bfloat16 366a19c2a1 fix(android): adjust the parsing of construction tables 2026-10-26 19:05:37 +08:00
bfloat16 ba1dacc0b2 fix(android): fix seed references 2026-10-25 20:46:41 +08:00
Momoko-Ayase 31a0c4139c bump: 1.3.3 2026-10-24 21:33:59 +08:00
Momoko-Ayase ea92d26d28 fix(windows): support legacy PE32 payload layouts
Handle fully encrypted PE32 payloads and compact shell, stage, and metadata layouts. Trial-decrypt each table candidate before selecting it, restoring the header and stage ciphertext between attempts.

Add synthetic regressions for full and prefix encryption, false table candidates, rollback, metadata placement, and malformed ranges. Preserve existing corpus outputs.
2026-10-24 20:40:23 +08:00
13 changed files with 1563 additions and 337 deletions

No files matched your search

Generated
+7 -7
View File
@@ -402,7 +402,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-cli" name = "senbei-cli"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"senbei-engine", "senbei-engine",
"senbei-io", "senbei-io",
@@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-crypto" name = "senbei-crypto"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"aes", "aes",
"thiserror", "thiserror",
@@ -421,7 +421,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-elf" name = "senbei-elf"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"goblin", "goblin",
"thiserror", "thiserror",
@@ -429,7 +429,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-engine" name = "senbei-engine"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"memmap2", "memmap2",
"senbei-crypto", "senbei-crypto",
@@ -444,7 +444,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-io" name = "senbei-io"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"indicatif", "indicatif",
@@ -465,7 +465,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-metadata" name = "senbei-metadata"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@@ -473,7 +473,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-pe" name = "senbei-pe"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
+1 -1
View File
@@ -15,7 +15,7 @@ exclude = ["senbei-wasm"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "1.3.2" version = "1.3.3"
edition = "2024" edition = "2024"
rust-version = "1.98.1" rust-version = "1.98.1"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
@@ -16,6 +16,7 @@ fn wanted_id(command_id: u32) -> bool {
pub(crate) struct Artifact { pub(crate) struct Artifact {
pub path: PathBuf, pub path: PathBuf,
pub size: u64, pub size: u64,
pub entry_offset: Option<u32>,
} }
pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact>> { pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact>> {
@@ -47,6 +48,10 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
Artifact { Artifact {
path: root.join(path), path: root.join(path),
size, size,
entry_offset: item
.get("entry_offset")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok()),
}, },
); );
} }
@@ -79,6 +84,10 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
Artifact { Artifact {
path: root.join(path), path: root.join(path),
size, size,
entry_offset: record
.get("entry_offset")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok()),
}, },
); );
} }
@@ -0,0 +1,418 @@
//! Evaluate the callback selector reached from the handoff module entry.
//!
//! This is a bounded interpreter for the selector's integer/control-flow
//! instructions, not a native module loader. Only stack writes are permitted;
//! image calls are recorded, never executed. Unknown instructions or memory
//! accesses reject a candidate. Table offsets, ordering, flags and the group
//! fallback decision consequently come from the module's own code.
use std::collections::{BTreeSet, HashSet};
use senbei_elf::{ElfLayout, read_u32, slice_u64};
use super::error::{Result, invalid};
use super::pipeline::{aarch64_adrp_target, virtual_address_to_file_offset};
const RUNTIME: u64 = 1 << 32;
const IMAGE: u64 = 2 << 32;
const STACK: u64 = 3 << 32;
const STACK_SIZE: usize = 0x10000;
const RETURN: u64 = u64::MAX - 3;
#[cfg(test)]
#[path = "constructors_tests.rs"]
mod tests;
fn signed(value: u32, bits: u32) -> i64 {
i64::from((value << (32 - bits)) as i32 >> (32 - bits))
}
fn branch_target(pc: u64, instruction: u32, bits: u32, shift: u32) -> Option<u64> {
pc.checked_add_signed(signed((instruction >> shift) & ((1 << bits) - 1), bits) * 4)
}
/// Follow direct control flow from the descriptor entry, without inspecting
/// unrelated code or searching the data image for plausible tables.
fn selector_functions(module: &[u8], entry: u32) -> Result<BTreeSet<u64>> {
let mut functions = vec![u64::from(entry)];
let mut visited_functions = HashSet::new();
let mut selectors = BTreeSet::new();
let mut budget = 100_000_usize;
while let Some(function) = functions.pop() {
if !visited_functions.insert(function) {
continue;
}
let mut pending = vec![function];
let mut visited = HashSet::new();
while let Some(pc) = pending.pop() {
if !visited.insert(pc) {
continue;
}
if budget == 0 {
return invalid("constructor module control flow exceeds analysis budget");
}
budget -= 1;
let Some(instruction) = usize::try_from(pc)
.ok()
.filter(|offset| offset.is_multiple_of(4))
.and_then(|offset| read_u32(module, offset).ok())
else {
return invalid("constructor module branches outside its image");
};
if instruction & 0x7c00_0000 == 0x1400_0000 {
let Some(target) = branch_target(pc, instruction, 26, 0) else {
return invalid("constructor module branch address overflows");
};
if instruction & 0x8000_0000 != 0 {
functions.push(target);
pending.push(pc + 4);
} else {
pending.push(target);
}
continue;
}
let conditional_bits = if instruction & 0xff00_0010 == 0x5400_0000
|| instruction & 0x7e00_0000 == 0x3400_0000
{
Some(19)
} else if instruction & 0x7e00_0000 == 0x3600_0000 {
Some(14)
} else {
None
};
if let Some(bits) = conditional_bits {
let Some(target) = branch_target(pc, instruction, bits, 5) else {
return invalid("constructor module conditional branch overflows");
};
pending.push(target);
}
if instruction & 0xffff_fc1f == 0xd63f_0000 {
selectors.insert(function);
} else if instruction & 0xffff_fc1f == 0xd61f_0000
|| instruction & 0xffff_fc1f == 0xd65f_0000
|| instruction == 0
{
continue;
}
pending.push(pc + 4);
}
}
Ok(selectors)
}
fn mask(bits: u32) -> u64 {
u64::MAX >> (64 - bits)
}
fn logical_immediate(instruction: u32, width: u32) -> Option<u64> {
let n = (instruction >> 22) & 1;
let imms = (instruction >> 10) & 63;
let immr = (instruction >> 16) & 63;
let encoded = (n << 6) | (!imms & 63);
let len = 31_u32.checked_sub(encoded.leading_zeros())?;
if len < 1 || (width == 32 && n != 0) {
return None;
}
let size = 1 << len;
let ones = (imms & (size - 1)) + 1;
if ones == size || size > width {
return None;
}
let rotate = immr & (size - 1);
let value = mask(ones);
let element = if rotate == 0 {
value
} else {
((value >> rotate) | (value << (size - rotate))) & mask(size)
};
let mut result = 0;
for position in (0..width).step_by(size as usize) {
result |= element << position;
}
Some(result)
}
struct Selector<'a> {
module: &'a [u8],
image: &'a [u8],
layout: &'a ElfLayout,
registers: [Option<u64>; 31],
sp: u64,
stack: Vec<Option<u8>>,
flags: Option<(bool, bool, bool, bool)>,
read_image_base: bool,
calls: Vec<u64>,
}
impl Selector<'_> {
fn reg(&self, index: u32, sp: bool) -> Option<u64> {
if index == 31 {
Some(if sp { self.sp } else { 0 })
} else {
self.registers[index as usize]
}
}
fn set(&mut self, index: u32, value: u64, width: u32, sp: bool) {
let value = value & mask(width);
if index != 31 {
self.registers[index as usize] = Some(value);
} else if sp {
self.sp = value;
}
}
fn read(&mut self, address: u64, size: usize) -> Option<u64> {
if address == RUNTIME + 8 && size == 8 {
self.read_image_base = true;
return Some(IMAGE);
}
let bytes = if address >= STACK {
let start = usize::try_from(address - STACK).ok()?;
self.stack
.get(start..start.checked_add(size)?)?
.iter()
.copied()
.collect::<Option<Vec<_>>>()?
} else if address >= IMAGE {
let offset = virtual_address_to_file_offset(self.layout, address - IMAGE, size as u64)?;
self.image.get(offset..offset.checked_add(size)?)?.to_vec()
} else {
slice_u64(self.module, address, size as u64).ok()?.to_vec()
};
Some(bytes.iter().enumerate().fold(0, |value, (index, byte)| {
value | (u64::from(*byte) << (index * 8))
}))
}
fn write(&mut self, address: u64, size: usize, value: u64) -> Option<()> {
let offset = usize::try_from(address.checked_sub(STACK)?).ok()?;
for (index, byte) in self
.stack
.get_mut(offset..offset.checked_add(size)?)?
.iter_mut()
.enumerate()
{
*byte = Some((value >> (index * 8)) as u8);
}
Some(())
}
fn condition(&self, condition: u32) -> Option<bool> {
let (negative, zero, carry, overflow) = self.flags?;
let matched = match condition >> 1 {
0 => zero,
1 => carry,
2 => negative,
3 => overflow,
4 => carry && !zero,
5 => negative == overflow,
6 => negative == overflow && !zero,
7 => return Some(true),
_ => return None,
};
Some(matched ^ (condition & 1 != 0))
}
fn run(mut self, start: u64, max_calls: usize) -> Option<Vec<u64>> {
let mut pc = start;
let budget = max_calls
.checked_add(1)?
.checked_mul(256)?
.checked_add(1024)?
.min(1_000_000);
for _ in 0..budget {
if !pc.is_multiple_of(4) {
return None;
}
let instruction = read_u32(self.module, usize::try_from(pc).ok()?).ok()?;
let mut next = pc.checked_add(4)?;
let rd = instruction & 31;
let rn = (instruction >> 5) & 31;
let width = if instruction >> 31 == 0 { 32 } else { 64 };
if instruction & 0xffff_fc1f == 0xd65f_0000 {
return (self.reg(rn, false)? == RETURN
&& self.sp == STACK + STACK_SIZE as u64
&& self.read_image_base
&& !self.calls.is_empty()
&& self.reg(0, false)? == 1)
.then_some(self.calls);
} else if instruction & 0xffff_fc1f == 0xd63f_0000 {
let address = self.reg(rn, false)?.checked_sub(IMAGE)?;
if self.calls.len() >= max_calls || !executable_address(self.layout, address) {
return None;
}
self.calls.push(address);
self.registers[..19].fill(None);
self.registers[30] = Some(next);
self.flags = None;
} else if instruction & 0xfc00_0000 == 0x1400_0000 {
next = branch_target(pc, instruction, 26, 0)?;
} else if instruction & 0xff00_0010 == 0x5400_0000 {
if self.condition(instruction & 15)? {
next = branch_target(pc, instruction, 19, 5)?;
}
} else if instruction & 0x7e00_0000 == 0x3400_0000 {
let nonzero = self.reg(rd, false)? & mask(width) != 0;
if nonzero == (instruction & (1 << 24) != 0) {
next = branch_target(pc, instruction, 19, 5)?;
}
} else if instruction & 0x9f00_0000 == 0x9000_0000 {
self.set(rd, aarch64_adrp_target(instruction, pc)?, 64, false);
} else if instruction & 0x1f80_0000 == 0x1280_0000 {
let shift = ((instruction >> 21) & 3) * 16;
if shift >= width {
return None;
}
let immediate = u64::from((instruction >> 5) & 0xffff) << shift;
let value = match (instruction >> 29) & 3 {
0 => !immediate,
2 => immediate,
3 => (self.reg(rd, false)? & !(0xffff_u64 << shift)) | immediate,
_ => return None,
};
self.set(rd, value, width, false);
} else if instruction & 0x1f00_0000 == 0x1100_0000
|| instruction & 0x1f20_0000 == 0x0b00_0000
{
let immediate = instruction & 0x1f00_0000 == 0x1100_0000;
let left = self.reg(rn, immediate)? & mask(width);
let right = if immediate {
u64::from((instruction >> 10) & 0xfff)
<< (if instruction & (1 << 22) != 0 { 12 } else { 0 })
} else {
if (instruction >> 22) & 3 != 0 {
return None;
}
let shift = (instruction >> 10) & 63;
if shift >= width {
return None;
}
(self.reg((instruction >> 16) & 31, false)? << shift) & mask(width)
};
let subtract = instruction & (1 << 30) != 0;
let set_flags = instruction & (1 << 29) != 0;
let value = if subtract {
left.wrapping_sub(right)
} else {
left.wrapping_add(right)
} & mask(width);
if set_flags {
let sign = 1_u64 << (width - 1);
let carry = if subtract {
left >= right
} else {
u128::from(left) + u128::from(right) > u128::from(mask(width))
};
let overflow = if subtract {
(left ^ right) & (left ^ value) & sign != 0
} else {
!(left ^ right) & (left ^ value) & sign != 0
};
self.flags = Some((value & sign != 0, value == 0, carry, overflow));
}
self.set(rd, value, width, immediate && !set_flags);
} else if instruction & 0x1f80_0000 == 0x1200_0000
|| instruction & 0x1f20_0000 == 0x0a00_0000
{
let left = self.reg(rn, false)?;
let right = if instruction & 0x1f80_0000 == 0x1200_0000 {
logical_immediate(instruction, width)?
} else {
if (instruction >> 22) & 3 != 0 {
return None;
}
let shift = (instruction >> 10) & 63;
if shift >= width {
return None;
}
self.reg((instruction >> 16) & 31, false)? << shift
};
let value = match (instruction >> 29) & 3 {
0 => left & right,
1 => left | right,
2 => left ^ right,
_ => return None,
};
self.set(rd, value, width, false);
} else if instruction & 0x7f80_0000 == 0x5300_0000 {
let rotate = (instruction >> 16) & 63;
let end = (instruction >> 10) & 63;
if rotate >= width || end >= width || (instruction >> 22) & 1 != width / 64 {
return None;
}
let source = self.reg(rn, false)? & mask(width);
let value = if rotate <= end {
(source >> rotate) & mask(end - rotate + 1)
} else {
(source & mask(end + 1)) << (width - rotate)
};
self.set(rd, value, width, false);
} else if instruction & 0x3b00_0000 == 0x3900_0000 && instruction & (1 << 26) == 0 {
let size = 1_usize << (instruction >> 30);
let address = self
.reg(rn, true)?
.checked_add(u64::from((instruction >> 10) & 0xfff) * size as u64)?;
match (instruction >> 22) & 3 {
0 => self.write(address, size, self.reg(rd, false)?)?,
1 => {
let value = self.read(address, size)?;
self.set(rd, value, if size == 8 { 64 } else { 32 }, false);
}
_ => return None,
}
} else {
return None;
}
pc = next;
}
None
}
}
pub(super) fn executable_address(layout: &ElfLayout, address: u64) -> bool {
address.is_multiple_of(4)
&& layout.program_headers.iter().any(|segment| {
segment.flags & 1 != 0
&& address >= segment.virtual_address
&& address
.checked_add(4)
.zip(segment.virtual_address.checked_add(segment.file_size))
.is_some_and(|(end, limit)| end <= limit)
})
}
pub(super) fn callbacks(
module: &[u8],
entry: u32,
image: &[u8],
layout: &ElfLayout,
max_calls: usize,
) -> Result<Vec<u64>> {
let mut matches = Vec::new();
for function in selector_functions(module, entry)? {
let mut registers = [None; 31];
registers[0] = Some(RUNTIME);
registers[30] = Some(RETURN);
let selector = Selector {
module,
image,
layout,
registers,
sp: STACK + STACK_SIZE as u64,
stack: vec![None; STACK_SIZE],
flags: None,
read_image_base: false,
calls: Vec::new(),
};
if let Some(calls) = selector.run(function, max_calls) {
matches.push(calls);
}
}
match matches.len() {
1 => Ok(matches.pop().unwrap()),
count => invalid(format!(
"constructor module entry reaches {count} validated callback selectors"
)),
}
}
@@ -0,0 +1,180 @@
use super::*;
use senbei_elf::LoadSegment;
fn fixture() -> (Vec<u8>, Vec<u8>, ElfLayout) {
let mut module = vec![0; 0x2000];
// Synthetic selector: check every primary, then call either all primary
// or all fallback offsets. Bit 1 disables a record's instruction check;
// null primaries are skipped. The entry reaches it through a direct call.
let code: [u32; 41] = [
0xd10043ff, // sub sp, sp, #16
0xf90003fe, // str x30, [sp]
0xf9400413, // ldr x19, [x0, #8]
0xb0000014, // adrp x20, table page
0x91200294, // add x20, x20, #0x800
0xb9400295, // ldr w21, [x20] (count)
0x91002294, // add x20, x20, #8 (records)
0x52800016, // mov w22, #0 (fallback)
0x52800017, // mov w23, #0 (index)
0x6b1502ff, // cmp w23, w21
0x540001e2, // b.hs checked
0x8b171298, // add x24, x20, x23, lsl #4
0xb9400300, // ldr w0, [x24]
0x34000140, // cbz w0, check_next
0xb9400f01, // ldr w1, [x24, #12]
0x121f0021, // and w1, w1, #2
0x350000e1, // cbnz w1, check_next
0x8b000260, // add x0, x19, x0
0xb9400000, // ldr w0, [x0]
0xb9400b01, // ldr w1, [x24, #8]
0x6b01001f, // cmp w0, w1
0x54000040, // b.eq check_next
0x52800036, // mov w22, #1
0x110006f7, // add w23, w23, #1
0x17fffff1, // b check
0x52800017, // mov w23, #0
0x6b1502ff, // cmp w23, w21
0x54000142, // b.hs done
0x8b171298, // add x24, x20, x23, lsl #4
0xb9400300, // ldr w0, [x24]
0x340000a0, // cbz w0, invoke_next
0x34000056, // cbz w22, primary
0xb9400700, // ldr w0, [x24, #4]
0x8b000260, // add x0, x19, x0
0xd63f0000, // blr x0
0x110006f7, // add w23, w23, #1
0x17fffff6, // b invoke
0x52800020, // mov w0, #1
0xf94003fe, // ldr x30, [sp]
0x910043ff, // add sp, sp, #16
0xd65f03c0, // ret
];
put(&mut module, 0x40, 0x9400_0030);
put(&mut module, 0x44, 0xd65f_03c0);
for (index, instruction) in code.into_iter().enumerate() {
put(&mut module, 0x100 + index * 4, instruction);
}
put(&mut module, 0x1800, 2);
for (index, record) in [[0x4400, 0x4500, 0x1234, 0], [0x4200, 0x4520, 0x5678, 0]]
.into_iter()
.enumerate()
{
for (field, word) in record.into_iter().enumerate() {
put(&mut module, 0x1808 + index * 16 + field * 4, word);
}
}
let mut image = vec![0; 0x1000];
put(&mut image, 0x400, 0x1234);
put(&mut image, 0x200, 0x5678);
let layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 1,
program_headers: vec![LoadSegment {
offset: 0,
virtual_address: 0x4000,
file_size: 0x1000,
memory_size: 0x1000,
flags: 5,
alignment: 0x1000,
}],
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: 0,
};
(module, image, layout)
}
fn put(data: &mut [u8], offset: usize, word: u32) {
data[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
#[test]
fn callback_order_comes_from_execution_even_after_module_relocation() {
let (module, image, layout) = fixture();
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 2).unwrap(),
[0x4400, 0x4200]
);
let mut relocated = vec![0; 0x5000];
relocated.extend_from_slice(&module);
// Unreachable data and code cannot become another table candidate.
relocated[..module.len()].copy_from_slice(&module);
assert_eq!(
callbacks(&relocated, 0x5040, &image, &layout, 2).unwrap(),
[0x4400, 0x4200]
);
}
#[test]
fn one_instruction_mismatch_selects_the_entire_fallback_group() {
let (module, mut image, layout) = fixture();
put(&mut image, 0x200, 0);
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 2).unwrap(),
[0x4500, 0x4520]
);
}
#[test]
fn callback_flags_null_records_and_single_entries_follow_the_code() {
let (mut module, mut image, layout) = fixture();
put(&mut image, 0x200, 0);
put(&mut module, 0x1824, 2);
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 2).unwrap(),
[0x4400, 0x4200]
);
put(&mut module, 0x1818, 0);
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 2).unwrap(),
[0x4400]
);
put(&mut module, 0x1800, 1);
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 1).unwrap(),
[0x4400]
);
}
#[test]
fn unsupported_code_invalid_memory_and_excess_calls_are_rejected() {
let (module, image, layout) = fixture();
assert!(callbacks(&module, 0x40, &image, &layout, 1).is_err());
for (offset, instruction) in [
(0x100, 0xd400_0001), // system call
(0x108, 0xf900_0413), // attempted write outside the stack
(0x150, 0x1400_0000), // nonterminating comparison loop
(0x1808, 0x9000), // primary is outside the image
(0x181c, 0x9000), // fallback is outside the image
(0x1800, u32::MAX), // count exceeds the module/table bounds
] {
let mut broken = module.clone();
put(&mut broken, offset, instruction);
let mut mismatched = image.clone();
if offset == 0x181c {
put(&mut mismatched, 0x200, 0);
}
assert!(
callbacks(&broken, 0x40, &mismatched, &layout, 2).is_err(),
"offset {offset:x}"
);
}
assert!(callbacks(&module[..0x1820], 0x40, &image, &layout, 2).is_err());
assert!(callbacks(&module, 0xffff_ffff, &image, &layout, 2).is_err());
}
#[test]
fn multiple_reachable_selectors_are_not_silently_chosen() {
let (mut module, image, layout) = fixture();
module.copy_within(0x100..0x1a4, 0x300);
put(&mut module, 0x44, 0x9400_00af); // BL 0x300
put(&mut module, 0x48, 0xd65f_03c0);
assert!(
callbacks(&module, 0x40, &image, &layout, 2)
.unwrap_err()
.to_string()
.contains("2 validated")
);
}
+1
View File
@@ -1,4 +1,5 @@
mod artifact; mod artifact;
mod constructors;
mod error; mod error;
mod pipeline; mod pipeline;
+447 -167
View File
@@ -602,19 +602,59 @@ fn dynamic_symbol_names(symbols: &[u8], strings: &[u8]) -> Result<Vec<Vec<u8>>>
.collect() .collect()
} }
fn auxiliary_seed_references(module: &[u8]) -> HashSet<u64> {
let mut references = HashSet::new();
// The relocation decoder loads the seed through the module GOT, then
// squares it in a W register. Follow ADRP/LDR X/LDR W/MUL W instead of
// treating a neighbouring module's function offset as a format constant.
for (index, code) in module.windows(16).step_by(4).enumerate() {
let word = |offset| u32::from_le_bytes(code[offset..offset + 4].try_into().unwrap());
let adrp = word(0);
let pointer = word(4);
let seed = word(8);
let square = word(12);
let page_register = adrp & 31;
let pointer_register = pointer & 31;
let seed_register = seed & 31;
if adrp & 0x9f00_0000 != 0x9000_0000
|| page_register == 31
|| pointer & 0xffc0_0000 != 0xf940_0000
|| (pointer >> 5) & 31 != page_register
|| pointer_register == 31
|| seed != 0xb940_0000 | (pointer_register << 5) | seed_register
|| seed_register == 31
|| square != 0x1b00_7c00 | (seed_register << 16) | (seed_register << 5) | seed_register
{
continue;
}
let Some(address) = aarch64_adrp_target(adrp, (index * 4) as u64)
.and_then(|page| page.checked_add(u64::from((pointer >> 10) & 0xfff) * 8))
else {
continue;
};
if let Ok(offset) = usize::try_from(address)
&& let Ok(target) = read_u64(module, offset)
{
references.insert(target);
}
}
references
}
fn discover_auxiliary_symbol_seed(module: &[u8]) -> Result<u32> { fn discover_auxiliary_symbol_seed(module: &[u8]) -> Result<u32> {
const PREFIX0: u32 = 0x8f; const PREFIX0: u32 = 0x8f;
const PREFIX1: u32 = 0x15ec;
const TYPE_ID: u32 = 0x81; const TYPE_ID: u32 = 0x81;
const MODE0: u32 = 1; const MODE0: u32 = 1;
const MODE1: u32 = 5; const MODE1: u32 = 5;
const SUFFIX0: u32 = 0x0a54; const SUFFIX0: u32 = 0x0a54;
const SUFFIX1: u32 = 0x12008; const SUFFIX1: u32 = 0x12008;
let references = auxiliary_seed_references(module);
let mut matches = Vec::new(); let mut matches = Vec::new();
for seed_offset in (0x20..module.len().saturating_sub(0x10)).step_by(4) { for seed_offset in (0x20..=module.len().saturating_sub(0x10)).step_by(4) {
if read_u32(module, seed_offset - 0x20)? == PREFIX0 if read_u32(module, seed_offset - 0x20)? == PREFIX0
&& read_u32(module, seed_offset - 0x1c)? == PREFIX1 && read_u32(module, seed_offset - 0x1c)? != 0
&& read_u32(module, seed_offset - 0x1c)?.is_multiple_of(4)
&& read_u32(module, seed_offset - 0x18)? == TYPE_ID && read_u32(module, seed_offset - 0x18)? == TYPE_ID
&& read_u32(module, seed_offset - 0x10)? == TYPE_ID && read_u32(module, seed_offset - 0x10)? == TYPE_ID
&& read_u32(module, seed_offset - 8)? == MODE0 && read_u32(module, seed_offset - 8)? == MODE0
@@ -622,6 +662,7 @@ fn discover_auxiliary_symbol_seed(module: &[u8]) -> Result<u32> {
&& read_u32(module, seed_offset + 4)? == SUFFIX0 && read_u32(module, seed_offset + 4)? == SUFFIX0
&& read_u32(module, seed_offset + 8)? == SUFFIX1 && read_u32(module, seed_offset + 8)? == SUFFIX1
&& read_u32(module, seed_offset + 0x0c)? == 0 && read_u32(module, seed_offset + 0x0c)? == 0
&& references.contains(&(seed_offset as u64))
{ {
matches.push(read_u32(module, seed_offset)?); matches.push(read_u32(module, seed_offset)?);
} }
@@ -687,7 +728,53 @@ fn aarch64_writes_w1_immediate(instruction: u32) -> bool {
) )
} }
pub(super) fn aarch64_adrp_target(instruction: u32, pc: u64) -> Option<u64> {
if instruction & 0x9f00_0000 != 0x9000_0000 {
return None;
}
let immediate = ((instruction >> 29) & 3) | ((instruction >> 5) & 0x7ffff) << 2;
let pages = i64::from(immediate);
let pages = if pages & (1 << 20) != 0 {
pages - (1 << 21)
} else {
pages
};
(pc & !0xfff).checked_add_signed(pages * 0x1000)
}
fn aarch64_plt_veneer_target(code: &[u8], pc: u64) -> Option<u64> {
let adrp = read_u32(code, 0).ok()?;
let add = read_u32(code, 4).ok()?;
let branch = read_u32(code, 8).ok()?;
let register = adrp & 31;
// Linker range-extension veneers preserve the call's arguments and LR:
// ADRP X16/X17, page; ADD X16/X17, X16/X17, lo12; BR X16/X17.
if !matches!(register, 16 | 17)
|| add & 0xffc0_03ff != 0x9100_0000 | (register << 5) | register
|| branch != 0xd61f_0000 | (register << 5)
{
return None;
}
aarch64_adrp_target(adrp, pc)?.checked_add(u64::from((add >> 10) & 0xfff))
}
fn plt_call_features(image: &[u8], layout: &ElfLayout, target: u64) -> Result<PltCallFeatures> { fn plt_call_features(image: &[u8], layout: &ElfLayout, target: u64) -> Result<PltCallFeatures> {
let mut targets = HashSet::from([target]);
for segment in &layout.program_headers {
if segment.flags & 1 == 0 {
continue;
}
let code = slice_u64(image, segment.offset, segment.file_size)?;
for (index, instructions) in code.windows(12).step_by(4).enumerate() {
let pc = segment
.virtual_address
.checked_add((index * 4) as u64)
.ok_or_else(|| Error::Invalid("veneer PC overflow".to_owned()))?;
if aarch64_plt_veneer_target(instructions, pc) == Some(target) {
targets.insert(pc);
}
}
}
let mut features = PltCallFeatures::default(); let mut features = PltCallFeatures::default();
for segment in &layout.program_headers { for segment in &layout.program_headers {
if segment.flags & 1 == 0 || segment.file_size < 4 { if segment.flags & 1 == 0 || segment.file_size < 4 {
@@ -708,7 +795,8 @@ fn plt_call_features(image: &[u8], layout: &ElfLayout, target: u64) -> Result<Pl
.checked_add(relative as u64) .checked_add(relative as u64)
.ok_or_else(|| Error::Invalid("executable PC overflow".to_owned()))?; .ok_or_else(|| Error::Invalid("executable PC overflow".to_owned()))?;
let instruction = read_u32(image, offset)?; let instruction = read_u32(image, offset)?;
if aarch64_bl_target(instruction, pc) != Some(target) { if !aarch64_bl_target(instruction, pc).is_some_and(|address| targets.contains(&address))
{
continue; continue;
} }
features.calls += 1; features.calls += 1;
@@ -838,7 +926,11 @@ struct ConstructorProfile {
fini_addend: u64, fini_addend: u64,
} }
fn virtual_address_to_file_offset(layout: &ElfLayout, address: u64, size: u64) -> Option<usize> { pub(super) fn virtual_address_to_file_offset(
layout: &ElfLayout,
address: u64,
size: u64,
) -> Option<usize> {
let end = address.checked_add(size)?; let end = address.checked_add(size)?;
for segment in &layout.program_headers { for segment in &layout.program_headers {
let segment_end = segment.virtual_address.checked_add(segment.file_size)?; let segment_end = segment.virtual_address.checked_add(segment.file_size)?;
@@ -852,131 +944,91 @@ fn virtual_address_to_file_offset(layout: &ElfLayout, address: u64, size: u64) -
None None
} }
fn discover_suppressed_constructors( // The bootstrap saves the incoming linker register frame, calls Stage 1,
module: &[u8], // restores that frame and returns. Validate this at the ELF init relocation;
source: &[u8], // e_entry may be zero for a shared library, so it cannot identify the slot.
layout: &ElfLayout, fn is_stage1_constructor(source: &[u8], layout: &ElfLayout, address: u64) -> bool {
max_count: usize, let Some(offset) = virtual_address_to_file_offset(layout, address, 0x84) else {
) -> Result<Option<Vec<u64>>> { return false;
const RECORD_SIZE: usize = 16; };
const MIN_COUNT: usize = 2; for pair in 0..15_u32 {
const MAX_COUNT: usize = 256; let first = pair * 2 + 1;
let mut matches = Vec::new(); let save = 0xa9bf_03e0 | ((first + 1) << 10) | first;
if module.len() < 8 { let restore = 0xa8c1_03e0 | ((first + 1) << 10) | first;
return Ok(None); if read_u32(source, offset + pair as usize * 4).ok() != Some(save)
} || read_u32(source, offset + 0x44 + (14 - pair) as usize * 4).ok() != Some(restore)
for base in (0..=module.len() - 8).step_by(4) {
let count = read_u32(module, base)? as usize;
if !(MIN_COUNT..=MAX_COUNT.min(max_count)).contains(&count)
|| read_u32(module, base + 4)? != 0
{ {
continue; return false;
}
let table_size = 8_usize
.checked_add(
count
.checked_mul(RECORD_SIZE)
.ok_or_else(|| Error::Invalid("constructor table size overflow".to_owned()))?,
)
.ok_or_else(|| Error::Invalid("constructor table size overflow".to_owned()))?;
if base
.checked_add(table_size)
.is_none_or(|end| end > module.len())
{
continue;
}
let mut constructors = Vec::with_capacity(count);
let mut valid = true;
for index in 0..count {
let record = base + 8 + index * RECORD_SIZE;
let constructor = u64::from(read_u32(module, record)?);
let fallback = u64::from(read_u32(module, record + 4)?);
let original = read_u32(module, record + 8)?;
let reserved = read_u32(module, record + 12)?;
let constructor_offset = virtual_address_to_file_offset(layout, constructor, 4);
let fallback_offset = virtual_address_to_file_offset(layout, fallback, 4);
if constructor == 0
|| fallback == 0
|| reserved != 0
|| constructor_offset.and_then(|offset| read_u32(source, offset).ok())
!= Some(original)
|| fallback_offset.and_then(|offset| read_u32(source, offset).ok())
!= Some(AARCH64_RET)
{
valid = false;
break;
}
constructors.push(constructor);
}
if valid
&& constructors.windows(2).all(|pair| pair[0] < pair[1])
&& constructors.iter().copied().collect::<HashSet<_>>().len() == count
{
matches.push(constructors);
} }
} }
match matches.len() { read_u32(source, offset + 0x3c).ok() == Some(0x9100_03e0)
1 => Ok(matches.pop()), && read_u32(source, offset + 0x40)
0 => Ok(None), .ok()
count => invalid(format!( .and_then(|instruction| aarch64_bl_target(instruction, address + 0x40))
"constructor control module has {count} validated suppression tables" .is_some_and(|target| super::constructors::executable_address(layout, target))
)), && read_u32(source, offset + 0x80).ok() == Some(AARCH64_RET)
}
} }
fn constructor_profile( fn constructor_profile(
module: &[u8], module: (&[u8], u32),
source: &[u8], source: &[u8],
protected: &[u8],
layout: &ElfLayout, layout: &ElfLayout,
init_array: SectionHeader, init_array: SectionHeader,
fini_array: SectionHeader, fini_array: SectionHeader,
old_dyn: RelocationTable<'_>, old_dyn: RelocationTable<'_>,
) -> Result<Option<ConstructorProfile>> { ) -> Result<ConstructorProfile> {
if !init_array.size.is_multiple_of(8) { if !init_array.size.is_multiple_of(8) {
return invalid("init-array size is not entry-aligned"); return invalid("init-array size is not entry-aligned");
} }
let init_count = usize_from_u64(init_array.size / 8, "init-array count")?; let init_count = usize_from_u64(init_array.size / 8, "init-array count")?;
let Some(mut init_addends) = let callbacks = super::constructors::callbacks(module.0, module.1, source, layout, init_count)?;
discover_suppressed_constructors(module, source, layout, init_count)?
else {
return Ok(None);
};
let max_suppressed = *init_addends
.last()
.ok_or_else(|| Error::Invalid("constructor table is empty".to_owned()))?;
let init_end = init_array let init_end = init_array
.address .address
.checked_add(init_array.size) .checked_add(init_array.size)
.ok_or_else(|| Error::Invalid("init-array range overflow".to_owned()))?; .ok_or_else(|| Error::Invalid("init-array range overflow".to_owned()))?;
let existing = old_dyn.collect_where(|relocation| { let mut existing = old_dyn.collect_where(|relocation| {
relocation.offset >= init_array.address && relocation.offset < init_end relocation.offset >= init_array.address && relocation.offset < init_end
})?; })?;
let extra = existing existing.sort_by_key(|relocation| relocation.offset);
.iter() if existing.len() != init_count {
.filter(|relocation| { return invalid("init-array does not have one relocation per slot");
relocation.kind() == R_AARCH64_RELATIVE }
&& relocation.symbol() == 0 let mut init_addends = Vec::new();
&& relocation.addend >= 0 let mut bootstraps = 0;
}) for (slot, relocation) in existing.iter().enumerate() {
.map(|relocation| relocation.addend as u64) if relocation.offset != init_array.address + slot as u64 * 8
.filter(|&addend| { || relocation.kind() != R_AARCH64_RELATIVE
addend != layout.entrypoint || relocation.symbol() != 0
&& addend > max_suppressed || relocation.addend < 0
&& addend <= max_suppressed.saturating_add(0x1000) {
}) return invalid("init-array contains an invalid relocation");
.collect::<Vec<_>>(); }
if extra.len() != 1 { let address = relocation.addend as u64;
if is_stage1_constructor(protected, layout, address) {
bootstraps += 1;
init_addends.extend_from_slice(&callbacks);
} else {
if !super::constructors::executable_address(layout, address) {
return invalid("init-array callback is outside executable segments");
}
let offset = virtual_address_to_file_offset(layout, address, 4).ok_or_else(|| {
Error::Invalid("init-array callback is not file-backed".to_owned())
})?;
// The linker still calls the unsuppressed entries after Stage 1
// returns. RET-only placeholders have no initialization effect.
if read_u32(source, offset)? != AARCH64_RET {
init_addends.push(address);
}
}
}
if bootstraps != 1 {
return invalid(format!( return invalid(format!(
"expected one unsuppressed constructor near the suppression table, found {}", "init-array has {bootstraps} validated Stage 1 callbacks"
extra.len()
)); ));
} }
init_addends.push(extra[0]);
if init_addends.len() > init_count { if init_addends.len() > init_count {
return invalid(format!( return invalid("restored constructors exceed the init-array capacity");
"constructor profile produced {} entries for {init_count} init slots",
init_addends.len()
));
} }
let fini = old_dyn.collect_where(|relocation| relocation.offset == fini_array.address)?; let fini = old_dyn.collect_where(|relocation| relocation.offset == fini_array.address)?;
if fini.len() != 1 if fini.len() != 1
@@ -986,10 +1038,10 @@ fn constructor_profile(
{ {
return invalid("fini-array relocation is not a single RELATIVE record"); return invalid("fini-array relocation is not a single RELATIVE record");
} }
Ok(Some(ConstructorProfile { Ok(ConstructorProfile {
init_addends, init_addends,
fini_addend: fini[0].addend as u64, fini_addend: fini[0].addend as u64,
})) })
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1540,7 +1592,7 @@ fn materialize_static_elf_tables(
symbol_patch_data: &[u8], symbol_patch_data: &[u8],
auxiliary_data: &[u8], auxiliary_data: &[u8],
auxiliary_symbol_seed: Option<u32>, auxiliary_symbol_seed: Option<u32>,
constructor_module: Option<&[u8]>, constructor_module: Option<(&[u8], u32)>,
) -> Result<(ElfLayout, ElfMaterializationReport, u64)> { ) -> Result<(ElfLayout, ElfMaterializationReport, u64)> {
let names = layout.section_names(source)?; let names = layout.section_names(source)?;
let indices = required_section_indices(&names)?; let indices = required_section_indices(&names)?;
@@ -1753,14 +1805,9 @@ fn materialize_static_elf_tables(
})?; })?;
let init_array = layout.section_headers[init_index]; let init_array = layout.section_headers[init_index];
let fini_array = layout.section_headers[fini_index]; let fini_array = layout.section_headers[fini_index];
let profile = let profile = constructor_profile(
constructor_profile(module, output, layout, init_array, fini_array, old_dyn)? module, output, source, layout, init_array, fini_array, old_dyn,
.ok_or_else(|| { )?;
Error::Invalid(
"constructor control module 0x98 has no validated constructor table"
.to_owned(),
)
})?;
Some((profile, init_index, fini_index, init_array, fini_array)) Some((profile, init_index, fini_index, init_array, fini_array))
} }
None => None, None => None,
@@ -2333,7 +2380,17 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
.transpose()?; .transpose()?;
let constructor_module = artifacts let constructor_module = artifacts
.get(&0x98) .get(&0x98)
.map(|artifact| read_file(&artifact.path)) .map(|artifact| -> Result<_> {
let entry = artifact
.entry_offset
.filter(|entry| *entry != 0)
.ok_or_else(|| {
Error::Invalid(
"constructor module 0x98 lacks its descriptor entry offset".to_owned(),
)
})?;
Ok((read_file(&artifact.path)?, entry))
})
.transpose()?; .transpose()?;
let config = Module9bConfig::parse(&module)?; let config = Module9bConfig::parse(&module)?;
@@ -2486,7 +2543,9 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
&symbol_patch_data, &symbol_patch_data,
&decoded, &decoded,
auxiliary_symbol_seed, auxiliary_symbol_seed,
constructor_module.as_deref(), constructor_module
.as_ref()
.map(|(image, entry)| (image.as_slice(), *entry)),
)?; )?;
restored_layout = new_layout; restored_layout = new_layout;
materialization = Some(report); materialization = Some(report);
@@ -2684,13 +2743,17 @@ mod tests {
assert_eq!(relocations[1].symbol(), 2); assert_eq!(relocations[1].symbol(), 2);
} }
#[test] fn auxiliary_seed_module(helper_offset: u32) -> Vec<u8> {
fn discovers_auxiliary_symbol_permutation_seed() { let mut module = vec![0_u8; 0x100];
let mut module = vec![0_u8; 0x80]; let seed_offset = 0xf0;
let seed_offset = 0x40;
for (offset, value) in [ for (offset, value) in [
(0x10, 0x9000_0000), // ADRP X0, page 0
(0x14, 0xf940_4000), // LDR X0, [X0, #0x80]
(0x18, 0xb940_0001), // LDR W1, [X0]
(0x1c, 0x1b01_7c21), // MUL W1, W1, W1
(0x80, seed_offset as u32),
(seed_offset - 0x20, 0x8f_u32), (seed_offset - 0x20, 0x8f_u32),
(seed_offset - 0x1c, 0x15ec), (seed_offset - 0x1c, helper_offset),
(seed_offset - 0x18, 0x81), (seed_offset - 0x18, 0x81),
(seed_offset - 0x10, 0x81), (seed_offset - 0x10, 0x81),
(seed_offset - 8, 1), (seed_offset - 8, 1),
@@ -2702,10 +2765,270 @@ mod tests {
] { ] {
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
} }
module
}
#[test]
fn discovers_auxiliary_symbol_seed_with_relocated_helper() {
// The configuration ends exactly at the module boundary.
for helper_offset in [0x15ec, 0x164c, 0x2400] {
assert_eq!(
discover_auxiliary_symbol_seed(&auxiliary_seed_module(helper_offset))
.expect("symbol seed"),
0x4db4_d9ec
);
}
}
#[test]
fn auxiliary_symbol_seed_requires_a_valid_decoder_reference() {
for (offset, value) in [
(0x10, 0xd503_201f_u32), // no ADRP
(0x14, 0xf940_4020), // wrong page register
(0x18, 0xb940_0002), // wrong seed register for MUL
(0x1c, 0x1b02_7c21), // multiplication by another value
(0x80, 0xec), // GOT points at a different field
(0x80, 0x1000), // GOT points outside the module
(0xd4, 0), // null helper offset
(0xd4, 0x164d), // unaligned helper offset
(0xe8, 2), // unsupported mode
] {
let mut module = auxiliary_seed_module(0x164c);
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
assert!(discover_auxiliary_symbol_seed(&module).is_err());
}
for length in [0, 0x10, 0x20, 0xff] {
assert!(
discover_auxiliary_symbol_seed(&auxiliary_seed_module(0x164c)[..length]).is_err()
);
}
}
#[test]
fn auxiliary_symbol_seed_trials_all_candidates_and_rejects_ambiguity() {
let mut module = auxiliary_seed_module(0x164c);
module.copy_within(0xd0..0x100, 0x40);
// An unreferenced configuration-shaped decoy must not hide the real one.
assert_eq!( assert_eq!(
discover_auxiliary_symbol_seed(&module).expect("symbol seed"), discover_auxiliary_symbol_seed(&module).unwrap(),
0x4db4_d9ec 0x4db4_d9ec
); );
module.copy_within(0x10..0x20, 0x20);
module[0x24..0x28].copy_from_slice(&0xf940_4400_u32.to_le_bytes());
module[0x88..0x90].copy_from_slice(&0x60_u64.to_le_bytes());
assert!(
discover_auxiliary_symbol_seed(&module)
.unwrap_err()
.to_string()
.contains("2 validated")
);
}
#[test]
fn auxiliary_seed_reference_supports_negative_page_displacements() {
let mut module = auxiliary_seed_module(0x164c);
module.resize(0x2010, 0);
module.copy_within(0x10..0x20, 0x2000);
module[0x10..0x20].fill(0);
// ADRP X0, PC page - 2 pages; GOT and seed remain in page zero.
module[0x2000..0x2004].copy_from_slice(&0xd0ff_ffe0_u32.to_le_bytes());
assert_eq!(
discover_auxiliary_symbol_seed(&module).unwrap(),
0x4db4_d9ec
);
}
#[test]
fn plt_call_features_follow_validated_range_extension_veneers() {
let mut image = vec![0_u8; 0x130];
for (offset, word) in [
(0x100, 0x2a1f_03e1_u32), // MOV W1, WZR
(0x104, 0x9400_03ff), // BL 0x2000
(0x108, 0x3400_0040), // CBZ W0
(0x110, 0x2a1f_03e1), // MOV W1, WZR
(0x114, 0x9400_0003), // BL 0x1020 (veneer)
(0x118, 0x3400_0040), // CBZ W0
(0x120, 0xb000_0010), // ADRP X16, 0x2000
(0x124, 0x9100_0210), // ADD X16, X16, #0
(0x128, 0xd61f_0200), // BR X16
] {
image[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
let layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 1,
program_headers: vec![LoadSegment {
offset: 0x100,
virtual_address: 0x1000,
file_size: 0x30,
memory_size: 0x30,
flags: PF_R | 1,
alignment: 0x1000,
}],
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: 0,
};
assert_eq!(
plt_call_features(&image, &layout, 0x2000).unwrap(),
PltCallFeatures {
calls: 2,
zero_arg1: 2,
w0_zero_branch: 2,
..PltCallFeatures::default()
}
);
let original = image.clone();
for (offset, word) in [
(0x120, 0xb000_000f_u32), // ADRP uses a different register
(0x124, 0x9100_0610), // veneer targets a different address
(0x128, 0xd63f_0200), // BLR would overwrite LR
(0x128, 0xd61f_0220), // BR uses a different register
] {
image.copy_from_slice(&original);
image[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
assert_eq!(plt_call_features(&image, &layout, 0x2000).unwrap().calls, 1);
}
assert!(aarch64_plt_veneer_target(&original[0x120..0x128], 0x1020).is_none());
image.copy_from_slice(&original);
for (offset, word) in [
(0x120, 0xb000_0011_u32),
(0x124, 0x9100_0231),
(0x128, 0xd61f_0220),
] {
image[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
assert_eq!(plt_call_features(&image, &layout, 0x2000).unwrap().calls, 2);
}
#[test]
fn constructor_profile_replaces_the_bootstrap_and_preserves_linker_order() {
let mut module = vec![0_u8; 0x100];
// Entry calls a selector that dispatches image + 0x400.
for (index, instruction) in [0x9400_0010_u32, 0xd65f_03c0].into_iter().enumerate() {
module[index * 4..index * 4 + 4].copy_from_slice(&instruction.to_le_bytes());
}
for (index, instruction) in [
0xd100_43ff_u32, // SUB SP, SP, #16
0xf900_03fe, // STR LR, [SP]
0xf940_0413, // LDR X19, [X0, #8]
0x5280_8000, // MOV W0, #0x400
0x8b00_0260, // ADD X0, X19, X0
0xd63f_0000, // BLR X0
0x5280_0020, // MOV W0, #1
0xf940_03fe, // LDR LR, [SP]
0x9100_43ff, // ADD SP, SP, #16
0xd65f_03c0, // RET
]
.into_iter()
.enumerate()
{
module[0x40 + index * 4..0x44 + index * 4].copy_from_slice(&instruction.to_le_bytes());
}
let mut protected = vec![0_u8; 0x8000];
for pair in 0..15_u32 {
let first = pair * 2 + 1;
for (offset, instruction) in [
(
0x1000 + pair as usize * 4,
0xa9bf_03e0 | ((first + 1) << 10) | first,
),
(
0x1044 + (14 - pair) as usize * 4,
0xa8c1_03e0 | ((first + 1) << 10) | first,
),
] {
protected[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes());
}
}
for (offset, instruction) in [
(0x103c, 0x9100_03e0_u32),
(0x1040, 0x9400_0070),
(0x1080, AARCH64_RET),
] {
protected[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes());
}
let mut restored = protected.clone();
restored[0x1000..0x1084].fill(0);
restored[0x2000..0x2004].copy_from_slice(&AARCH64_RET.to_le_bytes());
restored[0x6000..0x6004].copy_from_slice(&0xd503_201f_u32.to_le_bytes());
let layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 1,
program_headers: vec![LoadSegment {
offset: 0,
virtual_address: 0,
file_size: 0x8000,
memory_size: 0x8000,
flags: PF_R | 1,
alignment: 0x1000,
}],
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: 0,
};
let init = SectionHeader {
name: 0,
section_type: 14,
flags: 3,
address: 0x7000,
offset: 0x7000,
size: 24,
link: 0,
info: 0,
alignment: 8,
entry_size: 8,
};
let fini = SectionHeader {
address: 0x7100,
offset: 0x7100,
size: 8,
..init
};
for (original, expected) in [
([0x1000, 0x6000, 0x2000], vec![0x400, 0x6000]),
([0x6000, 0x1000, 0x2000], vec![0x6000, 0x400]),
] {
let mut data = Vec::new();
for (slot, addend) in original.into_iter().enumerate() {
Rela {
offset: 0x7000 + slot as u64 * 8,
info: u64::from(R_AARCH64_RELATIVE),
addend,
}
.encode(&mut data);
}
Rela {
offset: 0x7100,
info: u64::from(R_AARCH64_RELATIVE),
addend: 0x500,
}
.encode(&mut data);
let table = RelocationTable {
data: &data,
offset: 0,
count: 4,
remap: None,
};
let profile = constructor_profile(
(&module, 0),
&restored,
&protected,
&layout,
init,
fini,
table,
)
.unwrap();
assert_eq!(profile.init_addends, expected);
assert_eq!(profile.fini_addend, 0x500);
}
protected[0x1000] ^= 1;
assert!(!is_stage1_constructor(&protected, &layout, 0x1000));
} }
#[test] #[test]
@@ -2762,47 +3085,4 @@ mod tests {
] ]
); );
} }
#[test]
fn discovers_validated_constructor_table() {
const COUNT: usize = 43;
let mut source = vec![0_u8; 0x1000];
let mut module = vec![0_u8; 0x1000];
let table = 0x40;
module[table..table + 4].copy_from_slice(&(COUNT as u32).to_le_bytes());
for index in 0..COUNT {
let constructor = 0x100 + index * 4;
let fallback = 0x500 + index * 4;
let instruction = 0xa000_0000_u32 + index as u32;
source[constructor..constructor + 4].copy_from_slice(&instruction.to_le_bytes());
source[fallback..fallback + 4].copy_from_slice(&AARCH64_RET.to_le_bytes());
let record = table + 8 + index * 16;
module[record..record + 4].copy_from_slice(&(constructor as u32).to_le_bytes());
module[record + 4..record + 8].copy_from_slice(&(fallback as u32).to_le_bytes());
module[record + 8..record + 12].copy_from_slice(&instruction.to_le_bytes());
}
let layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 1,
program_headers: vec![LoadSegment {
offset: 0,
virtual_address: 0,
file_size: source.len() as u64,
memory_size: source.len() as u64,
flags: PF_R,
alignment: 0x1000,
}],
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: 0,
};
let constructors = discover_suppressed_constructors(&module, &source, &layout, COUNT)
.expect("constructor table")
.expect("validated constructor table");
assert_eq!(constructors.len(), COUNT);
assert_eq!(constructors[0], 0x100);
assert_eq!(constructors[COUNT - 1], 0x100 + ((COUNT - 1) * 4) as u64);
}
} }
+182 -13
View File
@@ -104,6 +104,45 @@ impl<'a> Unpacker<'a> {
} }
} }
fn load_payload(&mut self) -> Option<u32> {
self.decrypt_size = self.info[6].wrapping_sub(self.info[3]).wrapping_add(8192);
self.decrypt_data2();
let src_start = self.info[4]
.wrapping_add(4096)
.wrapping_add(self.decrypt_size) as usize;
let dst_start = self.info[3].wrapping_add(self.decrypt_size) as usize;
let len = self.info[5].wrapping_sub(self.decrypt_size) as usize;
self.decompressed[dst_start..dst_start + len]
.copy_from_slice(&self.file_data[src_start..src_start + len]);
write_u32(&mut self.decompressed, self.info[3], 4096);
self.decompressed[..4096].copy_from_slice(&self.file_data[..4096]);
let pe = get_u32(self.file_data, 60);
let is_pe32 = get_u16(self.file_data, pe.wrapping_add(24)) == 0x10B;
let mut table = if is_pe32 {
self.find_pe32_table()
} else {
None
};
// Older PE32 loaders encrypt the entire payload; the length also seeds
// the cipher, so the prefix-size key cannot decode even their shell.
if is_pe32 && self.info[5] > self.decrypt_size && table.is_none() {
let prefix_size = self.decrypt_size;
let start = self.info[3] as usize;
let end = start + self.info[5] as usize;
let backup = self.decompressed[start..end].to_vec();
self.decrypt_size = self.info[5];
self.decrypt_data2();
write_u32(&mut self.decompressed, self.info[3], 4096);
table = self.find_pe32_table();
if table.is_none() {
self.decompressed[start..end].copy_from_slice(&backup);
self.decrypt_size = prefix_size;
}
}
table
}
// Strategy (a): delegate to primitives::decrypt_data3 // Strategy (a): delegate to primitives::decrypt_data3
fn decrypt_data3(&mut self, pos: u32, key: u32, shift: u32) { fn decrypt_data3(&mut self, pos: u32, key: u32, shift: u32) {
primitives::decrypt_data3(&mut self.decompressed, pos, key, shift); primitives::decrypt_data3(&mut self.decompressed, pos, key, shift);
@@ -351,28 +390,18 @@ impl<'a> Unpacker<'a> {
}); });
} }
u.decompressed = vec![0u8; size_of_image as usize]; u.decompressed = vec![0u8; size_of_image as usize];
u.decrypt_size = u.info[6].wrapping_sub(u.info[3]).wrapping_add(8192);
if verbose { if verbose {
println!("[2/9] Decrypting payload..."); println!("[2/9] Decrypting payload...");
} }
u.decrypt_data2(); let pe32_table = u.load_payload();
{
let src_start = u.info[4].wrapping_add(4096).wrapping_add(u.decrypt_size) as usize;
let dst_start = u.info[3].wrapping_add(u.decrypt_size) as usize;
let len = u.info[5].wrapping_sub(u.decrypt_size) as usize;
u.decompressed[dst_start..dst_start + len]
.copy_from_slice(&u.file_data[src_start..src_start + len]);
}
write_u32(&mut u.decompressed, u.info[3], 4096);
u.decompressed[..4096].copy_from_slice(&u.file_data[..4096]);
// PE32 (32-bit) images use an entirely different config layout and // PE32 (32-bit) images use an entirely different config layout and
// final-output transform than PE32+ (64-bit). Dispatch here, after the // final-output transform than PE32+ (64-bit). Dispatch here, after the
// shared header/payload setup (the PE32 branch follows the common // shared header/payload setup (the PE32 branch follows the common
// Stage 1/2 work). pe_magic at pe_off+24: 0x10B = PE32, 0x20B = PE32+. // Stage 1/2 work). pe_magic at pe_off+24: 0x10B = PE32, 0x20B = PE32+.
if get_u16(u.file_data, pe_off.wrapping_add(24)) == 0x10B { if get_u16(u.file_data, pe_off.wrapping_add(24)) == 0x10B {
return u.run_pe32(pe_off, verbose); let table = pe32_table.ok_or(UnpackError::Pe32TblNotFound)?;
return u.run_pe32(pe_off, table, verbose);
} }
// The config block layout in the decrypted region varies between Crackproof // The config block layout in the decrypted region varies between Crackproof
@@ -1992,6 +2021,146 @@ impl<'a> Unpacker<'a> {
} }
} }
#[cfg(test)]
mod payload_tests {
use super::*;
fn check_payload(full: bool, valid_table: bool) {
let info = [
0x1234_5678,
0x4E4E_4F4B,
0,
0x2000,
0x200,
0x7000,
0x2400,
0,
];
let mut plain = vec![0u8; 0x10000];
let table = 0x2800;
let (shell_field, header_field) = if full { (0x78, 0x50) } else { (0x88, 0x58) };
write_u32(&mut plain, table + shell_field, info[6]);
write_u32(&mut plain, table + shell_field + 4, 0x1800);
write_u32(&mut plain, table + header_field, 0x80);
write_u32(
&mut plain,
table + header_field + 4,
if valid_table { 0x58 } else { 0 },
);
let ss_size = if full { 0xAEC } else { 0xBC0 };
write_u32(&mut plain, table + shell_field + 0x10, 0x6000);
write_u32(&mut plain, table + shell_field + 0x14, ss_size);
write_u32(&mut plain, table + shell_field + 0x20, info[6]);
write_u32(&mut plain, table + shell_field + 0x24, 0x1800);
write_u32(&mut plain, table + 0x40, 0x9ABC_DEF0);
let third_pair = 0x6000 + if full { 0xAB4 } else { 0xB8C };
write_u32(&mut plain, third_pair, 0x8000);
write_u32(&mut plain, third_pair + 4, 0x20);
if full {
// This earlier table has plausible descriptors but a wrong stage key.
for (field, value) in [
(0x40, 0x1122_3344),
(0x50, 0x80),
(0x54, 0x58),
(0x78, info[6]),
(0x7C, 0x1800),
(0x88, 0x7000),
(0x8C, 0xAEC),
(0x98, info[6]),
(0x9C, 0x1800),
] {
write_u32(&mut plain, 0x2500 + field, value);
}
}
write_u32(&mut plain, 0x8100, 0xAABB_CCDD);
let encrypted_size = if full {
info[5]
} else {
info[6] - info[3] + 8192
};
let src = 4096 + info[4];
let mut input = vec![0u8; (src + info[5]) as usize];
write_u32(&mut input, 0x3C, 0x80);
write_u16(&mut input, 0x98, 0x10B);
plain[..4096].copy_from_slice(&input[..4096]);
let mut key = primitives::calculate_checksum(&plain, table + header_field)
^ primitives::calculate_checksum(&plain, table + shell_field + 0x20)
^ get_u32(&plain, table + 0x40);
for i in 0..ss_size / 4 {
let word = get_u32(&plain, 0x6000 + i * 4);
write_u32(
&mut plain,
0x6000 + i * 4,
word.wrapping_add(i).rotate_left(21) ^ key,
);
key = key.wrapping_add(i);
}
let mut key = info[0].wrapping_add(!encrypted_size);
for i in 0..info[5] / 4 {
let word = get_u32(&plain, info[3] + i * 4);
let cell = if i * 4 < encrypted_size {
word ^ key
} else {
word
};
write_u32(&mut input, src + i * 4, cell);
key = i.wrapping_mul(i) ^ key.wrapping_add(cell).wrapping_add(i);
}
let mut unpacker = Unpacker {
file_data: &input,
decompressed: vec![0u8; plain.len()],
info,
key_offsets: [0; 4],
decrypt_size: 0,
};
let selected = unpacker.load_payload();
assert_eq!(selected, valid_table.then_some(table));
if !valid_table {
let prefix_size = info[6] - info[3] + 8192;
assert_eq!(unpacker.decrypt_size, prefix_size);
assert_eq!(
get_u32(&unpacker.decompressed, info[3] + prefix_size - 4),
{
let mut key = info[0].wrapping_add(!prefix_size);
let mut word = 0;
for i in 0..prefix_size / 4 {
let cell = get_u32(&input, src + i * 4);
word = key ^ cell;
key = i.wrapping_mul(i) ^ key.wrapping_add(cell).wrapping_add(i);
}
word
}
);
assert_eq!(
&unpacker.decompressed
[(info[3] + prefix_size) as usize..(info[3] + info[5]) as usize],
&input[(src + prefix_size) as usize..(src + info[5]) as usize]
);
return;
}
assert_eq!(unpacker.decrypt_size, encrypted_size);
assert_eq!(
&unpacker.decompressed[info[3] as usize + 4..(info[3] + info[5]) as usize],
&plain[info[3] as usize + 4..(info[3] + info[5]) as usize]
);
}
#[test]
fn pe32_full_payload_encryption_is_trial_validated() {
check_payload(true, true);
}
#[test]
fn pe32_prefix_payload_encryption_preserves_plaintext_tail() {
check_payload(false, true);
}
#[test]
fn pe32_invalid_full_payload_trial_restores_prefix() {
check_payload(true, false);
}
}
#[cfg(test)] #[cfg(test)]
mod error_tests { mod error_tests {
use super::*; use super::*;
+211 -111
View File
@@ -23,10 +23,153 @@ fn stage_key_rounds(data: &[u8], table: u32, slots: usize) -> Result<u32, Unpack
} }
impl<'a> Unpacker<'a> { impl<'a> Unpacker<'a> {
pub(super) fn find_pe32_table(&mut self) -> Option<u32> {
let pe_off = get_u32(self.file_data, 60);
let header = self.decompressed[..4096].to_vec();
for tbl in layout::find_tbl_pe32_candidates(&self.decompressed, &self.info) {
let compact = get_u32(&self.decompressed, tbl + 0x78) == self.info[6];
let pair = tbl + if compact { 0x88 } else { 0x98 };
let start = get_u32(&self.decompressed, pair) as usize;
let size = get_u32(&self.decompressed, pair + 4) as usize;
let Some(end) = start
.checked_add(size)
.filter(|&end| end <= self.decompressed.len())
else {
continue;
};
let ciphertext = self.decompressed[start..end].to_vec();
let valid = self.decrypt_pe32_second_stage(tbl, pe_off).is_ok();
self.decompressed[start..end].copy_from_slice(&ciphertext);
self.decompressed[..4096].copy_from_slice(&header);
if valid {
return Some(tbl);
}
}
None
}
fn decrypt_pe32_second_stage(&mut self, tbl: u32, pe_off: u32) -> Result<u32, UnpackError> {
let compact = get_u32(&self.decompressed, tbl.wrapping_add(0x78)) == self.info[6];
let header_cs_field = if compact { 0x50 } else { 0x58 };
let stage_field = if compact { 0x88 } else { 0x98 };
let resource_field = if compact { 0xB0 } else { 0xC8 };
if (pe_off as usize)
.checked_add(0xB8)
.is_none_or(|end| end > 0x1000)
{
return Err(UnpackError::Pe32RelocationDataNotFound);
}
let import_rva = get_u32(&self.decompressed, tbl + stage_field + 0x24);
let resource_rva = get_u32(&self.decompressed, tbl + resource_field);
let resource_size = get_u32(&self.decompressed, tbl + resource_field + 4);
write_u32(&mut self.decompressed, pe_off + 0x80, import_rva);
write_u32(&mut self.decompressed, pe_off + 0x88, resource_rva);
write_u32(&mut self.decompressed, pe_off + 0x8C, resource_size);
write_u32(&mut self.decompressed, pe_off + 0xB0, 0);
write_u32(&mut self.decompressed, pe_off + 0xB4, 0);
let cs = tbl + stage_field + 0x10;
let cs_start = get_u32(&self.decompressed, cs) as usize;
let cs_size = get_u32(&self.decompressed, cs + 4) as usize;
if cs_size == 0
|| cs_start
.checked_add(cs_size)
.is_none_or(|end| end > self.decompressed.len())
{
return Err(UnpackError::Pe32RelocationDataNotFound);
}
let first_stage_cs = self.calculate_checksum(cs);
let second_stage_key = get_u32(&self.decompressed, tbl + 0x40);
let ss_pair = tbl + stage_field;
let ss = get_u32(&self.decompressed, ss_pair);
let ss_size = get_u32(&self.decompressed, ss_pair + 4);
let ss_shift = ss_size.wrapping_sub(if compact { 0xAEC } else { 0xBC0 });
let third_pair_off = (if compact { 0xAB4u32 } else { 0xB8C }).wrapping_add(ss_shift);
let ss_lo = ss as usize;
let ss_hi = ss_lo.saturating_add(ss_size as usize);
if ss_hi > self.decompressed.len() || third_pair_off.saturating_add(8) > ss_size {
return Err(UnpackError::Pe32SecondStageRangeInvalid {
offset: ss,
size: ss_size,
image_len: self.decompressed.len(),
});
}
let ss_ct = self.decompressed[ss_lo..ss_hi].to_vec();
// Header checksums refer to the original PE header. Compact EXEs and
// native DLLs can carry a packer-added BaseReloc entry: trial both keys.
for zero_reloc in [false, true] {
if zero_reloc {
write_u32(&mut self.decompressed, pe_off + 0xA0, 0);
write_u32(&mut self.decompressed, pe_off + 0xA4, 0);
}
let mut hcs_addr = tbl + header_cs_field;
let mut header_checksum = 0;
loop {
if hcs_addr + 8 > tbl + stage_field - 0x10 {
return Err(UnpackError::Pe32RelocationDataNotFound);
}
let size = get_u32(&self.decompressed, hcs_addr + 4) as usize;
if size == 0 {
break;
}
let start = get_u32(&self.decompressed, hcs_addr) as usize;
if start.checked_add(size).is_none_or(|end| end > 0x1000) {
return Err(UnpackError::Pe32RelocationDataNotFound);
}
header_checksum ^= self.calculate_checksum(hcs_addr);
hcs_addr += 8;
}
self.decompressed[ss_lo..ss_hi].copy_from_slice(&ss_ct);
self.decrypt_data3(
ss_pair,
header_checksum ^ first_stage_cs ^ second_stage_key,
21,
);
let pair = ss + third_pair_off;
let off = get_u32(&self.decompressed, pair) as usize;
let size = get_u32(&self.decompressed, pair + 4) as usize;
if off > 0x1000
&& size >= 4
&& off
.checked_add(size)
.is_some_and(|end| end <= self.decompressed.len())
{
return Ok(header_checksum);
}
}
Err(UnpackError::Pe32RelocationDataNotFound)
}
fn read_pe32_metadata(&mut self, compact: bool) -> (u32, [u8; 128]) {
let base = self.info[3];
let layout_b = get_u32(&self.decompressed, base.wrapping_add(0x10)) > 0x10000;
let (offset, size, ep_offset, dirs_offset) = if layout_b {
// Compact metadata has an extra 16-byte prefix before the EP/dirs.
let shift = if compact { 0x10 } else { 0 };
(0x10, 0x290, 0x20 + shift, 0x30 + shift)
} else {
(0x40, 144, 0x40, 0x50)
};
let start = base.wrapping_add(offset) as usize;
let backup = self.decompressed[start..start + size].to_vec();
self.decrypt_data5(base.wrapping_add(offset), size as u32);
let ep = get_u32(&self.decompressed, base.wrapping_add(ep_offset));
let mut dirs = [0u8; 128];
let dirs_start = base.wrapping_add(dirs_offset) as usize;
dirs.copy_from_slice(&self.decompressed[dirs_start..dirs_start + 128]);
self.decompressed[start..start + size].copy_from_slice(&backup);
(ep, dirs)
}
/// PE32 (32-bit) unpack pipeline. The shared Stage 1/2 setup (info decrypt, /// PE32 (32-bit) unpack pipeline. The shared Stage 1/2 setup (info decrypt,
/// payload decrypt, raw copy, header restore) has already run in `run()` /// payload decrypt, raw copy, header restore) has already run in `run()`
/// before dispatch; this takes over from "Locating shell offsets". /// before dispatch; this takes over from "Locating shell offsets".
pub(super) fn run_pe32(&mut self, pe_off: u32, verbose: bool) -> Result<Vec<u8>, UnpackError> { pub(super) fn run_pe32(
&mut self,
pe_off: u32,
tbl: u32,
verbose: bool,
) -> Result<Vec<u8>, UnpackError> {
let info = self.info; let info = self.info;
let info3 = info[3]; let info3 = info[3];
@@ -43,94 +186,21 @@ impl<'a> Unpacker<'a> {
key key
}; };
// ---- Locate tbl in shell ---- // ---- Validated shell table ----
let tbl = let compact = get_u32(&self.decompressed, tbl.wrapping_add(0x78)) == info[6];
layout::find_tbl_pe32(&self.decompressed, &info).ok_or(UnpackError::Pe32TblNotFound)?; let stage_field = if compact { 0x88 } else { 0x98 };
if verbose { if verbose {
println!("[3/9] Locating config layout (PE32)..."); println!("[3/9] Locating config layout (PE32)...");
println!(" tbl = 0x{:X}", tbl); println!(" tbl = 0x{:X}, compact = {}", tbl, compact);
} }
// ---- PE header restore ----
let val_bc = get_u32(&self.decompressed, tbl.wrapping_add(0xBC));
let val_c8 = get_u32(&self.decompressed, tbl.wrapping_add(0xC8));
let val_cc = get_u32(&self.decompressed, tbl.wrapping_add(0xCC));
write_u32(&mut self.decompressed, pe_off.wrapping_add(0x80), val_bc);
write_u32(&mut self.decompressed, pe_off.wrapping_add(0x88), val_c8);
write_u32(&mut self.decompressed, pe_off.wrapping_add(0x8C), val_cc);
write_u32(&mut self.decompressed, pe_off.wrapping_add(0xB0), 0);
write_u32(&mut self.decompressed, pe_off.wrapping_add(0xB4), 0);
// ---- Header-independent checksum inputs ----
let first_stage_cs = self.calculate_checksum(tbl.wrapping_add(0xA8));
let second_stage_key = get_u32(&self.decompressed, tbl.wrapping_add(0x40));
// ---- Stage 3: SecondStage ---- // ---- Stage 3: SecondStage ----
// let ss_pair = tbl.wrapping_add(stage_field);
// ss_key = headerChecksum ^ firstStageCS ^ secondStageKey, where the
// header checksum (a XOR of crc32(region)^size over the sub-regions at
// tbl+0x58) is taken over the *original* pre-pack PE header. For EXEs the
// import/resource restore above reconstructs that header exactly. Native
// DLLs additionally carry a packer-added BaseReloc data-directory entry
// (dir 5) that was absent from the checksummed original, so the header
// checksum only matches once that entry is treated as zero. EXEs have no
// dir-5 entry, so zeroing it is a no-op for them.
//
// Rather than branch on EXE-vs-DLL, try the header as-is and, on failure,
// with the BaseReloc entry zeroed; keep whichever ss_key decrypts a
// SecondStage whose ThirdStage (off,size) pair lands inside the image.
// This uses the same shift/key trial-and-validate the later stages
// already use, and keeps EXE output byte-identical (the as-is variant
// wins first).
let ss_pair = tbl.wrapping_add(0x98);
let ss = get_u32(&self.decompressed, ss_pair); let ss = get_u32(&self.decompressed, ss_pair);
let ss_size = get_u32(&self.decompressed, ss_pair.wrapping_add(4)); let ss_size = get_u32(&self.decompressed, ss_pair.wrapping_add(4));
let ss_shift = ss_size.wrapping_sub(0xBC0); let ss_shift = ss_size.wrapping_sub(if compact { 0xAEC } else { 0xBC0 });
// Back up the SecondStage ciphertext so a failed trial can be retried. let third_pair_off = (if compact { 0xAB4u32 } else { 0xB8C }).wrapping_add(ss_shift);
let ss_lo = ss as usize; let header_checksum = self.decrypt_pe32_second_stage(tbl, pe_off)?;
let ss_hi = ss_lo.wrapping_add(ss_size as usize);
if ss_hi < ss_lo || ss_hi > self.decompressed.len() {
return Err(UnpackError::Pe32SecondStageRangeInvalid {
offset: ss,
size: ss_size,
image_len: self.decompressed.len(),
});
}
let ss_ct: Vec<u8> = self.decompressed[ss_lo..ss_hi].to_vec();
// PE32 data dir 5 (BaseReloc) = optional_header(pe+24) + 0x60 + 5*8 = pe+0xA0.
let reloc_dir = pe_off.wrapping_add(0xA0);
let len = self.decompressed.len() as u64;
let pair_off = 0xB8Cu32.wrapping_add(ss_shift);
let mut found = false;
// Holds the winning variant's header checksum; the later stages
// (Forth/Fifth/Seven/Eighth) reuse it as a key component.
let mut header_checksum: u32 = 0;
for zero_reloc in [false, true] {
if zero_reloc {
write_u32(&mut self.decompressed, reloc_dir, 0);
write_u32(&mut self.decompressed, reloc_dir.wrapping_add(4), 0);
}
let mut hcs_addr = tbl.wrapping_add(0x58);
header_checksum = 0;
while get_u32(&self.decompressed, hcs_addr.wrapping_add(4)) != 0 {
header_checksum ^= self.calculate_checksum(hcs_addr);
hcs_addr = hcs_addr.wrapping_add(8);
}
let ss_key = header_checksum ^ first_stage_cs ^ second_stage_key;
self.decompressed[ss_lo..ss_hi].copy_from_slice(&ss_ct);
self.decrypt_data3(ss_pair, ss_key, 21);
// Validate: the ThirdStage (off,size) pair must reference the image.
let pair = ss.wrapping_add(pair_off);
let off = get_u32(&self.decompressed, pair) as u64;
let sz = get_u32(&self.decompressed, pair.wrapping_add(4)) as u64;
if off > 0x1000 && off < len && sz >= 4 && off.saturating_add(sz) <= len {
found = true;
break;
}
}
if !found {
return Err(UnpackError::Pe32RelocationDataNotFound);
}
if verbose { if verbose {
println!( println!(
" ss = 0x{:08X}, size = 0x{:X}, shift = 0x{:X}", " ss = 0x{:08X}, size = 0x{:X}, shift = 0x{:X}",
@@ -139,13 +209,12 @@ impl<'a> Unpacker<'a> {
} }
// ---- PE32 fixed offsets ---- // ---- PE32 fixed offsets ----
let third_key_off = 0x968u32.wrapping_add(ss_shift); let third_key_off = (if compact { 0x950u32 } else { 0x968 }).wrapping_add(ss_shift);
let forth_key_off = 0x964u32.wrapping_add(ss_shift); let forth_key_off = (if compact { 0x94Cu32 } else { 0x964 }).wrapping_add(ss_shift);
let cs_base_off = 0x96Cu32.wrapping_add(ss_shift); let cs_base_off = (if compact { 0x954u32 } else { 0x96C }).wrapping_add(ss_shift);
let dp_base_off = 0xA9Cu32.wrapping_add(ss_shift); let dp_base_off = (if compact { 0x9C4u32 } else { 0xA9C }).wrapping_add(ss_shift);
// ---- Stage 4: ThirdStage (brute-force the rotate shift) ---- // ---- Stage 4: ThirdStage (brute-force the rotate shift) ----
let third_pair_off = 0xB8Cu32.wrapping_add(ss_shift);
let key = get_u32(&self.decompressed, ss.wrapping_add(third_key_off)); let key = get_u32(&self.decompressed, ss.wrapping_add(third_key_off));
let pair_addr = ss.wrapping_add(third_pair_off); let pair_addr = ss.wrapping_add(third_pair_off);
let ts_addr = get_u32(&self.decompressed, pair_addr); let ts_addr = get_u32(&self.decompressed, pair_addr);
@@ -238,7 +307,7 @@ impl<'a> Unpacker<'a> {
} }
// ---- Checksum addresses ---- // ---- Checksum addresses ----
let second_stage_cs_addr = tbl.wrapping_add(0xB0); let second_stage_cs_addr = tbl.wrapping_add(stage_field + 0x18);
let forth_stage_cs_addr = ss.wrapping_add(cs_base_off); let forth_stage_cs_addr = ss.wrapping_add(cs_base_off);
let fifth_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x08); let fifth_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x08);
let seven_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x10); let seven_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x10);
@@ -298,7 +367,9 @@ impl<'a> Unpacker<'a> {
); );
let seven_key = !get_u32( let seven_key = !get_u32(
&self.decompressed, &self.decompressed,
cs1_addr.wrapping_add(cs1_size).wrapping_sub(0x10), cs1_addr
.wrapping_add(cs1_size)
.wrapping_sub(if compact { 0xC } else { 0x10 }),
); );
let fk7 = header_checksum ^ fifth_cs ^ seven_key; let fk7 = header_checksum ^ fifth_cs ^ seven_key;
if let Err(reason) = self.decrypt_and_decompress_data(seven_addr, fk7, None) { if let Err(reason) = self.decrypt_and_decompress_data(seven_addr, fk7, None) {
@@ -640,28 +711,7 @@ impl<'a> Unpacker<'a> {
)?; )?;
// ---- PE32 metadata: EP and data dirs from info[3] ---- // ---- PE32 metadata: EP and data dirs from info[3] ----
let test_val = get_u32(&self.decompressed, info3.wrapping_add(0x10)); let (metadata_ep, metadata_dirs) = self.read_pe32_metadata(compact);
let metadata_ep: u32;
let mut metadata_dirs = [0u8; 128];
if test_val > 0x10000 {
// Layout B
let s = info3.wrapping_add(0x10) as usize;
let backup_meta = self.decompressed[s..s + 0x290].to_vec();
self.decrypt_data5(info3.wrapping_add(0x10), 0x290);
metadata_ep = get_u32(&self.decompressed, info3.wrapping_add(0x20));
let d = info3.wrapping_add(0x30) as usize;
metadata_dirs.copy_from_slice(&self.decompressed[d..d + 128]);
self.decompressed[s..s + 0x290].copy_from_slice(&backup_meta);
} else {
// Layout A
let s = info3.wrapping_add(0x40) as usize;
let backup_meta = self.decompressed[s..s + 144].to_vec();
self.decrypt_data5(info3.wrapping_add(0x40), 144);
metadata_ep = get_u32(&self.decompressed, info3.wrapping_add(0x40));
let d = info3.wrapping_add(0x50) as usize;
metadata_dirs.copy_from_slice(&self.decompressed[d..d + 128]);
self.decompressed[s..s + 144].copy_from_slice(&backup_meta);
}
// ---- Zero-out list (runs BEFORE decompression) ---- // ---- Zero-out list (runs BEFORE decompression) ----
let zero_list_addr = eighth_start.wrapping_add(off_zero_list); let zero_list_addr = eighth_start.wrapping_add(off_zero_list);
@@ -1157,6 +1207,56 @@ impl<'a> Unpacker<'a> {
mod tests { mod tests {
use super::*; use super::*;
fn check_metadata(compact: bool, layout_b: bool) {
let base = 0x1000u32;
let (offset, size, ep_offset, dirs_offset) = if layout_b {
if compact {
(0x10, 0x290, 0x30, 0x40)
} else {
(0x10, 0x290, 0x20, 0x30)
}
} else {
(0x40, 144, 0x40, 0x50)
};
let mut data = vec![0u8; 0x2000];
let ep = 0x123456;
let dirs = std::array::from_fn::<_, 128, _>(|i| i as u8 ^ 0x5A);
write_u32(&mut data, base + ep_offset, ep);
data[(base + dirs_offset) as usize..(base + dirs_offset + 128) as usize]
.copy_from_slice(&dirs);
for i in 0..size {
let key = (base + offset + i) as u8;
let byte = &mut data[(base + offset + i) as usize];
let mixed = (byte.rotate_right(2) ^ key).rotate_right(2) ^ key.wrapping_add(1);
*byte = mixed.rotate_right(2);
}
assert_eq!(get_u32(&data, base + 0x10) > 0x10000, layout_b);
let backup = data.clone();
let mut unpacker = Unpacker {
file_data: &[],
decompressed: data,
info: [0, 0, 0, base, 0, 0, 0, 0],
key_offsets: [0; 4],
decrypt_size: 0,
};
assert_eq!(unpacker.read_pe32_metadata(compact), (ep, dirs));
assert_eq!(
unpacker.decompressed, backup,
"metadata reads must be nondestructive"
);
}
#[test]
fn pe32_compact_metadata_restores_entry_point_and_directories() {
check_metadata(true, true);
}
#[test]
fn pe32_standard_metadata_preserves_both_layouts() {
check_metadata(false, false);
check_metadata(false, true);
}
#[test] #[test]
fn stage_key_rounds_follow_active_descriptor_prefix() { fn stage_key_rounds_follow_active_descriptor_prefix() {
for (sizes, expected) in [ for (sizes, expected) in [
+3 -2
View File
@@ -6,8 +6,9 @@ mod image;
pub(super) use dd8::{select_dd8_formula_pe32, select_dd8_shift}; pub(super) use dd8::{select_dd8_formula_pe32, select_dd8_shift};
pub(super) use discovery::{ pub(super) use discovery::{
discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos, find_tbl_pe32, discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos,
find_v_after_pad, find_v4_offset, get_string_to_null, section_name, trial_decrypt5_u32, find_tbl_pe32_candidates, find_v_after_pad, find_v4_offset, get_string_to_null, section_name,
trial_decrypt5_u32,
}; };
pub(super) use image::{ pub(super) use image::{
compact_memory_image_to_pe, move_pe32_imports_to_kmiat, pe32_imports_already_match_idata_layout, compact_memory_image_to_pe, move_pe32_imports_to_kmiat, pe32_imports_already_match_idata_layout,
+96 -28
View File
@@ -212,37 +212,54 @@ pub fn section_name(data: &[u8], offset: u32) -> String {
// PE32 (32-bit) helpers // PE32 (32-bit) helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// PE32 shell-table locator. Walks the shell region (`info[6]`) for a dword /// PE32 shell-table locator. The compact layout moves the shell descriptor
/// equal to `info[6]` followed by a plausible shell size, returning the table /// from +0x88 to +0x78 and the header-checksum list from +0x58 to +0x50.
/// base (`candidate = off - 0x88`) when `candidate+0x58` holds a valid pointer. /// Return structurally plausible candidates; the pipeline must trial-decrypt
pub fn find_tbl_pe32(data: &[u8], info: &[u32; 8]) -> Option<u32> { /// their second stages before selecting a table.
let shell = info[6]; pub fn find_tbl_pe32_candidates(data: &[u8], info: &[u32; 8]) -> Vec<u32> {
if (data.len() as u64) < 0x100 { let mut tables = Vec::new();
return None; let shell = info[6] as usize;
} let hi = shell.saturating_add(0x3000).min(data.len());
let hi = (shell as u64) for off in (shell..hi.saturating_sub(8)).step_by(4) {
.saturating_add(0x3000) if get_u32(data, off as u32) != info[6] {
.min(data.len() as u64 - 0x100) as u32; continue;
let mut off = shell; }
while off < hi { let shell_size = get_u32(data, off as u32 + 4) as usize;
if off as usize + 8 <= data.len() { if !(0x1001..0x100000).contains(&shell_size)
let candidate = off.wrapping_sub(0x88); || shell
if candidate >= shell && get_u32(data, off) == info[6] { .checked_add(shell_size)
let shell_size_val = get_u32(data, off.wrapping_add(4)); .is_none_or(|end| end > data.len())
if shell_size_val > 0x1000 && shell_size_val < 0x100000 { {
let v58_off = candidate.wrapping_add(0x58); continue;
if (v58_off as usize + 4) <= data.len() { }
let v58 = get_u32(data, v58_off); for (shell_field, header_field) in [(0x88, 0x58), (0x78, 0x50)] {
if v58 > 0 && (v58 as usize) < data.len() { let Some(candidate) = off.checked_sub(shell_field).filter(|&base| base >= shell) else {
return Some(candidate); continue;
} };
} let Some(table) = data.get(candidate..candidate + 0xD0) else {
} continue;
};
let header = get_u32(table, header_field) as usize;
let header_size = get_u32(table, header_field + 4) as usize;
let stage = get_u32(table, shell_field as u32 + 0x10) as usize;
let stage_size = get_u32(table, shell_field as u32 + 0x14) as usize;
if header > 0
&& header_size > 0
&& header
.checked_add(header_size)
.is_some_and(|end| end <= 0x1000)
&& stage >= info[3] as usize
&& stage_size >= 4
&& stage
.checked_add(stage_size)
.is_some_and(|end| end <= data.len())
&& !tables.contains(&(candidate as u32))
{
tables.push(candidate as u32);
} }
} }
off = off.wrapping_add(4);
} }
None tables
} }
/// Locate an LFSR-encrypted bytecode block (decrypt_data6 form) in a region. /// Locate an LFSR-encrypted bytecode block (decrypt_data6 form) in a region.
@@ -449,6 +466,57 @@ pub fn discover_eighth_slots(
mod tests { mod tests {
use super::*; use super::*;
fn pe32_table_fixture(compact: bool) -> (Vec<u8>, [u32; 8], u32) {
let mut data = vec![0u8; 0x10000];
let info = [0, 0, 0, 0x2000, 0, 0x7000, 0x2400, 0];
let table = 0x2800;
let (shell, header) = if compact { (0x78, 0x50) } else { (0x88, 0x58) };
for (field, value) in [
(header, 0x80),
(header + 4, 0x58),
(shell, info[6]),
(shell + 4, 0x1800),
(shell + 0x10, 0x6000),
(shell + 0x14, 0xAEC),
] {
senbei_crypto::primitives::write_u32(&mut data, table + field, value);
}
(data, info, table)
}
#[test]
fn pe32_table_supports_compact_header_descriptors() {
for compact in [false, true] {
let (data, info, table) = pe32_table_fixture(compact);
assert_eq!(find_tbl_pe32_candidates(&data, &info), vec![table]);
}
}
#[test]
fn pe32_table_rejects_a_shell_reference_without_valid_descriptors() {
let (mut data, info, table) = pe32_table_fixture(true);
for (field, value) in [(0x58, 0x80), (0x5C, 0x58), (0x88, info[6]), (0x8C, 0x1800)] {
senbei_crypto::primitives::write_u32(&mut data, 0x2500 + field, value);
}
assert_eq!(find_tbl_pe32_candidates(&data, &info), vec![table]);
senbei_crypto::primitives::write_u32(&mut data, table + 0x54, 0);
assert_eq!(find_tbl_pe32_candidates(&data, &info), Vec::<u32>::new());
}
#[test]
fn pe32_table_rejects_truncated_and_out_of_range_regions() {
let (mut data, mut info, table) = pe32_table_fixture(true);
assert_eq!(
find_tbl_pe32_candidates(&data[..0x28C0], &info),
Vec::<u32>::new()
);
senbei_crypto::primitives::write_u32(&mut data, table + 0x50, u32::MAX - 15);
assert_eq!(find_tbl_pe32_candidates(&data, &info), Vec::<u32>::new());
info[6] = u32::MAX;
assert_eq!(find_tbl_pe32_candidates(&data, &info), Vec::<u32>::new());
assert_eq!(find_tbl_pe32_candidates(&[], &info), Vec::<u32>::new());
}
/// Task 4.1 regression: build a synthetic buffer whose valid bytecode block /// Task 4.1 regression: build a synthetic buffer whose valid bytecode block
/// sits PAST `len` but within `len*2`. Assert that the smaller window misses /// sits PAST `len` but within `len*2`. Assert that the smaller window misses
/// it and the doubled window finds it. /// it and the doubled window finds it.
+7 -7
View File
@@ -412,7 +412,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-crypto" name = "senbei-crypto"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"aes", "aes",
"thiserror", "thiserror",
@@ -420,7 +420,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-elf" name = "senbei-elf"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"goblin", "goblin",
"thiserror", "thiserror",
@@ -428,7 +428,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-engine" name = "senbei-engine"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"memmap2", "memmap2",
"senbei-crypto", "senbei-crypto",
@@ -443,7 +443,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-io" name = "senbei-io"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"indicatif", "indicatif",
@@ -464,7 +464,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-metadata" name = "senbei-metadata"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@@ -472,14 +472,14 @@ dependencies = [
[[package]] [[package]]
name = "senbei-pe" name = "senbei-pe"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
[[package]] [[package]]
name = "senbei-wasm" name = "senbei-wasm"
version = "1.3.2" version = "1.3.3"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"senbei-engine", "senbei-engine",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "senbei-wasm" name = "senbei-wasm"
version = "1.3.2" version = "1.3.3"
edition = "2024" edition = "2024"
rust-version = "1.98.1" rust-version = "1.98.1"
description = "WebAssembly bindings for senbei (browser frontend assets live in web/)" description = "WebAssembly bindings for senbei (browser frontend assets live in web/)"