Files
Senbei/senbei-engine/src/android/restore/pipeline.rs
T

3089 lines
117 KiB
Rust

use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::time::Instant;
use memmap2::{Mmap, MmapMut, MmapOptions};
use senbei_crypto::android::{
ContainerHeader, HuffmanLzDecoder, Module9bConfig, ProtectedDescriptor, transform_segment,
};
use serde::Serialize;
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
use super::super::common;
use super::artifact::load_artifacts;
use super::error::{Error, Result, invalid};
use senbei_elf::{
DT_GNU_HASH, DT_HASH, DT_JMPREL, DT_PLTRELSZ, DT_RELA, DT_RELACOUNT, DT_RELASZ, DT_STRSZ,
DT_STRTAB, DT_SYMTAB, DT_VERNEED, DT_VERSYM, ELF64_RELA_SIZE, ELF64_SYMBOL_SIZE, ElfLayout,
LoadSegment, PF_R, R_AARCH64_ABS64, R_AARCH64_GLOB_DAT, R_AARCH64_JUMP_SLOT,
R_AARCH64_RELATIVE, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, VER_NDX_GLOBAL, align_up,
build_gnu_hash, build_sysv_hash, read_i64, read_u32, read_u64, slice, slice_u64,
usize_from_u64,
};
const CHUNK_SIZE: usize = 16 * 1024 * 1024;
const DT_INIT_ARRAYSZ: u64 = 27;
const DT_FINI_ARRAYSZ: u64 = 28;
const AARCH64_RET: u32 = 0xd65f_03c0;
/// Inputs and optional diagnostics for one `libil2cpp.so` restoration.
#[derive(Debug, Clone)]
pub struct RestoreOptions {
pub input: PathBuf,
pub output: PathBuf,
pub index: PathBuf,
pub dump_auxiliary: Option<PathBuf>,
pub outer_only: bool,
pub preserve_entrypoint: bool,
/// Print per-phase progress lines to stderr. Off for quiet/batch drivers.
pub verbose: bool,
}
/// Container decoding counters.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct DecodeStatistics {
pub segments: usize,
pub writers: usize,
pub compressed_writers: usize,
pub encoded_bytes: u64,
pub decoded_bytes: u64,
pub file_bytes_written: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StaticConfigReport {
pub header_seed: String,
pub container_seed: String,
pub aes_key_sha256: String,
pub schedule_offset: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DescriptorReport {
pub command_id: String,
pub flags: String,
pub outer_offset: String,
pub outer_expected_size: String,
pub auxiliary_offset: String,
pub auxiliary_expected_size: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct HiddenSymbolReport {
pub patch_blob_size: u32,
pub patched_symbols: u32,
pub copied_strings: usize,
pub secondary_record_count: u32,
pub first_target_index: u32,
pub last_target_index: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PlacementReport {
pub offset: u64,
pub size: usize,
}
struct TablePayload {
name: &'static str,
alignment: u64,
data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ElfMaterializationReport {
pub hidden_symbols: HiddenSymbolReport,
pub old_symbol_count: usize,
pub auxiliary_symbol_count: u32,
pub appended_symbols: usize,
pub new_symbol_count: usize,
pub old_dynstr_size: usize,
pub auxiliary_dynstr_size: u32,
pub new_dynstr_size: usize,
pub rela_dyn_count: usize,
pub rela_plt_count: usize,
pub relative_prefix_count: usize,
pub metadata_start: u64,
pub metadata_end: u64,
pub metadata_capacity_end: u64,
pub metadata_slack: u64,
pub placements: BTreeMap<String, PlacementReport>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CleaningReport {
pub private_section_index: usize,
pub private_offset: u64,
pub private_size: u64,
pub input_entrypoint: u64,
pub output_entrypoint: u64,
pub output_section_count: usize,
pub retained_sections: Vec<String>,
pub section_header_offset: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ValidationReport {
pub format: String,
pub machine: String,
pub sections: usize,
pub segments: usize,
pub dynamic_symbols: usize,
pub dynamic_relocations: usize,
pub pltgot_relocations: usize,
pub has_louser: bool,
}
/// Machine-readable result of the restoration.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RestoreReport {
pub input: String,
pub input_sha256: String,
pub output: String,
pub output_sha256: String,
pub output_size: u64,
pub module_index: String,
pub static_config: StaticConfigReport,
pub descriptor: DescriptorReport,
pub primary: DecodeStatistics,
pub auxiliary: Option<DecodeStatistics>,
pub elf_materialization: Option<ElfMaterializationReport>,
pub cleaning: CleaningReport,
pub validation: ValidationReport,
pub elapsed_seconds: f64,
}
fn map_read_only(file: &File, path: &Path) -> Result<Mmap> {
// SAFETY: the mapping is read-only and `file` remains open for the mapping's
// lifetime. The restoration never mutates or truncates the mapped source.
unsafe { MmapOptions::new().map(file) }.map_err(|error| Error::io("map", path, error))
}
fn map_mut(file: &File, length: usize, path: &Path) -> Result<MmapMut> {
// SAFETY: `length` is set on the private temporary output immediately
// before this call. No other handle mutates or truncates it while mapped.
unsafe { MmapOptions::new().len(length).map_mut(file) }
.map_err(|error| Error::io("map temporary output", path, error))
}
fn read_file(path: &Path) -> Result<Vec<u8>> {
std::fs::read(path).map_err(|error| Error::io("read", path, error))
}
fn sha256_bytes(data: &[u8]) -> String {
common::sha256(data)
}
fn sha256_file(path: &Path) -> Result<String> {
let mut file = File::open(path).map_err(|error| Error::io("open", path, error))?;
let mut digest = Sha256::new();
let mut buffer = vec![0_u8; CHUNK_SIZE];
loop {
let read = file
.read(&mut buffer)
.map_err(|error| Error::io("hash", path, error))?;
if read == 0 {
break;
}
digest.update(&buffer[..read]);
}
Ok(senbei_crypto::hex_digest(&digest.finalize()))
}
fn copy_range(source: &[u8], output: &mut File, size: usize, path: &Path) -> Result<()> {
for chunk in source[..size].chunks(CHUNK_SIZE) {
output
.write_all(chunk)
.map_err(|error| Error::io("write temporary output", path, error))?;
}
Ok(())
}
fn checked_add(base: usize, value: usize, field: &str) -> Result<usize> {
base.checked_add(value)
.ok_or_else(|| Error::Invalid(format!("{field} overflow")))
}
struct FileLayoutWriter<'a> {
output: &'a mut [u8],
layout: &'a ElfLayout,
load_end: u64,
}
impl FileLayoutWriter<'_> {
fn write(&mut self, virtual_address: u64, data: &[u8]) -> Result<usize> {
let data_len = u64::try_from(data.len())
.map_err(|_| Error::Invalid("decoded write length exceeds u64".to_owned()))?;
let end = virtual_address
.checked_add(data_len)
.ok_or_else(|| Error::Invalid("decoded write range overflow".to_owned()))?;
if end > self.load_end {
return invalid(format!(
"decoded write 0x{virtual_address:x}..0x{end:x} exceeds target load image"
));
}
let mut written = 0_u64;
let mut covered_memory = 0_u64;
for segment in &self.layout.program_headers {
let memory_end = segment
.virtual_address
.checked_add(segment.memory_size)
.ok_or_else(|| Error::Invalid("PT_LOAD memory end overflow".to_owned()))?;
let overlap_start = virtual_address.max(segment.virtual_address);
let overlap_end = end.min(memory_end);
if overlap_start >= overlap_end {
continue;
}
covered_memory = covered_memory
.checked_add(overlap_end - overlap_start)
.ok_or_else(|| Error::Invalid("covered memory count overflow".to_owned()))?;
let file_end_va = segment
.virtual_address
.checked_add(segment.file_size)
.ok_or_else(|| Error::Invalid("PT_LOAD file VA end overflow".to_owned()))?;
let file_overlap_end = overlap_end.min(file_end_va);
if overlap_start < file_overlap_end {
let source_offset =
usize_from_u64(overlap_start - virtual_address, "write source offset")?;
let file_offset = usize_from_u64(
segment
.offset
.checked_add(overlap_start - segment.virtual_address)
.ok_or_else(|| Error::Invalid("write file offset overflow".to_owned()))?,
"write file offset",
)?;
let count = usize_from_u64(file_overlap_end - overlap_start, "write size")?;
let destination = self
.output
.get_mut(file_offset..file_offset + count)
.ok_or_else(|| {
Error::Invalid("decoded write exceeds temporary output".to_owned())
})?;
destination.copy_from_slice(&data[source_offset..source_offset + count]);
written += count as u64;
}
}
if covered_memory != data_len {
return invalid(format!(
"decoded write 0x{virtual_address:x}..0x{end:x} is not covered by PT_LOAD memory"
));
}
Ok(usize_from_u64(written, "written byte count")?)
}
}
fn decode_container<F>(
payload: &[u8],
header: &ContainerHeader,
config: &Module9bConfig,
verbose: bool,
mut writer: F,
) -> Result<DecodeStatistics>
where
F: FnMut(u64, &[u8]) -> Result<usize>,
{
let decoder = HuffmanLzDecoder::new(&header.tree)?;
let mut statistics = DecodeStatistics {
segments: header.segments.len(),
..DecodeStatistics::default()
};
let decrypt_aes = !(config.skip_aes || header.skip_aes);
for (segment_index, encoded) in header.segments.iter().enumerate() {
let start = checked_add(
header.start,
encoded.offset as usize,
"encoded segment start",
)?;
let encoded_data = slice(payload, start, encoded.size as usize)?;
let transformed = transform_segment(
encoded_data,
config.container_seed,
&config.aes_key,
decrypt_aes,
)?;
if transformed.len() < 16 {
return invalid(format!(
"decoded segment {segment_index} is shorter than its header"
));
}
let base_offset = u64::from(read_u32(&transformed, 0)?);
let writer_count = read_u32(&transformed, 4)? as usize;
let table_offset = read_u32(&transformed, 8)? as usize;
let data_offset = read_u32(&transformed, 12)? as usize;
let table_end = table_offset
.checked_add(
writer_count
.checked_mul(16)
.ok_or_else(|| Error::Invalid("writer table size overflow".to_owned()))?,
)
.ok_or_else(|| Error::Invalid("writer table end overflow".to_owned()))?;
if table_end > transformed.len() || data_offset > transformed.len() {
return invalid(format!(
"decoded segment {segment_index} has invalid writer offsets"
));
}
let mut data_cursor = data_offset;
for writer_index in 0..writer_count {
let record = table_offset + writer_index * 16;
let output_offset = u64::from(read_u32(&transformed, record)?);
let output_size = read_u32(&transformed, record + 4)? as usize;
let encoded_size = read_u32(&transformed, record + 8)? as usize;
let reserved = read_u32(&transformed, record + 12)?;
let encoded_end = data_cursor
.checked_add(encoded_size)
.ok_or_else(|| Error::Invalid("writer data end overflow".to_owned()))?;
if reserved != 0 || encoded_end > transformed.len() {
return invalid(format!(
"segment {segment_index} writer {writer_index} has invalid bounds"
));
}
let source = &transformed[data_cursor..encoded_end];
let decoded = if encoded_size == output_size {
None
} else {
statistics.compressed_writers += 1;
Some(decoder.decode(source, output_size)?)
};
let decoded_slice = decoded.as_deref().unwrap_or(source);
let target = base_offset
.checked_add(output_offset)
.ok_or_else(|| Error::Invalid("writer target address overflow".to_owned()))?;
statistics.file_bytes_written += writer(target, decoded_slice)? as u64;
statistics.writers += 1;
statistics.encoded_bytes += encoded_size as u64;
statistics.decoded_bytes += output_size as u64;
data_cursor = encoded_end;
}
if verbose {
eprintln!(
"[{current:02}/{total:02}] writers={writer_count} encoded=0x{size:x}",
current = segment_index + 1,
total = header.segments.len(),
size = encoded.size,
);
}
}
Ok(statistics)
}
fn read_c_string(data: &[u8], offset: usize, limit: usize) -> Result<&[u8]> {
if offset >= limit || limit > data.len() {
return invalid(format!("invalid string offset 0x{offset:x}/0x{limit:x}"));
}
let relative_end = data[offset..limit]
.iter()
.position(|&byte| byte == 0)
.ok_or_else(|| Error::Invalid(format!("unterminated string at 0x{offset:x}")))?;
Ok(&data[offset..offset + relative_end])
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct AuxiliaryElfImage {
dynstr_offset: u32,
dynstr_size: u32,
dynsym_offset: u32,
dynsym_count: u32,
relocation1_offset: u32,
relocation1_count: u32,
relocation2_offset: u32,
relocation2_count: u32,
}
impl AuxiliaryElfImage {
fn parse(data: &[u8]) -> Result<Self> {
if data.len() < 0x40 {
return invalid("decoded auxiliary ELF image is truncated");
}
let mut words = [0_u32; 16];
for (index, word) in words.iter_mut().enumerate() {
*word = read_u32(data, index * 4)?;
}
if [1, 3, 12, 13, 15]
.into_iter()
.any(|index| words[index] != 0)
{
return invalid("unexpected nonzero auxiliary ELF header field");
}
if words[14] != 0xb7 {
return invalid(format!(
"unexpected auxiliary ELF machine 0x{:x}",
words[14]
));
}
let result = Self {
dynstr_offset: words[4],
dynstr_size: words[5],
dynsym_offset: words[6],
dynsym_count: words[7],
relocation1_offset: words[8],
relocation1_count: words[9],
relocation2_offset: words[10],
relocation2_count: words[11],
};
if result.dynsym_count == 0 {
return invalid("auxiliary dynamic symbol table has no null entry");
}
if result.relocation1_offset != 0x40 {
return invalid("auxiliary relocation table does not follow its header");
}
let relocation1_end = u64::from(result.relocation1_offset)
+ u64::from(result.relocation1_count) * ELF64_RELA_SIZE as u64;
let relocation2_end = u64::from(result.relocation2_offset)
+ u64::from(result.relocation2_count) * ELF64_RELA_SIZE as u64;
let dynsym_end = u64::from(result.dynsym_offset)
+ u64::from(result.dynsym_count) * ELF64_SYMBOL_SIZE as u64;
let dynstr_end = u64::from(result.dynstr_offset) + u64::from(result.dynstr_size);
let expected_relocation2 = align_up(relocation1_end, 0x10)?;
let expected_dynsym = align_up(relocation2_end, 0x10)?;
if expected_relocation2 != u64::from(result.relocation2_offset)
|| expected_dynsym != u64::from(result.dynsym_offset)
|| dynsym_end != u64::from(result.dynstr_offset)
|| dynstr_end != data.len() as u64
{
return invalid(format!(
"auxiliary ELF layout mismatch: rela1_end=0x{relocation1_end:x}/rela2=0x{:x}, rela2_end=0x{relocation2_end:x}/dynsym=0x{:x}, dynsym_end=0x{dynsym_end:x}/dynstr=0x{:x}, dynstr_end=0x{dynstr_end:x}/size=0x{:x}",
result.relocation2_offset,
result.dynsym_offset,
result.dynstr_offset,
data.len()
));
}
for (start, end) in [
(relocation1_end, expected_relocation2),
(relocation2_end, expected_dynsym),
] {
if slice_u64(data, start, end - start)?
.iter()
.any(|&byte| byte != 0)
{
return invalid(format!(
"auxiliary ELF alignment padding 0x{start:x}..0x{end:x} is nonzero"
));
}
}
if slice(data, result.dynsym_offset as usize, ELF64_SYMBOL_SIZE)?
.iter()
.any(|&byte| byte != 0)
{
return invalid("auxiliary dynamic symbol zero entry is not empty");
}
Ok(result)
}
}
fn restore_hidden_symbols(
output: &mut [u8],
dynsym: SectionHeader,
dynstr: SectionHeader,
patch_data: &[u8],
) -> Result<(Vec<u8>, Vec<u8>, HiddenSymbolReport)> {
if dynsym.entry_size != ELF64_SYMBOL_SIZE as u64
|| !dynsym.size.is_multiple_of(ELF64_SYMBOL_SIZE as u64)
{
return invalid("unexpected .dynsym entry layout");
}
let symbol_count = usize_from_u64(
dynsym.size / ELF64_SYMBOL_SIZE as u64,
"dynamic symbol count",
)?;
let mut symbols = slice_u64(output, dynsym.offset, dynsym.size)?.to_vec();
let mut strings = slice_u64(output, dynstr.offset, dynstr.size)?.to_vec();
if patch_data.len() < 0x18 {
return invalid("0x9E symbol patch data is truncated");
}
let blob_size = read_u32(patch_data, 0)?;
let secondary_record_count = read_u32(patch_data, 4)?;
let table_base = 8_usize;
let table_end = table_base
.checked_add(blob_size as usize)
.ok_or_else(|| Error::Invalid("0x9E patch blob end overflow".to_owned()))?;
if table_end > patch_data.len() {
return invalid("0x9E primary symbol patch blob exceeds its artifact");
}
let count = read_u32(patch_data, table_base)?;
let symbol_offset = read_u32(patch_data, table_base + 4)? as usize;
let index_offset = read_u32(patch_data, table_base + 8)? as usize;
let string_offset = read_u32(patch_data, table_base + 12)? as usize;
let count_usize = count as usize;
if symbol_offset
.checked_add(count_usize * ELF64_SYMBOL_SIZE)
.is_none_or(|end| end > blob_size as usize)
|| index_offset
.checked_add(count_usize * 4)
.is_none_or(|end| end > blob_size as usize)
|| string_offset >= blob_size as usize
{
return invalid("0x9E symbol patch table has invalid offsets");
}
let mut cursor = table_base + string_offset;
let mut patched_indices = HashSet::with_capacity(count_usize);
let mut copied_strings = 0_usize;
for index in 0..count_usize {
let source_offset = table_base + symbol_offset + index * ELF64_SYMBOL_SIZE;
let source_symbol = slice(patch_data, source_offset, ELF64_SYMBOL_SIZE)?;
let target_index = read_u32(patch_data, table_base + index_offset + index * 4)?;
if target_index == 0 || target_index as usize >= symbol_count {
return invalid(format!(
"0x9E target symbol index {target_index} is invalid"
));
}
if !patched_indices.insert(target_index) {
return invalid(format!("0x9E patches symbol {target_index} more than once"));
}
let name = read_c_string(patch_data, cursor, table_end)?;
cursor += name.len() + 1;
let name_offset = read_u32(source_symbol, 0)? as usize;
if name_offset
.checked_add(name.len() + 1)
.is_none_or(|end| end > strings.len())
{
return invalid(format!("0x9E symbol {target_index} name exceeds .dynstr"));
}
let existing = read_c_string(&strings, name_offset, strings.len())?;
if existing.is_empty() {
strings[name_offset..name_offset + name.len()].copy_from_slice(name);
strings[name_offset + name.len()] = 0;
copied_strings += 1;
} else if existing != name {
return invalid(format!(
"0x9E symbol {target_index} conflicts with existing .dynstr data"
));
}
let target_offset = target_index as usize * ELF64_SYMBOL_SIZE;
symbols[target_offset..target_offset + ELF64_SYMBOL_SIZE].copy_from_slice(source_symbol);
}
let string_padding = patch_data.get(cursor..table_end).ok_or_else(|| {
Error::Invalid("0x9E symbol strings exceed the primary patch blob".to_owned())
})?;
if string_padding.len() > 3 || string_padding.iter().any(|&byte| byte != 0) {
return invalid(format!(
"0x9E symbol strings have invalid padding at 0x{cursor:x}..0x{table_end:x}"
));
}
let first_target_index = patched_indices
.iter()
.copied()
.min()
.ok_or_else(|| Error::Invalid("0x9E patch table is empty".to_owned()))?;
let last_target_index = patched_indices
.iter()
.copied()
.max()
.ok_or_else(|| Error::Invalid("0x9E patch table is empty".to_owned()))?;
Ok((
symbols,
strings,
HiddenSymbolReport {
patch_blob_size: blob_size,
patched_symbols: count,
copied_strings,
secondary_record_count,
first_target_index,
last_target_index,
},
))
}
fn dynamic_symbol_names(symbols: &[u8], strings: &[u8]) -> Result<Vec<Vec<u8>>> {
if !symbols.len().is_multiple_of(ELF64_SYMBOL_SIZE) {
return invalid("dynamic symbol table is not entry-aligned");
}
symbols
.as_chunks::<ELF64_SYMBOL_SIZE>()
.0
.iter()
.map(|symbol| {
let name_offset = read_u32(symbol, 0)? as usize;
Ok(read_c_string(strings, name_offset, strings.len())?.to_vec())
})
.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 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) {
if read_u32(module, seed_offset - 0x20)? == PREFIX0
&& 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
&& read_u32(module, seed_offset - 4)? == MODE1
&& 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)?);
}
}
match matches.as_slice() {
[seed] => Ok(*seed),
[] => invalid("module 0x96 has no validated auxiliary symbol seed"),
_ => invalid(format!(
"module 0x96 has {} validated auxiliary symbol seeds",
matches.len()
)),
}
}
// These libc calls are intentionally omitted from the saved relocation pack.
// Their GOT slots are therefore the five holes left after combining the
// protected and auxiliary PLT relocations. Resolve the holes by call ABI rather
// than by fixed slot numbers; the slot order differs between protected games.
const SPECIAL_PLT_IMPORT_NAMES: [&str; 5] = ["strlen", "strcmp", "memset", "strcpy", "strchr"];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct PltCallFeatures {
calls: usize,
zero_arg1: usize,
w0_zero_branch: usize,
pointer_zero_branch_with_immediate_arg1: usize,
x0_to_x2: usize,
}
fn aarch64_bl_target(instruction: u32, pc: u64) -> Option<u64> {
if instruction & 0xfc00_0000 != 0x9400_0000 {
return None;
}
let immediate = i64::from(instruction & 0x03ff_ffff);
let signed = if immediate & (1 << 25) != 0 {
immediate - (1 << 26)
} else {
immediate
};
let target = i128::from(pc) + i128::from(signed) * 4;
u64::try_from(target).ok()
}
fn aarch64_cb_register(instruction: u32) -> Option<(bool, u32)> {
(instruction & 0x7e00_0000 == 0x3400_0000).then_some((instruction >> 31 != 0, instruction & 31))
}
fn aarch64_mov_x_from_x0(instruction: u32) -> Option<u32> {
if instruction & 0xffe0_ffe0 == 0xaa00_03e0 && (instruction >> 16) & 31 == 0 {
Some(instruction & 31)
} else {
None
}
}
fn aarch64_writes_w1_immediate(instruction: u32) -> bool {
if instruction & 31 != 1 {
return false;
}
matches!(
instruction & 0x7f80_0000,
0x5280_0000 | 0x1280_0000 | 0x3200_0000
)
}
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 {
continue;
}
let segment_offset = usize_from_u64(segment.offset, "executable segment offset")?;
let segment_size = usize_from_u64(segment.file_size, "executable segment size")?;
let segment_end = segment_offset
.checked_add(segment_size)
.ok_or_else(|| Error::Invalid("executable segment range overflow".to_owned()))?;
if segment_end > image.len() {
return invalid("executable segment exceeds restored image");
}
for relative in (0..=segment_size - 4).step_by(4) {
let offset = segment_offset + relative;
let pc = segment
.virtual_address
.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).is_some_and(|address| targets.contains(&address))
{
continue;
}
features.calls += 1;
let previous = |back: usize| -> Option<u32> {
offset
.checked_sub(back.checked_mul(4)?)
.and_then(|position| read_u32(image, position).ok())
};
let following = |forward: usize| -> Option<u32> {
offset
.checked_add(forward.checked_mul(4)?)
.and_then(|position| read_u32(image, position).ok())
};
if (1..=5).any(|back| matches!(previous(back), Some(0x2a1f_03e1 | 0xaa1f_03e1))) {
features.zero_arg1 += 1;
}
if following(1).and_then(aarch64_cb_register) == Some((false, 0)) {
features.w0_zero_branch += 1;
}
let pointer_zero_branch = if following(1).and_then(aarch64_cb_register)
== Some((true, 0))
{
true
} else if let (Some(first), Some(second)) = (following(1), following(2)) {
aarch64_mov_x_from_x0(first)
.is_some_and(|register| aarch64_cb_register(second) == Some((true, register)))
} else {
false
};
if pointer_zero_branch
&& (1..=4).any(|back| previous(back).is_some_and(aarch64_writes_w1_immediate))
{
features.pointer_zero_branch_with_immediate_arg1 += 1;
}
if (1..=3).any(|forward| following(forward).and_then(aarch64_mov_x_from_x0) == Some(2))
{
features.x0_to_x2 += 1;
}
}
}
Ok(features)
}
fn classify_special_plt_slots(
candidates: &[(usize, PltCallFeatures)],
) -> Result<Vec<(usize, &'static str)>> {
if candidates.len() != SPECIAL_PLT_IMPORT_NAMES.len() {
return invalid(format!(
"expected {} unresolved special PLT slots, found {}",
SPECIAL_PLT_IMPORT_NAMES.len(),
candidates.len()
));
}
let mut remaining = candidates.to_vec();
let mut result = Vec::with_capacity(SPECIAL_PLT_IMPORT_NAMES.len());
let mut take_unique =
|name: &'static str, predicate: &dyn Fn(PltCallFeatures) -> bool| -> Result<()> {
let matches = remaining
.iter()
.enumerate()
.filter_map(|(index, &(_, features))| predicate(features).then_some(index))
.collect::<Vec<_>>();
if matches.len() != 1 {
return invalid(format!(
"special PLT classifier found {} candidates for {name}",
matches.len()
));
}
let (slot, _) = remaining.remove(matches[0]);
result.push((slot, name));
Ok(())
};
take_unique("memset", &|features| {
features.calls != 0 && features.zero_arg1 * 2 > features.calls
})?;
take_unique("strcmp", &|features| {
features.calls != 0 && features.w0_zero_branch * 2 > features.calls
})?;
take_unique("strchr", &|features| {
features.calls != 0 && features.pointer_zero_branch_with_immediate_arg1 == features.calls
})?;
take_unique("strlen", &|features| features.x0_to_x2 != 0)?;
if remaining.len() != 1 {
return invalid("special PLT classifier did not leave exactly one strcpy slot");
}
let (slot, features) = remaining.pop().expect("one strcpy slot");
if features.calls == 0 {
return invalid("special PLT strcpy candidate has no direct call sites");
}
result.push((slot, "strcpy"));
result.sort_by_key(|(slot, _)| *slot);
Ok(result)
}
fn append_undefined_function_symbol(
symbols: &mut Vec<u8>,
strings: &mut Vec<u8>,
name: &str,
) -> Result<usize> {
let name_offset = u32::try_from(strings.len())
.map_err(|_| Error::Invalid("dynamic string table exceeds u32".to_owned()))?;
strings.extend_from_slice(name.as_bytes());
strings.push(0);
let index = symbols.len() / ELF64_SYMBOL_SIZE;
symbols.extend_from_slice(&name_offset.to_le_bytes());
symbols.push(0x12); // STB_GLOBAL | STT_FUNC
symbols.push(0);
symbols.extend_from_slice(&0_u16.to_le_bytes()); // SHN_UNDEF
symbols.extend_from_slice(&0_u64.to_le_bytes());
symbols.extend_from_slice(&0_u64.to_le_bytes());
Ok(index)
}
fn symbol_definition(symbols: &[u8], index: usize) -> Result<(u16, u64)> {
let start = index
.checked_mul(ELF64_SYMBOL_SIZE)
.ok_or_else(|| Error::Invalid("dynamic symbol index overflow".to_owned()))?;
let symbol = slice(symbols, start, ELF64_SYMBOL_SIZE)?;
let section_index = u16::from_le_bytes([symbol[6], symbol[7]]);
let value = read_u64(symbol, 8)?;
Ok((section_index, value))
}
#[derive(Debug)]
struct ConstructorProfile {
init_addends: Vec<u64>,
fini_addend: u64,
}
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)?;
if segment.virtual_address <= address && end <= segment_end {
let offset = segment
.offset
.checked_add(address.checked_sub(segment.virtual_address)?)?;
return usize::try_from(offset).ok();
}
}
None
}
// 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)
{
return false;
}
}
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], u32),
source: &[u8],
protected: &[u8],
layout: &ElfLayout,
init_array: SectionHeader,
fini_array: SectionHeader,
old_dyn: RelocationTable<'_>,
) -> 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 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 mut existing = old_dyn.collect_where(|relocation| {
relocation.offset >= init_array.address && relocation.offset < init_end
})?;
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!(
"init-array has {bootstraps} validated Stage 1 callbacks"
));
}
if init_addends.len() > init_count {
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
|| fini[0].kind() != R_AARCH64_RELATIVE
|| fini[0].symbol() != 0
|| fini[0].addend < 0
{
return invalid("fini-array relocation is not a single RELATIVE record");
}
Ok(ConstructorProfile {
init_addends,
fini_addend: fini[0].addend as u64,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Rela {
offset: u64,
info: u64,
addend: i64,
}
impl Rela {
fn parse(data: &[u8], offset: usize) -> Result<Self> {
Ok(Self {
offset: read_u64(data, offset)?,
info: read_u64(data, offset + 8)?,
addend: read_i64(data, offset + 0x10)?,
})
}
fn kind(self) -> u32 {
self.info as u32
}
fn symbol(self) -> u64 {
self.info >> 32
}
fn encode(self, output: &mut Vec<u8>) {
output.extend_from_slice(&self.offset.to_le_bytes());
output.extend_from_slice(&self.info.to_le_bytes());
output.extend_from_slice(&self.addend.to_le_bytes());
}
}
#[derive(Clone, Copy)]
struct RelocationRemap {
base_symbol_count: usize,
auxiliary_symbol_count: u32,
permutation_seed: Option<u32>,
}
fn decode_auxiliary_symbol(encoded: u64, row: usize, symbol_count: u32, seed: u32) -> Result<u64> {
if encoded == 0 {
return Ok(0);
}
if encoded >= u64::from(symbol_count) || symbol_count < 2 {
return invalid("auxiliary relocation references an invalid encoded symbol");
}
let modulus = symbol_count - 1;
let squared = seed.wrapping_mul(seed);
let row = u32::try_from(row)
.map_err(|_| Error::Invalid("auxiliary relocation row exceeds u32".to_owned()))?;
let right = (squared >> (row & 0x1b)).wrapping_add(0x1bc5);
let left = squared.wrapping_shl(row & 3).wrapping_add(0x2048);
let rotation = (right | left) % modulus;
let mut decoded = u64::from(symbol_count - 2)
.checked_add(encoded)
.and_then(|value| value.checked_sub(u64::from(rotation)))
.ok_or_else(|| Error::Invalid("auxiliary symbol permutation overflow".to_owned()))?;
if decoded >= u64::from(modulus) {
decoded -= u64::from(modulus);
}
Ok(decoded + 1)
}
#[derive(Clone, Copy)]
struct RelocationTable<'a> {
data: &'a [u8],
offset: usize,
count: usize,
remap: Option<RelocationRemap>,
}
impl RelocationTable<'_> {
fn relocation(self, index: usize) -> Result<Rela> {
if index >= self.count {
return invalid("relocation index is out of range");
}
let offset = self
.offset
.checked_add(
index
.checked_mul(ELF64_RELA_SIZE)
.ok_or_else(|| Error::Invalid("relocation index overflow".to_owned()))?,
)
.ok_or_else(|| Error::Invalid("relocation offset overflow".to_owned()))?;
let mut relocation = Rela::parse(self.data, offset)?;
if let Some(remap) = self.remap {
let encoded = relocation.symbol();
if encoded >= u64::from(remap.auxiliary_symbol_count) {
return invalid("auxiliary relocation references an invalid symbol");
}
let symbol = match remap.permutation_seed {
Some(seed) => {
decode_auxiliary_symbol(encoded, index, remap.auxiliary_symbol_count, seed)?
}
None => encoded,
};
if symbol != 0 {
let base =
u64::try_from(remap.base_symbol_count.checked_sub(1).ok_or_else(|| {
Error::Invalid("old dynamic symbol table is empty".to_owned())
})?)
.map_err(|_| Error::Invalid("old symbol count exceeds u64".to_owned()))?;
let remapped = symbol
.checked_add(base)
.ok_or_else(|| Error::Invalid("remapped symbol index overflow".to_owned()))?;
relocation.info = (remapped << 32) | u64::from(relocation.kind());
}
}
Ok(relocation)
}
fn validate(self, allowed: &[u32], description: &str) -> Result<()> {
for index in 0..self.count {
let kind = self.relocation(index)?.kind();
if !allowed.contains(&kind) {
return invalid(format!(
"{description} contains unsupported relocation type 0x{kind:x}"
));
}
}
Ok(())
}
fn collect_where(self, predicate: impl Fn(Rela) -> bool) -> Result<Vec<Rela>> {
let mut result = Vec::new();
for index in 0..self.count {
let relocation = self.relocation(index)?;
if predicate(relocation) {
result.push(relocation);
}
}
Ok(result)
}
}
fn relocation_references_symbol_range(relocation: Rela, range: Option<(u64, u64)>) -> bool {
let Some((start, end)) = range else {
return false;
};
let symbol = relocation.symbol();
symbol >= start && symbol < end
}
fn convert_defined_symbol_relocations(relocations: &mut [Rela], symbols: &[u8]) -> Result<usize> {
let mut converted = 0_usize;
for relocation in relocations {
if relocation.kind() == R_AARCH64_RELATIVE || relocation.symbol() == 0 {
continue;
}
let symbol = usize::try_from(relocation.symbol())
.map_err(|_| Error::Invalid("relocation symbol exceeds usize".to_owned()))?;
let (section_index, value) = symbol_definition(symbols, symbol)?;
if section_index == 0 {
continue;
}
let addend = i128::from(value) + i128::from(relocation.addend);
relocation.addend = i64::try_from(addend)
.map_err(|_| Error::Invalid("local relocation addend exceeds i64".to_owned()))?;
relocation.info = u64::from(R_AARCH64_RELATIVE);
converted += 1;
}
Ok(converted)
}
fn discover_special_plt_imports(
image: &[u8],
layout: &ElfLayout,
names: &[String],
old_plt: &[Rela],
auxiliary_plt: &[Rela],
) -> Result<Vec<(usize, &'static str)>> {
let plt_index = names
.iter()
.position(|name| name == ".plt")
.ok_or_else(|| Error::Invalid("restored image lacks .plt".to_owned()))?;
let got_plt_index = names
.iter()
.position(|name| name == ".got.plt")
.ok_or_else(|| Error::Invalid("restored image lacks .got.plt".to_owned()))?;
let plt = layout.section_headers[plt_index];
let got_plt = layout.section_headers[got_plt_index];
if got_plt.size < 0x18 || !(got_plt.size - 0x18).is_multiple_of(8) {
return invalid(".got.plt has an invalid slot layout");
}
let slot_count = usize_from_u64((got_plt.size - 0x18) / 8, ".got.plt slot count")?;
let first_slot = got_plt
.address
.checked_add(0x18)
.ok_or_else(|| Error::Invalid(".got.plt slot address overflow".to_owned()))?;
let slot_end = first_slot
.checked_add((slot_count as u64) * 8)
.ok_or_else(|| Error::Invalid(".got.plt slot range overflow".to_owned()))?;
let mut occupied = HashSet::new();
for table in [old_plt, auxiliary_plt] {
for &relocation in table
.iter()
.filter(|relocation| relocation.offset >= first_slot && relocation.offset < slot_end)
{
if !(relocation.offset - first_slot).is_multiple_of(8) {
return invalid("PLT relocation target is not slot-aligned");
}
if !occupied.insert(relocation.offset) {
return invalid(format!(
"duplicate PLT relocation target 0x{:x}",
relocation.offset
));
}
}
}
let missing = (0..slot_count)
.filter(|&slot| !occupied.contains(&(first_slot + (slot as u64) * 8)))
.collect::<Vec<_>>();
if missing.is_empty() {
return Ok(Vec::new());
}
if missing.len() != SPECIAL_PLT_IMPORT_NAMES.len() {
return invalid(format!(
".got.plt has {} unresolved slots, expected {} special libc imports",
missing.len(),
SPECIAL_PLT_IMPORT_NAMES.len()
));
}
if plt.size < 0x20 {
return invalid(".plt is too small for a PLT0 entry");
}
let first_plt = plt
.address
.checked_add(0x20)
.ok_or_else(|| Error::Invalid("first PLT entry overflow".to_owned()))?;
let required_plt_size = 0x20_u64
.checked_add((slot_count as u64) * 0x10)
.ok_or_else(|| Error::Invalid("PLT slot range overflow".to_owned()))?;
if plt.size < required_plt_size {
return invalid(format!(
".plt size 0x{:x} is too small for {slot_count} GOT slots",
plt.size
));
}
let mut candidates = Vec::with_capacity(missing.len());
for slot in missing {
let target = first_plt
.checked_add((slot as u64) * 0x10)
.ok_or_else(|| Error::Invalid("PLT entry address overflow".to_owned()))?;
candidates.push((slot, plt_call_features(image, layout, target)?));
}
classify_special_plt_slots(&candidates)
}
fn patch_dynamic_tags(
output: &mut [u8],
dynamic: SectionHeader,
values: &BTreeMap<u64, u64>,
) -> Result<()> {
if !dynamic.size.is_multiple_of(0x10) {
return invalid(".dynamic size is not entry-aligned");
}
let start = usize_from_u64(dynamic.offset, ".dynamic offset")?;
let size = usize_from_u64(dynamic.size, ".dynamic size")?;
let end = start
.checked_add(size)
.ok_or_else(|| Error::Invalid(".dynamic end overflow".to_owned()))?;
slice(output, start, size)?;
let mut found = HashSet::with_capacity(values.len());
for offset in (start..end).step_by(0x10) {
let tag = read_u64(output, offset)?;
if let Some(&value) = values.get(&tag) {
if !found.insert(tag) {
return invalid(format!("dynamic tag 0x{tag:x} occurs more than once"));
}
output[offset + 8..offset + 0x10].copy_from_slice(&value.to_le_bytes());
}
if tag == 0 {
break;
}
}
let missing = values
.keys()
.filter(|tag| !found.contains(tag))
.map(|tag| format!("0x{tag:x}"))
.collect::<Vec<_>>();
if !missing.is_empty() {
return invalid(format!("missing dynamic tags: {}", missing.join(", ")));
}
Ok(())
}
fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> Result<bool> {
if !dynamic.size.is_multiple_of(0x10) {
return invalid(".dynamic size is not entry-aligned");
}
let start = usize_from_u64(dynamic.offset, ".dynamic offset")?;
let size = usize_from_u64(dynamic.size, ".dynamic size")?;
let end = start
.checked_add(size)
.ok_or_else(|| Error::Invalid(".dynamic end overflow".to_owned()))?;
slice(output, start, size)?;
for offset in (start..end).step_by(0x10) {
let tag = read_u64(output, offset)?;
if tag == wanted {
return Ok(true);
}
if tag == 0 {
break;
}
}
Ok(false)
}
fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, usize>> {
let present = senbei_elf::SYMBOL_HASH_SECTION_NAMES
.iter()
.filter(|name| names.iter().any(|section| section == *name))
.count();
if present == 0 {
return invalid(
"ELF must carry at least one of the .gnu.hash / .hash symbol-hash sections",
);
}
let mut result = HashMap::with_capacity(
senbei_elf::DYNAMIC_SECTION_NAMES.len() + senbei_elf::SYMBOL_HASH_SECTION_NAMES.len(),
);
for required in senbei_elf::DYNAMIC_SECTION_NAMES {
let indices = names
.iter()
.enumerate()
.filter_map(|(index, name)| (name == required).then_some(index))
.collect::<Vec<_>>();
match indices.as_slice() {
[index] => {
result.insert(required, *index);
}
[] => return invalid(format!("ELF lacks required section {required}")),
_ => return invalid(format!("ELF contains duplicate section {required}")),
}
}
for hash_table in senbei_elf::SYMBOL_HASH_SECTION_NAMES {
let indices = names
.iter()
.enumerate()
.filter_map(|(index, name)| (name == hash_table).then_some(index))
.collect::<Vec<_>>();
match indices.as_slice() {
[index] => {
result.insert(hash_table, *index);
}
[] => {}
_ => return invalid(format!("ELF contains duplicate section {hash_table}")),
}
}
Ok(result)
}
fn metadata_capacity_end(
layout: &ElfLayout,
indices: &HashMap<&'static str, usize>,
metadata_start: u64,
) -> Result<u64> {
let table_indices = indices.values().copied().collect::<HashSet<_>>();
let file_end = layout.private_section()?.offset;
let next_section = layout
.section_headers
.iter()
.enumerate()
.filter(|(index, section)| {
!table_indices.contains(index)
&& section.section_type != SHT_NOBITS
&& section.size != 0
&& section.offset >= metadata_start
})
.map(|(_, section)| section.offset)
.min()
.unwrap_or(file_end);
let capacity_end = next_section.min(file_end);
// A zero-length window is a valid result: the caller can move the whole
// table set to a new PT_LOAD instead of overwriting an adjacent section.
Ok(capacity_end.max(metadata_start))
}
fn metadata_mapping_length(
source: &[u8],
layout: &ElfLayout,
auxiliary_data: &[u8],
) -> Result<usize> {
let names = layout.section_names(source)?;
let indices = required_section_indices(&names)?;
let section = |name: &'static str| -> SectionHeader { layout.section_headers[indices[name]] };
let dynsym = section(".dynsym");
let versym = section(".gnu.version");
let verneed = section(".gnu.version_r");
let dynstr = section(".dynstr");
let rela_dyn = section(".rela.dyn");
let rela_plt = section(".rela.plt");
if dynsym.entry_size != ELF64_SYMBOL_SIZE as u64
|| dynsym.size % ELF64_SYMBOL_SIZE as u64 != 0
|| versym.entry_size != 2
|| rela_dyn.entry_size != ELF64_RELA_SIZE as u64
|| rela_plt.entry_size != ELF64_RELA_SIZE as u64
|| rela_dyn.size % ELF64_RELA_SIZE as u64 != 0
|| rela_plt.size % ELF64_RELA_SIZE as u64 != 0
{
return invalid("unexpected dynamic-table entry layout");
}
let auxiliary = AuxiliaryElfImage::parse(auxiliary_data)?;
let old_symbol_count = usize_from_u64(
dynsym.size / ELF64_SYMBOL_SIZE as u64,
"dynamic symbol count",
)?;
let auxiliary_appended_count =
usize::try_from(auxiliary.dynsym_count.checked_sub(1).ok_or_else(|| {
Error::Invalid("auxiliary symbol table has no null entry".to_owned())
})?)
.map_err(|_| Error::Invalid("auxiliary symbol count exceeds usize".to_owned()))?;
// The legacy 212-symbol auxiliary format omits five libc imports that are
// reconstructed during materialization. Keep sizing conservative here: the
// protected table still includes three protector-only symbols that will be
// removed later, so reserving from the raw count intentionally overallocates
// by three entries rather than depending on unrelated relocation slack.
let special_count = SPECIAL_PLT_IMPORT_NAMES.len();
let appended_count = auxiliary_appended_count
.checked_add(special_count)
.ok_or_else(|| Error::Invalid("appended dynamic symbol count overflow".to_owned()))?;
let new_symbol_count = old_symbol_count
.checked_add(appended_count)
.ok_or_else(|| Error::Invalid("merged dynamic symbol count overflow".to_owned()))?;
let special_string_size = SPECIAL_PLT_IMPORT_NAMES
.iter()
.map(|name| name.len() + 1)
.sum();
let merged_dynstr_size = usize_from_u64(dynstr.size, ".dynstr size")?
.checked_add(auxiliary.dynstr_size as usize)
.and_then(|size| size.checked_add(special_string_size))
.ok_or_else(|| Error::Invalid("merged dynamic string size overflow".to_owned()))?;
let merged_rela_dyn_count =
usize_from_u64(rela_dyn.size / ELF64_RELA_SIZE as u64, ".rela.dyn count")?
.checked_add(auxiliary.relocation1_count as usize)
.and_then(|count| count.checked_add(auxiliary.relocation2_count as usize))
.ok_or_else(|| Error::Invalid("merged .rela.dyn count overflow".to_owned()))?;
let merged_rela_plt_count =
usize_from_u64(rela_plt.size / ELF64_RELA_SIZE as u64, ".rela.plt count")?
.checked_add(auxiliary.relocation2_count as usize)
.and_then(|count| count.checked_add(special_count))
.ok_or_else(|| Error::Invalid("merged .rela.plt count overflow".to_owned()))?;
let gnu_hash_size = if indices.contains_key(".gnu.hash") {
28_usize
.checked_add(
new_symbol_count
.checked_sub(1)
.ok_or_else(|| {
Error::Invalid("dynamic symbol table is unexpectedly empty".to_owned())
})?
.checked_mul(4)
.ok_or_else(|| Error::Invalid("GNU hash size overflow".to_owned()))?,
)
.ok_or_else(|| Error::Invalid("GNU hash size overflow".to_owned()))?
} else {
0
};
let sysv_hash_size = indices.contains_key(".hash").then(|| {
new_symbol_count
.checked_mul(2)
.and_then(|count| count.checked_add(2))
.and_then(|count| count.checked_mul(4))
.ok_or_else(|| Error::Invalid("SysV hash size overflow".to_owned()))
});
let sysv_hash_size = match sysv_hash_size {
Some(size) => size?,
None => 0,
};
let program_header_reservation = u64::try_from(layout.additional_program_header_reservation()?)
.map_err(|_| Error::Invalid("program-header reservation exceeds u64".to_owned()))?;
let mut cursor = program_header_reservation;
for (size, alignment) in [
(
new_symbol_count
.checked_mul(ELF64_SYMBOL_SIZE)
.ok_or_else(|| Error::Invalid("merged .dynsym size overflow".to_owned()))?,
8,
),
(
usize_from_u64(versym.size, ".gnu.version size")?
.checked_add(appended_count.checked_mul(2).ok_or_else(|| {
Error::Invalid("merged .gnu.version size overflow".to_owned())
})?)
.ok_or_else(|| Error::Invalid("merged .gnu.version size overflow".to_owned()))?,
2,
),
(usize_from_u64(verneed.size, ".gnu.version_r size")?, 4),
(gnu_hash_size, 8),
(sysv_hash_size, 4),
(merged_dynstr_size, 1),
(
merged_rela_dyn_count
.checked_mul(ELF64_RELA_SIZE)
.ok_or_else(|| Error::Invalid("merged .rela.dyn size overflow".to_owned()))?,
8,
),
(
merged_rela_plt_count
.checked_mul(ELF64_RELA_SIZE)
.ok_or_else(|| Error::Invalid("merged .rela.plt size overflow".to_owned()))?,
8,
),
] {
cursor = align_up(cursor, alignment)?;
cursor = cursor
.checked_add(size as u64)
.ok_or_else(|| Error::Invalid("dynamic-table reserve overflow".to_owned()))?;
}
let extension_alignment = layout.load_alignment()?;
let extension_start = align_up(layout.private_section()?.offset, extension_alignment)?;
let end = extension_start
.checked_add(cursor)
.ok_or_else(|| Error::Invalid("dynamic-table mapping end overflow".to_owned()))?;
Ok(usize_from_u64(end, "dynamic-table mapping length")?)
}
fn table_placements(
tables: &[TablePayload],
start: u64,
) -> Result<(BTreeMap<String, PlacementReport>, u64)> {
let mut cursor = start;
let mut placements = BTreeMap::new();
for table in tables {
cursor = align_up(cursor, table.alignment)?;
placements.insert(
table.name.to_owned(),
PlacementReport {
offset: cursor,
size: table.data.len(),
},
);
cursor = cursor
.checked_add(table.data.len() as u64)
.ok_or_else(|| Error::Invalid("rebuilt ELF metadata end overflow".to_owned()))?;
}
Ok((placements, cursor))
}
fn table_end(tables: &[TablePayload], start: u64) -> Result<u64> {
table_placements(tables, start).map(|(_, end)| end)
}
fn materialize_static_elf_tables(
output: &mut [u8],
source: &[u8],
layout: &ElfLayout,
symbol_patch_data: &[u8],
auxiliary_data: &[u8],
auxiliary_symbol_seed: Option<u32>,
constructor_module: Option<(&[u8], u32)>,
) -> Result<(ElfLayout, ElfMaterializationReport, u64)> {
let names = layout.section_names(source)?;
let indices = required_section_indices(&names)?;
let section = |name: &'static str| -> SectionHeader { layout.section_headers[indices[name]] };
let dynsym = section(".dynsym");
let dynstr = section(".dynstr");
let versym = section(".gnu.version");
let verneed = section(".gnu.version_r");
let rela_dyn = section(".rela.dyn");
let rela_plt = section(".rela.plt");
let dynamic = section(".dynamic");
let (mut old_symbols, old_strings, hidden_symbols) =
restore_hidden_symbols(output, dynsym, dynstr, symbol_patch_data)?;
let source_symbol_count = old_symbols.len() / ELF64_SYMBOL_SIZE;
if versym.size != (source_symbol_count * 2) as u64 {
return invalid(".gnu.version count does not match .dynsym");
}
let mut old_versions = slice_u64(output, versym.offset, versym.size)?.to_vec();
let version_requirements = slice_u64(output, verneed.offset, verneed.size)?.to_vec();
let auxiliary = AuxiliaryElfImage::parse(auxiliary_data)?;
let old_names = dynamic_symbol_names(&old_symbols, &old_strings)?;
let protector_names = ["dlerror", "__stack_chk_guard", "__stack_chk_fail"];
let protector_tail_start = usize::try_from(hidden_symbols.last_target_index.saturating_add(1))
.map_err(|_| Error::Invalid("protector symbol index exceeds usize".to_owned()))?;
let protector_symbol_range = if protector_tail_start < source_symbol_count {
let tail = &old_names[protector_tail_start..];
if tail.iter().all(|name| {
protector_names
.iter()
.any(|protector| name.as_slice() == protector.as_bytes())
}) {
old_symbols.truncate(protector_tail_start * ELF64_SYMBOL_SIZE);
old_versions.truncate(protector_tail_start * 2);
Some((
u64::try_from(protector_tail_start)
.map_err(|_| Error::Invalid("protector symbol index exceeds u64".to_owned()))?,
u64::try_from(source_symbol_count)
.map_err(|_| Error::Invalid("source symbol count exceeds u64".to_owned()))?,
))
} else {
None
}
} else {
None
};
let old_symbol_count = old_symbols.len() / ELF64_SYMBOL_SIZE;
let auxiliary_strings = slice(
auxiliary_data,
auxiliary.dynstr_offset as usize,
auxiliary.dynstr_size as usize,
)?;
let auxiliary_appended_count =
usize::try_from(auxiliary.dynsym_count.checked_sub(1).ok_or_else(|| {
Error::Invalid("auxiliary symbol table has no null entry".to_owned())
})?)
.map_err(|_| Error::Invalid("auxiliary symbol count exceeds usize".to_owned()))?;
let mut appended_symbols = Vec::with_capacity(auxiliary_appended_count * ELF64_SYMBOL_SIZE);
for index in 1..auxiliary.dynsym_count as usize {
let offset = auxiliary.dynsym_offset as usize + index * ELF64_SYMBOL_SIZE;
let symbol = slice(auxiliary_data, offset, ELF64_SYMBOL_SIZE)?;
let name_offset = read_u32(symbol, 0)? as usize;
read_c_string(auxiliary_strings, name_offset, auxiliary_strings.len())?;
let section_index = u16::from_le_bytes([symbol[6], symbol[7]]);
if section_index != 0 {
return invalid("auxiliary dynamic symbol is unexpectedly defined");
}
let merged_name_offset = old_strings
.len()
.checked_add(name_offset)
.ok_or_else(|| Error::Invalid("merged dynamic string offset overflow".to_owned()))?;
let merged_name_offset = u32::try_from(merged_name_offset)
.map_err(|_| Error::Invalid("merged dynamic string offset exceeds u32".to_owned()))?;
appended_symbols.extend_from_slice(&merged_name_offset.to_le_bytes());
appended_symbols.extend_from_slice(&symbol[4..]);
}
let mut merged_symbols = Vec::with_capacity(old_symbols.len() + appended_symbols.len());
merged_symbols.extend_from_slice(&old_symbols);
merged_symbols.extend_from_slice(&appended_symbols);
let mut merged_strings = Vec::with_capacity(old_strings.len() + auxiliary_strings.len() + 64);
merged_strings.extend_from_slice(&old_strings);
merged_strings.extend_from_slice(auxiliary_strings);
let mut merged_versions =
Vec::with_capacity(old_versions.len() + (auxiliary_appended_count + 5) * 2);
merged_versions.extend_from_slice(&old_versions);
for _ in 0..auxiliary_appended_count {
merged_versions.extend_from_slice(&VER_NDX_GLOBAL.to_le_bytes());
}
if rela_dyn.entry_size != ELF64_RELA_SIZE as u64
|| rela_plt.entry_size != ELF64_RELA_SIZE as u64
|| rela_dyn.size % ELF64_RELA_SIZE as u64 != 0
|| rela_plt.size % ELF64_RELA_SIZE as u64 != 0
{
return invalid("unexpected relocation entry layout");
}
let old_dyn = RelocationTable {
data: output,
offset: usize_from_u64(rela_dyn.offset, ".rela.dyn offset")?,
count: usize_from_u64(rela_dyn.size / ELF64_RELA_SIZE as u64, ".rela.dyn count")?,
remap: None,
};
let old_plt = RelocationTable {
data: output,
offset: usize_from_u64(rela_plt.offset, ".rela.plt offset")?,
count: usize_from_u64(rela_plt.size / ELF64_RELA_SIZE as u64, ".rela.plt count")?,
remap: None,
};
let auxiliary1 = RelocationTable {
data: auxiliary_data,
offset: auxiliary.relocation1_offset as usize,
count: auxiliary.relocation1_count as usize,
remap: Some(RelocationRemap {
base_symbol_count: old_symbol_count,
auxiliary_symbol_count: auxiliary.dynsym_count,
permutation_seed: auxiliary_symbol_seed,
}),
};
let auxiliary2 = RelocationTable {
data: auxiliary_data,
offset: auxiliary.relocation2_offset as usize,
count: auxiliary.relocation2_count as usize,
remap: Some(RelocationRemap {
base_symbol_count: old_symbol_count,
auxiliary_symbol_count: auxiliary.dynsym_count,
permutation_seed: auxiliary_symbol_seed,
}),
};
old_dyn.validate(
&[R_AARCH64_RELATIVE, R_AARCH64_GLOB_DAT, R_AARCH64_ABS64],
"existing .rela.dyn",
)?;
old_plt.validate(&[R_AARCH64_JUMP_SLOT], "existing .rela.plt")?;
auxiliary1.validate(
&[R_AARCH64_RELATIVE, R_AARCH64_GLOB_DAT, R_AARCH64_ABS64],
"auxiliary relocation table 1",
)?;
auxiliary2.validate(
&[R_AARCH64_RELATIVE, R_AARCH64_JUMP_SLOT],
"auxiliary relocation table 2",
)?;
let old_dyn_relocations = old_dyn.collect_where(|relocation| {
!relocation_references_symbol_range(relocation, protector_symbol_range)
})?;
let old_plt_relocations = old_plt.collect_where(|relocation| {
!relocation_references_symbol_range(relocation, protector_symbol_range)
})?;
let auxiliary1_relocations = auxiliary1.collect_where(|_| true)?;
let auxiliary2_relocations = auxiliary2.collect_where(|_| true)?;
let special_imports = discover_special_plt_imports(
output,
layout,
&names,
&old_plt_relocations,
&auxiliary2_relocations,
)?;
let existing_names = dynamic_symbol_names(&merged_symbols, &merged_strings)?;
for &name in &SPECIAL_PLT_IMPORT_NAMES {
if existing_names
.iter()
.any(|existing| existing.as_slice() == name.as_bytes())
&& special_imports.iter().any(|(_, special)| *special == name)
{
return invalid(format!(
"special PLT import {name} already exists in the dynamic symbol table"
));
}
}
let mut special_plt_symbols = Vec::with_capacity(special_imports.len());
for &(slot, name) in &special_imports {
let symbol =
append_undefined_function_symbol(&mut merged_symbols, &mut merged_strings, name)?;
merged_versions.extend_from_slice(&VER_NDX_GLOBAL.to_le_bytes());
special_plt_symbols.push((slot, symbol));
}
let appended_count = auxiliary_appended_count + special_plt_symbols.len();
let merged_names = dynamic_symbol_names(&merged_symbols, &merged_strings)?;
let sysv_hash = indices
.contains_key(".hash")
.then(|| build_sysv_hash(&merged_names))
.transpose()?;
let gnu_hash_table = indices
.contains_key(".gnu.hash")
.then(|| build_gnu_hash(&merged_names))
.transpose()?;
let new_symbol_count = merged_names.len();
let new_dynstr_size = merged_strings.len();
let constructors = match constructor_module {
Some(module) => {
let init_index = names
.iter()
.position(|name| name == ".init_array")
.ok_or_else(|| {
Error::Invalid(
"constructor control module 0x98 is present but .init_array is missing"
.to_owned(),
)
})?;
let fini_index = names
.iter()
.position(|name| name == ".fini_array")
.ok_or_else(|| {
Error::Invalid(
"constructor control module 0x98 is present but .fini_array is missing"
.to_owned(),
)
})?;
let init_array = layout.section_headers[init_index];
let fini_array = layout.section_headers[fini_index];
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,
};
let got_plt_index = names
.iter()
.position(|name| name == ".got.plt")
.ok_or_else(|| Error::Invalid("restored image lacks .got.plt".to_owned()))?;
let got_plt = layout.section_headers[got_plt_index];
if got_plt.size < 0x18 || !(got_plt.size - 0x18).is_multiple_of(8) {
return invalid(".got.plt has an invalid slot layout");
}
let slot_count = usize_from_u64((got_plt.size - 0x18) / 8, ".got.plt slot count")?;
let first_slot = got_plt
.address
.checked_add(0x18)
.ok_or_else(|| Error::Invalid(".got.plt slot address overflow".to_owned()))?;
let slot_end = first_slot
.checked_add((slot_count as u64) * 8)
.ok_or_else(|| Error::Invalid(".got.plt slot end overflow".to_owned()))?;
let old_plt_outside = old_plt_relocations
.iter()
.copied()
.filter(|relocation| relocation.offset < first_slot || relocation.offset >= slot_end)
.collect::<Vec<_>>();
if old_plt_outside.iter().any(|relocation| {
let symbol = relocation.symbol() as usize;
symbol < old_symbol_count || symbol >= source_symbol_count
}) {
return invalid("protected .rela.plt has a non-protector target outside .got.plt");
}
let mut plt_relocations = old_plt_relocations
.iter()
.copied()
.filter(|relocation| relocation.offset >= first_slot && relocation.offset < slot_end)
.collect::<Vec<_>>();
plt_relocations.extend(
auxiliary2_relocations
.iter()
.copied()
.filter(|relocation| relocation.offset >= first_slot && relocation.offset < slot_end),
);
for &(slot, symbol) in &special_plt_symbols {
let offset = first_slot
.checked_add((slot as u64) * 8)
.ok_or_else(|| Error::Invalid("special PLT slot overflow".to_owned()))?;
let symbol = u64::try_from(symbol)
.map_err(|_| Error::Invalid("special symbol index exceeds u64".to_owned()))?;
plt_relocations.push(Rela {
offset,
info: (symbol << 32) | u64::from(R_AARCH64_JUMP_SLOT),
addend: 0,
});
}
plt_relocations.sort_by_key(|relocation| relocation.offset);
if plt_relocations.len() != slot_count {
return invalid(format!(
"PLT restoration produced {} slots, expected {slot_count}",
plt_relocations.len()
));
}
for (slot, relocation) in plt_relocations.iter().enumerate() {
let expected = first_slot + (slot as u64) * 8;
if relocation.offset != expected {
return invalid(format!(
"PLT slot {slot} targets 0x{:x}, expected 0x{expected:x}",
relocation.offset
));
}
}
let mut dyn_relocations = old_dyn_relocations;
dyn_relocations.extend(auxiliary1_relocations);
let auxiliary2_dynamic = auxiliary2_relocations
.iter()
.copied()
.filter(|relocation| relocation.offset < first_slot || relocation.offset >= slot_end)
.collect::<Vec<_>>();
if auxiliary2_dynamic.iter().any(|relocation| {
!matches!(
relocation.kind(),
R_AARCH64_RELATIVE | R_AARCH64_GLOB_DAT | R_AARCH64_ABS64
)
}) {
return invalid("auxiliary PLT table has a non-PLT relocation of an unsupported type");
}
dyn_relocations.extend(auxiliary2_dynamic);
convert_defined_symbol_relocations(&mut dyn_relocations, &merged_symbols)?;
dyn_relocations
.sort_by_key(|relocation| (relocation.kind() != R_AARCH64_RELATIVE, relocation.offset));
let relative_count = dyn_relocations
.iter()
.take_while(|relocation| relocation.kind() == R_AARCH64_RELATIVE)
.count();
if let Some((profile, _, _, init_array, fini_array)) = &constructors {
for (slot, &addend) in profile.init_addends.iter().enumerate() {
let target = init_array
.address
.checked_add((slot as u64) * 8)
.ok_or_else(|| Error::Invalid("init relocation target overflow".to_owned()))?;
let matches = dyn_relocations
.iter()
.enumerate()
.filter_map(|(index, relocation)| (relocation.offset == target).then_some(index))
.collect::<Vec<_>>();
if matches.len() != 1 {
return invalid(format!(
"init slot {slot} has {} relocation records",
matches.len()
));
}
let relocation = &mut dyn_relocations[matches[0]];
if relocation.kind() != R_AARCH64_RELATIVE || relocation.symbol() != 0 {
return invalid(format!("init slot {slot} is not RELATIVE"));
}
relocation.addend = i64::try_from(addend)
.map_err(|_| Error::Invalid("constructor RVA exceeds i64".to_owned()))?;
}
let matches = dyn_relocations
.iter()
.enumerate()
.filter_map(|(index, relocation)| {
(relocation.offset == fini_array.address).then_some(index)
})
.collect::<Vec<_>>();
if matches.len() != 1 {
return invalid(format!(
"fini slot has {} relocation records",
matches.len()
));
}
let relocation = &mut dyn_relocations[matches[0]];
if relocation.kind() != R_AARCH64_RELATIVE || relocation.symbol() != 0 {
return invalid("fini slot is not RELATIVE");
}
relocation.addend = i64::try_from(profile.fini_addend)
.map_err(|_| Error::Invalid("fini RVA exceeds i64".to_owned()))?;
}
let mut merged_rela_dyn = Vec::with_capacity(dyn_relocations.len() * ELF64_RELA_SIZE);
for relocation in dyn_relocations {
relocation.encode(&mut merged_rela_dyn);
}
let mut merged_rela_plt = Vec::with_capacity(plt_relocations.len() * ELF64_RELA_SIZE);
for relocation in plt_relocations {
relocation.encode(&mut merged_rela_plt);
}
let rela_dyn_count = merged_rela_dyn.len() / ELF64_RELA_SIZE;
let rela_plt_count = merged_rela_plt.len() / ELF64_RELA_SIZE;
if let Some((profile, _, _, init_array, fini_array)) = &constructors {
let init_start = usize_from_u64(init_array.offset, "init-array offset")?;
let init_size = usize_from_u64(init_array.size, "init-array size")?;
output
.get_mut(init_start..init_start + init_size)
.ok_or_else(|| Error::Invalid("init-array exceeds output".to_owned()))?
.fill(0);
for (slot, &addend) in profile.init_addends.iter().enumerate() {
let start = init_start + slot * 8;
output[start..start + 8].copy_from_slice(&addend.to_le_bytes());
}
let fini_start = usize_from_u64(fini_array.offset, "fini-array offset")?;
let fini_size = usize_from_u64(fini_array.size, "fini-array size")?;
output
.get_mut(fini_start..fini_start + fini_size)
.ok_or_else(|| Error::Invalid("fini-array exceeds output".to_owned()))?
.fill(0);
output[fini_start..fini_start + 8].copy_from_slice(&profile.fini_addend.to_le_bytes());
}
let gnu_hash = gnu_hash_table.map(|data| TablePayload {
name: ".gnu.hash",
alignment: 8,
data,
});
let mut tables = vec![
TablePayload {
name: ".dynsym",
alignment: 8,
data: merged_symbols,
},
TablePayload {
name: ".gnu.version",
alignment: 2,
data: merged_versions,
},
TablePayload {
name: ".gnu.version_r",
alignment: 4,
data: version_requirements,
},
];
if let Some(payload) = gnu_hash {
tables.push(payload);
}
if let Some(data) = sysv_hash {
tables.push(TablePayload {
name: ".hash",
alignment: 4,
data,
});
}
tables.extend([
TablePayload {
name: ".dynstr",
alignment: 1,
data: merged_strings,
},
TablePayload {
name: ".rela.dyn",
alignment: 8,
data: merged_rela_dyn,
},
TablePayload {
name: ".rela.plt",
alignment: 8,
data: merged_rela_plt,
},
]);
let mut placement_layout = layout.clone();
let mut metadata_start = dynsym.offset;
let (mut placements, mut cursor) = table_placements(&tables, metadata_start)?;
let mut capacity_end = metadata_capacity_end(layout, &indices, metadata_start)?;
if cursor > capacity_end {
let alignment = layout.load_alignment()?;
let program_header_reservation = layout.additional_program_header_reservation()?;
let extension_start = align_up(layout.private_section()?.offset, alignment)?;
let extension_metadata_start = extension_start
.checked_add(program_header_reservation as u64)
.ok_or_else(|| Error::Invalid("dynamic-table extension start overflow".to_owned()))?;
let extension_end = table_end(&tables, extension_metadata_start)?;
let extension_size = extension_end
.checked_sub(extension_start)
.ok_or_else(|| Error::Invalid("dynamic-table extension underflow".to_owned()))?;
let extension_address = align_up(layout.load_end()?, alignment)?;
placement_layout = layout.append_load_segment(
output,
LoadSegment {
offset: extension_start,
virtual_address: extension_address,
file_size: extension_size,
memory_size: extension_size,
flags: PF_R,
alignment,
},
program_header_reservation,
)?;
metadata_start = extension_metadata_start;
(placements, cursor) = table_placements(&tables, metadata_start)?;
capacity_end = cursor;
}
let zero_start = usize_from_u64(dynsym.offset, "metadata start")?;
let zero_end = usize_from_u64(
metadata_capacity_end(layout, &indices, dynsym.offset)?,
"metadata capacity end",
)?;
output
.get_mut(zero_start..zero_end)
.ok_or_else(|| Error::Invalid("metadata capacity exceeds output mapping".to_owned()))?
.fill(0);
if metadata_start != dynsym.offset {
let extension_start = usize_from_u64(metadata_start, "dynamic-table extension start")?;
let extension_end = usize_from_u64(cursor, "dynamic-table extension end")?;
output
.get_mut(extension_start..extension_end)
.ok_or_else(|| {
Error::Invalid("dynamic-table extension exceeds output mapping".to_owned())
})?
.fill(0);
}
let mut updated_sections = layout.section_headers.clone();
for table in &tables {
let placement = placements
.get(table.name)
.ok_or_else(|| Error::Invalid("table placement disappeared".to_owned()))?;
let offset = usize_from_u64(placement.offset, "table placement offset")?;
let end = offset
.checked_add(table.data.len())
.ok_or_else(|| Error::Invalid("table placement end overflow".to_owned()))?;
output
.get_mut(offset..end)
.ok_or_else(|| Error::Invalid("table placement exceeds output mapping".to_owned()))?
.copy_from_slice(&table.data);
let index = indices[table.name];
let mut updated = updated_sections[index];
updated.address = placement_layout
.file_offset_to_virtual_address(placement.offset, table.data.len() as u64)?;
updated.offset = placement.offset;
updated.size = table.data.len() as u64;
updated_sections[index] = updated;
}
if let Some((profile, init_index, fini_index, _, _)) = &constructors {
let init_size = u64::try_from(profile.init_addends.len())
.map_err(|_| Error::Invalid("constructor count exceeds u64".to_owned()))?
.checked_mul(8)
.ok_or_else(|| Error::Invalid("init-array size overflow".to_owned()))?;
updated_sections[*init_index].size = init_size;
updated_sections[*fini_index].size = 8;
}
let section_address = |name: &'static str| -> u64 { updated_sections[indices[name]].address };
let mut dynamic_values = BTreeMap::from([
(DT_PLTRELSZ, (rela_plt_count * ELF64_RELA_SIZE) as u64),
(DT_STRTAB, section_address(".dynstr")),
(DT_SYMTAB, section_address(".dynsym")),
(DT_RELA, section_address(".rela.dyn")),
(DT_RELASZ, (rela_dyn_count * ELF64_RELA_SIZE) as u64),
(DT_STRSZ, new_dynstr_size as u64),
(DT_JMPREL, section_address(".rela.plt")),
(DT_VERSYM, section_address(".gnu.version")),
(DT_VERNEED, section_address(".gnu.version_r")),
]);
if let Some((profile, _, _, _, _)) = &constructors {
let init_size = u64::try_from(profile.init_addends.len())
.map_err(|_| Error::Invalid("constructor count exceeds u64".to_owned()))?
.checked_mul(8)
.ok_or_else(|| Error::Invalid("init-array size overflow".to_owned()))?;
dynamic_values.insert(DT_INIT_ARRAYSZ, init_size);
dynamic_values.insert(DT_FINI_ARRAYSZ, 8);
}
if indices.contains_key(".gnu.hash") {
dynamic_values.insert(DT_GNU_HASH, section_address(".gnu.hash"));
}
if dynamic_contains_tag(output, dynamic, DT_RELACOUNT)? {
dynamic_values.insert(DT_RELACOUNT, relative_count as u64);
}
if indices.contains_key(".hash") {
dynamic_values.insert(DT_HASH, section_address(".hash"));
}
patch_dynamic_tags(output, dynamic, &dynamic_values)?;
let mut restored_layout = placement_layout;
restored_layout.section_headers = updated_sections;
let data_end = if metadata_start == dynsym.offset {
layout.private_section()?.offset
} else {
cursor
};
Ok((
restored_layout,
ElfMaterializationReport {
hidden_symbols,
old_symbol_count,
auxiliary_symbol_count: auxiliary.dynsym_count,
appended_symbols: appended_count,
new_symbol_count,
old_dynstr_size: old_strings.len(),
auxiliary_dynstr_size: auxiliary.dynstr_size,
new_dynstr_size,
rela_dyn_count,
rela_plt_count,
relative_prefix_count: relative_count,
metadata_start,
metadata_end: cursor,
metadata_capacity_end: capacity_end,
metadata_slack: capacity_end.saturating_sub(cursor),
placements,
},
data_end,
))
}
fn write_padding(file: &mut File, size: u64, path: &Path) -> Result<()> {
const ZEROES: [u8; 4096] = [0; 4096];
let mut remaining = size;
while remaining != 0 {
let count = usize::try_from(remaining.min(ZEROES.len() as u64))
.map_err(|_| Error::Invalid("padding size exceeds usize".to_owned()))?;
file.write_all(&ZEROES[..count])
.map_err(|error| Error::io("write padding", path, error))?;
remaining -= count as u64;
}
Ok(())
}
fn finalize_clean_elf(
stream: &mut File,
temporary_path: &Path,
source: &[u8],
layout: &ElfLayout,
data_start: u64,
preserve_entrypoint: bool,
) -> Result<CleaningReport> {
let private = layout.private_section()?;
if data_start < private.offset {
return invalid("ELF data start precedes the private section");
}
let names = layout.section_names(source)?;
if layout.private_section_index + 1 != layout.section_headers.len() {
return invalid("SHT_LOUSER section is not the final section");
}
let retained = &layout.section_headers[..layout.private_section_index];
let mut updated = Vec::with_capacity(retained.len());
stream
.seek(SeekFrom::Start(data_start))
.map_err(|error| Error::io("seek temporary output", temporary_path, error))?;
for &section in retained {
if section.section_type == SHT_NOBITS || section.flags & SHF_ALLOC != 0 || section.size == 0
{
updated.push(section);
continue;
}
let section_data = slice_u64(source, section.offset, section.size)?;
let alignment = section.alignment.max(1);
let position = stream
.stream_position()
.map_err(|error| Error::io("query temporary output position", temporary_path, error))?;
let padding = (alignment - position % alignment) % alignment;
write_padding(stream, padding, temporary_path)?;
let new_offset = stream
.stream_position()
.map_err(|error| Error::io("query temporary output position", temporary_path, error))?;
stream
.write_all(section_data)
.map_err(|error| Error::io("append ELF section", temporary_path, error))?;
let mut relocated = section;
relocated.offset = new_offset;
updated.push(relocated);
}
let position = stream
.stream_position()
.map_err(|error| Error::io("query temporary output position", temporary_path, error))?;
write_padding(stream, (8 - position % 8) % 8, temporary_path)?;
let section_header_offset = stream
.stream_position()
.map_err(|error| Error::io("query section-header position", temporary_path, error))?;
for section in &updated {
stream
.write_all(&section.encode())
.map_err(|error| Error::io("write section header", temporary_path, error))?;
}
let mut elf_header = [0_u8; 0x40];
stream
.seek(SeekFrom::Start(0))
.and_then(|_| stream.read_exact(&mut elf_header))
.map_err(|error| Error::io("read ELF header", temporary_path, error))?;
if !preserve_entrypoint {
elf_header[0x18..0x20].copy_from_slice(&0_u64.to_le_bytes());
}
elf_header[0x28..0x30].copy_from_slice(&section_header_offset.to_le_bytes());
let section_count = u16::try_from(updated.len())
.map_err(|_| Error::Invalid("output section count exceeds u16".to_owned()))?;
elf_header[0x3c..0x3e].copy_from_slice(&section_count.to_le_bytes());
stream
.seek(SeekFrom::Start(0))
.and_then(|_| stream.write_all(&elf_header))
.map_err(|error| Error::io("patch ELF header", temporary_path, error))?;
stream
.flush()
.and_then(|_| stream.sync_all())
.map_err(|error| Error::io("flush temporary output", temporary_path, error))?;
Ok(CleaningReport {
private_section_index: layout.private_section_index,
private_offset: private.offset,
private_size: private.size,
input_entrypoint: layout.entrypoint,
output_entrypoint: if preserve_entrypoint {
layout.entrypoint
} else {
0
},
output_section_count: updated.len(),
retained_sections: names[..layout.private_section_index].to_vec(),
section_header_offset,
})
}
fn section_by_name<'a>(
layout: &'a ElfLayout,
names: &[String],
wanted: &str,
) -> Result<&'a SectionHeader> {
let indices = names
.iter()
.enumerate()
.filter_map(|(index, name)| (name == wanted).then_some(index))
.collect::<Vec<_>>();
match indices.as_slice() {
[index] => Ok(&layout.section_headers[*index]),
[] => invalid(format!("restored ELF lacks {wanted}")),
_ => invalid(format!("restored ELF contains duplicate {wanted}")),
}
}
fn validate_restored_binary(
data: &[u8],
preserve_entrypoint: bool,
materialization: Option<&ElfMaterializationReport>,
) -> Result<ValidationReport> {
let layout = ElfLayout::parse(data, false)?;
let has_louser = layout
.section_headers
.iter()
.any(|section| section.section_type == SHT_LOUSER);
if has_louser {
return invalid("restored output still contains SHT_LOUSER");
}
if !preserve_entrypoint && layout.entrypoint != 0 {
return invalid("restored output retains the protector entrypoint");
}
let names = layout.section_names(data)?;
let dynsym = section_by_name(&layout, &names, ".dynsym")?;
let rela_dyn = section_by_name(&layout, &names, ".rela.dyn")?;
let rela_plt = section_by_name(&layout, &names, ".rela.plt")?;
let dynamic_symbols = usize_from_u64(
dynsym.size / ELF64_SYMBOL_SIZE as u64,
"restored dynamic symbol count",
)?;
let dynamic_relocations = usize_from_u64(
rela_dyn.size / ELF64_RELA_SIZE as u64,
"restored dynamic relocation count",
)?;
let pltgot_relocations = usize_from_u64(
rela_plt.size / ELF64_RELA_SIZE as u64,
"restored PLT relocation count",
)?;
if let Some(expected) = materialization
&& (dynamic_symbols != expected.new_symbol_count
|| dynamic_relocations != expected.rela_dyn_count
|| pltgot_relocations != expected.rela_plt_count)
{
return invalid("restored ELF table counts do not match materialization report");
}
Ok(ValidationReport {
format: "ELF64".to_owned(),
machine: "AARCH64".to_owned(),
sections: layout.section_headers.len(),
segments: layout.program_headers.len(),
dynamic_symbols,
dynamic_relocations,
pltgot_relocations,
has_louser,
})
}
fn absolute(path: &Path) -> Result<PathBuf> {
common::absolute(path).map_err(|error| Error::io("query current directory", path, error))
}
fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
common::write_atomic(path, data).map_err(|error| Error::io("write temporary file", path, error))
}
/// Restore the current protected `libil2cpp.so` without executing protector code.
pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
let started = Instant::now();
let input_path = absolute(&options.input)?;
let output_path = absolute(&options.output)?;
let index_path = absolute(&options.index)?;
if input_path == output_path
|| (output_path.exists()
&& std::fs::canonicalize(&input_path).ok() == std::fs::canonicalize(&output_path).ok())
{
return invalid("refusing to overwrite the protected input in place");
}
let artifacts = load_artifacts(&index_path)?;
let module = read_file(&artifacts[&0x9b].path)?;
let symbol_patch_data = read_file(&artifacts[&0x9e].path)?;
let auxiliary_symbol_seed = artifacts
.get(&0x96)
.map(|artifact| read_file(&artifact.path))
.transpose()?
.as_deref()
.map(discover_auxiliary_symbol_seed)
.transpose()?;
let constructor_module = artifacts
.get(&0x98)
.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)?;
let input_file = File::open(&input_path)
.map_err(|error| Error::io("open protected input", &input_path, error))?;
let source = map_read_only(&input_file, &input_path)?;
let payload_path = &artifacts[&0x9d].path;
let payload_file = File::open(payload_path)
.map_err(|error| Error::io("open 0x9D artifact", payload_path, error))?;
let payload = map_read_only(&payload_file, payload_path)?;
let layout = ElfLayout::parse(&source, true)?;
let private = layout.private_section()?;
let file_load_end = layout.file_load_end()?;
let aligned_load_end = align_up(file_load_end, 0x10)?;
if private.offset != aligned_load_end {
return invalid(format!(
"SHT_LOUSER offset 0x{:x} != aligned file-backed PT_LOAD end 0x{aligned_load_end:x} (raw 0x{file_load_end:x})",
private.offset
));
}
let load_padding = slice_u64(&source, file_load_end, private.offset - file_load_end)?;
if load_padding.iter().any(|&byte| byte != 0) {
return invalid(format!(
"nonzero padding between PT_LOAD end 0x{file_load_end:x} and SHT_LOUSER 0x{:x}",
private.offset
));
}
let descriptor = ProtectedDescriptor::decrypt(&payload, config.header_seed)?;
let load_end = layout.load_end()?;
if u64::from(descriptor.outer_expected_size) != load_end {
return invalid(format!(
"0x9D target size 0x{:x} != ELF load size 0x{load_end:x}",
descriptor.outer_expected_size
));
}
let outer = ContainerHeader::parse(
&payload,
descriptor.outer_offset as usize,
config.container_seed,
)?;
if u64::from(outer.output_size) != load_end {
return invalid(format!(
"primary container output 0x{:x} != ELF load size 0x{load_end:x}",
outer.output_size
));
}
let auxiliary_header = ContainerHeader::parse(
&payload,
descriptor.auxiliary_offset as usize,
config.container_seed,
)?;
if auxiliary_header.output_size != descriptor.auxiliary_expected_size {
return invalid("auxiliary container output size does not match the 0x9D descriptor");
}
if outer.encoded_end()? != descriptor.auxiliary_offset as usize {
return invalid("primary and auxiliary 0x9D containers are not contiguous");
}
let parent = output_path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(parent)
.map_err(|error| Error::io("create output directory", parent, error))?;
let mut temporary = NamedTempFile::new_in(parent)
.map_err(|error| Error::io("create temporary output", parent, error))?;
let temporary_path = temporary.path().to_path_buf();
let private_size = usize_from_u64(private.offset, "private section offset")?;
copy_range(
&source,
temporary.as_file_mut(),
private_size,
&temporary_path,
)?;
temporary
.as_file_mut()
.flush()
.map_err(|error| Error::io("flush initial output", &temporary_path, error))?;
temporary
.as_file()
.set_len(private.offset)
.map_err(|error| Error::io("size temporary output", &temporary_path, error))?;
let mut restored_layout = layout.clone();
let mut auxiliary_stats = None;
let mut materialization = None;
let mut temporary_end = private.offset;
let primary_stats;
{
let mut output = map_mut(temporary.as_file(), private_size, &temporary_path)?;
if options.verbose {
eprintln!("Decoding primary 0x9D target-image container...");
}
let mut writer = FileLayoutWriter {
output: &mut output,
layout: &layout,
load_end,
};
primary_stats = decode_container(
&payload,
&outer,
&config,
options.verbose,
|address, data| writer.write(address, data),
)?;
output
.flush()
.map_err(|error| Error::io("flush restored image", &temporary_path, error))?;
}
if !options.outer_only {
if options.verbose {
eprintln!("Decoding auxiliary 0x9D ELF materialization container...");
}
let mut decoded = vec![0_u8; auxiliary_header.output_size as usize];
let stats = decode_container(
&payload,
&auxiliary_header,
&config,
options.verbose,
|offset, data| {
let start = usize_from_u64(offset, "auxiliary write offset")?;
let end = start
.checked_add(data.len())
.ok_or_else(|| Error::Invalid("auxiliary decoded write overflow".to_owned()))?;
let destination = decoded.get_mut(start..end).ok_or_else(|| {
Error::Invalid("auxiliary decoded write is out of range".to_owned())
})?;
destination.copy_from_slice(data);
Ok(data.len())
},
)?;
if let Some(path) = &options.dump_auxiliary {
write_atomic(&absolute(path)?, &decoded)?;
}
let mapping_length = metadata_mapping_length(&source, &layout, &decoded)?;
if mapping_length < private_size {
return invalid("dynamic-table mapping is shorter than the ELF image");
}
temporary
.as_file()
.set_len(mapping_length as u64)
.map_err(|error| Error::io("extend temporary output", &temporary_path, error))?;
if options.verbose {
eprintln!("Rebuilding static ELF dynamic-linker tables...");
}
{
let mut output = map_mut(temporary.as_file(), mapping_length, &temporary_path)?;
let (new_layout, report, data_end) = materialize_static_elf_tables(
&mut output,
&source,
&layout,
&symbol_patch_data,
&decoded,
auxiliary_symbol_seed,
constructor_module
.as_ref()
.map(|(image, entry)| (image.as_slice(), *entry)),
)?;
restored_layout = new_layout;
materialization = Some(report);
auxiliary_stats = Some(stats);
temporary_end = data_end;
output
.flush()
.map_err(|error| Error::io("flush restored image", &temporary_path, error))?;
}
temporary
.as_file()
.set_len(temporary_end)
.map_err(|error| Error::io("trim temporary output", &temporary_path, error))?;
}
let cleaning = finalize_clean_elf(
temporary.as_file_mut(),
&temporary_path,
&source,
&restored_layout,
temporary_end,
options.preserve_entrypoint,
)?;
let validation = {
let restored = map_read_only(temporary.as_file(), &temporary_path)?;
validate_restored_binary(
&restored,
options.preserve_entrypoint,
materialization.as_ref(),
)?
};
temporary
.persist(&output_path)
.map_err(|error| Error::io("replace restored output", &output_path, error.error))?;
Ok(RestoreReport {
input: input_path.display().to_string(),
input_sha256: sha256_file(&input_path)?,
output: output_path.display().to_string(),
output_sha256: sha256_file(&output_path)?,
output_size: std::fs::metadata(&output_path)
.map_err(|error| Error::io("inspect restored output", &output_path, error))?
.len(),
module_index: index_path.display().to_string(),
static_config: StaticConfigReport {
header_seed: format!("0x{:08X}", config.header_seed),
container_seed: format!("0x{:08X}", config.container_seed),
aes_key_sha256: sha256_bytes(&config.aes_key),
schedule_offset: format!("0x{:X}", config.schedule_offset),
},
descriptor: DescriptorReport {
command_id: format!("0x{:X}", descriptor.command_id),
flags: format!("0x{:X}", descriptor.flags),
outer_offset: format!("0x{:X}", descriptor.outer_offset),
outer_expected_size: format!("0x{:X}", descriptor.outer_expected_size),
auxiliary_offset: format!("0x{:X}", descriptor.auxiliary_offset),
auxiliary_expected_size: format!("0x{:X}", descriptor.auxiliary_expected_size),
},
primary: primary_stats,
auxiliary: auxiliary_stats,
elf_materialization: materialization,
cleaning,
validation,
elapsed_seconds: started.elapsed().as_secs_f64(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn auxiliary_image(symbol_count: u32) -> Vec<u8> {
let mut data = vec![0_u8; 0x279];
let words = [
0x40_u32,
0,
0x40,
0,
0x278,
1,
0x260,
symbol_count,
0x40,
3,
0x90,
19,
0,
0,
0xb7,
0,
];
for (index, word) in words.into_iter().enumerate() {
data[index * 4..index * 4 + 4].copy_from_slice(&word.to_le_bytes());
}
data
}
#[test]
fn auxiliary_accepts_null_only_dynamic_symbol_table() {
let parsed = AuxiliaryElfImage::parse(&auxiliary_image(1)).expect("null-only dynsym");
assert_eq!(parsed.dynsym_count, 1);
}
#[test]
fn auxiliary_rejects_missing_null_dynamic_symbol() {
let error = AuxiliaryElfImage::parse(&auxiliary_image(0)).expect_err("missing null symbol");
assert!(error.to_string().contains("no null entry"));
}
fn dynamic_section_names(hash_sections: &[&str]) -> Vec<String> {
senbei_elf::DYNAMIC_SECTION_NAMES
.into_iter()
.chain(hash_sections.iter().copied())
.map(str::to_owned)
.collect()
}
#[test]
fn required_sections_accept_each_hash_layout() {
for hashes in [
[".gnu.hash"].as_slice(),
[".hash"].as_slice(),
[".gnu.hash", ".hash"].as_slice(),
] {
let indices = required_section_indices(&dynamic_section_names(hashes))
.expect("valid hash layout");
for hash in hashes {
assert!(indices.contains_key(hash));
}
}
}
#[test]
fn required_sections_reject_missing_symbol_hash() {
let error =
required_section_indices(&dynamic_section_names(&[])).expect_err("missing hash");
assert!(error.to_string().contains("at least one"));
}
#[test]
fn seeded_auxiliary_symbol_permutation_decodes_known_vector() {
assert_eq!(
decode_auxiliary_symbol(120, 268, 0xd4, 0xbd93_5573).expect("decode"),
171
);
}
#[test]
fn protector_tail_relocations_are_identified_before_auxiliary_remap() {
let tail = Some((500, 503));
assert!(relocation_references_symbol_range(
Rela {
offset: 0x1000,
info: (501_u64 << 32) | u64::from(R_AARCH64_GLOB_DAT),
addend: 0,
},
tail,
));
assert!(!relocation_references_symbol_range(
Rela {
offset: 0x1008,
info: (499_u64 << 32) | u64::from(R_AARCH64_GLOB_DAT),
addend: 0,
},
tail,
));
}
#[test]
fn converts_defined_symbol_relocations_without_profile_fingerprint() {
let mut symbols = vec![0_u8; ELF64_SYMBOL_SIZE * 3];
// Symbol 1 is image-defined at value 0x1234, symbol 2 is undefined.
symbols[ELF64_SYMBOL_SIZE + 6..ELF64_SYMBOL_SIZE + 8].copy_from_slice(&1_u16.to_le_bytes());
symbols[ELF64_SYMBOL_SIZE + 8..ELF64_SYMBOL_SIZE + 16]
.copy_from_slice(&0x1234_u64.to_le_bytes());
let mut relocations = vec![
Rela {
offset: 0x2000,
info: (1_u64 << 32) | u64::from(R_AARCH64_GLOB_DAT),
addend: 4,
},
Rela {
offset: 0x2008,
info: (2_u64 << 32) | u64::from(R_AARCH64_ABS64),
addend: 0,
},
];
assert_eq!(
convert_defined_symbol_relocations(&mut relocations, &symbols)
.expect("convert local relocation"),
1
);
assert_eq!(relocations[0].kind(), R_AARCH64_RELATIVE);
assert_eq!(relocations[0].symbol(), 0);
assert_eq!(relocations[0].addend, 0x1238);
assert_eq!(relocations[1].kind(), R_AARCH64_ABS64);
assert_eq!(relocations[1].symbol(), 2);
}
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, helper_offset),
(seed_offset - 0x18, 0x81),
(seed_offset - 0x10, 0x81),
(seed_offset - 8, 1),
(seed_offset - 4, 5),
(seed_offset, 0x4db4_d9ec),
(seed_offset + 4, 0x0a54),
(seed_offset + 8, 0x12008),
(seed_offset + 0x0c, 0),
] {
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
module
}
#[test]
fn discovers_auxiliary_symbol_seed_with_relocated_helper() {
// The configuration ends exactly at the module boundary.
for helper_offset in [0x15ec, 0x164c, 0x2400] {
assert_eq!(
discover_auxiliary_symbol_seed(&auxiliary_seed_module(helper_offset))
.expect("symbol seed"),
0x4db4_d9ec
);
}
}
#[test]
fn auxiliary_symbol_seed_requires_a_valid_decoder_reference() {
for (offset, value) in [
(0x10, 0xd503_201f_u32), // no ADRP
(0x14, 0xf940_4020), // wrong page register
(0x18, 0xb940_0002), // wrong seed register for MUL
(0x1c, 0x1b02_7c21), // multiplication by another value
(0x80, 0xec), // GOT points at a different field
(0x80, 0x1000), // GOT points outside the module
(0xd4, 0), // null helper offset
(0xd4, 0x164d), // unaligned helper offset
(0xe8, 2), // unsupported mode
] {
let mut module = auxiliary_seed_module(0x164c);
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
assert!(discover_auxiliary_symbol_seed(&module).is_err());
}
for length in [0, 0x10, 0x20, 0xff] {
assert!(
discover_auxiliary_symbol_seed(&auxiliary_seed_module(0x164c)[..length]).is_err()
);
}
}
#[test]
fn auxiliary_symbol_seed_trials_all_candidates_and_rejects_ambiguity() {
let mut module = auxiliary_seed_module(0x164c);
module.copy_within(0xd0..0x100, 0x40);
// An unreferenced configuration-shaped decoy must not hide the real one.
assert_eq!(
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() {
let candidates = [
(
20,
PltCallFeatures {
calls: 2427,
zero_arg1: 2363,
..PltCallFeatures::default()
},
),
(
71,
PltCallFeatures {
calls: 58,
x0_to_x2: 1,
..PltCallFeatures::default()
},
),
(
136,
PltCallFeatures {
calls: 76,
w0_zero_branch: 63,
..PltCallFeatures::default()
},
),
(
158,
PltCallFeatures {
calls: 5,
..PltCallFeatures::default()
},
),
(
283,
PltCallFeatures {
calls: 7,
pointer_zero_branch_with_immediate_arg1: 7,
..PltCallFeatures::default()
},
),
];
assert_eq!(
classify_special_plt_slots(&candidates).expect("special imports"),
vec![
(20, "memset"),
(71, "strlen"),
(136, "strcmp"),
(158, "strcpy"),
(283, "strchr"),
]
);
}
}