From aa1bcaa2ebdc89c3e31711feb3d10f27c299125d Mon Sep 17 00:00:00 2001 From: bfloat16 Date: Mon, 7 Sep 2026 16:31:38 +0800 Subject: [PATCH] fix(android): support compact ELF dynamic table layouts --- docs/design.md | 2 + senbei-crypto/src/android/protector.rs | 1 - senbei-engine/src/android/restore/layout.rs | 198 +++++++- senbei-engine/src/android/restore/pipeline.rs | 444 ++++++++++++++---- 4 files changed, 564 insertions(+), 81 deletions(-) diff --git a/docs/design.md b/docs/design.md index 2780e71..267e1bb 100644 --- a/docs/design.md +++ b/docs/design.md @@ -37,6 +37,8 @@ External companion inputs are reconstructed as `stub[..4096]` followed by the ma Android protection primitives are in `senbei-crypto/src/android/`. Android metadata restoration is in `senbei-metadata/src/android/` and only rewrites MethodDef token fields. The Windows structural metadata transform is in `senbei-metadata/src/windows/`. +Android ELF dynamic tables are located from the input section table and its actual file ranges. When the original gap is too small, restoration adds a validated read-only `PT_LOAD` after the existing load image and updates the dynamic tags; it never overwrites an adjacent section or emits a partial image. + ## Scanning and Packages Folder scanning uses platform target names to avoid opening bulk assets: Windows candidates are `.exe`, `.dll`, and `global-metadata.dat`; Android candidates are `.so` and `global-metadata.dat`. A Windows `.exe._` or `.dll._` companion is auxiliary input for its sibling stub and is excluded from the skipped count. diff --git a/senbei-crypto/src/android/protector.rs b/senbei-crypto/src/android/protector.rs index f86b24f..2c962c7 100644 --- a/senbei-crypto/src/android/protector.rs +++ b/senbei-crypto/src/android/protector.rs @@ -542,7 +542,6 @@ impl HuffmanLzDecoder { } /// Apply the native word transform and optional AES-256-CBC decryption. -#[allow(clippy::chunks_exact_to_as_chunks)] pub fn transform_segment( data: &[u8], seed: u32, diff --git a/senbei-engine/src/android/restore/layout.rs b/senbei-engine/src/android/restore/layout.rs index 51e8476..80d5919 100644 --- a/senbei-engine/src/android/restore/layout.rs +++ b/senbei-engine/src/android/restore/layout.rs @@ -4,6 +4,8 @@ pub(crate) const SHT_NOBITS: u32 = 8; pub(crate) const SHT_STRTAB: u32 = 3; pub(crate) const SHT_LOUSER: u32 = 0x8000_0000; pub(crate) const SHF_ALLOC: u64 = 2; +const PT_LOAD: u32 = 1; +pub(crate) const PF_R: u32 = 4; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct LoadSegment { @@ -12,6 +14,7 @@ pub(crate) struct LoadSegment { pub file_size: u64, pub memory_size: u64, pub flags: u32, + pub alignment: u64, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -65,6 +68,9 @@ impl SectionHeader { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ElfLayout { pub entrypoint: u64, + pub program_header_offset: usize, + pub program_header_size: usize, + pub program_header_count: usize, pub program_headers: Vec, pub section_headers: Vec, pub section_name_index: usize, @@ -95,7 +101,7 @@ impl ElfLayout { let mut program_headers = Vec::new(); for index in 0..program_header_count { let offset = checked_index(program_header_offset, index, program_header_size)?; - if read_u32(data, offset)? != 1 { + if read_u32(data, offset)? != PT_LOAD { continue; } let segment = LoadSegment { @@ -104,6 +110,7 @@ impl ElfLayout { virtual_address: read_u64(data, offset + 0x10)?, file_size: read_u64(data, offset + 0x20)?, memory_size: read_u64(data, offset + 0x28)?, + alignment: read_u64(data, offset + 0x30)?, }; let file_end = segment .offset @@ -148,6 +155,9 @@ impl ElfLayout { }; let layout = Self { entrypoint, + program_header_offset, + program_header_size, + program_header_count, program_headers, section_headers, section_name_index, @@ -197,6 +207,116 @@ impl ElfLayout { .ok_or_else(|| Error::Invalid("ELF has no PT_LOAD file range".to_owned())) } + pub fn load_alignment(&self) -> Result { + let alignment = self + .program_headers + .iter() + .map(|segment| segment.alignment) + .max() + .ok_or_else(|| Error::Invalid("ELF has no PT_LOAD alignment".to_owned()))?; + if alignment == 0 || !alignment.is_power_of_two() { + return invalid(format!("invalid PT_LOAD alignment 0x{alignment:x}")); + } + Ok(alignment) + } + + pub fn append_load_segment(&self, output: &mut [u8], segment: LoadSegment) -> Result { + if self.program_header_size != 0x38 { + return invalid("unexpected ELF program header size"); + } + if segment.file_size == 0 { + return invalid("new PT_LOAD has no file contents"); + } + if segment.memory_size < segment.file_size { + return invalid("new PT_LOAD memory size is smaller than file size"); + } + if segment.alignment == 0 || !segment.alignment.is_power_of_two() { + return invalid(format!( + "invalid new PT_LOAD alignment 0x{:x}", + segment.alignment + )); + } + if segment.offset % segment.alignment != segment.virtual_address % segment.alignment { + return invalid("new PT_LOAD offset and address are misaligned"); + } + let segment_file_end = segment + .offset + .checked_add(segment.file_size) + .ok_or_else(|| Error::Invalid("new PT_LOAD file range overflow".to_owned()))?; + let segment_memory_end = segment + .virtual_address + .checked_add(segment.memory_size) + .ok_or_else(|| Error::Invalid("new PT_LOAD memory range overflow".to_owned()))?; + if segment_file_end > output.len() as u64 { + return invalid("new PT_LOAD exceeds output mapping"); + } + for existing in &self.program_headers { + let existing_file_end = existing + .offset + .checked_add(existing.file_size) + .ok_or_else(|| Error::Invalid("PT_LOAD file range overflow".to_owned()))?; + if segment.offset < existing_file_end && existing.offset < segment_file_end { + return invalid("new PT_LOAD overlaps an existing file range"); + } + let existing_memory_end = existing + .virtual_address + .checked_add(existing.memory_size) + .ok_or_else(|| Error::Invalid("PT_LOAD memory range overflow".to_owned()))?; + if segment.virtual_address < existing_memory_end + && existing.virtual_address < segment_memory_end + { + return invalid("new PT_LOAD overlaps an existing memory range"); + } + } + let new_count = self + .program_header_count + .checked_add(1) + .ok_or_else(|| Error::Invalid("program header count overflow".to_owned()))?; + let new_count_u16 = u16::try_from(new_count) + .map_err(|_| Error::Invalid("program header count exceeds u16".to_owned()))?; + let header_offset = checked_index( + self.program_header_offset, + self.program_header_count, + self.program_header_size, + )?; + let header_end = header_offset + .checked_add(self.program_header_size) + .ok_or_else(|| Error::Invalid("new program header range overflow".to_owned()))?; + slice(output, header_offset, self.program_header_size)?; + let first_file_section = self + .section_headers + .iter() + .filter(|section| section.section_type != SHT_NOBITS && section.size != 0) + .map(|section| section.offset) + .min(); + if first_file_section.is_some_and(|offset| header_end as u64 > offset) { + return invalid("no space for an additional program header"); + } + + let mut header = [0_u8; 0x38]; + header[0..4].copy_from_slice(&PT_LOAD.to_le_bytes()); + header[4..8].copy_from_slice(&segment.flags.to_le_bytes()); + header[8..0x10].copy_from_slice(&segment.offset.to_le_bytes()); + header[0x10..0x18].copy_from_slice(&segment.virtual_address.to_le_bytes()); + header[0x18..0x20].copy_from_slice(&segment.virtual_address.to_le_bytes()); + header[0x20..0x28].copy_from_slice(&segment.file_size.to_le_bytes()); + header[0x28..0x30].copy_from_slice(&segment.memory_size.to_le_bytes()); + header[0x30..0x38].copy_from_slice(&segment.alignment.to_le_bytes()); + output + .get_mut(header_offset..header_end) + .ok_or_else(|| Error::Invalid("new program header exceeds output".to_owned()))? + .copy_from_slice(&header); + output + .get_mut(0x38..0x3a) + .ok_or_else(|| Error::Invalid("ELF header is truncated".to_owned()))? + .copy_from_slice(&new_count_u16.to_le_bytes()); + + let mut updated = self.clone(); + updated.program_header_count = new_count; + updated.program_headers.push(segment); + Ok(updated) + } + /// Resolve every section's name from the ELF `shstrtab` section. /// /// The returned names are source data, not role labels supplied by the @@ -349,6 +469,9 @@ mod tests { fn layout(name_index: u32) -> ElfLayout { ElfLayout { entrypoint: 0, + program_header_offset: 0, + program_header_size: 0x38, + program_header_count: 0, program_headers: Vec::new(), section_headers: vec![ SectionHeader { @@ -437,4 +560,77 @@ mod tests { .expect_err("unterminated table"); assert!(error.to_string().contains("not NUL terminated")); } + + #[test] + fn append_load_segment_updates_program_headers() { + let elf_layout = ElfLayout { + entrypoint: 0, + program_header_offset: 0, + program_header_size: 0x38, + program_header_count: 0, + program_headers: Vec::new(), + section_headers: Vec::new(), + section_name_index: 0, + private_section_index: usize::MAX, + }; + let mut output = vec![0_u8; 0x2000]; + let updated = elf_layout + .append_load_segment( + &mut output, + LoadSegment { + offset: 0x1000, + virtual_address: 0x2000, + file_size: 0x20, + memory_size: 0x20, + flags: PF_R, + alignment: 0x1000, + }, + ) + .expect("append segment"); + assert_eq!(updated.program_header_count, 1); + assert_eq!(updated.program_headers[0].virtual_address, 0x2000); + assert_eq!(&output[0..4], &PT_LOAD.to_le_bytes()); + assert_eq!(&output[0x38..0x3a], &1_u16.to_le_bytes()); + } + + #[test] + fn append_load_segment_rejects_program_header_overlap() { + let mut elf_layout = ElfLayout { + entrypoint: 0, + program_header_offset: 0, + program_header_size: 0x38, + program_header_count: 0, + program_headers: Vec::new(), + section_headers: Vec::new(), + section_name_index: 0, + private_section_index: usize::MAX, + }; + elf_layout.section_headers.push(SectionHeader { + name: 0, + section_type: 1, + flags: 0, + address: 0, + offset: 0x20, + size: 1, + link: 0, + info: 0, + alignment: 1, + entry_size: 0, + }); + let mut output = vec![0_u8; 0x100]; + let error = elf_layout + .append_load_segment( + &mut output, + LoadSegment { + offset: 0x80, + virtual_address: 0x1080, + file_size: 0x20, + memory_size: 0x20, + flags: PF_R, + alignment: 0x1000, + }, + ) + .expect_err("overlapping program header"); + assert!(error.to_string().contains("additional program header")); + } } diff --git a/senbei-engine/src/android/restore/pipeline.rs b/senbei-engine/src/android/restore/pipeline.rs index a0243b8..56d21bd 100644 --- a/senbei-engine/src/android/restore/pipeline.rs +++ b/senbei-engine/src/android/restore/pipeline.rs @@ -16,8 +16,8 @@ use super::artifact::load_artifacts; use super::error::{Error, Result, invalid}; use super::hash::{build_gnu_hash, build_sysv_hash}; use super::layout::{ - ElfLayout, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, align_up, read_i64, read_u32, - read_u64, slice, slice_u64, usize_from_u64, + ElfLayout, LoadSegment, PF_R, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, align_up, + read_i64, read_u32, read_u64, slice, slice_u64, usize_from_u64, }; const CHUNK_SIZE: usize = 16 * 1024 * 1024; @@ -100,6 +100,12 @@ pub struct PlacementReport { 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, @@ -432,6 +438,9 @@ impl AuxiliaryElfImage { 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"); } @@ -470,9 +479,6 @@ impl AuxiliaryElfImage { )); } } - if result.dynsym_count < 2 { - return invalid("auxiliary dynamic symbol table is empty"); - } if slice(data, result.dynsym_offset as usize, ELF64_SYMBOL_SIZE)? .iter() .any(|&byte| byte != 0) @@ -743,8 +749,30 @@ fn patch_dynamic_tags( Ok(()) } +fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> Result { + if dynamic.size % 0x10 != 0 { + 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> { - const REQUIRED: [&str; 9] = [ + const REQUIRED: [&str; 8] = [ ".dynsym", ".gnu.version", ".gnu.version_r", @@ -753,7 +781,6 @@ fn required_section_indices(names: &[String]) -> Result Result, + 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 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 new_symbol_count = old_symbol_count + .checked_add(appended_count) + .ok_or_else(|| Error::Invalid("merged dynamic symbol count overflow".to_owned()))?; + let merged_dynstr_size = usize_from_u64(dynstr.size, ".dynstr size")? + .checked_add(auxiliary.dynstr_size as usize) + .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) + .ok_or_else(|| Error::Invalid("merged .rela.plt count overflow".to_owned()))?; + let gnu_hash_size = 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()))?; + 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 mut cursor = 0_u64; + 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()))?; + 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], -) -> Result<(ElfLayout, ElfMaterializationReport)> { +) -> 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]] }; @@ -802,7 +998,6 @@ fn materialize_static_elf_tables( let rela_dyn = section(".rela.dyn"); let rela_plt = section(".rela.plt"); let dynamic = section(".dynamic"); - let rodata = section(".rodata"); let (old_symbols, old_strings, hidden_symbols) = restore_hidden_symbols(output, dynsym, dynstr, symbol_patch_data)?; @@ -819,7 +1014,11 @@ fn materialize_static_elf_tables( auxiliary.dynstr_offset as usize, auxiliary.dynstr_size as usize, )?; - let appended_count = auxiliary.dynsym_count as usize - 1; + let 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); for index in 1..auxiliary.dynsym_count as usize { let offset = auxiliary.dynsym_offset as usize + index * ELF64_SYMBOL_SIZE; @@ -926,11 +1125,6 @@ fn materialize_static_elf_tables( let rela_dyn_count = merged_rela_dyn.len() / ELF64_RELA_SIZE; let rela_plt_count = merged_rela_plt.len() / ELF64_RELA_SIZE; - struct TablePayload { - name: &'static str, - alignment: u64, - data: Vec, - } let mut tables = vec![ TablePayload { name: ".dynsym", @@ -977,34 +1171,52 @@ fn materialize_static_elf_tables( data: merged_rela_plt, }, ]); - let metadata_start = dynsym.offset; - let mut cursor = metadata_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(), + 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 extension_start = align_up(layout.private_section()?.offset, alignment)?; + let extension_end = table_end(&tables, extension_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, }, - ); - cursor = cursor - .checked_add(table.data.len() as u64) - .ok_or_else(|| Error::Invalid("rebuilt ELF metadata end overflow".to_owned()))?; + )?; + metadata_start = extension_start; + (placements, cursor) = table_placements(&tables, metadata_start)?; + capacity_end = cursor; } - if cursor > rodata.offset { - return invalid(format!( - "rebuilt ELF tables end at 0x{cursor:x}, beyond .rodata 0x{:x}", - rodata.offset - )); - } - let zero_start = usize_from_u64(metadata_start, "metadata start")?; - let zero_end = usize_from_u64(rodata.offset, ".rodata offset")?; + 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 { @@ -1021,8 +1233,8 @@ fn materialize_static_elf_tables( .copy_from_slice(&table.data); let index = indices[table.name]; let mut updated = updated_sections[index]; - updated.address = - layout.file_offset_to_virtual_address(placement.offset, table.data.len() as u64)?; + 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; @@ -1039,16 +1251,23 @@ fn materialize_static_elf_tables( (DT_JMPREL, section_address(".rela.plt")), (DT_GNU_HASH, section_address(".gnu.hash")), (DT_VERSYM, section_address(".gnu.version")), - (DT_RELACOUNT, relative_count as u64), (DT_VERNEED, section_address(".gnu.version_r")), ]); + 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 = layout.clone(); + 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 { @@ -1065,10 +1284,11 @@ fn materialize_static_elf_tables( relative_prefix_count: relative_count, metadata_start, metadata_end: cursor, - metadata_capacity_end: rodata.offset, - metadata_slack: rodata.offset - cursor, + metadata_capacity_end: capacity_end, + metadata_slack: capacity_end.saturating_sub(cursor), placements, }, + data_end, )) } @@ -1090,9 +1310,13 @@ fn finalize_clean_elf( 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"); @@ -1100,7 +1324,7 @@ fn finalize_clean_elf( let retained = &layout.section_headers[..layout.private_section_index]; let mut updated = Vec::with_capacity(retained.len()); stream - .seek(SeekFrom::Start(private.offset)) + .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 @@ -1363,9 +1587,9 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result { .map_err(|error| Error::io("size temporary output", &temporary_path, error))?; let mut restored_layout = layout.clone(); - let mut auxiliary_data = None; 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)?; @@ -1384,35 +1608,50 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result { options.verbose, |address, data| writer.write(address, data), )?; - 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)?; - } - if options.verbose { - eprintln!("Rebuilding static ELF dynamic-linker tables..."); - } - let (new_layout, report) = materialize_static_elf_tables( + 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, @@ -1422,19 +1661,23 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result { restored_layout = new_layout; materialization = Some(report); auxiliary_stats = Some(stats); - auxiliary_data = Some(decoded); + temporary_end = data_end; + output + .flush() + .map_err(|error| Error::io("flush restored image", &temporary_path, error))?; } - 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))?; } - drop(auxiliary_data); let cleaning = finalize_clean_elf( temporary.as_file_mut(), &temporary_path, &source, &restored_layout, + temporary_end, options.preserve_entrypoint, )?; let validation = { @@ -1489,3 +1732,46 @@ fn hex_digest(data: &[u8]) -> String { } out } + +#[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")); + } +}