diff --git a/senbei-engine/src/android/restore/artifact.rs b/senbei-engine/src/android/restore/artifact.rs index 2e6c51d..0282356 100644 --- a/senbei-engine/src/android/restore/artifact.rs +++ b/senbei-engine/src/android/restore/artifact.rs @@ -16,6 +16,7 @@ fn wanted_id(command_id: u32) -> bool { pub(crate) struct Artifact { pub path: PathBuf, pub size: u64, + pub entry_offset: Option, } pub(crate) fn load_artifacts(index_path: &Path) -> Result> { @@ -47,6 +48,10 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result Result i64 { + i64::from((value << (32 - bits)) as i32 >> (32 - bits)) +} + +fn branch_target(pc: u64, instruction: u32, bits: u32, shift: u32) -> Option { + pc.checked_add_signed(signed((instruction >> shift) & ((1 << bits) - 1), bits) * 4) +} + +/// Follow direct control flow from the descriptor entry, without inspecting +/// unrelated code or searching the data image for plausible tables. +fn selector_functions(module: &[u8], entry: u32) -> Result> { + let mut functions = vec![u64::from(entry)]; + let mut visited_functions = HashSet::new(); + let mut selectors = BTreeSet::new(); + let mut budget = 100_000_usize; + while let Some(function) = functions.pop() { + if !visited_functions.insert(function) { + continue; + } + let mut pending = vec![function]; + let mut visited = HashSet::new(); + while let Some(pc) = pending.pop() { + if !visited.insert(pc) { + continue; + } + if budget == 0 { + return invalid("constructor module control flow exceeds analysis budget"); + } + budget -= 1; + let Some(instruction) = usize::try_from(pc) + .ok() + .filter(|offset| offset.is_multiple_of(4)) + .and_then(|offset| read_u32(module, offset).ok()) + else { + return invalid("constructor module branches outside its image"); + }; + if instruction & 0x7c00_0000 == 0x1400_0000 { + let Some(target) = branch_target(pc, instruction, 26, 0) else { + return invalid("constructor module branch address overflows"); + }; + if instruction & 0x8000_0000 != 0 { + functions.push(target); + pending.push(pc + 4); + } else { + pending.push(target); + } + continue; + } + let conditional_bits = if instruction & 0xff00_0010 == 0x5400_0000 + || instruction & 0x7e00_0000 == 0x3400_0000 + { + Some(19) + } else if instruction & 0x7e00_0000 == 0x3600_0000 { + Some(14) + } else { + None + }; + if let Some(bits) = conditional_bits { + let Some(target) = branch_target(pc, instruction, bits, 5) else { + return invalid("constructor module conditional branch overflows"); + }; + pending.push(target); + } + if instruction & 0xffff_fc1f == 0xd63f_0000 { + selectors.insert(function); + } else if instruction & 0xffff_fc1f == 0xd61f_0000 + || instruction & 0xffff_fc1f == 0xd65f_0000 + || instruction == 0 + { + continue; + } + pending.push(pc + 4); + } + } + Ok(selectors) +} + +fn mask(bits: u32) -> u64 { + u64::MAX >> (64 - bits) +} + +fn logical_immediate(instruction: u32, width: u32) -> Option { + let n = (instruction >> 22) & 1; + let imms = (instruction >> 10) & 63; + let immr = (instruction >> 16) & 63; + let encoded = (n << 6) | (!imms & 63); + let len = 31_u32.checked_sub(encoded.leading_zeros())?; + if len < 1 || (width == 32 && n != 0) { + return None; + } + let size = 1 << len; + let ones = (imms & (size - 1)) + 1; + if ones == size || size > width { + return None; + } + let rotate = immr & (size - 1); + let value = mask(ones); + let element = if rotate == 0 { + value + } else { + ((value >> rotate) | (value << (size - rotate))) & mask(size) + }; + let mut result = 0; + for position in (0..width).step_by(size as usize) { + result |= element << position; + } + Some(result) +} + +struct Selector<'a> { + module: &'a [u8], + image: &'a [u8], + layout: &'a ElfLayout, + registers: [Option; 31], + sp: u64, + stack: Vec>, + flags: Option<(bool, bool, bool, bool)>, + read_image_base: bool, + calls: Vec, +} + +impl Selector<'_> { + fn reg(&self, index: u32, sp: bool) -> Option { + if index == 31 { + Some(if sp { self.sp } else { 0 }) + } else { + self.registers[index as usize] + } + } + + fn set(&mut self, index: u32, value: u64, width: u32, sp: bool) { + let value = value & mask(width); + if index != 31 { + self.registers[index as usize] = Some(value); + } else if sp { + self.sp = value; + } + } + + fn read(&mut self, address: u64, size: usize) -> Option { + if address == RUNTIME + 8 && size == 8 { + self.read_image_base = true; + return Some(IMAGE); + } + let bytes = if address >= STACK { + let start = usize::try_from(address - STACK).ok()?; + self.stack + .get(start..start.checked_add(size)?)? + .iter() + .copied() + .collect::>>()? + } else if address >= IMAGE { + let offset = virtual_address_to_file_offset(self.layout, address - IMAGE, size as u64)?; + self.image.get(offset..offset.checked_add(size)?)?.to_vec() + } else { + slice_u64(self.module, address, size as u64).ok()?.to_vec() + }; + Some(bytes.iter().enumerate().fold(0, |value, (index, byte)| { + value | (u64::from(*byte) << (index * 8)) + })) + } + + fn write(&mut self, address: u64, size: usize, value: u64) -> Option<()> { + let offset = usize::try_from(address.checked_sub(STACK)?).ok()?; + for (index, byte) in self + .stack + .get_mut(offset..offset.checked_add(size)?)? + .iter_mut() + .enumerate() + { + *byte = Some((value >> (index * 8)) as u8); + } + Some(()) + } + + fn condition(&self, condition: u32) -> Option { + let (negative, zero, carry, overflow) = self.flags?; + let matched = match condition >> 1 { + 0 => zero, + 1 => carry, + 2 => negative, + 3 => overflow, + 4 => carry && !zero, + 5 => negative == overflow, + 6 => negative == overflow && !zero, + 7 => return Some(true), + _ => return None, + }; + Some(matched ^ (condition & 1 != 0)) + } + + fn run(mut self, start: u64, max_calls: usize) -> Option> { + let mut pc = start; + let budget = max_calls + .checked_add(1)? + .checked_mul(256)? + .checked_add(1024)? + .min(1_000_000); + for _ in 0..budget { + if !pc.is_multiple_of(4) { + return None; + } + let instruction = read_u32(self.module, usize::try_from(pc).ok()?).ok()?; + let mut next = pc.checked_add(4)?; + let rd = instruction & 31; + let rn = (instruction >> 5) & 31; + let width = if instruction >> 31 == 0 { 32 } else { 64 }; + if instruction & 0xffff_fc1f == 0xd65f_0000 { + return (self.reg(rn, false)? == RETURN + && self.sp == STACK + STACK_SIZE as u64 + && self.read_image_base + && !self.calls.is_empty() + && self.reg(0, false)? == 1) + .then_some(self.calls); + } else if instruction & 0xffff_fc1f == 0xd63f_0000 { + let address = self.reg(rn, false)?.checked_sub(IMAGE)?; + if self.calls.len() >= max_calls || !executable_address(self.layout, address) { + return None; + } + self.calls.push(address); + self.registers[..19].fill(None); + self.registers[30] = Some(next); + self.flags = None; + } else if instruction & 0xfc00_0000 == 0x1400_0000 { + next = branch_target(pc, instruction, 26, 0)?; + } else if instruction & 0xff00_0010 == 0x5400_0000 { + if self.condition(instruction & 15)? { + next = branch_target(pc, instruction, 19, 5)?; + } + } else if instruction & 0x7e00_0000 == 0x3400_0000 { + let nonzero = self.reg(rd, false)? & mask(width) != 0; + if nonzero == (instruction & (1 << 24) != 0) { + next = branch_target(pc, instruction, 19, 5)?; + } + } else if instruction & 0x9f00_0000 == 0x9000_0000 { + self.set(rd, aarch64_adrp_target(instruction, pc)?, 64, false); + } else if instruction & 0x1f80_0000 == 0x1280_0000 { + let shift = ((instruction >> 21) & 3) * 16; + if shift >= width { + return None; + } + let immediate = u64::from((instruction >> 5) & 0xffff) << shift; + let value = match (instruction >> 29) & 3 { + 0 => !immediate, + 2 => immediate, + 3 => (self.reg(rd, false)? & !(0xffff_u64 << shift)) | immediate, + _ => return None, + }; + self.set(rd, value, width, false); + } else if instruction & 0x1f00_0000 == 0x1100_0000 + || instruction & 0x1f20_0000 == 0x0b00_0000 + { + let immediate = instruction & 0x1f00_0000 == 0x1100_0000; + let left = self.reg(rn, immediate)? & mask(width); + let right = if immediate { + u64::from((instruction >> 10) & 0xfff) + << (if instruction & (1 << 22) != 0 { 12 } else { 0 }) + } else { + if (instruction >> 22) & 3 != 0 { + return None; + } + let shift = (instruction >> 10) & 63; + if shift >= width { + return None; + } + (self.reg((instruction >> 16) & 31, false)? << shift) & mask(width) + }; + let subtract = instruction & (1 << 30) != 0; + let set_flags = instruction & (1 << 29) != 0; + let value = if subtract { + left.wrapping_sub(right) + } else { + left.wrapping_add(right) + } & mask(width); + if set_flags { + let sign = 1_u64 << (width - 1); + let carry = if subtract { + left >= right + } else { + u128::from(left) + u128::from(right) > u128::from(mask(width)) + }; + let overflow = if subtract { + (left ^ right) & (left ^ value) & sign != 0 + } else { + !(left ^ right) & (left ^ value) & sign != 0 + }; + self.flags = Some((value & sign != 0, value == 0, carry, overflow)); + } + self.set(rd, value, width, immediate && !set_flags); + } else if instruction & 0x1f80_0000 == 0x1200_0000 + || instruction & 0x1f20_0000 == 0x0a00_0000 + { + let left = self.reg(rn, false)?; + let right = if instruction & 0x1f80_0000 == 0x1200_0000 { + logical_immediate(instruction, width)? + } else { + if (instruction >> 22) & 3 != 0 { + return None; + } + let shift = (instruction >> 10) & 63; + if shift >= width { + return None; + } + self.reg((instruction >> 16) & 31, false)? << shift + }; + let value = match (instruction >> 29) & 3 { + 0 => left & right, + 1 => left | right, + 2 => left ^ right, + _ => return None, + }; + self.set(rd, value, width, false); + } else if instruction & 0x7f80_0000 == 0x5300_0000 { + let rotate = (instruction >> 16) & 63; + let end = (instruction >> 10) & 63; + if rotate >= width || end >= width || (instruction >> 22) & 1 != width / 64 { + return None; + } + let source = self.reg(rn, false)? & mask(width); + let value = if rotate <= end { + (source >> rotate) & mask(end - rotate + 1) + } else { + (source & mask(end + 1)) << (width - rotate) + }; + self.set(rd, value, width, false); + } else if instruction & 0x3b00_0000 == 0x3900_0000 && instruction & (1 << 26) == 0 { + let size = 1_usize << (instruction >> 30); + let address = self + .reg(rn, true)? + .checked_add(u64::from((instruction >> 10) & 0xfff) * size as u64)?; + match (instruction >> 22) & 3 { + 0 => self.write(address, size, self.reg(rd, false)?)?, + 1 => { + let value = self.read(address, size)?; + self.set(rd, value, if size == 8 { 64 } else { 32 }, false); + } + _ => return None, + } + } else { + return None; + } + pc = next; + } + None + } +} + +pub(super) fn executable_address(layout: &ElfLayout, address: u64) -> bool { + address.is_multiple_of(4) + && layout.program_headers.iter().any(|segment| { + segment.flags & 1 != 0 + && address >= segment.virtual_address + && address + .checked_add(4) + .zip(segment.virtual_address.checked_add(segment.file_size)) + .is_some_and(|(end, limit)| end <= limit) + }) +} + +pub(super) fn callbacks( + module: &[u8], + entry: u32, + image: &[u8], + layout: &ElfLayout, + max_calls: usize, +) -> Result> { + let mut matches = Vec::new(); + for function in selector_functions(module, entry)? { + let mut registers = [None; 31]; + registers[0] = Some(RUNTIME); + registers[30] = Some(RETURN); + let selector = Selector { + module, + image, + layout, + registers, + sp: STACK + STACK_SIZE as u64, + stack: vec![None; STACK_SIZE], + flags: None, + read_image_base: false, + calls: Vec::new(), + }; + if let Some(calls) = selector.run(function, max_calls) { + matches.push(calls); + } + } + match matches.len() { + 1 => Ok(matches.pop().unwrap()), + count => invalid(format!( + "constructor module entry reaches {count} validated callback selectors" + )), + } +} diff --git a/senbei-engine/src/android/restore/constructors_tests.rs b/senbei-engine/src/android/restore/constructors_tests.rs new file mode 100644 index 0000000..b52a2e5 --- /dev/null +++ b/senbei-engine/src/android/restore/constructors_tests.rs @@ -0,0 +1,180 @@ +use super::*; +use senbei_elf::LoadSegment; + +fn fixture() -> (Vec, Vec, ElfLayout) { + let mut module = vec![0; 0x2000]; + // Synthetic selector: check every primary, then call either all primary + // or all fallback offsets. Bit 1 disables a record's instruction check; + // null primaries are skipped. The entry reaches it through a direct call. + let code: [u32; 41] = [ + 0xd10043ff, // sub sp, sp, #16 + 0xf90003fe, // str x30, [sp] + 0xf9400413, // ldr x19, [x0, #8] + 0xb0000014, // adrp x20, table page + 0x91200294, // add x20, x20, #0x800 + 0xb9400295, // ldr w21, [x20] (count) + 0x91002294, // add x20, x20, #8 (records) + 0x52800016, // mov w22, #0 (fallback) + 0x52800017, // mov w23, #0 (index) + 0x6b1502ff, // cmp w23, w21 + 0x540001e2, // b.hs checked + 0x8b171298, // add x24, x20, x23, lsl #4 + 0xb9400300, // ldr w0, [x24] + 0x34000140, // cbz w0, check_next + 0xb9400f01, // ldr w1, [x24, #12] + 0x121f0021, // and w1, w1, #2 + 0x350000e1, // cbnz w1, check_next + 0x8b000260, // add x0, x19, x0 + 0xb9400000, // ldr w0, [x0] + 0xb9400b01, // ldr w1, [x24, #8] + 0x6b01001f, // cmp w0, w1 + 0x54000040, // b.eq check_next + 0x52800036, // mov w22, #1 + 0x110006f7, // add w23, w23, #1 + 0x17fffff1, // b check + 0x52800017, // mov w23, #0 + 0x6b1502ff, // cmp w23, w21 + 0x54000142, // b.hs done + 0x8b171298, // add x24, x20, x23, lsl #4 + 0xb9400300, // ldr w0, [x24] + 0x340000a0, // cbz w0, invoke_next + 0x34000056, // cbz w22, primary + 0xb9400700, // ldr w0, [x24, #4] + 0x8b000260, // add x0, x19, x0 + 0xd63f0000, // blr x0 + 0x110006f7, // add w23, w23, #1 + 0x17fffff6, // b invoke + 0x52800020, // mov w0, #1 + 0xf94003fe, // ldr x30, [sp] + 0x910043ff, // add sp, sp, #16 + 0xd65f03c0, // ret + ]; + put(&mut module, 0x40, 0x9400_0030); + put(&mut module, 0x44, 0xd65f_03c0); + for (index, instruction) in code.into_iter().enumerate() { + put(&mut module, 0x100 + index * 4, instruction); + } + put(&mut module, 0x1800, 2); + for (index, record) in [[0x4400, 0x4500, 0x1234, 0], [0x4200, 0x4520, 0x5678, 0]] + .into_iter() + .enumerate() + { + for (field, word) in record.into_iter().enumerate() { + put(&mut module, 0x1808 + index * 16 + field * 4, word); + } + } + let mut image = vec![0; 0x1000]; + put(&mut image, 0x400, 0x1234); + put(&mut image, 0x200, 0x5678); + 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: 0x4000, + file_size: 0x1000, + memory_size: 0x1000, + flags: 5, + alignment: 0x1000, + }], + section_headers: Vec::new(), + section_name_index: 0, + private_section_index: 0, + }; + (module, image, layout) +} + +fn put(data: &mut [u8], offset: usize, word: u32) { + data[offset..offset + 4].copy_from_slice(&word.to_le_bytes()); +} + +#[test] +fn callback_order_comes_from_execution_even_after_module_relocation() { + let (module, image, layout) = fixture(); + assert_eq!( + callbacks(&module, 0x40, &image, &layout, 2).unwrap(), + [0x4400, 0x4200] + ); + let mut relocated = vec![0; 0x5000]; + relocated.extend_from_slice(&module); + // Unreachable data and code cannot become another table candidate. + relocated[..module.len()].copy_from_slice(&module); + assert_eq!( + callbacks(&relocated, 0x5040, &image, &layout, 2).unwrap(), + [0x4400, 0x4200] + ); +} + +#[test] +fn one_instruction_mismatch_selects_the_entire_fallback_group() { + let (module, mut image, layout) = fixture(); + put(&mut image, 0x200, 0); + assert_eq!( + callbacks(&module, 0x40, &image, &layout, 2).unwrap(), + [0x4500, 0x4520] + ); +} + +#[test] +fn callback_flags_null_records_and_single_entries_follow_the_code() { + let (mut module, mut image, layout) = fixture(); + put(&mut image, 0x200, 0); + put(&mut module, 0x1824, 2); + assert_eq!( + callbacks(&module, 0x40, &image, &layout, 2).unwrap(), + [0x4400, 0x4200] + ); + put(&mut module, 0x1818, 0); + assert_eq!( + callbacks(&module, 0x40, &image, &layout, 2).unwrap(), + [0x4400] + ); + put(&mut module, 0x1800, 1); + assert_eq!( + callbacks(&module, 0x40, &image, &layout, 1).unwrap(), + [0x4400] + ); +} + +#[test] +fn unsupported_code_invalid_memory_and_excess_calls_are_rejected() { + let (module, image, layout) = fixture(); + assert!(callbacks(&module, 0x40, &image, &layout, 1).is_err()); + for (offset, instruction) in [ + (0x100, 0xd400_0001), // system call + (0x108, 0xf900_0413), // attempted write outside the stack + (0x150, 0x1400_0000), // nonterminating comparison loop + (0x1808, 0x9000), // primary is outside the image + (0x181c, 0x9000), // fallback is outside the image + (0x1800, u32::MAX), // count exceeds the module/table bounds + ] { + let mut broken = module.clone(); + put(&mut broken, offset, instruction); + let mut mismatched = image.clone(); + if offset == 0x181c { + put(&mut mismatched, 0x200, 0); + } + assert!( + callbacks(&broken, 0x40, &mismatched, &layout, 2).is_err(), + "offset {offset:x}" + ); + } + assert!(callbacks(&module[..0x1820], 0x40, &image, &layout, 2).is_err()); + assert!(callbacks(&module, 0xffff_ffff, &image, &layout, 2).is_err()); +} + +#[test] +fn multiple_reachable_selectors_are_not_silently_chosen() { + let (mut module, image, layout) = fixture(); + module.copy_within(0x100..0x1a4, 0x300); + put(&mut module, 0x44, 0x9400_00af); // BL 0x300 + put(&mut module, 0x48, 0xd65f_03c0); + assert!( + callbacks(&module, 0x40, &image, &layout, 2) + .unwrap_err() + .to_string() + .contains("2 validated") + ); +} diff --git a/senbei-engine/src/android/restore/mod.rs b/senbei-engine/src/android/restore/mod.rs index a979e45..1bc0bf4 100644 --- a/senbei-engine/src/android/restore/mod.rs +++ b/senbei-engine/src/android/restore/mod.rs @@ -1,4 +1,5 @@ mod artifact; +mod constructors; mod error; mod pipeline; diff --git a/senbei-engine/src/android/restore/pipeline.rs b/senbei-engine/src/android/restore/pipeline.rs index 031bb9a..2a1097a 100644 --- a/senbei-engine/src/android/restore/pipeline.rs +++ b/senbei-engine/src/android/restore/pipeline.rs @@ -728,7 +728,7 @@ fn aarch64_writes_w1_immediate(instruction: u32) -> bool { ) } -fn aarch64_adrp_target(instruction: u32, pc: u64) -> Option { +pub(super) fn aarch64_adrp_target(instruction: u32, pc: u64) -> Option { if instruction & 0x9f00_0000 != 0x9000_0000 { return None; } @@ -926,7 +926,11 @@ struct ConstructorProfile { fini_addend: u64, } -fn virtual_address_to_file_offset(layout: &ElfLayout, address: u64, size: u64) -> Option { +pub(super) fn virtual_address_to_file_offset( + layout: &ElfLayout, + address: u64, + size: u64, +) -> Option { let end = address.checked_add(size)?; for segment in &layout.program_headers { let segment_end = segment.virtual_address.checked_add(segment.file_size)?; @@ -940,131 +944,91 @@ fn virtual_address_to_file_offset(layout: &ElfLayout, address: u64, size: u64) - 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(); - 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 +// The bootstrap saves the incoming linker register frame, calls Stage 1, +// restores that frame and returns. Validate this at the ELF init relocation; +// e_entry may be zero for a shared library, so it cannot identify the slot. +fn is_stage1_constructor(source: &[u8], layout: &ElfLayout, address: u64) -> bool { + let Some(offset) = virtual_address_to_file_offset(layout, address, 0x84) else { + return false; + }; + for pair in 0..15_u32 { + let first = pair * 2 + 1; + let save = 0xa9bf_03e0 | ((first + 1) << 10) | first; + let restore = 0xa8c1_03e0 | ((first + 1) << 10) | first; + if read_u32(source, offset + pair as usize * 4).ok() != Some(save) + || read_u32(source, offset + 0x44 + (14 - pair) as usize * 4).ok() != Some(restore) { - continue; - } - 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 { - let record = base + 8 + index * RECORD_SIZE; - 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 - || 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); - } - if valid - && constructors.windows(2).all(|pair| pair[0] < pair[1]) - && constructors.iter().copied().collect::>().len() == count - { - matches.push(constructors); + return false; } } - match matches.len() { - 1 => Ok(matches.pop()), - 0 => Ok(None), - count => invalid(format!( - "constructor control module has {count} validated suppression tables" - )), - } + read_u32(source, offset + 0x3c).ok() == Some(0x9100_03e0) + && read_u32(source, offset + 0x40) + .ok() + .and_then(|instruction| aarch64_bl_target(instruction, address + 0x40)) + .is_some_and(|target| super::constructors::executable_address(layout, target)) + && read_u32(source, offset + 0x80).ok() == Some(AARCH64_RET) } fn constructor_profile( - module: &[u8], + module: (&[u8], u32), source: &[u8], + protected: &[u8], layout: &ElfLayout, init_array: SectionHeader, fini_array: SectionHeader, old_dyn: RelocationTable<'_>, -) -> Result> { +) -> 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 callbacks = super::constructors::callbacks(module.0, module.1, source, layout, init_count)?; let init_end = init_array .address .checked_add(init_array.size) .ok_or_else(|| Error::Invalid("init-array range overflow".to_owned()))?; - let existing = old_dyn.collect_where(|relocation| { + let mut 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 { + existing.sort_by_key(|relocation| relocation.offset); + if existing.len() != init_count { + return invalid("init-array does not have one relocation per slot"); + } + let mut init_addends = Vec::new(); + let mut bootstraps = 0; + for (slot, relocation) in existing.iter().enumerate() { + if relocation.offset != init_array.address + slot as u64 * 8 + || relocation.kind() != R_AARCH64_RELATIVE + || relocation.symbol() != 0 + || relocation.addend < 0 + { + return invalid("init-array contains an invalid relocation"); + } + let address = relocation.addend as u64; + if is_stage1_constructor(protected, layout, address) { + bootstraps += 1; + init_addends.extend_from_slice(&callbacks); + } else { + if !super::constructors::executable_address(layout, address) { + return invalid("init-array callback is outside executable segments"); + } + let offset = virtual_address_to_file_offset(layout, address, 4).ok_or_else(|| { + Error::Invalid("init-array callback is not file-backed".to_owned()) + })?; + // The linker still calls the unsuppressed entries after Stage 1 + // returns. RET-only placeholders have no initialization effect. + if read_u32(source, offset)? != AARCH64_RET { + init_addends.push(address); + } + } + } + if bootstraps != 1 { return invalid(format!( - "expected one unsuppressed constructor near the suppression table, found {}", - extra.len() + "init-array has {bootstraps} validated Stage 1 callbacks" )); } - 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() - )); + return invalid("restored constructors exceed the init-array capacity"); } let fini = old_dyn.collect_where(|relocation| relocation.offset == fini_array.address)?; if fini.len() != 1 @@ -1074,10 +1038,10 @@ fn constructor_profile( { return invalid("fini-array relocation is not a single RELATIVE record"); } - Ok(Some(ConstructorProfile { + Ok(ConstructorProfile { init_addends, fini_addend: fini[0].addend as u64, - })) + }) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1628,7 +1592,7 @@ fn materialize_static_elf_tables( symbol_patch_data: &[u8], auxiliary_data: &[u8], auxiliary_symbol_seed: Option, - constructor_module: Option<&[u8]>, + constructor_module: Option<(&[u8], u32)>, ) -> Result<(ElfLayout, ElfMaterializationReport, u64)> { let names = layout.section_names(source)?; let indices = required_section_indices(&names)?; @@ -1841,14 +1805,9 @@ fn materialize_static_elf_tables( })?; 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(), - ) - })?; + let profile = constructor_profile( + module, output, source, layout, init_array, fini_array, old_dyn, + )?; Some((profile, init_index, fini_index, init_array, fini_array)) } None => None, @@ -2421,7 +2380,17 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result { .transpose()?; let constructor_module = artifacts .get(&0x98) - .map(|artifact| read_file(&artifact.path)) + .map(|artifact| -> Result<_> { + let entry = artifact + .entry_offset + .filter(|entry| *entry != 0) + .ok_or_else(|| { + Error::Invalid( + "constructor module 0x98 lacks its descriptor entry offset".to_owned(), + ) + })?; + Ok((read_file(&artifact.path)?, entry)) + }) .transpose()?; let config = Module9bConfig::parse(&module)?; @@ -2574,7 +2543,9 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result { &symbol_patch_data, &decoded, auxiliary_symbol_seed, - constructor_module.as_deref(), + constructor_module + .as_ref() + .map(|(image, entry)| (image.as_slice(), *entry)), )?; restored_layout = new_layout; materialization = Some(report); @@ -2932,6 +2903,134 @@ mod tests { assert_eq!(plt_call_features(&image, &layout, 0x2000).unwrap().calls, 2); } + #[test] + fn constructor_profile_replaces_the_bootstrap_and_preserves_linker_order() { + let mut module = vec![0_u8; 0x100]; + // Entry calls a selector that dispatches image + 0x400. + for (index, instruction) in [0x9400_0010_u32, 0xd65f_03c0].into_iter().enumerate() { + module[index * 4..index * 4 + 4].copy_from_slice(&instruction.to_le_bytes()); + } + for (index, instruction) in [ + 0xd100_43ff_u32, // SUB SP, SP, #16 + 0xf900_03fe, // STR LR, [SP] + 0xf940_0413, // LDR X19, [X0, #8] + 0x5280_8000, // MOV W0, #0x400 + 0x8b00_0260, // ADD X0, X19, X0 + 0xd63f_0000, // BLR X0 + 0x5280_0020, // MOV W0, #1 + 0xf940_03fe, // LDR LR, [SP] + 0x9100_43ff, // ADD SP, SP, #16 + 0xd65f_03c0, // RET + ] + .into_iter() + .enumerate() + { + module[0x40 + index * 4..0x44 + index * 4].copy_from_slice(&instruction.to_le_bytes()); + } + let mut protected = vec![0_u8; 0x8000]; + for pair in 0..15_u32 { + let first = pair * 2 + 1; + for (offset, instruction) in [ + ( + 0x1000 + pair as usize * 4, + 0xa9bf_03e0 | ((first + 1) << 10) | first, + ), + ( + 0x1044 + (14 - pair) as usize * 4, + 0xa8c1_03e0 | ((first + 1) << 10) | first, + ), + ] { + protected[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes()); + } + } + for (offset, instruction) in [ + (0x103c, 0x9100_03e0_u32), + (0x1040, 0x9400_0070), + (0x1080, AARCH64_RET), + ] { + protected[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes()); + } + let mut restored = protected.clone(); + restored[0x1000..0x1084].fill(0); + restored[0x2000..0x2004].copy_from_slice(&AARCH64_RET.to_le_bytes()); + restored[0x6000..0x6004].copy_from_slice(&0xd503_201f_u32.to_le_bytes()); + let layout = ElfLayout { + entrypoint: 0, + program_header_offset: 0, + program_header_size: 0x38, + program_header_count: 1, + program_headers: vec![LoadSegment { + offset: 0, + virtual_address: 0, + file_size: 0x8000, + memory_size: 0x8000, + flags: PF_R | 1, + alignment: 0x1000, + }], + section_headers: Vec::new(), + section_name_index: 0, + private_section_index: 0, + }; + let init = SectionHeader { + name: 0, + section_type: 14, + flags: 3, + address: 0x7000, + offset: 0x7000, + size: 24, + link: 0, + info: 0, + alignment: 8, + entry_size: 8, + }; + let fini = SectionHeader { + address: 0x7100, + offset: 0x7100, + size: 8, + ..init + }; + for (original, expected) in [ + ([0x1000, 0x6000, 0x2000], vec![0x400, 0x6000]), + ([0x6000, 0x1000, 0x2000], vec![0x6000, 0x400]), + ] { + let mut data = Vec::new(); + for (slot, addend) in original.into_iter().enumerate() { + Rela { + offset: 0x7000 + slot as u64 * 8, + info: u64::from(R_AARCH64_RELATIVE), + addend, + } + .encode(&mut data); + } + Rela { + offset: 0x7100, + info: u64::from(R_AARCH64_RELATIVE), + addend: 0x500, + } + .encode(&mut data); + let table = RelocationTable { + data: &data, + offset: 0, + count: 4, + remap: None, + }; + let profile = constructor_profile( + (&module, 0), + &restored, + &protected, + &layout, + init, + fini, + table, + ) + .unwrap(); + assert_eq!(profile.init_addends, expected); + assert_eq!(profile.fini_addend, 0x500); + } + protected[0x1000] ^= 1; + assert!(!is_stage1_constructor(&protected, &layout, 0x1000)); + } + #[test] fn classifies_special_plt_slots_from_call_shapes() { let candidates = [ @@ -2986,47 +3085,4 @@ mod tests { ] ); } - - #[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 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); - } }