fix(android): support compact ELF dynamic table layouts

This commit is contained in:
bfloat16
2026-09-07 16:31:38 +08:00
parent 2d92360d87
commit aa1bcaa2eb
4 changed files with 564 additions and 81 deletions
+2
View File
@@ -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 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 ## 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. 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.
-1
View File
@@ -542,7 +542,6 @@ impl HuffmanLzDecoder {
} }
/// Apply the native word transform and optional AES-256-CBC decryption. /// Apply the native word transform and optional AES-256-CBC decryption.
#[allow(clippy::chunks_exact_to_as_chunks)]
pub fn transform_segment( pub fn transform_segment(
data: &[u8], data: &[u8],
seed: u32, seed: u32,
+197 -1
View File
@@ -4,6 +4,8 @@ pub(crate) const SHT_NOBITS: u32 = 8;
pub(crate) const SHT_STRTAB: u32 = 3; pub(crate) const SHT_STRTAB: u32 = 3;
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000; pub(crate) const SHT_LOUSER: u32 = 0x8000_0000;
pub(crate) const SHF_ALLOC: u64 = 2; 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LoadSegment { pub(crate) struct LoadSegment {
@@ -12,6 +14,7 @@ pub(crate) struct LoadSegment {
pub file_size: u64, pub file_size: u64,
pub memory_size: u64, pub memory_size: u64,
pub flags: u32, pub flags: u32,
pub alignment: u64,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -65,6 +68,9 @@ impl SectionHeader {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ElfLayout { pub(crate) struct ElfLayout {
pub entrypoint: u64, pub entrypoint: u64,
pub program_header_offset: usize,
pub program_header_size: usize,
pub program_header_count: usize,
pub program_headers: Vec<LoadSegment>, pub program_headers: Vec<LoadSegment>,
pub section_headers: Vec<SectionHeader>, pub section_headers: Vec<SectionHeader>,
pub section_name_index: usize, pub section_name_index: usize,
@@ -95,7 +101,7 @@ impl ElfLayout {
let mut program_headers = Vec::new(); let mut program_headers = Vec::new();
for index in 0..program_header_count { for index in 0..program_header_count {
let offset = checked_index(program_header_offset, index, program_header_size)?; 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; continue;
} }
let segment = LoadSegment { let segment = LoadSegment {
@@ -104,6 +110,7 @@ impl ElfLayout {
virtual_address: read_u64(data, offset + 0x10)?, virtual_address: read_u64(data, offset + 0x10)?,
file_size: read_u64(data, offset + 0x20)?, file_size: read_u64(data, offset + 0x20)?,
memory_size: read_u64(data, offset + 0x28)?, memory_size: read_u64(data, offset + 0x28)?,
alignment: read_u64(data, offset + 0x30)?,
}; };
let file_end = segment let file_end = segment
.offset .offset
@@ -148,6 +155,9 @@ impl ElfLayout {
}; };
let layout = Self { let layout = Self {
entrypoint, entrypoint,
program_header_offset,
program_header_size,
program_header_count,
program_headers, program_headers,
section_headers, section_headers,
section_name_index, section_name_index,
@@ -197,6 +207,116 @@ impl ElfLayout {
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD file range".to_owned())) .ok_or_else(|| Error::Invalid("ELF has no PT_LOAD file range".to_owned()))
} }
pub fn load_alignment(&self) -> Result<u64> {
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<Self> {
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. /// Resolve every section's name from the ELF `shstrtab` section.
/// ///
/// The returned names are source data, not role labels supplied by the /// The returned names are source data, not role labels supplied by the
@@ -349,6 +469,9 @@ mod tests {
fn layout(name_index: u32) -> ElfLayout { fn layout(name_index: u32) -> ElfLayout {
ElfLayout { ElfLayout {
entrypoint: 0, entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 0,
program_headers: Vec::new(), program_headers: Vec::new(),
section_headers: vec![ section_headers: vec![
SectionHeader { SectionHeader {
@@ -437,4 +560,77 @@ mod tests {
.expect_err("unterminated table"); .expect_err("unterminated table");
assert!(error.to_string().contains("not NUL terminated")); 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"));
}
} }
+338 -52
View File
@@ -16,8 +16,8 @@ use super::artifact::load_artifacts;
use super::error::{Error, Result, invalid}; use super::error::{Error, Result, invalid};
use super::hash::{build_gnu_hash, build_sysv_hash}; use super::hash::{build_gnu_hash, build_sysv_hash};
use super::layout::{ use super::layout::{
ElfLayout, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, align_up, read_i64, read_u32, ElfLayout, LoadSegment, PF_R, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, align_up,
read_u64, slice, slice_u64, usize_from_u64, read_i64, read_u32, read_u64, slice, slice_u64, usize_from_u64,
}; };
const CHUNK_SIZE: usize = 16 * 1024 * 1024; const CHUNK_SIZE: usize = 16 * 1024 * 1024;
@@ -100,6 +100,12 @@ pub struct PlacementReport {
pub size: usize, pub size: usize,
} }
struct TablePayload {
name: &'static str,
alignment: u64,
data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ElfMaterializationReport { pub struct ElfMaterializationReport {
pub hidden_symbols: HiddenSymbolReport, pub hidden_symbols: HiddenSymbolReport,
@@ -432,6 +438,9 @@ impl AuxiliaryElfImage {
relocation2_offset: words[10], relocation2_offset: words[10],
relocation2_count: words[11], relocation2_count: words[11],
}; };
if result.dynsym_count == 0 {
return invalid("auxiliary dynamic symbol table has no null entry");
}
if result.relocation1_offset != 0x40 { if result.relocation1_offset != 0x40 {
return invalid("auxiliary relocation table does not follow its header"); 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)? if slice(data, result.dynsym_offset as usize, ELF64_SYMBOL_SIZE)?
.iter() .iter()
.any(|&byte| byte != 0) .any(|&byte| byte != 0)
@@ -743,8 +749,30 @@ fn patch_dynamic_tags(
Ok(()) Ok(())
} }
fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> Result<bool> {
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<HashMap<&'static str, usize>> { fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, usize>> {
const REQUIRED: [&str; 9] = [ const REQUIRED: [&str; 8] = [
".dynsym", ".dynsym",
".gnu.version", ".gnu.version",
".gnu.version_r", ".gnu.version_r",
@@ -753,7 +781,6 @@ fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, us
".rela.dyn", ".rela.dyn",
".rela.plt", ".rela.plt",
".dynamic", ".dynamic",
".rodata",
]; ];
let mut result = HashMap::with_capacity(REQUIRED.len()); let mut result = HashMap::with_capacity(REQUIRED.len());
for required in REQUIRED { for required in REQUIRED {
@@ -785,13 +812,182 @@ fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, us
Ok(result) Ok(result)
} }
fn metadata_capacity_end(
layout: &ElfLayout,
indices: &HashMap<&'static str, usize>,
metadata_start: u64,
) -> Result<u64> {
let table_indices = indices.values().copied().collect::<HashSet<_>>();
let file_end = layout.private_section()?.offset;
let next_section = layout
.section_headers
.iter()
.enumerate()
.filter(|(index, section)| {
!table_indices.contains(index)
&& section.section_type != SHT_NOBITS
&& section.size != 0
&& section.offset >= metadata_start
})
.map(|(_, section)| section.offset)
.min()
.unwrap_or(file_end);
let capacity_end = next_section.min(file_end);
// A zero-length window is a valid result: the caller can move the whole
// table set to a new PT_LOAD instead of overwriting an adjacent section.
Ok(capacity_end.max(metadata_start))
}
fn metadata_mapping_length(
source: &[u8],
layout: &ElfLayout,
auxiliary_data: &[u8],
) -> Result<usize> {
let names = layout.section_names(source)?;
let indices = required_section_indices(&names)?;
let section = |name: &'static str| -> SectionHeader { layout.section_headers[indices[name]] };
let dynsym = section(".dynsym");
let versym = section(".gnu.version");
let verneed = section(".gnu.version_r");
let dynstr = section(".dynstr");
let rela_dyn = section(".rela.dyn");
let rela_plt = section(".rela.plt");
if dynsym.entry_size != ELF64_SYMBOL_SIZE as u64
|| dynsym.size % ELF64_SYMBOL_SIZE as u64 != 0
|| versym.entry_size != 2
|| rela_dyn.entry_size != ELF64_RELA_SIZE as u64
|| rela_plt.entry_size != ELF64_RELA_SIZE as u64
|| rela_dyn.size % ELF64_RELA_SIZE as u64 != 0
|| rela_plt.size % ELF64_RELA_SIZE as u64 != 0
{
return invalid("unexpected dynamic-table entry layout");
}
let auxiliary = AuxiliaryElfImage::parse(auxiliary_data)?;
let old_symbol_count = usize_from_u64(
dynsym.size / ELF64_SYMBOL_SIZE as u64,
"dynamic symbol count",
)?;
let 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<String, PlacementReport>, u64)> {
let mut cursor = start;
let mut placements = BTreeMap::new();
for table in tables {
cursor = align_up(cursor, table.alignment)?;
placements.insert(
table.name.to_owned(),
PlacementReport {
offset: cursor,
size: table.data.len(),
},
);
cursor = cursor
.checked_add(table.data.len() as u64)
.ok_or_else(|| Error::Invalid("rebuilt ELF metadata end overflow".to_owned()))?;
}
Ok((placements, cursor))
}
fn table_end(tables: &[TablePayload], start: u64) -> Result<u64> {
table_placements(tables, start).map(|(_, end)| end)
}
fn materialize_static_elf_tables( fn materialize_static_elf_tables(
output: &mut [u8], output: &mut [u8],
source: &[u8], source: &[u8],
layout: &ElfLayout, layout: &ElfLayout,
symbol_patch_data: &[u8], symbol_patch_data: &[u8],
auxiliary_data: &[u8], auxiliary_data: &[u8],
) -> Result<(ElfLayout, ElfMaterializationReport)> { ) -> Result<(ElfLayout, ElfMaterializationReport, u64)> {
let names = layout.section_names(source)?; let names = layout.section_names(source)?;
let indices = required_section_indices(&names)?; let indices = required_section_indices(&names)?;
let section = |name: &'static str| -> SectionHeader { layout.section_headers[indices[name]] }; 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_dyn = section(".rela.dyn");
let rela_plt = section(".rela.plt"); let rela_plt = section(".rela.plt");
let dynamic = section(".dynamic"); let dynamic = section(".dynamic");
let rodata = section(".rodata");
let (old_symbols, old_strings, hidden_symbols) = let (old_symbols, old_strings, hidden_symbols) =
restore_hidden_symbols(output, dynsym, dynstr, symbol_patch_data)?; 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_offset as usize,
auxiliary.dynstr_size 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); let mut appended_symbols = Vec::with_capacity(appended_count * ELF64_SYMBOL_SIZE);
for index in 1..auxiliary.dynsym_count as usize { for index in 1..auxiliary.dynsym_count as usize {
let offset = auxiliary.dynsym_offset as usize + index * ELF64_SYMBOL_SIZE; 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_dyn_count = merged_rela_dyn.len() / ELF64_RELA_SIZE;
let rela_plt_count = merged_rela_plt.len() / ELF64_RELA_SIZE; let rela_plt_count = merged_rela_plt.len() / ELF64_RELA_SIZE;
struct TablePayload {
name: &'static str,
alignment: u64,
data: Vec<u8>,
}
let mut tables = vec![ let mut tables = vec![
TablePayload { TablePayload {
name: ".dynsym", name: ".dynsym",
@@ -977,34 +1171,52 @@ fn materialize_static_elf_tables(
data: merged_rela_plt, data: merged_rela_plt,
}, },
]); ]);
let metadata_start = dynsym.offset; let mut placement_layout = layout.clone();
let mut cursor = metadata_start; let mut metadata_start = dynsym.offset;
let mut placements = BTreeMap::new(); let (mut placements, mut cursor) = table_placements(&tables, metadata_start)?;
for table in &tables { let mut capacity_end = metadata_capacity_end(layout, &indices, metadata_start)?;
cursor = align_up(cursor, table.alignment)?; if cursor > capacity_end {
placements.insert( let alignment = layout.load_alignment()?;
table.name.to_owned(), let extension_start = align_up(layout.private_section()?.offset, alignment)?;
PlacementReport { let extension_end = table_end(&tables, extension_start)?;
offset: cursor, let extension_size = extension_end
size: table.data.len(), .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 metadata_start = extension_start;
.checked_add(table.data.len() as u64) (placements, cursor) = table_placements(&tables, metadata_start)?;
.ok_or_else(|| Error::Invalid("rebuilt ELF metadata end overflow".to_owned()))?; capacity_end = cursor;
} }
if cursor > rodata.offset { let zero_start = usize_from_u64(dynsym.offset, "metadata start")?;
return invalid(format!( let zero_end = usize_from_u64(
"rebuilt ELF tables end at 0x{cursor:x}, beyond .rodata 0x{:x}", metadata_capacity_end(layout, &indices, dynsym.offset)?,
rodata.offset "metadata capacity end",
)); )?;
}
let zero_start = usize_from_u64(metadata_start, "metadata start")?;
let zero_end = usize_from_u64(rodata.offset, ".rodata offset")?;
output output
.get_mut(zero_start..zero_end) .get_mut(zero_start..zero_end)
.ok_or_else(|| Error::Invalid("metadata capacity exceeds output mapping".to_owned()))? .ok_or_else(|| Error::Invalid("metadata capacity exceeds output mapping".to_owned()))?
.fill(0); .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(); let mut updated_sections = layout.section_headers.clone();
for table in &tables { for table in &tables {
@@ -1021,8 +1233,8 @@ fn materialize_static_elf_tables(
.copy_from_slice(&table.data); .copy_from_slice(&table.data);
let index = indices[table.name]; let index = indices[table.name];
let mut updated = updated_sections[index]; let mut updated = updated_sections[index];
updated.address = updated.address = placement_layout
layout.file_offset_to_virtual_address(placement.offset, table.data.len() as u64)?; .file_offset_to_virtual_address(placement.offset, table.data.len() as u64)?;
updated.offset = placement.offset; updated.offset = placement.offset;
updated.size = table.data.len() as u64; updated.size = table.data.len() as u64;
updated_sections[index] = updated; updated_sections[index] = updated;
@@ -1039,16 +1251,23 @@ fn materialize_static_elf_tables(
(DT_JMPREL, section_address(".rela.plt")), (DT_JMPREL, section_address(".rela.plt")),
(DT_GNU_HASH, section_address(".gnu.hash")), (DT_GNU_HASH, section_address(".gnu.hash")),
(DT_VERSYM, section_address(".gnu.version")), (DT_VERSYM, section_address(".gnu.version")),
(DT_RELACOUNT, relative_count as u64),
(DT_VERNEED, section_address(".gnu.version_r")), (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") { if indices.contains_key(".hash") {
dynamic_values.insert(DT_HASH, section_address(".hash")); dynamic_values.insert(DT_HASH, section_address(".hash"));
} }
patch_dynamic_tags(output, dynamic, &dynamic_values)?; 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; restored_layout.section_headers = updated_sections;
let data_end = if metadata_start == dynsym.offset {
layout.private_section()?.offset
} else {
cursor
};
Ok(( Ok((
restored_layout, restored_layout,
ElfMaterializationReport { ElfMaterializationReport {
@@ -1065,10 +1284,11 @@ fn materialize_static_elf_tables(
relative_prefix_count: relative_count, relative_prefix_count: relative_count,
metadata_start, metadata_start,
metadata_end: cursor, metadata_end: cursor,
metadata_capacity_end: rodata.offset, metadata_capacity_end: capacity_end,
metadata_slack: rodata.offset - cursor, metadata_slack: capacity_end.saturating_sub(cursor),
placements, placements,
}, },
data_end,
)) ))
} }
@@ -1090,9 +1310,13 @@ fn finalize_clean_elf(
temporary_path: &Path, temporary_path: &Path,
source: &[u8], source: &[u8],
layout: &ElfLayout, layout: &ElfLayout,
data_start: u64,
preserve_entrypoint: bool, preserve_entrypoint: bool,
) -> Result<CleaningReport> { ) -> Result<CleaningReport> {
let private = layout.private_section()?; 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)?; let names = layout.section_names(source)?;
if layout.private_section_index + 1 != layout.section_headers.len() { if layout.private_section_index + 1 != layout.section_headers.len() {
return invalid("SHT_LOUSER section is not the final section"); 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 retained = &layout.section_headers[..layout.private_section_index];
let mut updated = Vec::with_capacity(retained.len()); let mut updated = Vec::with_capacity(retained.len());
stream stream
.seek(SeekFrom::Start(private.offset)) .seek(SeekFrom::Start(data_start))
.map_err(|error| Error::io("seek temporary output", temporary_path, error))?; .map_err(|error| Error::io("seek temporary output", temporary_path, error))?;
for &section in retained { for &section in retained {
if section.section_type == SHT_NOBITS || section.flags & SHF_ALLOC != 0 || section.size == 0 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<RestoreReport> {
.map_err(|error| Error::io("size temporary output", &temporary_path, error))?; .map_err(|error| Error::io("size temporary output", &temporary_path, error))?;
let mut restored_layout = layout.clone(); let mut restored_layout = layout.clone();
let mut auxiliary_data = None;
let mut auxiliary_stats = None; let mut auxiliary_stats = None;
let mut materialization = None; let mut materialization = None;
let mut temporary_end = private.offset;
let primary_stats; let primary_stats;
{ {
let mut output = map_mut(temporary.as_file(), private_size, &temporary_path)?; let mut output = map_mut(temporary.as_file(), private_size, &temporary_path)?;
@@ -1384,6 +1608,11 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
options.verbose, options.verbose,
|address, data| writer.write(address, data), |address, data| writer.write(address, data),
)?; )?;
output
.flush()
.map_err(|error| Error::io("flush restored image", &temporary_path, error))?;
}
if !options.outer_only { if !options.outer_only {
if options.verbose { if options.verbose {
eprintln!("Decoding auxiliary 0x9D ELF materialization container..."); eprintln!("Decoding auxiliary 0x9D ELF materialization container...");
@@ -1396,9 +1625,9 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
options.verbose, options.verbose,
|offset, data| { |offset, data| {
let start = usize_from_u64(offset, "auxiliary write offset")?; let start = usize_from_u64(offset, "auxiliary write offset")?;
let end = start.checked_add(data.len()).ok_or_else(|| { let end = start
Error::Invalid("auxiliary decoded write overflow".to_owned()) .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(|| { let destination = decoded.get_mut(start..end).ok_or_else(|| {
Error::Invalid("auxiliary decoded write is out of range".to_owned()) Error::Invalid("auxiliary decoded write is out of range".to_owned())
})?; })?;
@@ -1409,10 +1638,20 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
if let Some(path) = &options.dump_auxiliary { if let Some(path) = &options.dump_auxiliary {
write_atomic(&absolute(path)?, &decoded)?; 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 { if options.verbose {
eprintln!("Rebuilding static ELF dynamic-linker tables..."); eprintln!("Rebuilding static ELF dynamic-linker tables...");
} }
let (new_layout, report) = materialize_static_elf_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, &mut output,
&source, &source,
&layout, &layout,
@@ -1422,19 +1661,23 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
restored_layout = new_layout; restored_layout = new_layout;
materialization = Some(report); materialization = Some(report);
auxiliary_stats = Some(stats); auxiliary_stats = Some(stats);
auxiliary_data = Some(decoded); temporary_end = data_end;
}
output output
.flush() .flush()
.map_err(|error| Error::io("flush restored image", &temporary_path, error))?; .map_err(|error| Error::io("flush restored image", &temporary_path, error))?;
} }
drop(auxiliary_data); temporary
.as_file()
.set_len(temporary_end)
.map_err(|error| Error::io("trim temporary output", &temporary_path, error))?;
}
let cleaning = finalize_clean_elf( let cleaning = finalize_clean_elf(
temporary.as_file_mut(), temporary.as_file_mut(),
&temporary_path, &temporary_path,
&source, &source,
&restored_layout, &restored_layout,
temporary_end,
options.preserve_entrypoint, options.preserve_entrypoint,
)?; )?;
let validation = { let validation = {
@@ -1489,3 +1732,46 @@ fn hex_digest(data: &[u8]) -> String {
} }
out out
} }
#[cfg(test)]
mod tests {
use super::*;
fn auxiliary_image(symbol_count: u32) -> Vec<u8> {
let mut data = vec![0_u8; 0x279];
let words = [
0x40_u32,
0,
0x40,
0,
0x278,
1,
0x260,
symbol_count,
0x40,
3,
0x90,
19,
0,
0,
0xb7,
0,
];
for (index, word) in words.into_iter().enumerate() {
data[index * 4..index * 4 + 4].copy_from_slice(&word.to_le_bytes());
}
data
}
#[test]
fn auxiliary_accepts_null_only_dynamic_symbol_table() {
let parsed = AuxiliaryElfImage::parse(&auxiliary_image(1)).expect("null-only dynsym");
assert_eq!(parsed.dynsym_count, 1);
}
#[test]
fn auxiliary_rejects_missing_null_dynamic_symbol() {
let error = AuxiliaryElfImage::parse(&auxiliary_image(0)).expect_err("missing null symbol");
assert!(error.to_string().contains("no null entry"));
}
}