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, 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, } #[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, } #[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, 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, pub elf_materialization: Option, pub cleaning: CleaningReport, pub validation: ValidationReport, pub elapsed_seconds: f64, } fn map_read_only(file: &File, path: &Path) -> Result { // 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 { // 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> { 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 { 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 { 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 { 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( payload: &[u8], header: &ContainerHeader, config: &Module9bConfig, verbose: bool, mut writer: F, ) -> Result where F: FnMut(u64, &[u8]) -> Result, { 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 { 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, Vec, 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>> { if !symbols.len().is_multiple_of(ELF64_SYMBOL_SIZE) { return invalid("dynamic symbol table is not entry-aligned"); } symbols .as_chunks::() .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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { offset .checked_sub(back.checked_mul(4)?) .and_then(|position| read_u32(image, position).ok()) }; let following = |forward: usize| -> Option { 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> { 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::>(); 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, strings: &mut Vec, name: &str, ) -> Result { 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, fini_addend: u64, } pub(super) fn virtual_address_to_file_offset( layout: &ElfLayout, address: u64, size: u64, ) -> Option { 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 { 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 { 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) { 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, } fn decode_auxiliary_symbol(encoded: u64, row: usize, symbol_count: u32, seed: u32) -> Result { 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, } impl RelocationTable<'_> { fn relocation(self, index: usize) -> Result { 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> { 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 { 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> { 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::>(); 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, ) -> 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::>(); if !missing.is_empty() { return invalid(format!("missing dynamic tags: {}", missing.join(", "))); } Ok(()) } fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: 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)?; 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> { 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::>(); 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::>(); 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 { let table_indices = indices.values().copied().collect::>(); 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 { 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, 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 { 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, 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::>(); 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::>(); 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::>(); 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::>(); 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::>(); 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 { 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 §ion 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(§ion.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(§ion_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(§ion_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::>(); 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 { 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 { 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 { 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 { 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 { 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 { 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"), ] ); } }