diff --git a/senbei-elf/src/lib.rs b/senbei-elf/src/lib.rs index 43aba57..a95dde9 100644 --- a/senbei-elf/src/lib.rs +++ b/senbei-elf/src/lib.rs @@ -15,26 +15,23 @@ pub use layout::{ /// ELF machine identifier for AArch64. pub const AARCH64_MACHINE: u16 = EM_AARCH64; -/// Dynamic sections required by the restored AArch64 loader image. -pub const DYNAMIC_SECTION_NAMES: [&str; 8] = [ +/// Dynamic sections required by every restored AArch64 loader image. +pub const DYNAMIC_SECTION_NAMES: [&str; 7] = [ ".dynsym", ".gnu.version", ".gnu.version_r", - ".gnu.hash", ".dynstr", ".rela.dyn", ".rela.plt", ".dynamic", ]; +/// Supported dynamic symbol-hash sections. At least one must be present. +pub const SYMBOL_HASH_SECTION_NAMES: [&str; 2] = [".gnu.hash", ".hash"]; + /// Dynamic sections needed to identify a protected image before extraction. -pub const PROBE_SECTION_NAMES: [&str; 5] = [ - ".dynsym", - ".dynstr", - ".gnu.hash", - ".gnu.version", - ".gnu.version_r", -]; +/// Hash tables are checked separately because either GNU or SysV hashing is valid. +pub const PROBE_SECTION_NAMES: [&str; 4] = [".dynsym", ".dynstr", ".gnu.version", ".gnu.version_r"]; /// ELF64 dynamic table record sizes. pub const ELF64_SYMBOL_SIZE: usize = 0x18; diff --git a/senbei-engine/src/android/extract/stage1.rs b/senbei-engine/src/android/extract/stage1.rs index d18600d..da53087 100644 --- a/senbei-engine/src/android/extract/stage1.rs +++ b/senbei-engine/src/android/extract/stage1.rs @@ -70,6 +70,16 @@ pub(crate) fn inspect( return invalid(format!("protected ELF lacks required section {wanted}")); } } + let has_symbol_hash = senbei_elf::SYMBOL_HASH_SECTION_NAMES.iter().any(|wanted| { + elf.section_headers.iter().any(|section| { + elf.shdr_strtab + .get_at(section.sh_name) + .is_some_and(|name| name == *wanted) + }) + }); + if !has_symbol_hash { + return invalid("protected ELF lacks a .gnu.hash or .hash symbol-hash section"); + } let (section_index, section) = matches[0]; let section_offset = usize::try_from(section.sh_offset) .map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?; diff --git a/senbei-engine/src/android/restore/pipeline.rs b/senbei-engine/src/android/restore/pipeline.rs index 63ea019..17a4759 100644 --- a/senbei-engine/src/android/restore/pipeline.rs +++ b/senbei-engine/src/android/restore/pipeline.rs @@ -757,7 +757,18 @@ fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> R } fn required_section_indices(names: &[String]) -> Result> { - let mut result = HashMap::with_capacity(senbei_elf::DYNAMIC_SECTION_NAMES.len()); + let present = senbei_elf::SYMBOL_HASH_SECTION_NAMES + .iter() + .filter(|name| names.iter().any(|section| section == *name)) + .count(); + if present == 0 { + return invalid( + "ELF must carry at least one of the .gnu.hash / .hash symbol-hash sections", + ); + } + let mut result = HashMap::with_capacity( + senbei_elf::DYNAMIC_SECTION_NAMES.len() + senbei_elf::SYMBOL_HASH_SECTION_NAMES.len(), + ); for required in senbei_elf::DYNAMIC_SECTION_NAMES { let indices = names .iter() @@ -772,17 +783,19 @@ fn required_section_indices(names: &[String]) -> Result return invalid(format!("ELF contains duplicate section {required}")), } } - let sysv_hash = names - .iter() - .enumerate() - .filter_map(|(index, name)| (name == ".hash").then_some(index)) - .collect::>(); - match sysv_hash.as_slice() { - [index] => { - result.insert(".hash", *index); + for hash_table in senbei_elf::SYMBOL_HASH_SECTION_NAMES { + let indices = names + .iter() + .enumerate() + .filter_map(|(index, name)| (name == hash_table).then_some(index)) + .collect::>(); + match indices.as_slice() { + [index] => { + result.insert(hash_table, *index); + } + [] => {} + _ => return invalid(format!("ELF contains duplicate section {hash_table}")), } - [] => {} - _ => return invalid("ELF contains duplicate section .hash"), } Ok(result) } @@ -862,17 +875,21 @@ fn metadata_mapping_length( 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 gnu_hash_size = if indices.contains_key(".gnu.hash") { + 28_usize + .checked_add( + new_symbol_count + .checked_sub(1) + .ok_or_else(|| { + Error::Invalid("dynamic symbol table is unexpectedly empty".to_owned()) + })? + .checked_mul(4) + .ok_or_else(|| Error::Invalid("GNU hash size overflow".to_owned()))?, + ) + .ok_or_else(|| Error::Invalid("GNU hash size overflow".to_owned()))? + } else { + 0 + }; let sysv_hash_size = indices.contains_key(".hash").then(|| { new_symbol_count .checked_mul(2) @@ -1031,7 +1048,10 @@ fn materialize_static_elf_tables( .contains_key(".hash") .then(|| build_sysv_hash(&merged_names)) .transpose()?; - let gnu_hash_table = build_gnu_hash(&merged_names)?; + 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(); @@ -1102,6 +1122,11 @@ 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; + let gnu_hash = gnu_hash_table.map(|data| TablePayload { + name: ".gnu.hash", + alignment: 8, + data, + }); let mut tables = vec![ TablePayload { name: ".dynsym", @@ -1118,12 +1143,10 @@ fn materialize_static_elf_tables( alignment: 4, data: version_requirements, }, - TablePayload { - name: ".gnu.hash", - alignment: 8, - data: gnu_hash_table, - }, ]; + if let Some(payload) = gnu_hash { + tables.push(payload); + } if let Some(data) = sysv_hash { tables.push(TablePayload { name: ".hash", @@ -1231,10 +1254,12 @@ fn materialize_static_elf_tables( (DT_RELASZ, (rela_dyn_count * ELF64_RELA_SIZE) as u64), (DT_STRSZ, new_dynstr_size as u64), (DT_JMPREL, section_address(".rela.plt")), - (DT_GNU_HASH, section_address(".gnu.hash")), (DT_VERSYM, section_address(".gnu.version")), (DT_VERNEED, section_address(".gnu.version_r")), ]); + if indices.contains_key(".gnu.hash") { + dynamic_values.insert(DT_GNU_HASH, section_address(".gnu.hash")); + } if dynamic_contains_tag(output, dynamic, DT_RELACOUNT)? { dynamic_values.insert(DT_RELACOUNT, relative_count as u64); } @@ -1727,4 +1752,34 @@ mod tests { let error = AuxiliaryElfImage::parse(&auxiliary_image(0)).expect_err("missing null symbol"); assert!(error.to_string().contains("no null entry")); } + + fn dynamic_section_names(hash_sections: &[&str]) -> Vec { + senbei_elf::DYNAMIC_SECTION_NAMES + .into_iter() + .chain(hash_sections.iter().copied()) + .map(str::to_owned) + .collect() + } + + #[test] + fn required_sections_accept_each_hash_layout() { + for hashes in [ + [".gnu.hash"].as_slice(), + [".hash"].as_slice(), + [".gnu.hash", ".hash"].as_slice(), + ] { + let indices = required_section_indices(&dynamic_section_names(hashes)) + .expect("valid hash layout"); + for hash in hashes { + assert!(indices.contains_key(hash)); + } + } + } + + #[test] + fn required_sections_reject_missing_symbol_hash() { + let error = + required_section_indices(&dynamic_section_names(&[])).expect_err("missing hash"); + assert!(error.to_string().contains("at least one")); + } }