mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-03 10:31:28 -05:00
Compare commits
5
Commits
5bd1b0b07d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
366a19c2a1 | ||
|
|
ba1dacc0b2 | ||
|
|
31a0c4139c | ||
|
|
ea92d26d28 | ||
|
|
6e233bbf18 |
No files matched your search
Generated
+7
-7
@@ -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
@@ -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,4 +1,5 @@
|
|||||||
mod artifact;
|
mod artifact;
|
||||||
|
mod constructors;
|
||||||
mod error;
|
mod error;
|
||||||
mod pipeline;
|
mod pipeline;
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 +2765,271 @@ 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!(
|
assert_eq!(
|
||||||
discover_auxiliary_symbol_seed(&module).expect("symbol seed"),
|
discover_auxiliary_symbol_seed(&auxiliary_seed_module(helper_offset))
|
||||||
|
.expect("symbol seed"),
|
||||||
0x4db4_d9ec
|
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!(
|
||||||
|
discover_auxiliary_symbol_seed(&module).unwrap(),
|
||||||
|
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]
|
||||||
fn classifies_special_plt_slots_from_call_shapes() {
|
fn classifies_special_plt_slots_from_call_shapes() {
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -823,8 +873,16 @@ impl<'a> Unpacker<'a> {
|
|||||||
let d = export_va as usize;
|
let d = export_va as usize;
|
||||||
let s = export_file_off as usize;
|
let s = export_file_off as usize;
|
||||||
let n = export_size as usize;
|
let n = export_size as usize;
|
||||||
|
// Spliced companions may omit the stub's plaintext exports;
|
||||||
|
// the I/O adapter overlays those from the original stub later.
|
||||||
|
if s.checked_add(n)
|
||||||
|
.is_some_and(|end| end <= self.file_data.len())
|
||||||
|
&& d.checked_add(n)
|
||||||
|
.is_some_and(|end| end <= self.decompressed.len())
|
||||||
|
{
|
||||||
self.decompressed[d..d + n].copy_from_slice(&self.file_data[s..s + n]);
|
self.decompressed[d..d + n].copy_from_slice(&self.file_data[s..s + n]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- .text decrypt with decrypt_data8 (PE32 auto-detected formula) ----
|
// ---- .text decrypt with decrypt_data8 (PE32 auto-detected formula) ----
|
||||||
// `select_dd8_formula_pe32` returns None when `.text` was not packer-dd8-
|
// `select_dd8_formula_pe32` returns None when `.text` was not packer-dd8-
|
||||||
@@ -1149,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 [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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());
|
||||||
|
for off in (shell..hi.saturating_sub(8)).step_by(4) {
|
||||||
|
if get_u32(data, off as u32) != info[6] {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
let hi = (shell as u64)
|
let shell_size = get_u32(data, off as u32 + 4) as usize;
|
||||||
.saturating_add(0x3000)
|
if !(0x1001..0x100000).contains(&shell_size)
|
||||||
.min(data.len() as u64 - 0x100) as u32;
|
|| shell
|
||||||
let mut off = shell;
|
.checked_add(shell_size)
|
||||||
while off < hi {
|
.is_none_or(|end| end > data.len())
|
||||||
if off as usize + 8 <= data.len() {
|
{
|
||||||
let candidate = off.wrapping_sub(0x88);
|
continue;
|
||||||
if candidate >= shell && get_u32(data, off) == info[6] {
|
}
|
||||||
let shell_size_val = get_u32(data, off.wrapping_add(4));
|
for (shell_field, header_field) in [(0x88, 0x58), (0x78, 0x50)] {
|
||||||
if shell_size_val > 0x1000 && shell_size_val < 0x100000 {
|
let Some(candidate) = off.checked_sub(shell_field).filter(|&base| base >= shell) else {
|
||||||
let v58_off = candidate.wrapping_add(0x58);
|
continue;
|
||||||
if (v58_off as usize + 4) <= data.len() {
|
};
|
||||||
let v58 = get_u32(data, v58_off);
|
let Some(table) = data.get(candidate..candidate + 0xD0) else {
|
||||||
if v58 > 0 && (v58 as usize) < data.len() {
|
continue;
|
||||||
return Some(candidate);
|
};
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
tables
|
||||||
}
|
|
||||||
off = off.wrapping_add(4);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
|
|||||||
@@ -331,6 +331,13 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
|
|||||||
let opt_hdr = pe_header.wrapping_add(24);
|
let opt_hdr = pe_header.wrapping_add(24);
|
||||||
let sec_table = opt_hdr.wrapping_add(opt_hdr_size);
|
let sec_table = opt_hdr.wrapping_add(opt_hdr_size);
|
||||||
let num_sections = get_u16(data, pe_header.wrapping_add(6)) as u32;
|
let num_sections = get_u16(data, pe_header.wrapping_add(6)) as u32;
|
||||||
|
let export_dir = opt_hdr.wrapping_add(if get_u16(data, opt_hdr) == 0x20B {
|
||||||
|
112
|
||||||
|
} else {
|
||||||
|
96
|
||||||
|
});
|
||||||
|
let export_rva = get_u32(data, export_dir);
|
||||||
|
let export_size = get_u32(data, export_dir.wrapping_add(4));
|
||||||
|
|
||||||
struct SecLayout {
|
struct SecLayout {
|
||||||
sec_off: u32,
|
sec_off: u32,
|
||||||
@@ -365,11 +372,21 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let meaningful = if last_nonzero >= 0 {
|
let mut meaningful = if last_nonzero >= 0 {
|
||||||
(last_nonzero + 1) as u32
|
(last_nonzero + 1) as u32
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
// Companion exports are overlaid after compaction. Keep their whole
|
||||||
|
// range file-backed even when the payload leaves it zero-filled.
|
||||||
|
if export_size != 0
|
||||||
|
&& let Some(end) = export_rva
|
||||||
|
.checked_sub(va)
|
||||||
|
.and_then(|delta| delta.checked_add(export_size))
|
||||||
|
&& end as usize <= section_data.len()
|
||||||
|
{
|
||||||
|
meaningful = meaningful.max(end);
|
||||||
|
}
|
||||||
let mut raw_size = if meaningful != 0 {
|
let mut raw_size = if meaningful != 0 {
|
||||||
align_up_u32(meaningful, FILE_ALIGNMENT)
|
align_up_u32(meaningful, FILE_ALIGNMENT)
|
||||||
} else {
|
} else {
|
||||||
@@ -433,6 +450,45 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compaction_keeps_export_space_without_changing_the_next_section() {
|
||||||
|
for (rva, size, expected_raw_size) in [
|
||||||
|
(0, 0, 0x200),
|
||||||
|
(0x1280, 0x181, 0x600),
|
||||||
|
(0x1f00, 0x200, 0x200),
|
||||||
|
(0xffff_fff0, 0x40, 0x200),
|
||||||
|
] {
|
||||||
|
let mut data = vec![0; 0x3000];
|
||||||
|
data[..2].copy_from_slice(b"MZ");
|
||||||
|
data[0x80..0x84].copy_from_slice(b"PE\0\0");
|
||||||
|
write_u16(&mut data, 0x86, 2);
|
||||||
|
write_u16(&mut data, 0x94, 0xe0);
|
||||||
|
write_u16(&mut data, 0x98, 0x10b);
|
||||||
|
for (offset, value) in [
|
||||||
|
(0x3c, 0x80),
|
||||||
|
(0xf8, rva),
|
||||||
|
(0xfc, size),
|
||||||
|
(0x180, 0x1000),
|
||||||
|
(0x184, 0x1000),
|
||||||
|
(0x1a8, 0x1000),
|
||||||
|
(0x1ac, 0x2000),
|
||||||
|
] {
|
||||||
|
write_u32(&mut data, offset, value);
|
||||||
|
}
|
||||||
|
data[0x1000] = 0xaa;
|
||||||
|
data[0x2000..0x200c].copy_from_slice(b"next section");
|
||||||
|
let out = compact_memory_image_to_pe(&data, 0x80).unwrap();
|
||||||
|
assert_eq!(get_u32(&out, 0x188), expected_raw_size);
|
||||||
|
let headers = senbei_pe::parse(&out).unwrap();
|
||||||
|
let next = senbei_pe::rva_range(&out, headers, 0x2000, 12).unwrap();
|
||||||
|
assert_eq!(&out[next], b"next section");
|
||||||
|
if rva == 0x1280 {
|
||||||
|
let exports = senbei_pe::rva_range(&out, headers, rva, size).unwrap();
|
||||||
|
assert!(out[exports].iter().all(|&byte| byte == 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Review regression: a zero last-section VA (corrupt section table) must
|
/// Review regression: a zero last-section VA (corrupt section table) must
|
||||||
/// bail instead of building .kmiat at RVA 0 — the old code zeroed
|
/// bail instead of building .kmiat at RVA 0 — the old code zeroed
|
||||||
/// `[0, 0x7000)`, wiping the DOS/PE headers, and returned the broken image
|
/// `[0, 0x7000)`, wiping the DOS/PE headers, and returned the broken image
|
||||||
|
|||||||
@@ -128,18 +128,19 @@ pub(crate) fn overlay_exports_from_stub(out: &mut [u8], stub: &[u8]) {
|
|||||||
Some(v) if v.1 != 0 => v,
|
Some(v) if v.1 != 0 => v,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let dst = match rva_to_file_off(out, export_rva) {
|
let dst = match senbei_pe::parse(out)
|
||||||
Some(o) => o,
|
.and_then(|headers| senbei_pe::rva_range(out, headers, export_rva, export_size))
|
||||||
None => return,
|
{
|
||||||
|
Ok(range) => range,
|
||||||
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
let src = match rva_to_file_off(stub, export_rva) {
|
let src = match senbei_pe::parse(stub)
|
||||||
Some(o) => o,
|
.and_then(|headers| senbei_pe::rva_range(stub, headers, export_rva, export_size))
|
||||||
None => return,
|
{
|
||||||
|
Ok(range) => range,
|
||||||
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
let n = export_size as usize;
|
out[dst].copy_from_slice(&stub[src]);
|
||||||
if dst + n <= out.len() && src + n <= stub.len() {
|
|
||||||
out[dst..dst + n].copy_from_slice(&stub[src..src + n]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore the CLR regions retained by an external-companion loader stub.
|
/// Restore the CLR regions retained by an external-companion loader stub.
|
||||||
@@ -633,6 +634,34 @@ mod tests {
|
|||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_overlay_requires_the_entire_range_to_be_file_backed() {
|
||||||
|
for is_pe32_plus in [false, true] {
|
||||||
|
let dirs = 0x98 + if is_pe32_plus { 112 } else { 96 };
|
||||||
|
let section = 0x98 + if is_pe32_plus { 0xf0 } else { 0xe0 };
|
||||||
|
let mut stub = managed_fixture(is_pe32_plus, 0x600);
|
||||||
|
stub[0x780..0x880].fill(0x5a);
|
||||||
|
for (stub_raw_size, out_raw_size) in
|
||||||
|
[(0x600u32, 0x600u32), (0x200, 0x600), (0x600, 0x200)]
|
||||||
|
{
|
||||||
|
let mut out = managed_fixture(is_pe32_plus, 0x400);
|
||||||
|
out[dirs..dirs + 4].copy_from_slice(&0x2180u32.to_le_bytes());
|
||||||
|
out[dirs + 4..dirs + 8].copy_from_slice(&0x100u32.to_le_bytes());
|
||||||
|
out[section + 16..section + 20].copy_from_slice(&out_raw_size.to_le_bytes());
|
||||||
|
stub[section + 16..section + 20].copy_from_slice(&stub_raw_size.to_le_bytes());
|
||||||
|
let before = out.clone();
|
||||||
|
overlay_exports_from_stub(&mut out, &stub);
|
||||||
|
if stub_raw_size == 0x600 && out_raw_size == 0x600 {
|
||||||
|
assert_eq!(&out[0x580..0x680], &[0x5a; 0x100]);
|
||||||
|
assert_eq!(&out[..0x580], &before[..0x580]);
|
||||||
|
assert_eq!(&out[0x680..], &before[0x680..]);
|
||||||
|
} else {
|
||||||
|
assert_eq!(out, before);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn managed_companion_restores_rva_mapped_regions_without_overwriting_il() {
|
fn managed_companion_restores_rva_mapped_regions_without_overwriting_il() {
|
||||||
for is_pe32_plus in [false, true] {
|
for is_pe32_plus in [false, true] {
|
||||||
|
|||||||
Generated
+7
-7
@@ -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,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/)"
|
||||||
|
|||||||
Reference in new issue
Block a user