mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-03 10:31:28 -05:00
Compare commits
2
Commits
31a0c4139c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
366a19c2a1 | ||
|
|
ba1dacc0b2 |
No files matched your search
@@ -16,6 +16,7 @@ fn wanted_id(command_id: u32) -> bool {
|
||||
pub(crate) struct Artifact {
|
||||
pub path: PathBuf,
|
||||
pub size: u64,
|
||||
pub entry_offset: Option<u32>,
|
||||
}
|
||||
|
||||
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 {
|
||||
path: root.join(path),
|
||||
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 {
|
||||
path: root.join(path),
|
||||
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 constructors;
|
||||
mod error;
|
||||
mod pipeline;
|
||||
|
||||
|
||||
@@ -602,19 +602,59 @@ fn dynamic_symbol_names(symbols: &[u8], strings: &[u8]) -> Result<Vec<Vec<u8>>>
|
||||
.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> {
|
||||
const PREFIX0: u32 = 0x8f;
|
||||
const PREFIX1: u32 = 0x15ec;
|
||||
const TYPE_ID: u32 = 0x81;
|
||||
const MODE0: u32 = 1;
|
||||
const MODE1: u32 = 5;
|
||||
const SUFFIX0: u32 = 0x0a54;
|
||||
const SUFFIX1: u32 = 0x12008;
|
||||
|
||||
let references = auxiliary_seed_references(module);
|
||||
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
|
||||
&& 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 - 0x10)? == TYPE_ID
|
||||
&& 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 + 8)? == SUFFIX1
|
||||
&& read_u32(module, seed_offset + 0x0c)? == 0
|
||||
&& references.contains(&(seed_offset as u64))
|
||||
{
|
||||
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> {
|
||||
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();
|
||||
for segment in &layout.program_headers {
|
||||
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)
|
||||
.ok_or_else(|| Error::Invalid("executable PC overflow".to_owned()))?;
|
||||
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;
|
||||
}
|
||||
features.calls += 1;
|
||||
@@ -838,7 +926,11 @@ struct ConstructorProfile {
|
||||
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)?;
|
||||
for segment in &layout.program_headers {
|
||||
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
|
||||
}
|
||||
|
||||
fn discover_suppressed_constructors(
|
||||
module: &[u8],
|
||||
source: &[u8],
|
||||
layout: &ElfLayout,
|
||||
max_count: usize,
|
||||
) -> Result<Option<Vec<u64>>> {
|
||||
const RECORD_SIZE: usize = 16;
|
||||
const MIN_COUNT: usize = 2;
|
||||
const MAX_COUNT: usize = 256;
|
||||
let mut matches = Vec::new();
|
||||
if module.len() < 8 {
|
||||
return Ok(None);
|
||||
}
|
||||
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
|
||||
// The bootstrap saves the incoming linker register frame, calls Stage 1,
|
||||
// restores that frame and returns. Validate this at the ELF init relocation;
|
||||
// e_entry may be zero for a shared library, so it cannot identify the slot.
|
||||
fn is_stage1_constructor(source: &[u8], layout: &ElfLayout, address: u64) -> bool {
|
||||
let Some(offset) = virtual_address_to_file_offset(layout, address, 0x84) else {
|
||||
return false;
|
||||
};
|
||||
for pair in 0..15_u32 {
|
||||
let first = pair * 2 + 1;
|
||||
let save = 0xa9bf_03e0 | ((first + 1) << 10) | first;
|
||||
let restore = 0xa8c1_03e0 | ((first + 1) << 10) | first;
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
match matches.len() {
|
||||
1 => Ok(matches.pop()),
|
||||
0 => Ok(None),
|
||||
count => invalid(format!(
|
||||
"constructor control module has {count} validated suppression tables"
|
||||
)),
|
||||
}
|
||||
read_u32(source, offset + 0x3c).ok() == Some(0x9100_03e0)
|
||||
&& read_u32(source, offset + 0x40)
|
||||
.ok()
|
||||
.and_then(|instruction| aarch64_bl_target(instruction, address + 0x40))
|
||||
.is_some_and(|target| super::constructors::executable_address(layout, target))
|
||||
&& read_u32(source, offset + 0x80).ok() == Some(AARCH64_RET)
|
||||
}
|
||||
|
||||
fn constructor_profile(
|
||||
module: &[u8],
|
||||
module: (&[u8], u32),
|
||||
source: &[u8],
|
||||
protected: &[u8],
|
||||
layout: &ElfLayout,
|
||||
init_array: SectionHeader,
|
||||
fini_array: SectionHeader,
|
||||
old_dyn: RelocationTable<'_>,
|
||||
) -> Result<Option<ConstructorProfile>> {
|
||||
) -> Result<ConstructorProfile> {
|
||||
if !init_array.size.is_multiple_of(8) {
|
||||
return invalid("init-array size is not entry-aligned");
|
||||
}
|
||||
let init_count = usize_from_u64(init_array.size / 8, "init-array count")?;
|
||||
let Some(mut init_addends) =
|
||||
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 callbacks = super::constructors::callbacks(module.0, module.1, source, layout, init_count)?;
|
||||
let init_end = init_array
|
||||
.address
|
||||
.checked_add(init_array.size)
|
||||
.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
|
||||
})?;
|
||||
let extra = existing
|
||||
.iter()
|
||||
.filter(|relocation| {
|
||||
relocation.kind() == R_AARCH64_RELATIVE
|
||||
&& relocation.symbol() == 0
|
||||
&& relocation.addend >= 0
|
||||
})
|
||||
.map(|relocation| relocation.addend as u64)
|
||||
.filter(|&addend| {
|
||||
addend != layout.entrypoint
|
||||
&& addend > max_suppressed
|
||||
&& addend <= max_suppressed.saturating_add(0x1000)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if extra.len() != 1 {
|
||||
existing.sort_by_key(|relocation| relocation.offset);
|
||||
if existing.len() != init_count {
|
||||
return invalid("init-array does not have one relocation per slot");
|
||||
}
|
||||
let mut init_addends = Vec::new();
|
||||
let mut bootstraps = 0;
|
||||
for (slot, relocation) in existing.iter().enumerate() {
|
||||
if relocation.offset != init_array.address + slot as u64 * 8
|
||||
|| relocation.kind() != R_AARCH64_RELATIVE
|
||||
|| relocation.symbol() != 0
|
||||
|| relocation.addend < 0
|
||||
{
|
||||
return invalid("init-array contains an invalid relocation");
|
||||
}
|
||||
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!(
|
||||
"expected one unsuppressed constructor near the suppression table, found {}",
|
||||
extra.len()
|
||||
"init-array has {bootstraps} validated Stage 1 callbacks"
|
||||
));
|
||||
}
|
||||
init_addends.push(extra[0]);
|
||||
if init_addends.len() > init_count {
|
||||
return invalid(format!(
|
||||
"constructor profile produced {} entries for {init_count} init slots",
|
||||
init_addends.len()
|
||||
));
|
||||
return invalid("restored constructors exceed the init-array capacity");
|
||||
}
|
||||
let fini = old_dyn.collect_where(|relocation| relocation.offset == fini_array.address)?;
|
||||
if fini.len() != 1
|
||||
@@ -986,10 +1038,10 @@ fn constructor_profile(
|
||||
{
|
||||
return invalid("fini-array relocation is not a single RELATIVE record");
|
||||
}
|
||||
Ok(Some(ConstructorProfile {
|
||||
Ok(ConstructorProfile {
|
||||
init_addends,
|
||||
fini_addend: fini[0].addend as u64,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -1540,7 +1592,7 @@ fn materialize_static_elf_tables(
|
||||
symbol_patch_data: &[u8],
|
||||
auxiliary_data: &[u8],
|
||||
auxiliary_symbol_seed: Option<u32>,
|
||||
constructor_module: Option<&[u8]>,
|
||||
constructor_module: Option<(&[u8], u32)>,
|
||||
) -> Result<(ElfLayout, ElfMaterializationReport, u64)> {
|
||||
let names = layout.section_names(source)?;
|
||||
let indices = required_section_indices(&names)?;
|
||||
@@ -1753,14 +1805,9 @@ fn materialize_static_elf_tables(
|
||||
})?;
|
||||
let init_array = layout.section_headers[init_index];
|
||||
let fini_array = layout.section_headers[fini_index];
|
||||
let profile =
|
||||
constructor_profile(module, output, layout, init_array, fini_array, old_dyn)?
|
||||
.ok_or_else(|| {
|
||||
Error::Invalid(
|
||||
"constructor control module 0x98 has no validated constructor table"
|
||||
.to_owned(),
|
||||
)
|
||||
})?;
|
||||
let profile = constructor_profile(
|
||||
module, output, source, layout, init_array, fini_array, old_dyn,
|
||||
)?;
|
||||
Some((profile, init_index, fini_index, init_array, fini_array))
|
||||
}
|
||||
None => None,
|
||||
@@ -2333,7 +2380,17 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
.transpose()?;
|
||||
let constructor_module = artifacts
|
||||
.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()?;
|
||||
let config = Module9bConfig::parse(&module)?;
|
||||
|
||||
@@ -2486,7 +2543,9 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
&symbol_patch_data,
|
||||
&decoded,
|
||||
auxiliary_symbol_seed,
|
||||
constructor_module.as_deref(),
|
||||
constructor_module
|
||||
.as_ref()
|
||||
.map(|(image, entry)| (image.as_slice(), *entry)),
|
||||
)?;
|
||||
restored_layout = new_layout;
|
||||
materialization = Some(report);
|
||||
@@ -2684,13 +2743,17 @@ mod tests {
|
||||
assert_eq!(relocations[1].symbol(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_auxiliary_symbol_permutation_seed() {
|
||||
let mut module = vec![0_u8; 0x80];
|
||||
let seed_offset = 0x40;
|
||||
fn auxiliary_seed_module(helper_offset: u32) -> Vec<u8> {
|
||||
let mut module = vec![0_u8; 0x100];
|
||||
let seed_offset = 0xf0;
|
||||
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 - 0x1c, 0x15ec),
|
||||
(seed_offset - 0x1c, helper_offset),
|
||||
(seed_offset - 0x18, 0x81),
|
||||
(seed_offset - 0x10, 0x81),
|
||||
(seed_offset - 8, 1),
|
||||
@@ -2702,11 +2765,271 @@ mod tests {
|
||||
] {
|
||||
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
module
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_auxiliary_symbol_seed_with_relocated_helper() {
|
||||
// The configuration ends exactly at the module boundary.
|
||||
for helper_offset in [0x15ec, 0x164c, 0x2400] {
|
||||
assert_eq!(
|
||||
discover_auxiliary_symbol_seed(&module).expect("symbol seed"),
|
||||
discover_auxiliary_symbol_seed(&auxiliary_seed_module(helper_offset))
|
||||
.expect("symbol seed"),
|
||||
0x4db4_d9ec
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auxiliary_symbol_seed_requires_a_valid_decoder_reference() {
|
||||
for (offset, value) in [
|
||||
(0x10, 0xd503_201f_u32), // no ADRP
|
||||
(0x14, 0xf940_4020), // wrong page register
|
||||
(0x18, 0xb940_0002), // wrong seed register for MUL
|
||||
(0x1c, 0x1b02_7c21), // multiplication by another value
|
||||
(0x80, 0xec), // GOT points at a different field
|
||||
(0x80, 0x1000), // GOT points outside the module
|
||||
(0xd4, 0), // null helper offset
|
||||
(0xd4, 0x164d), // unaligned helper offset
|
||||
(0xe8, 2), // unsupported mode
|
||||
] {
|
||||
let mut module = auxiliary_seed_module(0x164c);
|
||||
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
|
||||
assert!(discover_auxiliary_symbol_seed(&module).is_err());
|
||||
}
|
||||
for length in [0, 0x10, 0x20, 0xff] {
|
||||
assert!(
|
||||
discover_auxiliary_symbol_seed(&auxiliary_seed_module(0x164c)[..length]).is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auxiliary_symbol_seed_trials_all_candidates_and_rejects_ambiguity() {
|
||||
let mut module = auxiliary_seed_module(0x164c);
|
||||
module.copy_within(0xd0..0x100, 0x40);
|
||||
// An unreferenced configuration-shaped decoy must not hide the real one.
|
||||
assert_eq!(
|
||||
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]
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user