From 9083c60d452f5163b78990e6b4a5d52f15a831f7 Mon Sep 17 00:00:00 2001 From: y52 Date: Thu, 17 Sep 2026 22:39:11 +0000 Subject: [PATCH 1/4] fix(android): restore protected runtime ELF state --- senbei-engine/src/android/restore/artifact.rs | 9 +- senbei-engine/src/android/restore/pipeline.rs | 604 ++++++++++++++++-- 2 files changed, 568 insertions(+), 45 deletions(-) diff --git a/senbei-engine/src/android/restore/artifact.rs b/senbei-engine/src/android/restore/artifact.rs index ceee104..47efc34 100644 --- a/senbei-engine/src/android/restore/artifact.rs +++ b/senbei-engine/src/android/restore/artifact.rs @@ -6,6 +6,11 @@ use serde_json::Value; use super::error::{Error, Result, invalid}; const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e]; +const OPTIONAL_IDS: [u32; 1] = [0x98]; + +fn wanted_id(command_id: u32) -> bool { + REQUIRED_IDS.contains(&command_id) || OPTIONAL_IDS.contains(&command_id) +} #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct Artifact { @@ -27,7 +32,7 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result Result Result>> .collect() } +const LEGACY_AUX_SYMBOL_SEED: u32 = 0xbd93_5573; +const LEGACY_SPECIAL_PLT_IMPORTS: [(usize, &str); 5] = [ + (1, "strlen"), + (202, "strcmp"), + (218, "memset"), + (237, "strcpy"), + (267, "strchr"), +]; + +fn is_legacy_import_profile( + source_symbol_count: usize, + hidden: &HiddenSymbolReport, + auxiliary: &AuxiliaryElfImage, +) -> bool { + source_symbol_count == 503 + && hidden.patched_symbols == 497 + && hidden.first_target_index == 3 + && hidden.last_target_index == 499 + && auxiliary.dynsym_count == 0xd4 + && auxiliary.relocation1_count == 0x6e159 + && auxiliary.relocation2_count == 0x143 +} + +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, +} + +fn discover_suppressed_constructors(module: &[u8], source: &[u8]) -> Result> { + const COUNT: usize = 43; + const RECORD_SIZE: usize = 16; + let table_size = 8 + COUNT * RECORD_SIZE; + if module.len() < table_size { + return invalid("constructor control module is too small"); + } + let mut matches = Vec::new(); + for base in (0..=module.len() - table_size).step_by(4) { + if read_u32(module, base)? != COUNT as u32 || read_u32(module, base + 4)? != 0 { + continue; + } + let mut constructors = Vec::with_capacity(COUNT); + let mut valid = true; + for index in 0..COUNT { + let record = base + 8 + index * RECORD_SIZE; + let constructor = read_u32(module, record)? as usize; + let fallback = read_u32(module, record + 4)? as usize; + let original = read_u32(module, record + 8)?; + let reserved = read_u32(module, record + 12)?; + if constructor == 0 + || fallback == 0 + || reserved != 0 + || read_u32(source, constructor).ok() != Some(original) + || read_u32(source, fallback).ok() != Some(AARCH64_RET) + { + valid = false; + break; + } + constructors.push(constructor as u64); + } + if valid + && constructors.windows(2).all(|pair| pair[0] < pair[1]) + && constructors.iter().copied().collect::>().len() == COUNT + { + matches.push(constructors); + } + } + match matches.len() { + 1 => Ok(matches.pop().expect("one constructor table")), + 0 => invalid("constructor control module has no validated suppression table"), + count => invalid(format!( + "constructor control module has {count} validated suppression tables" + )), + } +} + +fn constructor_profile( + module: &[u8], + source: &[u8], + layout: &ElfLayout, + init_array: SectionHeader, + fini_array: SectionHeader, + old_dyn: RelocationTable<'_>, +) -> Result { + let mut init_addends = discover_suppressed_constructors(module, source)?; + let max_suppressed = *init_addends + .last() + .ok_or_else(|| Error::Invalid("constructor table is empty".to_owned()))?; + let init_end = init_array + .address + .checked_add(44 * 8) + .ok_or_else(|| Error::Invalid("init-array range overflow".to_owned()))?; + let existing = old_dyn.collect_where(|relocation| { + relocation.offset >= init_array.address && relocation.offset < init_end + })?; + let extra = existing + .iter() + .filter(|relocation| { + relocation.kind() == R_AARCH64_RELATIVE + && relocation.symbol() == 0 + && relocation.addend >= 0 + }) + .map(|relocation| relocation.addend as u64) + .filter(|&addend| { + addend != layout.entrypoint + && addend > max_suppressed + && addend <= max_suppressed.saturating_add(0x1000) + }) + .collect::>(); + if extra.len() != 1 { + return invalid(format!( + "expected one unsuppressed constructor near the suppression table, found {}", + extra.len() + )); + } + init_addends.push(extra[0]); + 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, @@ -630,12 +792,43 @@ impl Rela { } } +#[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<(usize, u32)>, + remap: Option, } impl RelocationTable<'_> { @@ -652,16 +845,23 @@ impl RelocationTable<'_> { ) .ok_or_else(|| Error::Invalid("relocation offset overflow".to_owned()))?; let mut relocation = Rela::parse(self.data, offset)?; - if let Some((old_symbol_count, auxiliary_symbol_count)) = self.remap { - let symbol = relocation.symbol(); - if symbol >= u64::from(auxiliary_symbol_count) { + 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(old_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 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()))?; @@ -683,16 +883,15 @@ impl RelocationTable<'_> { Ok(()) } - fn append_where(self, output: &mut Vec, predicate: impl Fn(u32) -> bool) -> Result { - let mut count = 0_usize; + 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.kind()) { - relocation.encode(output); - count += 1; + if predicate(relocation) { + result.push(relocation); } } - Ok(count) + Ok(result) } } @@ -855,16 +1054,41 @@ fn metadata_mapping_length( dynsym.size / ELF64_SYMBOL_SIZE as u64, "dynamic symbol count", )?; - let appended_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 legacy_auxiliary_shape = auxiliary.dynsym_count == 0xd4 + && auxiliary.relocation1_count == 0x6e159 + && auxiliary.relocation2_count == 0x143; + let special_count = if legacy_auxiliary_shape { + LEGACY_SPECIAL_PLT_IMPORTS.len() + } else { + 0 + }; + 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 = if legacy_auxiliary_shape { + LEGACY_SPECIAL_PLT_IMPORTS + .iter() + .map(|(_, name)| name.len() + 1) + .sum() + } else { + 0 + }; 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")? @@ -874,6 +1098,7 @@ fn metadata_mapping_length( 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 @@ -981,6 +1206,7 @@ fn materialize_static_elf_tables( layout: &ElfLayout, symbol_patch_data: &[u8], auxiliary_data: &[u8], + constructor_module: Option<&[u8]>, ) -> Result<(ElfLayout, ElfMaterializationReport, u64)> { let names = layout.section_names(source)?; let indices = required_section_indices(&names)?; @@ -993,27 +1219,44 @@ fn materialize_static_elf_tables( let rela_plt = section(".rela.plt"); let dynamic = section(".dynamic"); - let (old_symbols, old_strings, hidden_symbols) = + let (mut old_symbols, old_strings, hidden_symbols) = restore_hidden_symbols(output, dynsym, dynstr, symbol_patch_data)?; - let old_symbol_count = old_symbols.len() / ELF64_SYMBOL_SIZE; - if versym.size != (old_symbol_count * 2) as u64 { + 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 old_versions = slice_u64(output, versym.offset, versym.size)?.to_vec(); + 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 legacy_import_profile = + is_legacy_import_profile(source_symbol_count, &hidden_symbols, &auxiliary); + if legacy_import_profile { + let old_names = dynamic_symbol_names(&old_symbols, &old_strings)?; + let protector_names = ["dlerror", "__stack_chk_guard", "__stack_chk_fail"]; + if old_names.len() != 503 + || old_names[500..] + .iter() + .map(|name| String::from_utf8_lossy(name)) + .ne(protector_names.iter().copied()) + { + return invalid("legacy import profile has unexpected protector symbols"); + } + old_symbols.truncate(500 * ELF64_SYMBOL_SIZE); + old_versions.truncate(500 * 2); + } + 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 appended_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()))?; - let mut appended_symbols = Vec::with_capacity(appended_count * ELF64_SYMBOL_SIZE); + 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)?; @@ -1035,14 +1278,29 @@ fn materialize_static_elf_tables( 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()); + 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() + appended_count * 2); + 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..appended_count { + for _ in 0..auxiliary_appended_count { merged_versions.extend_from_slice(&VER_NDX_GLOBAL.to_le_bytes()); } + let mut special_plt_symbols = Vec::new(); + if legacy_import_profile { + let custom_names = dynamic_symbol_names(&appended_symbols, &merged_strings)?; + if custom_names.get(170).map(Vec::as_slice) != Some(b"malloc") { + return invalid("legacy auxiliary symbol permutation anchor is missing"); + } + for &(slot, name) in &LEGACY_SPECIAL_PLT_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") @@ -1078,13 +1336,21 @@ fn materialize_static_elf_tables( data: auxiliary_data, offset: auxiliary.relocation1_offset as usize, count: auxiliary.relocation1_count as usize, - remap: Some((old_symbol_count, auxiliary.dynsym_count)), + remap: Some(RelocationRemap { + base_symbol_count: old_symbol_count, + auxiliary_symbol_count: auxiliary.dynsym_count, + permutation_seed: legacy_import_profile.then_some(LEGACY_AUX_SYMBOL_SEED), + }), }; let auxiliary2 = RelocationTable { data: auxiliary_data, offset: auxiliary.relocation2_offset as usize, count: auxiliary.relocation2_count as usize, - remap: Some((old_symbol_count, auxiliary.dynsym_count)), + remap: Some(RelocationRemap { + base_symbol_count: old_symbol_count, + auxiliary_symbol_count: auxiliary.dynsym_count, + permutation_seed: legacy_import_profile.then_some(LEGACY_AUX_SYMBOL_SEED), + }), }; old_dyn.validate( &[R_AARCH64_RELATIVE, R_AARCH64_GLOB_DAT, R_AARCH64_ABS64], @@ -1100,28 +1366,235 @@ fn materialize_static_elf_tables( "auxiliary relocation table 2", )?; - let estimated_dyn = (old_dyn.count + auxiliary1.count + auxiliary2.count) - .checked_mul(ELF64_RELA_SIZE) - .ok_or_else(|| Error::Invalid("merged .rela.dyn capacity overflow".to_owned()))?; - let mut merged_rela_dyn = Vec::with_capacity(estimated_dyn); - let mut relative_count = 0_usize; - for table in [old_dyn, auxiliary1, auxiliary2] { - relative_count += - table.append_where(&mut merged_rela_dyn, |kind| kind == R_AARCH64_RELATIVE)?; + let constructors = if legacy_import_profile { + let module = constructor_module.ok_or_else(|| { + Error::Invalid("legacy import profile lacks constructor control module 0x98".to_owned()) + })?; + let init_index = names + .iter() + .position(|name| name == ".init_array") + .ok_or_else(|| Error::Invalid("legacy image lacks .init_array".to_owned()))?; + let fini_index = names + .iter() + .position(|name| name == ".fini_array") + .ok_or_else(|| Error::Invalid("legacy image lacks .fini_array".to_owned()))?; + let init_array = layout.section_headers[init_index]; + let fini_array = layout.section_headers[fini_index]; + let profile = constructor_profile(module, output, layout, init_array, fini_array, old_dyn)?; + if profile.init_addends.len() != 44 { + return invalid(format!( + "constructor profile produced {} init entries, expected 44", + profile.init_addends.len() + )); + } + Some((profile, init_index, fini_index, init_array, fini_array)) + } else { + None + }; + + let (dyn_relocations, plt_relocations, relative_count) = if legacy_import_profile { + let mut dynamic = old_dyn.collect_where(|_| true)?; + dynamic.extend(auxiliary1.collect_where(|_| true)?); + 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 = dynamic + .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 dynamic[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 = dynamic + .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 dynamic[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 converted_local = 0_usize; + for relocation in &mut dynamic { + 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(&merged_symbols, symbol)?; + if section_index != 0 { + 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_local += 1; + } + } + dynamic + .sort_by_key(|relocation| (relocation.kind() != R_AARCH64_RELATIVE, relocation.offset)); + let relative_count = dynamic + .iter() + .take_while(|relocation| relocation.kind() == R_AARCH64_RELATIVE) + .count(); + if dynamic.len() != 450_990 || relative_count != 450_980 || converted_local != 37 { + return invalid(format!( + "legacy dynamic relocation profile mismatch: total={}, relative={}, local={converted_local}", + dynamic.len(), + relative_count + )); + } + + let got_plt_index = names + .iter() + .position(|name| name == ".got.plt") + .ok_or_else(|| Error::Invalid("legacy import profile 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("legacy .got.plt has an invalid slot layout"); + } + let slot_count = usize_from_u64((got_plt.size - 0x18) / 8, ".got.plt slot count")?; + if slot_count != 366 { + return invalid(format!( + "legacy .got.plt has {slot_count} dynamic slots, expected 366" + )); + } + let first_slot = got_plt + .address + .checked_add(0x18) + .ok_or_else(|| Error::Invalid(".got.plt slot address overflow".to_owned()))?; + let end = got_plt + .address + .checked_add(got_plt.size) + .ok_or_else(|| Error::Invalid(".got.plt end overflow".to_owned()))?; + let mut plt = old_plt.collect_where(|relocation| { + relocation.offset >= first_slot && relocation.offset < end + })?; + if plt.len() != 38 { + return invalid(format!( + "legacy protected .rela.plt has {} retained slots, expected 38", + plt.len() + )); + } + plt.extend(auxiliary2.collect_where(|_| true)?); + 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.push(Rela { + offset, + info: (symbol << 32) | u64::from(R_AARCH64_JUMP_SLOT), + addend: 0, + }); + } + plt.sort_by_key(|relocation| relocation.offset); + if plt.len() != slot_count { + return invalid(format!( + "legacy PLT restoration produced {} slots, expected {slot_count}", + plt.len() + )); + } + for (slot, relocation) in plt.iter().enumerate() { + let expected = first_slot + (slot as u64) * 8; + if relocation.offset != expected { + return invalid(format!( + "legacy PLT slot {slot} targets 0x{:x}, expected 0x{expected:x}", + relocation.offset + )); + } + } + let jump_slots = plt + .iter() + .filter(|relocation| relocation.kind() == R_AARCH64_JUMP_SLOT) + .count(); + let relatives = plt + .iter() + .filter(|relocation| relocation.kind() == R_AARCH64_RELATIVE) + .count(); + if (jump_slots, relatives) != (247, 119) { + return invalid(format!( + "legacy PLT type profile mismatch: {jump_slots} JUMP_SLOT / {relatives} RELATIVE" + )); + } + (dynamic, plt, relative_count) + } else { + let mut dynamic = Vec::new(); + for table in [old_dyn, auxiliary1, auxiliary2] { + dynamic + .extend(table.collect_where(|relocation| relocation.kind() == R_AARCH64_RELATIVE)?); + } + let relative_count = dynamic.len(); + for table in [old_dyn, auxiliary1] { + dynamic + .extend(table.collect_where(|relocation| relocation.kind() != R_AARCH64_RELATIVE)?); + } + let mut plt = old_plt.collect_where(|_| true)?; + plt.extend( + auxiliary2.collect_where(|relocation| relocation.kind() == R_AARCH64_JUMP_SLOT)?, + ); + (dynamic, plt, relative_count) + }; + 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); } - for table in [old_dyn, auxiliary1] { - table.append_where(&mut merged_rela_dyn, |kind| kind != R_AARCH64_RELATIVE)?; + 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 mut merged_rela_plt = Vec::with_capacity( - (old_plt.count + auxiliary2.count) - .checked_mul(ELF64_RELA_SIZE) - .ok_or_else(|| Error::Invalid("merged .rela.plt capacity overflow".to_owned()))?, - ); - old_plt.append_where(&mut merged_rela_plt, |_| true)?; - auxiliary2.append_where(&mut merged_rela_plt, |kind| kind == R_AARCH64_JUMP_SLOT)?; 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, @@ -1244,6 +1717,10 @@ fn materialize_static_elf_tables( updated.size = table.data.len() as u64; updated_sections[index] = updated; } + if let Some((_, init_index, fini_index, _, _)) = &constructors { + updated_sections[*init_index].size = 44 * 8; + updated_sections[*fini_index].size = 8; + } let section_address = |name: &'static str| -> u64 { updated_sections[indices[name]].address }; let mut dynamic_values = BTreeMap::from([ @@ -1257,6 +1734,10 @@ fn materialize_static_elf_tables( (DT_VERSYM, section_address(".gnu.version")), (DT_VERNEED, section_address(".gnu.version_r")), ]); + if constructors.is_some() { + dynamic_values.insert(DT_INIT_ARRAYSZ, 44 * 8); + dynamic_values.insert(DT_FINI_ARRAYSZ, 8); + } if indices.contains_key(".gnu.hash") { dynamic_values.insert(DT_GNU_HASH, section_address(".gnu.hash")); } @@ -1495,6 +1976,10 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result { let artifacts = load_artifacts(&index_path)?; let module = read_file(&artifacts[&0x9b].path)?; let symbol_patch_data = read_file(&artifacts[&0x9e].path)?; + let constructor_module = artifacts + .get(&0x98) + .map(|artifact| read_file(&artifact.path)) + .transpose()?; let config = Module9bConfig::parse(&module)?; let input_file = File::open(&input_path) @@ -1645,6 +2130,7 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result { &layout, &symbol_patch_data, &decoded, + constructor_module.as_deref(), )?; restored_layout = new_layout; materialization = Some(report); @@ -1782,4 +2268,36 @@ mod tests { required_section_indices(&dynamic_section_names(&[])).expect_err("missing hash"); assert!(error.to_string().contains("at least one")); } + #[test] + fn legacy_symbol_permutation_decodes_malloc_anchor() { + assert_eq!( + decode_auxiliary_symbol(120, 268, 0xd4, LEGACY_AUX_SYMBOL_SEED).expect("decode"), + 171 + ); + } + + #[test] + fn discovers_validated_constructor_table() { + const COUNT: usize = 43; + let mut source = vec![0_u8; 0x1000]; + let mut module = vec![0_u8; 0x1000]; + let table = 0x40; + module[table..table + 4].copy_from_slice(&(COUNT as u32).to_le_bytes()); + for index in 0..COUNT { + let constructor = 0x100 + index * 4; + let fallback = 0x500 + index * 4; + let instruction = 0xa000_0000_u32 + index as u32; + source[constructor..constructor + 4].copy_from_slice(&instruction.to_le_bytes()); + source[fallback..fallback + 4].copy_from_slice(&AARCH64_RET.to_le_bytes()); + let record = table + 8 + index * 16; + module[record..record + 4].copy_from_slice(&(constructor as u32).to_le_bytes()); + module[record + 4..record + 8].copy_from_slice(&(fallback as u32).to_le_bytes()); + module[record + 8..record + 12].copy_from_slice(&instruction.to_le_bytes()); + } + let constructors = + discover_suppressed_constructors(&module, &source).expect("constructor table"); + assert_eq!(constructors.len(), COUNT); + assert_eq!(constructors[0], 0x100); + assert_eq!(constructors[COUNT - 1], 0x100 + ((COUNT - 1) * 4) as u64); + } } From 53f31d22102829e97b0bebbbb7a4c8c0abdbc006 Mon Sep 17 00:00:00 2001 From: y52 Date: Fri, 18 Sep 2026 20:55:29 +0000 Subject: [PATCH 2/4] fix(android): restore newer runtime protection profiles --- senbei-engine/src/android/restore/artifact.rs | 2 +- senbei-engine/src/android/restore/pipeline.rs | 952 +++++++++++++----- 2 files changed, 708 insertions(+), 246 deletions(-) diff --git a/senbei-engine/src/android/restore/artifact.rs b/senbei-engine/src/android/restore/artifact.rs index 47efc34..2e6c51d 100644 --- a/senbei-engine/src/android/restore/artifact.rs +++ b/senbei-engine/src/android/restore/artifact.rs @@ -6,7 +6,7 @@ use serde_json::Value; use super::error::{Error, Result, invalid}; const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e]; -const OPTIONAL_IDS: [u32; 1] = [0x98]; +const OPTIONAL_IDS: [u32; 2] = [0x96, 0x98]; fn wanted_id(command_id: u32) -> bool { REQUIRED_IDS.contains(&command_id) || OPTIONAL_IDS.contains(&command_id) diff --git a/senbei-engine/src/android/restore/pipeline.rs b/senbei-engine/src/android/restore/pipeline.rs index 3c3c62f..891f7e3 100644 --- a/senbei-engine/src/android/restore/pipeline.rs +++ b/senbei-engine/src/android/restore/pipeline.rs @@ -602,14 +602,206 @@ fn dynamic_symbol_names(symbols: &[u8], strings: &[u8]) -> Result>> .collect() } -const LEGACY_AUX_SYMBOL_SEED: u32 = 0xbd93_5573; -const LEGACY_SPECIAL_PLT_IMPORTS: [(usize, &str); 5] = [ - (1, "strlen"), - (202, "strcmp"), - (218, "memset"), - (237, "strcpy"), - (267, "strchr"), -]; +fn discover_auxiliary_symbol_seed(module: &[u8]) -> Result { + const PREFIX0: u32 = 0x8f; + const PREFIX1: u32 = 0x15ec; + const TYPE_ID: u32 = 0x81; + const MODE0: u32 = 1; + const MODE1: u32 = 5; + const SUFFIX0: u32 = 0x0a54; + const SUFFIX1: u32 = 0x12008; + + let 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)? == PREFIX1 + && 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 + { + 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 + ) +} + +fn plt_call_features(image: &[u8], layout: &ElfLayout, target: u64) -> Result { + 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) != Some(target) { + 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 is_legacy_import_profile( source_symbol_count: usize, @@ -660,47 +852,86 @@ struct ConstructorProfile { fini_addend: u64, } -fn discover_suppressed_constructors(module: &[u8], source: &[u8]) -> Result> { - const COUNT: usize = 43; - const RECORD_SIZE: usize = 16; - let table_size = 8 + COUNT * RECORD_SIZE; - if module.len() < table_size { - return invalid("constructor control module is too small"); +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 +} + +fn discover_suppressed_constructors( + module: &[u8], + source: &[u8], + layout: &ElfLayout, + max_count: usize, +) -> Result>> { + const RECORD_SIZE: usize = 16; + const MIN_COUNT: usize = 2; + const MAX_COUNT: usize = 256; let mut matches = Vec::new(); - for base in (0..=module.len() - table_size).step_by(4) { - if read_u32(module, base)? != COUNT as u32 || read_u32(module, base + 4)? != 0 { + if module.len() < 8 { + return Ok(None); + } + for base in (0..=module.len() - 8).step_by(4) { + let count = read_u32(module, base)? as usize; + if !(MIN_COUNT..=MAX_COUNT.min(max_count)).contains(&count) + || read_u32(module, base + 4)? != 0 + { continue; } - let mut constructors = Vec::with_capacity(COUNT); + let table_size = 8_usize + .checked_add( + count + .checked_mul(RECORD_SIZE) + .ok_or_else(|| Error::Invalid("constructor table size overflow".to_owned()))?, + ) + .ok_or_else(|| Error::Invalid("constructor table size overflow".to_owned()))?; + if base + .checked_add(table_size) + .is_none_or(|end| end > module.len()) + { + continue; + } + let mut constructors = Vec::with_capacity(count); let mut valid = true; - for index in 0..COUNT { + for index in 0..count { let record = base + 8 + index * RECORD_SIZE; - let constructor = read_u32(module, record)? as usize; - let fallback = read_u32(module, record + 4)? as usize; + let constructor = u64::from(read_u32(module, record)?); + let fallback = u64::from(read_u32(module, record + 4)?); let original = read_u32(module, record + 8)?; let reserved = read_u32(module, record + 12)?; + let constructor_offset = virtual_address_to_file_offset(layout, constructor, 4); + let fallback_offset = virtual_address_to_file_offset(layout, fallback, 4); if constructor == 0 || fallback == 0 || reserved != 0 - || read_u32(source, constructor).ok() != Some(original) - || read_u32(source, fallback).ok() != Some(AARCH64_RET) + || constructor_offset.and_then(|offset| read_u32(source, offset).ok()) + != Some(original) + || fallback_offset.and_then(|offset| read_u32(source, offset).ok()) + != Some(AARCH64_RET) { valid = false; break; } - constructors.push(constructor as u64); + constructors.push(constructor); } if valid && constructors.windows(2).all(|pair| pair[0] < pair[1]) - && constructors.iter().copied().collect::>().len() == COUNT + && constructors.iter().copied().collect::>().len() == count { matches.push(constructors); } } match matches.len() { - 1 => Ok(matches.pop().expect("one constructor table")), - 0 => invalid("constructor control module has no validated suppression table"), + 1 => Ok(matches.pop()), + 0 => Ok(None), count => invalid(format!( "constructor control module has {count} validated suppression tables" )), @@ -714,14 +945,22 @@ fn constructor_profile( init_array: SectionHeader, fini_array: SectionHeader, old_dyn: RelocationTable<'_>, -) -> Result { - let mut init_addends = discover_suppressed_constructors(module, source)?; +) -> 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 Some(mut init_addends) = + discover_suppressed_constructors(module, source, layout, init_count)? + else { + return Ok(None); + }; let max_suppressed = *init_addends .last() .ok_or_else(|| Error::Invalid("constructor table is empty".to_owned()))?; let init_end = init_array .address - .checked_add(44 * 8) + .checked_add(init_array.size) .ok_or_else(|| Error::Invalid("init-array range overflow".to_owned()))?; let existing = old_dyn.collect_where(|relocation| { relocation.offset >= init_array.address && relocation.offset < init_end @@ -747,6 +986,12 @@ fn constructor_profile( )); } init_addends.push(extra[0]); + if init_addends.len() > init_count { + return invalid(format!( + "constructor profile produced {} entries for {init_count} init slots", + init_addends.len() + )); + } let fini = old_dyn.collect_where(|relocation| relocation.offset == fini_array.address)?; if fini.len() != 1 || fini[0].kind() != R_AARCH64_RELATIVE @@ -755,10 +1000,10 @@ fn constructor_profile( { return invalid("fini-array relocation is not a single RELATIVE record"); } - Ok(ConstructorProfile { + Ok(Some(ConstructorProfile { init_addends, fini_addend: fini[0].addend as u64, - }) + })) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -895,6 +1140,89 @@ impl RelocationTable<'_> { } } +fn discover_special_plt_imports( + image: &[u8], + layout: &ElfLayout, + names: &[String], + old_plt: RelocationTable<'_>, + auxiliary_plt: RelocationTable<'_>, +) -> 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.collect_where(|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, @@ -1064,28 +1392,17 @@ fn metadata_mapping_length( // 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 legacy_auxiliary_shape = auxiliary.dynsym_count == 0xd4 - && auxiliary.relocation1_count == 0x6e159 - && auxiliary.relocation2_count == 0x143; - let special_count = if legacy_auxiliary_shape { - LEGACY_SPECIAL_PLT_IMPORTS.len() - } else { - 0 - }; + 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 = if legacy_auxiliary_shape { - LEGACY_SPECIAL_PLT_IMPORTS - .iter() - .map(|(_, name)| name.len() + 1) - .sum() - } else { - 0 - }; + 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)) @@ -1206,6 +1523,7 @@ fn materialize_static_elf_tables( layout: &ElfLayout, symbol_patch_data: &[u8], auxiliary_data: &[u8], + auxiliary_symbol_seed: Option, constructor_module: Option<&[u8]>, ) -> Result<(ElfLayout, ElfMaterializationReport, u64)> { let names = layout.section_names(source)?; @@ -1231,19 +1549,25 @@ fn materialize_static_elf_tables( let auxiliary = AuxiliaryElfImage::parse(auxiliary_data)?; let legacy_import_profile = is_legacy_import_profile(source_symbol_count, &hidden_symbols, &auxiliary); - if legacy_import_profile { - let old_names = dynamic_symbol_names(&old_symbols, &old_strings)?; - let protector_names = ["dlerror", "__stack_chk_guard", "__stack_chk_fail"]; - if old_names.len() != 503 - || old_names[500..] + 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()))?; + if protector_tail_start < source_symbol_count { + let tail = &old_names[protector_tail_start..]; + if tail.iter().all(|name| { + protector_names .iter() - .map(|name| String::from_utf8_lossy(name)) - .ne(protector_names.iter().copied()) - { + .any(|protector| name.as_slice() == protector.as_bytes()) + }) { + old_symbols.truncate(protector_tail_start * ELF64_SYMBOL_SIZE); + old_versions.truncate(protector_tail_start * 2); + } else if legacy_import_profile { return invalid("legacy import profile has unexpected protector symbols"); } - old_symbols.truncate(500 * ELF64_SYMBOL_SIZE); - old_versions.truncate(500 * 2); + } + if legacy_import_profile && old_symbols.len() / ELF64_SYMBOL_SIZE != 500 { + return invalid("legacy import profile has unexpected dynamic symbol count"); } let old_symbol_count = old_symbols.len() / ELF64_SYMBOL_SIZE; let auxiliary_strings = slice( @@ -1287,32 +1611,6 @@ fn materialize_static_elf_tables( for _ in 0..auxiliary_appended_count { merged_versions.extend_from_slice(&VER_NDX_GLOBAL.to_le_bytes()); } - let mut special_plt_symbols = Vec::new(); - if legacy_import_profile { - let custom_names = dynamic_symbol_names(&appended_symbols, &merged_strings)?; - if custom_names.get(170).map(Vec::as_slice) != Some(b"malloc") { - return invalid("legacy auxiliary symbol permutation anchor is missing"); - } - for &(slot, name) in &LEGACY_SPECIAL_PLT_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(); - 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 @@ -1339,7 +1637,7 @@ fn materialize_static_elf_tables( remap: Some(RelocationRemap { base_symbol_count: old_symbol_count, auxiliary_symbol_count: auxiliary.dynsym_count, - permutation_seed: legacy_import_profile.then_some(LEGACY_AUX_SYMBOL_SEED), + permutation_seed: auxiliary_symbol_seed, }), }; let auxiliary2 = RelocationTable { @@ -1349,7 +1647,7 @@ fn materialize_static_elf_tables( remap: Some(RelocationRemap { base_symbol_count: old_symbol_count, auxiliary_symbol_count: auxiliary.dynsym_count, - permutation_seed: legacy_import_profile.then_some(LEGACY_AUX_SYMBOL_SEED), + permutation_seed: auxiliary_symbol_seed, }), }; old_dyn.validate( @@ -1366,84 +1664,159 @@ fn materialize_static_elf_tables( "auxiliary relocation table 2", )?; - let constructors = if legacy_import_profile { - let module = constructor_module.ok_or_else(|| { - Error::Invalid("legacy import profile lacks constructor control module 0x98".to_owned()) - })?; - let init_index = names + if legacy_import_profile { + let custom_names = dynamic_symbol_names(&appended_symbols, &merged_strings)?; + if custom_names.get(170).map(Vec::as_slice) != Some(b"malloc") { + return invalid("legacy auxiliary symbol permutation anchor is missing"); + } + } + let special_imports = + discover_special_plt_imports(output, layout, &names, old_plt, auxiliary2)?; + let existing_names = dynamic_symbol_names(&merged_symbols, &merged_strings)?; + for &name in &SPECIAL_PLT_IMPORT_NAMES { + if existing_names .iter() - .position(|name| name == ".init_array") - .ok_or_else(|| Error::Invalid("legacy image lacks .init_array".to_owned()))?; - let fini_index = names - .iter() - .position(|name| name == ".fini_array") - .ok_or_else(|| Error::Invalid("legacy image lacks .fini_array".to_owned()))?; - let init_array = layout.section_headers[init_index]; - let fini_array = layout.section_headers[fini_index]; - let profile = constructor_profile(module, output, layout, init_array, fini_array, old_dyn)?; - if profile.init_addends.len() != 44 { + .any(|existing| existing.as_slice() == name.as_bytes()) + && special_imports.iter().any(|(_, special)| *special == name) + { return invalid(format!( - "constructor profile produced {} init entries, expected 44", - profile.init_addends.len() + "special PLT import {name} already exists in the dynamic symbol table" )); } - Some((profile, init_index, fini_index, init_array, fini_array)) - } else { - None + } + 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"); + let fini_index = names.iter().position(|name| name == ".fini_array"); + match (init_index, fini_index) { + (Some(init_index), Some(fini_index)) => { + let init_array = layout.section_headers[init_index]; + let fini_array = layout.section_headers[fini_index]; + match constructor_profile( + module, output, layout, init_array, fini_array, old_dyn, + )? { + Some(profile) => { + Some((profile, init_index, fini_index, init_array, fini_array)) + } + None if legacy_import_profile => { + return invalid( + "legacy import profile has no validated constructor table", + ); + } + None => None, + } + } + _ if legacy_import_profile => { + return invalid("legacy image lacks init/fini array sections"); + } + _ => None, + } + } + None if legacy_import_profile => { + return invalid("legacy import profile lacks constructor control module 0x98"); + } + None => None, }; - let (dyn_relocations, plt_relocations, relative_count) = if legacy_import_profile { - let mut dynamic = old_dyn.collect_where(|_| true)?; - dynamic.extend(auxiliary1.collect_where(|_| true)?); - 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 = dynamic - .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 dynamic[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 = dynamic - .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 dynamic[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 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 mut converted_local = 0_usize; - for relocation in &mut dynamic { + let old_plt_outside = old_plt.collect_where(|relocation| { + relocation.offset < first_slot || relocation.offset >= slot_end + })?; + 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.collect_where(|relocation| { + relocation.offset >= first_slot && relocation.offset < slot_end + })?; + plt_relocations.extend(auxiliary2.collect_where(|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.collect_where(|_| true)?; + dyn_relocations.extend(auxiliary1.collect_where(|_| true)?); + let auxiliary2_dynamic = auxiliary2.collect_where(|relocation| { + relocation.offset < first_slot || relocation.offset >= slot_end + })?; + 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); + + let mut converted_local = 0_usize; + if legacy_import_profile { + for relocation in &mut dyn_relocations { if relocation.kind() == R_AARCH64_RELATIVE || relocation.symbol() == 0 { continue; } @@ -1459,85 +1832,32 @@ fn materialize_static_elf_tables( converted_local += 1; } } - dynamic - .sort_by_key(|relocation| (relocation.kind() != R_AARCH64_RELATIVE, relocation.offset)); - let relative_count = dynamic - .iter() - .take_while(|relocation| relocation.kind() == R_AARCH64_RELATIVE) - .count(); - if dynamic.len() != 450_990 || relative_count != 450_980 || converted_local != 37 { + } + 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 legacy_import_profile { + if dyn_relocations.len() != 450_990 || relative_count != 450_980 || converted_local != 37 { return invalid(format!( "legacy dynamic relocation profile mismatch: total={}, relative={}, local={converted_local}", - dynamic.len(), + dyn_relocations.len(), relative_count )); } - - let got_plt_index = names - .iter() - .position(|name| name == ".got.plt") - .ok_or_else(|| Error::Invalid("legacy import profile 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("legacy .got.plt has an invalid slot layout"); - } - let slot_count = usize_from_u64((got_plt.size - 0x18) / 8, ".got.plt slot count")?; if slot_count != 366 { return invalid(format!( "legacy .got.plt has {slot_count} dynamic slots, expected 366" )); } - let first_slot = got_plt - .address - .checked_add(0x18) - .ok_or_else(|| Error::Invalid(".got.plt slot address overflow".to_owned()))?; - let end = got_plt - .address - .checked_add(got_plt.size) - .ok_or_else(|| Error::Invalid(".got.plt end overflow".to_owned()))?; - let mut plt = old_plt.collect_where(|relocation| { - relocation.offset >= first_slot && relocation.offset < end - })?; - if plt.len() != 38 { - return invalid(format!( - "legacy protected .rela.plt has {} retained slots, expected 38", - plt.len() - )); - } - plt.extend(auxiliary2.collect_where(|_| true)?); - 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.push(Rela { - offset, - info: (symbol << 32) | u64::from(R_AARCH64_JUMP_SLOT), - addend: 0, - }); - } - plt.sort_by_key(|relocation| relocation.offset); - if plt.len() != slot_count { - return invalid(format!( - "legacy PLT restoration produced {} slots, expected {slot_count}", - plt.len() - )); - } - for (slot, relocation) in plt.iter().enumerate() { - let expected = first_slot + (slot as u64) * 8; - if relocation.offset != expected { - return invalid(format!( - "legacy PLT slot {slot} targets 0x{:x}, expected 0x{expected:x}", - relocation.offset - )); - } - } - let jump_slots = plt + let jump_slots = plt_relocations .iter() .filter(|relocation| relocation.kind() == R_AARCH64_JUMP_SLOT) .count(); - let relatives = plt + let relatives = plt_relocations .iter() .filter(|relocation| relocation.kind() == R_AARCH64_RELATIVE) .count(); @@ -1546,24 +1866,53 @@ fn materialize_static_elf_tables( "legacy PLT type profile mismatch: {jump_slots} JUMP_SLOT / {relatives} RELATIVE" )); } - (dynamic, plt, relative_count) - } else { - let mut dynamic = Vec::new(); - for table in [old_dyn, auxiliary1, auxiliary2] { - dynamic - .extend(table.collect_where(|relocation| relocation.kind() == R_AARCH64_RELATIVE)?); + } + + 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 relative_count = dynamic.len(); - for table in [old_dyn, auxiliary1] { - dynamic - .extend(table.collect_where(|relocation| relocation.kind() != R_AARCH64_RELATIVE)?); + 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 mut plt = old_plt.collect_where(|_| true)?; - plt.extend( - auxiliary2.collect_where(|relocation| relocation.kind() == R_AARCH64_JUMP_SLOT)?, - ); - (dynamic, plt, relative_count) - }; + 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); @@ -1717,8 +2066,12 @@ fn materialize_static_elf_tables( updated.size = table.data.len() as u64; updated_sections[index] = updated; } - if let Some((_, init_index, fini_index, _, _)) = &constructors { - updated_sections[*init_index].size = 44 * 8; + 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; } @@ -1734,8 +2087,12 @@ fn materialize_static_elf_tables( (DT_VERSYM, section_address(".gnu.version")), (DT_VERNEED, section_address(".gnu.version_r")), ]); - if constructors.is_some() { - dynamic_values.insert(DT_INIT_ARRAYSZ, 44 * 8); + 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") { @@ -1976,6 +2333,13 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result { 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| read_file(&artifact.path)) @@ -2130,6 +2494,7 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result { &layout, &symbol_patch_data, &decoded, + auxiliary_symbol_seed, constructor_module.as_deref(), )?; restored_layout = new_layout; @@ -2271,11 +2636,90 @@ mod tests { #[test] fn legacy_symbol_permutation_decodes_malloc_anchor() { assert_eq!( - decode_auxiliary_symbol(120, 268, 0xd4, LEGACY_AUX_SYMBOL_SEED).expect("decode"), + decode_auxiliary_symbol(120, 268, 0xd4, 0xbd93_5573).expect("decode"), 171 ); } + #[test] + fn discovers_auxiliary_symbol_permutation_seed() { + let mut module = vec![0_u8; 0x80]; + let seed_offset = 0x40; + for (offset, value) in [ + (seed_offset - 0x20, 0x8f_u32), + (seed_offset - 0x1c, 0x15ec), + (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()); + } + assert_eq!( + discover_auxiliary_symbol_seed(&module).expect("symbol seed"), + 0x4db4_d9ec + ); + } + + #[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"), + ] + ); + } + #[test] fn discovers_validated_constructor_table() { const COUNT: usize = 43; @@ -2294,8 +2738,26 @@ mod tests { module[record + 4..record + 8].copy_from_slice(&(fallback as u32).to_le_bytes()); module[record + 8..record + 12].copy_from_slice(&instruction.to_le_bytes()); } - let constructors = - discover_suppressed_constructors(&module, &source).expect("constructor table"); + let layout = ElfLayout { + entrypoint: 0, + program_header_offset: 0, + program_header_size: 0x38, + program_header_count: 1, + program_headers: vec![LoadSegment { + offset: 0, + virtual_address: 0, + file_size: source.len() as u64, + memory_size: source.len() as u64, + flags: PF_R, + alignment: 0x1000, + }], + section_headers: Vec::new(), + section_name_index: 0, + private_section_index: 0, + }; + let constructors = discover_suppressed_constructors(&module, &source, &layout, COUNT) + .expect("constructor table") + .expect("validated constructor table"); assert_eq!(constructors.len(), COUNT); assert_eq!(constructors[0], 0x100); assert_eq!(constructors[COUNT - 1], 0x100 + ((COUNT - 1) * 4) as u64); From 921cd4bfb49122540f7d61d118f4ab99cfb2db98 Mon Sep 17 00:00:00 2001 From: y52 Date: Sat, 19 Sep 2026 06:11:39 +0000 Subject: [PATCH 3/4] fix(android): address runtime restoration review gaps --- senbei-engine/src/android/restore/pipeline.rs | 290 ++++++++++++------ 1 file changed, 195 insertions(+), 95 deletions(-) diff --git a/senbei-engine/src/android/restore/pipeline.rs b/senbei-engine/src/android/restore/pipeline.rs index 891f7e3..7084788 100644 --- a/senbei-engine/src/android/restore/pipeline.rs +++ b/senbei-engine/src/android/restore/pipeline.rs @@ -1140,12 +1140,41 @@ impl RelocationTable<'_> { } } +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: RelocationTable<'_>, - auxiliary_plt: RelocationTable<'_>, + old_plt: &[Rela], + auxiliary_plt: &[Rela], ) -> Result> { let plt_index = names .iter() @@ -1170,9 +1199,10 @@ fn discover_special_plt_imports( .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.collect_where(|relocation| { - relocation.offset >= first_slot && relocation.offset < slot_end - })? { + 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"); } @@ -1553,7 +1583,7 @@ fn materialize_static_elf_tables( 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()))?; - if protector_tail_start < source_symbol_count { + 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 @@ -1562,13 +1592,26 @@ fn materialize_static_elf_tables( }) { old_symbols.truncate(protector_tail_start * ELF64_SYMBOL_SIZE); old_versions.truncate(protector_tail_start * 2); - } else if legacy_import_profile { - return invalid("legacy import profile has unexpected protector symbols"); + 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 { + debug_assert!( + !legacy_import_profile, + "legacy protector symbol tail changed" + ); + None } - } - if legacy_import_profile && old_symbols.len() / ELF64_SYMBOL_SIZE != 500 { - return invalid("legacy import profile has unexpected dynamic symbol count"); - } + } else { + None + }; + debug_assert!( + !legacy_import_profile || old_symbols.len() / ELF64_SYMBOL_SIZE == 500, + "legacy dynamic symbol count changed" + ); let old_symbol_count = old_symbols.len() / ELF64_SYMBOL_SIZE; let auxiliary_strings = slice( auxiliary_data, @@ -1664,14 +1707,30 @@ fn materialize_static_elf_tables( "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)?; + if legacy_import_profile { let custom_names = dynamic_symbol_names(&appended_symbols, &merged_strings)?; - if custom_names.get(170).map(Vec::as_slice) != Some(b"malloc") { - return invalid("legacy auxiliary symbol permutation anchor is missing"); - } + debug_assert_eq!( + custom_names.get(170).map(Vec::as_slice), + Some(b"malloc".as_slice()), + "legacy auxiliary permutation anchor changed" + ); } - let special_imports = - discover_special_plt_imports(output, layout, &names, old_plt, auxiliary2)?; + 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 @@ -1706,34 +1765,35 @@ fn materialize_static_elf_tables( let constructors = match constructor_module { Some(module) => { - let init_index = names.iter().position(|name| name == ".init_array"); - let fini_index = names.iter().position(|name| name == ".fini_array"); - match (init_index, fini_index) { - (Some(init_index), Some(fini_index)) => { - let init_array = layout.section_headers[init_index]; - let fini_array = layout.section_headers[fini_index]; - match constructor_profile( - module, output, layout, init_array, fini_array, old_dyn, - )? { - Some(profile) => { - Some((profile, init_index, fini_index, init_array, fini_array)) - } - None if legacy_import_profile => { - return invalid( - "legacy import profile has no validated constructor table", - ); - } - None => None, - } - } - _ if legacy_import_profile => { - return invalid("legacy image lacks init/fini array sections"); - } - _ => None, - } - } - None if legacy_import_profile => { - return invalid("legacy import profile lacks constructor control module 0x98"); + 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, layout, init_array, fini_array, old_dyn)? + .ok_or_else(|| { + Error::Invalid( + "constructor control module 0x98 has no validated constructor table" + .to_owned(), + ) + })?; + Some((profile, init_index, fini_index, init_array, fini_array)) } None => None, }; @@ -1755,21 +1815,28 @@ fn materialize_static_elf_tables( .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.collect_where(|relocation| { - relocation.offset < first_slot || relocation.offset >= slot_end - })?; + 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.collect_where(|relocation| { - relocation.offset >= first_slot && relocation.offset < slot_end - })?; - plt_relocations.extend(auxiliary2.collect_where(|relocation| { - relocation.offset >= first_slot && relocation.offset < slot_end - })?); + 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) @@ -1799,11 +1866,13 @@ fn materialize_static_elf_tables( } } - let mut dyn_relocations = old_dyn.collect_where(|_| true)?; - dyn_relocations.extend(auxiliary1.collect_where(|_| true)?); - let auxiliary2_dynamic = auxiliary2.collect_where(|relocation| { - relocation.offset < first_slot || relocation.offset >= slot_end - })?; + 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(), @@ -1814,25 +1883,8 @@ fn materialize_static_elf_tables( } dyn_relocations.extend(auxiliary2_dynamic); - let mut converted_local = 0_usize; - if legacy_import_profile { - for relocation in &mut dyn_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(&merged_symbols, symbol)?; - if section_index != 0 { - 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_local += 1; - } - } - } + let converted_local = + 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 @@ -1841,18 +1893,14 @@ fn materialize_static_elf_tables( .count(); if legacy_import_profile { - if dyn_relocations.len() != 450_990 || relative_count != 450_980 || converted_local != 37 { - return invalid(format!( - "legacy dynamic relocation profile mismatch: total={}, relative={}, local={converted_local}", - dyn_relocations.len(), - relative_count - )); - } - if slot_count != 366 { - return invalid(format!( - "legacy .got.plt has {slot_count} dynamic slots, expected 366" - )); - } + debug_assert_eq!( + dyn_relocations.len(), + 450_990, + "legacy relocation count changed" + ); + debug_assert_eq!(relative_count, 450_980, "legacy RELATIVE count changed"); + debug_assert_eq!(converted_local, 37, "legacy local relocation count changed"); + debug_assert_eq!(slot_count, 366, "legacy GOT/PLT slot count changed"); let jump_slots = plt_relocations .iter() .filter(|relocation| relocation.kind() == R_AARCH64_JUMP_SLOT) @@ -1861,11 +1909,11 @@ fn materialize_static_elf_tables( .iter() .filter(|relocation| relocation.kind() == R_AARCH64_RELATIVE) .count(); - if (jump_slots, relatives) != (247, 119) { - return invalid(format!( - "legacy PLT type profile mismatch: {jump_slots} JUMP_SLOT / {relatives} RELATIVE" - )); - } + debug_assert_eq!( + (jump_slots, relatives), + (247, 119), + "legacy PLT type profile changed" + ); } if let Some((profile, _, _, init_array, fini_array)) = &constructors { @@ -2641,6 +2689,58 @@ mod tests { ); } + #[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); + } + #[test] fn discovers_auxiliary_symbol_permutation_seed() { let mut module = vec![0_u8; 0x80]; From 7728c6e7c22aaf8acdc9487a5d4b69facbff87d8 Mon Sep 17 00:00:00 2001 From: y52 Date: Sat, 19 Sep 2026 06:28:29 +0000 Subject: [PATCH 4/4] refactor(android): remove sample-specific runtime fingerprint --- senbei-engine/src/android/restore/pipeline.rs | 61 +------------------ 1 file changed, 2 insertions(+), 59 deletions(-) diff --git a/senbei-engine/src/android/restore/pipeline.rs b/senbei-engine/src/android/restore/pipeline.rs index 7084788..6712575 100644 --- a/senbei-engine/src/android/restore/pipeline.rs +++ b/senbei-engine/src/android/restore/pipeline.rs @@ -803,20 +803,6 @@ fn classify_special_plt_slots( Ok(result) } -fn is_legacy_import_profile( - source_symbol_count: usize, - hidden: &HiddenSymbolReport, - auxiliary: &AuxiliaryElfImage, -) -> bool { - source_symbol_count == 503 - && hidden.patched_symbols == 497 - && hidden.first_target_index == 3 - && hidden.last_target_index == 499 - && auxiliary.dynsym_count == 0xd4 - && auxiliary.relocation1_count == 0x6e159 - && auxiliary.relocation2_count == 0x143 -} - fn append_undefined_function_symbol( symbols: &mut Vec, strings: &mut Vec, @@ -1577,8 +1563,6 @@ fn materialize_static_elf_tables( let version_requirements = slice_u64(output, verneed.offset, verneed.size)?.to_vec(); let auxiliary = AuxiliaryElfImage::parse(auxiliary_data)?; - let legacy_import_profile = - is_legacy_import_profile(source_symbol_count, &hidden_symbols, &auxiliary); 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)) @@ -1599,19 +1583,11 @@ fn materialize_static_elf_tables( .map_err(|_| Error::Invalid("source symbol count exceeds u64".to_owned()))?, )) } else { - debug_assert!( - !legacy_import_profile, - "legacy protector symbol tail changed" - ); None } } else { None }; - debug_assert!( - !legacy_import_profile || old_symbols.len() / ELF64_SYMBOL_SIZE == 500, - "legacy dynamic symbol count changed" - ); let old_symbol_count = old_symbols.len() / ELF64_SYMBOL_SIZE; let auxiliary_strings = slice( auxiliary_data, @@ -1716,14 +1692,6 @@ fn materialize_static_elf_tables( let auxiliary1_relocations = auxiliary1.collect_where(|_| true)?; let auxiliary2_relocations = auxiliary2.collect_where(|_| true)?; - if legacy_import_profile { - let custom_names = dynamic_symbol_names(&appended_symbols, &merged_strings)?; - debug_assert_eq!( - custom_names.get(170).map(Vec::as_slice), - Some(b"malloc".as_slice()), - "legacy auxiliary permutation anchor changed" - ); - } let special_imports = discover_special_plt_imports( output, layout, @@ -1883,8 +1851,7 @@ fn materialize_static_elf_tables( } dyn_relocations.extend(auxiliary2_dynamic); - let converted_local = - convert_defined_symbol_relocations(&mut dyn_relocations, &merged_symbols)?; + 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 @@ -1892,30 +1859,6 @@ fn materialize_static_elf_tables( .take_while(|relocation| relocation.kind() == R_AARCH64_RELATIVE) .count(); - if legacy_import_profile { - debug_assert_eq!( - dyn_relocations.len(), - 450_990, - "legacy relocation count changed" - ); - debug_assert_eq!(relative_count, 450_980, "legacy RELATIVE count changed"); - debug_assert_eq!(converted_local, 37, "legacy local relocation count changed"); - debug_assert_eq!(slot_count, 366, "legacy GOT/PLT slot count changed"); - let jump_slots = plt_relocations - .iter() - .filter(|relocation| relocation.kind() == R_AARCH64_JUMP_SLOT) - .count(); - let relatives = plt_relocations - .iter() - .filter(|relocation| relocation.kind() == R_AARCH64_RELATIVE) - .count(); - debug_assert_eq!( - (jump_slots, relatives), - (247, 119), - "legacy PLT type profile changed" - ); - } - if let Some((profile, _, _, init_array, fini_array)) = &constructors { for (slot, &addend) in profile.init_addends.iter().enumerate() { let target = init_array @@ -2682,7 +2625,7 @@ mod tests { assert!(error.to_string().contains("at least one")); } #[test] - fn legacy_symbol_permutation_decodes_malloc_anchor() { + fn seeded_auxiliary_symbol_permutation_decodes_known_vector() { assert_eq!( decode_auxiliary_symbol(120, 268, 0xd4, 0xbd93_5573).expect("decode"), 171