19 Commits
Author SHA1 Message Date
bfloat16 366a19c2a1 fix(android): adjust the parsing of construction tables 2026-10-26 19:05:37 +08:00
bfloat16 ba1dacc0b2 fix(android): fix seed references 2026-10-25 20:46:41 +08:00
Momoko-Ayase 31a0c4139c bump: 1.3.3 2026-10-24 21:33:59 +08:00
Momoko-Ayase ea92d26d28 fix(windows): support legacy PE32 payload layouts
Handle fully encrypted PE32 payloads and compact shell, stage, and metadata layouts. Trial-decrypt each table candidate before selecting it, restoring the header and stage ciphertext between attempts.

Add synthetic regressions for full and prefix encryption, false table candidates, rollback, metadata placement, and malformed ranges. Preserve existing corpus outputs.
2026-10-24 20:40:23 +08:00
bfloat16 6e233bbf18 fix(windows): preserve companion export tables
Guard PE32 export copies when the companion omits plaintext stub data. Reserve the complete export directory during compaction and validate file backing before overlaying it to prevent writes into the next section.

Add regression coverage for zero-filled export space, invalid directory ranges, and truncated source or destination sections.
2026-10-08 23:49:36 +08:00
Momoko-Ayase 5bd1b0b07d chore: bump version to 1.3.2 2026-09-27 14:12:42 +08:00
Momoko-Ayase 01fdc58d2b Remove CLAUDE.md symlink
Starting Claude Code 2.1.277 if no CLAUDE.md it will check for AGENTS.md.
2026-09-27 14:05:04 +08:00
Momoko-Ayase f7d2d8b7e1 Merge pull request #8 from y52en/chatgpt/android-runtime-restoration-v2
fix(android): restore protected IL2CPP runtime state
2026-09-27 13:54:37 +08:00
y52 7728c6e7c2 refactor(android): remove sample-specific runtime fingerprint 2026-09-27 13:52:44 +08:00
y52 921cd4bfb4 fix(android): address runtime restoration review gaps 2026-09-27 13:52:44 +08:00
y52 53f31d2210 fix(android): restore newer runtime protection profiles 2026-09-27 13:52:44 +08:00
y52 9083c60d45 fix(android): restore protected runtime ELF state 2026-09-27 13:52:44 +08:00
Momoko-Ayase 25939c9738 Merge PR #7: restore IL2CPP 24.1 method indices 2026-09-20 10:04:49 +08:00
Momoko-Ayase 2501aabc57 Merge PR #6: support SysV-only ELF hash tables 2026-09-20 10:04:23 +08:00
Momoko-Ayase da06b4f5fd Merge PR #5: preserve program headers when extending crowded ELFs 2026-09-20 10:03:48 +08:00
y52 b355d8ea8f fix(android): carry v24 runtime context across package scans 2026-09-19 06:10:41 +00:00
y52 539861acbd fix(elf): relocate crowded program header tables 2026-09-18 19:36:03 +00:00
y52 a45cc8aa90 fix(metadata): restore IL2CPP 24.1 method indices 2026-09-17 21:07:18 +00:00
y52 d4613305fd fix(android): support SysV-only ELF hash tables 2026-09-17 18:12:36 +00:00
24 changed files with 3748 additions and 371 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
AGENTS.md
Generated
+7 -7
View File
@@ -402,7 +402,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-cli" name = "senbei-cli"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"senbei-engine", "senbei-engine",
"senbei-io", "senbei-io",
@@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-crypto" name = "senbei-crypto"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"aes", "aes",
"thiserror", "thiserror",
@@ -421,7 +421,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-elf" name = "senbei-elf"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"goblin", "goblin",
"thiserror", "thiserror",
@@ -429,7 +429,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-engine" name = "senbei-engine"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"memmap2", "memmap2",
"senbei-crypto", "senbei-crypto",
@@ -444,7 +444,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-io" name = "senbei-io"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"indicatif", "indicatif",
@@ -465,7 +465,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-metadata" name = "senbei-metadata"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@@ -473,7 +473,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-pe" name = "senbei-pe"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
+1 -1
View File
@@ -15,7 +15,7 @@ exclude = ["senbei-wasm"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "1.3.1" version = "1.3.3"
edition = "2024" edition = "2024"
rust-version = "1.98.1" rust-version = "1.98.1"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
+362 -33
View File
@@ -5,6 +5,8 @@ pub const SHT_STRTAB: u32 = 3;
pub const SHT_LOUSER: u32 = 0x8000_0000; pub const SHT_LOUSER: u32 = 0x8000_0000;
pub const SHF_ALLOC: u64 = 2; pub const SHF_ALLOC: u64 = 2;
const PT_LOAD: u32 = 1; const PT_LOAD: u32 = 1;
const PT_NOTE: u32 = 4;
const PT_PHDR: u32 = 6;
pub const PF_R: u32 = 4; pub const PF_R: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -220,7 +222,89 @@ impl ElfLayout {
Ok(alignment) Ok(alignment)
} }
pub fn append_load_segment(&self, output: &mut [u8], segment: LoadSegment) -> Result<Self> { pub fn additional_program_header_reservation(&self) -> Result<usize> {
if self.program_header_size != 0x38 {
return invalid("unexpected ELF program header size");
}
let new_count = self
.program_header_count
.checked_add(1)
.ok_or_else(|| Error::Invalid("program header count overflow".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()))?;
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_none_or(|offset| header_end as u64 <= offset) {
return Ok(0);
}
new_count
.checked_mul(self.program_header_size)
.ok_or_else(|| Error::Invalid("relocated program header size overflow".to_owned()))
}
fn has_program_header_type(&self, data: &[u8], program_type: u32) -> Result<bool> {
for index in 0..self.program_header_count {
let offset =
checked_index(self.program_header_offset, index, self.program_header_size)?;
if read_u32(data, offset)? == program_type {
return Ok(true);
}
}
Ok(false)
}
fn reusable_note_program_header(&self, data: &[u8]) -> Result<Option<usize>> {
let mut note_count = 0_usize;
let mut candidates = Vec::new();
for index in 0..self.program_header_count {
let offset =
checked_index(self.program_header_offset, index, self.program_header_size)?;
if read_u32(data, offset)? != PT_NOTE {
continue;
}
note_count += 1;
let file_offset = read_u64(data, offset + 8)?;
let file_size = read_u64(data, offset + 0x20)?;
let file_end = file_offset
.checked_add(file_size)
.ok_or_else(|| Error::Invalid("PT_NOTE file range overflow".to_owned()))?;
let contained = self.program_headers.iter().any(|segment| {
segment
.offset
.checked_add(segment.file_size)
.is_some_and(|segment_end| {
segment.offset <= file_offset && file_end <= segment_end
})
});
if contained {
candidates.push((file_offset, index));
}
}
if note_count < 2 {
return Ok(None);
}
Ok(candidates
.into_iter()
.max_by_key(|(file_offset, _)| *file_offset)
.map(|(_, index)| index))
}
pub fn append_load_segment(
&self,
output: &mut [u8],
segment: LoadSegment,
reserved_program_header_bytes: usize,
) -> Result<Self> {
if self.program_header_size != 0x38 { if self.program_header_size != 0x38 {
return invalid("unexpected ELF program header size"); return invalid("unexpected ELF program header size");
} }
@@ -274,23 +358,11 @@ impl ElfLayout {
.ok_or_else(|| Error::Invalid("program header count overflow".to_owned()))?; .ok_or_else(|| Error::Invalid("program header count overflow".to_owned()))?;
let new_count_u16 = u16::try_from(new_count) let new_count_u16 = u16::try_from(new_count)
.map_err(|_| Error::Invalid("program header count exceeds u16".to_owned()))?; .map_err(|_| Error::Invalid("program header count exceeds u16".to_owned()))?;
let header_offset = checked_index( let reservation = self.additional_program_header_reservation()?;
self.program_header_offset, if reserved_program_header_bytes != reservation {
self.program_header_count, return invalid(format!(
self.program_header_size, "program-header reservation mismatch: expected 0x{reservation:x}, got 0x{reserved_program_header_bytes:x}"
)?; ));
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]; let mut header = [0_u8; 0x38];
@@ -302,17 +374,95 @@ impl ElfLayout {
header[0x20..0x28].copy_from_slice(&segment.file_size.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[0x28..0x30].copy_from_slice(&segment.memory_size.to_le_bytes());
header[0x30..0x38].copy_from_slice(&segment.alignment.to_le_bytes()); header[0x30..0x38].copy_from_slice(&segment.alignment.to_le_bytes());
output
.get_mut(header_offset..header_end) let old_table_size = self
.ok_or_else(|| Error::Invalid("new program header exceeds output".to_owned()))? .program_header_count
.copy_from_slice(&header); .checked_mul(self.program_header_size)
output .ok_or_else(|| Error::Invalid("program header table size overflow".to_owned()))?;
.get_mut(0x38..0x3a) let new_table_size = new_count
.ok_or_else(|| Error::Invalid("ELF header is truncated".to_owned()))? .checked_mul(self.program_header_size)
.copy_from_slice(&new_count_u16.to_le_bytes()); .ok_or_else(|| Error::Invalid("program header table size overflow".to_owned()))?;
let mut updated_program_header_offset = self.program_header_offset;
let mut updated_program_header_count = new_count;
if reservation == 0 {
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()))?;
output
.get_mut(header_offset..header_end)
.ok_or_else(|| Error::Invalid("new program header exceeds output".to_owned()))?
.copy_from_slice(&header);
update_pt_phdr(
output,
self.program_header_offset,
self.program_header_count,
self.program_header_size,
None,
new_table_size as u64,
)?;
} else if let Some(note_index) = self.reusable_note_program_header(output)? {
let header_offset = checked_index(
self.program_header_offset,
note_index,
self.program_header_size,
)?;
let header_end = header_offset
.checked_add(self.program_header_size)
.ok_or_else(|| Error::Invalid("reused program header range overflow".to_owned()))?;
output
.get_mut(header_offset..header_end)
.ok_or_else(|| Error::Invalid("reused program header exceeds output".to_owned()))?
.copy_from_slice(&header);
updated_program_header_count = self.program_header_count;
} else {
if !self.has_program_header_type(output, PT_PHDR)? {
return invalid(
"cannot safely relocate program headers without PT_PHDR or a redundant PT_NOTE",
);
}
if reservation != new_table_size {
return invalid("relocated program-header reservation has unexpected size");
}
if segment.file_size < reservation as u64 {
return invalid("new PT_LOAD is too small for relocated program headers");
}
let relocated = usize_from_u64(segment.offset, "relocated program header offset")?;
let relocated_end = relocated.checked_add(new_table_size).ok_or_else(|| {
Error::Invalid("relocated program header range overflow".to_owned())
})?;
slice(output, relocated, new_table_size)?;
let old_table = slice(output, self.program_header_offset, old_table_size)?.to_vec();
output[relocated..relocated + old_table_size].copy_from_slice(&old_table);
output[relocated + old_table_size..relocated_end].copy_from_slice(&header);
output
.get_mut(0x20..0x28)
.ok_or_else(|| Error::Invalid("ELF header is truncated".to_owned()))?
.copy_from_slice(&segment.offset.to_le_bytes());
update_pt_phdr(
output,
relocated,
self.program_header_count,
self.program_header_size,
Some((segment.offset, segment.virtual_address)),
new_table_size as u64,
)?;
updated_program_header_offset = relocated;
}
if updated_program_header_count == new_count {
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(); let mut updated = self.clone();
updated.program_header_count = new_count; updated.program_header_offset = updated_program_header_offset;
updated.program_header_count = updated_program_header_count;
updated.program_headers.push(segment); updated.program_headers.push(segment);
Ok(updated) Ok(updated)
} }
@@ -394,6 +544,47 @@ impl ElfLayout {
} }
} }
fn update_pt_phdr(
output: &mut [u8],
table_offset: usize,
old_count: usize,
entry_size: usize,
relocated: Option<(u64, u64)>,
table_size: u64,
) -> Result<()> {
for index in 0..old_count {
let offset = checked_index(table_offset, index, entry_size)?;
if read_u32(output, offset)? != PT_PHDR {
continue;
}
if let Some((file_offset, virtual_address)) = relocated {
output
.get_mut(offset + 8..offset + 0x10)
.ok_or_else(|| Error::Invalid("PT_PHDR file offset exceeds output".to_owned()))?
.copy_from_slice(&file_offset.to_le_bytes());
output
.get_mut(offset + 0x10..offset + 0x18)
.ok_or_else(|| Error::Invalid("PT_PHDR virtual address exceeds output".to_owned()))?
.copy_from_slice(&virtual_address.to_le_bytes());
output
.get_mut(offset + 0x18..offset + 0x20)
.ok_or_else(|| {
Error::Invalid("PT_PHDR physical address exceeds output".to_owned())
})?
.copy_from_slice(&virtual_address.to_le_bytes());
}
output
.get_mut(offset + 0x20..offset + 0x28)
.ok_or_else(|| Error::Invalid("PT_PHDR file size exceeds output".to_owned()))?
.copy_from_slice(&table_size.to_le_bytes());
output
.get_mut(offset + 0x28..offset + 0x30)
.ok_or_else(|| Error::Invalid("PT_PHDR memory size exceeds output".to_owned()))?
.copy_from_slice(&table_size.to_le_bytes());
}
Ok(())
}
pub fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> { pub fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
let end = offset let end = offset
.checked_add(size) .checked_add(size)
@@ -585,6 +776,7 @@ mod tests {
flags: PF_R, flags: PF_R,
alignment: 0x1000, alignment: 0x1000,
}, },
0,
) )
.expect("append segment"); .expect("append segment");
assert_eq!(updated.program_header_count, 1); assert_eq!(updated.program_header_count, 1);
@@ -594,7 +786,7 @@ mod tests {
} }
#[test] #[test]
fn append_load_segment_rejects_program_header_overlap() { fn append_load_segment_fails_closed_when_no_safe_phdr_slot() {
let mut elf_layout = ElfLayout { let mut elf_layout = ElfLayout {
entrypoint: 0, entrypoint: 0,
program_header_offset: 0, program_header_offset: 0,
@@ -617,20 +809,157 @@ mod tests {
alignment: 1, alignment: 1,
entry_size: 0, entry_size: 0,
}); });
let mut output = vec![0_u8; 0x100]; let reservation = elf_layout
.additional_program_header_reservation()
.expect("reservation");
assert_eq!(reservation, 0x38);
let mut output = vec![0_u8; 0x200];
let error = elf_layout let error = elf_layout
.append_load_segment( .append_load_segment(
&mut output, &mut output,
LoadSegment { LoadSegment {
offset: 0x80, offset: 0x80,
virtual_address: 0x1080, virtual_address: 0x1080,
file_size: 0x20, file_size: 0x80,
memory_size: 0x20, memory_size: 0x80,
flags: PF_R, flags: PF_R,
alignment: 0x1000, alignment: 0x1000,
}, },
reservation,
) )
.expect_err("overlapping program header"); .expect_err("unsafe relocation must fail");
assert!(error.to_string().contains("additional program header")); assert!(error.to_string().contains("cannot safely relocate"));
}
#[test]
fn append_load_segment_reuses_redundant_note_when_no_slack() {
let mut elf_layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0x40,
program_header_size: 0x38,
program_header_count: 3,
program_headers: vec![LoadSegment {
offset: 0,
virtual_address: 0,
file_size: 0x400,
memory_size: 0x400,
flags: PF_R,
alignment: 0x1000,
}],
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: 0xe8,
size: 1,
link: 0,
info: 0,
alignment: 1,
entry_size: 0,
});
let mut output = vec![0_u8; 0x2000];
output[0x38..0x3a].copy_from_slice(&3_u16.to_le_bytes());
output[0x40..0x44].copy_from_slice(&PT_LOAD.to_le_bytes());
output[0x78..0x7c].copy_from_slice(&PT_NOTE.to_le_bytes());
output[0x80..0x88].copy_from_slice(&0x100_u64.to_le_bytes());
output[0x98..0xa0].copy_from_slice(&0x20_u64.to_le_bytes());
output[0xb0..0xb4].copy_from_slice(&PT_NOTE.to_le_bytes());
output[0xb8..0xc0].copy_from_slice(&0x200_u64.to_le_bytes());
output[0xd0..0xd8].copy_from_slice(&0x20_u64.to_le_bytes());
let reservation = elf_layout
.additional_program_header_reservation()
.expect("reservation");
assert_eq!(reservation, 0xe0);
let updated = elf_layout
.append_load_segment(
&mut output,
LoadSegment {
offset: 0x1000,
virtual_address: 0x2000,
file_size: 0x100,
memory_size: 0x100,
flags: PF_R,
alignment: 0x1000,
},
reservation,
)
.expect("reuse redundant PT_NOTE");
assert_eq!(updated.program_header_offset, 0x40);
assert_eq!(updated.program_header_count, 3);
assert_eq!(updated.program_headers.len(), 2);
assert_eq!(&output[0x38..0x3a], &3_u16.to_le_bytes());
assert_eq!(&output[0x78..0x7c], &PT_NOTE.to_le_bytes());
assert_eq!(&output[0xb0..0xb4], &PT_LOAD.to_le_bytes());
assert_eq!(&output[0xb8..0xc0], &0x1000_u64.to_le_bytes());
assert_eq!(&output[0xc0..0xc8], &0x2000_u64.to_le_bytes());
}
#[test]
fn relocated_program_headers_update_pt_phdr() {
let mut elf_layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0x40,
program_header_size: 0x38,
program_header_count: 1,
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: 0x78,
size: 1,
link: 0,
info: 0,
alignment: 1,
entry_size: 0,
});
let mut output = vec![0_u8; 0x2000];
output[0x20..0x28].copy_from_slice(&0x40_u64.to_le_bytes());
output[0x40..0x44].copy_from_slice(&PT_PHDR.to_le_bytes());
output[0x48..0x50].copy_from_slice(&0x40_u64.to_le_bytes());
output[0x50..0x58].copy_from_slice(&0x40_u64.to_le_bytes());
output[0x58..0x60].copy_from_slice(&0x40_u64.to_le_bytes());
output[0x60..0x68].copy_from_slice(&0x38_u64.to_le_bytes());
output[0x68..0x70].copy_from_slice(&0x38_u64.to_le_bytes());
let reservation = elf_layout
.additional_program_header_reservation()
.expect("reservation");
assert_eq!(reservation, 0x70);
let updated = elf_layout
.append_load_segment(
&mut output,
LoadSegment {
offset: 0x1000,
virtual_address: 0x2000,
file_size: 0x100,
memory_size: 0x100,
flags: PF_R,
alignment: 0x1000,
},
reservation,
)
.expect("relocate program headers");
assert_eq!(updated.program_header_offset, 0x1000);
assert_eq!(&output[0x20..0x28], &0x1000_u64.to_le_bytes());
assert_eq!(&output[0x1000..0x1004], &PT_PHDR.to_le_bytes());
assert_eq!(&output[0x1008..0x1010], &0x1000_u64.to_le_bytes());
assert_eq!(&output[0x1010..0x1018], &0x2000_u64.to_le_bytes());
assert_eq!(&output[0x1020..0x1028], &0x70_u64.to_le_bytes());
assert_eq!(&output[0x1028..0x1030], &0x70_u64.to_le_bytes());
assert_eq!(&output[0x1038..0x103c], &PT_LOAD.to_le_bytes());
} }
} }
+7 -10
View File
@@ -15,26 +15,23 @@ pub use layout::{
/// ELF machine identifier for AArch64. /// ELF machine identifier for AArch64.
pub const AARCH64_MACHINE: u16 = EM_AARCH64; pub const AARCH64_MACHINE: u16 = EM_AARCH64;
/// Dynamic sections required by the restored AArch64 loader image. /// Dynamic sections required by every restored AArch64 loader image.
pub const DYNAMIC_SECTION_NAMES: [&str; 8] = [ pub const DYNAMIC_SECTION_NAMES: [&str; 7] = [
".dynsym", ".dynsym",
".gnu.version", ".gnu.version",
".gnu.version_r", ".gnu.version_r",
".gnu.hash",
".dynstr", ".dynstr",
".rela.dyn", ".rela.dyn",
".rela.plt", ".rela.plt",
".dynamic", ".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. /// Dynamic sections needed to identify a protected image before extraction.
pub const PROBE_SECTION_NAMES: [&str; 5] = [ /// Hash tables are checked separately because either GNU or SysV hashing is valid.
".dynsym", pub const PROBE_SECTION_NAMES: [&str; 4] = [".dynsym", ".dynstr", ".gnu.version", ".gnu.version_r"];
".dynstr",
".gnu.hash",
".gnu.version",
".gnu.version_r",
];
/// ELF64 dynamic table record sizes. /// ELF64 dynamic table record sizes.
pub const ELF64_SYMBOL_SIZE: usize = 0x18; pub const ELF64_SYMBOL_SIZE: usize = 0x18;
@@ -70,6 +70,16 @@ pub(crate) fn inspect(
return invalid(format!("protected ELF lacks required section {wanted}")); 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_index, section) = matches[0];
let section_offset = usize::try_from(section.sh_offset) let section_offset = usize::try_from(section.sh_offset)
.map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?; .map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?;
+16 -2
View File
@@ -6,11 +6,17 @@ use serde_json::Value;
use super::error::{Error, Result, invalid}; use super::error::{Error, Result, invalid};
const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e]; const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e];
const OPTIONAL_IDS: [u32; 2] = [0x96, 0x98];
fn wanted_id(command_id: u32) -> bool {
REQUIRED_IDS.contains(&command_id) || OPTIONAL_IDS.contains(&command_id)
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Artifact { pub(crate) struct Artifact {
pub path: PathBuf, pub path: PathBuf,
pub size: u64, pub size: u64,
pub entry_offset: Option<u32>,
} }
pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact>> { pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact>> {
@@ -27,7 +33,7 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
}; };
let command_id = u32::try_from(command_id) let command_id = u32::try_from(command_id)
.map_err(|_| Error::Invalid("module command ID exceeds u32".to_owned()))?; .map_err(|_| Error::Invalid("module command ID exceeds u32".to_owned()))?;
if !REQUIRED_IDS.contains(&command_id) { if !wanted_id(command_id) {
continue; continue;
} }
let Some(path) = item.get("image_path").and_then(Value::as_str) else { let Some(path) = item.get("image_path").and_then(Value::as_str) else {
@@ -42,6 +48,10 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
Artifact { Artifact {
path: root.join(path), path: root.join(path),
size, size,
entry_offset: item
.get("entry_offset")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok()),
}, },
); );
} }
@@ -57,7 +67,7 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
}; };
let command_id = u32::try_from(command_id) let command_id = u32::try_from(command_id)
.map_err(|_| Error::Invalid("record command ID exceeds u32".to_owned()))?; .map_err(|_| Error::Invalid("record command ID exceeds u32".to_owned()))?;
if !REQUIRED_IDS.contains(&command_id) { if !wanted_id(command_id) {
continue; continue;
} }
let Some(image) = record.get("image") else { let Some(image) = record.get("image") else {
@@ -74,6 +84,10 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
Artifact { Artifact {
path: root.join(path), path: root.join(path),
size, size,
entry_offset: record
.get("entry_offset")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok()),
}, },
); );
} }
@@ -0,0 +1,418 @@
//! Evaluate the callback selector reached from the handoff module entry.
//!
//! This is a bounded interpreter for the selector's integer/control-flow
//! instructions, not a native module loader. Only stack writes are permitted;
//! image calls are recorded, never executed. Unknown instructions or memory
//! accesses reject a candidate. Table offsets, ordering, flags and the group
//! fallback decision consequently come from the module's own code.
use std::collections::{BTreeSet, HashSet};
use senbei_elf::{ElfLayout, read_u32, slice_u64};
use super::error::{Result, invalid};
use super::pipeline::{aarch64_adrp_target, virtual_address_to_file_offset};
const RUNTIME: u64 = 1 << 32;
const IMAGE: u64 = 2 << 32;
const STACK: u64 = 3 << 32;
const STACK_SIZE: usize = 0x10000;
const RETURN: u64 = u64::MAX - 3;
#[cfg(test)]
#[path = "constructors_tests.rs"]
mod tests;
fn signed(value: u32, bits: u32) -> i64 {
i64::from((value << (32 - bits)) as i32 >> (32 - bits))
}
fn branch_target(pc: u64, instruction: u32, bits: u32, shift: u32) -> Option<u64> {
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<BTreeSet<u64>> {
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<u64> {
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<u64>; 31],
sp: u64,
stack: Vec<Option<u8>>,
flags: Option<(bool, bool, bool, bool)>,
read_image_base: bool,
calls: Vec<u64>,
}
impl Selector<'_> {
fn reg(&self, index: u32, sp: bool) -> Option<u64> {
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<u64> {
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::<Option<Vec<_>>>()?
} 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<bool> {
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<Vec<u64>> {
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<Vec<u64>> {
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"
)),
}
}
@@ -0,0 +1,180 @@
use super::*;
use senbei_elf::LoadSegment;
fn fixture() -> (Vec<u8>, Vec<u8>, 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")
);
}
+1
View File
@@ -1,4 +1,5 @@
mod artifact; mod artifact;
mod constructors;
mod error; mod error;
mod pipeline; mod pipeline;
File diff suppressed because it is too large. Load diff
+182 -13
View File
@@ -104,6 +104,45 @@ impl<'a> Unpacker<'a> {
} }
} }
fn load_payload(&mut self) -> Option<u32> {
self.decrypt_size = self.info[6].wrapping_sub(self.info[3]).wrapping_add(8192);
self.decrypt_data2();
let src_start = self.info[4]
.wrapping_add(4096)
.wrapping_add(self.decrypt_size) as usize;
let dst_start = self.info[3].wrapping_add(self.decrypt_size) as usize;
let len = self.info[5].wrapping_sub(self.decrypt_size) as usize;
self.decompressed[dst_start..dst_start + len]
.copy_from_slice(&self.file_data[src_start..src_start + len]);
write_u32(&mut self.decompressed, self.info[3], 4096);
self.decompressed[..4096].copy_from_slice(&self.file_data[..4096]);
let pe = get_u32(self.file_data, 60);
let is_pe32 = get_u16(self.file_data, pe.wrapping_add(24)) == 0x10B;
let mut table = if is_pe32 {
self.find_pe32_table()
} else {
None
};
// Older PE32 loaders encrypt the entire payload; the length also seeds
// the cipher, so the prefix-size key cannot decode even their shell.
if is_pe32 && self.info[5] > self.decrypt_size && table.is_none() {
let prefix_size = self.decrypt_size;
let start = self.info[3] as usize;
let end = start + self.info[5] as usize;
let backup = self.decompressed[start..end].to_vec();
self.decrypt_size = self.info[5];
self.decrypt_data2();
write_u32(&mut self.decompressed, self.info[3], 4096);
table = self.find_pe32_table();
if table.is_none() {
self.decompressed[start..end].copy_from_slice(&backup);
self.decrypt_size = prefix_size;
}
}
table
}
// Strategy (a): delegate to primitives::decrypt_data3 // Strategy (a): delegate to primitives::decrypt_data3
fn decrypt_data3(&mut self, pos: u32, key: u32, shift: u32) { fn decrypt_data3(&mut self, pos: u32, key: u32, shift: u32) {
primitives::decrypt_data3(&mut self.decompressed, pos, key, shift); primitives::decrypt_data3(&mut self.decompressed, pos, key, shift);
@@ -351,28 +390,18 @@ impl<'a> Unpacker<'a> {
}); });
} }
u.decompressed = vec![0u8; size_of_image as usize]; u.decompressed = vec![0u8; size_of_image as usize];
u.decrypt_size = u.info[6].wrapping_sub(u.info[3]).wrapping_add(8192);
if verbose { if verbose {
println!("[2/9] Decrypting payload..."); println!("[2/9] Decrypting payload...");
} }
u.decrypt_data2(); let pe32_table = u.load_payload();
{
let src_start = u.info[4].wrapping_add(4096).wrapping_add(u.decrypt_size) as usize;
let dst_start = u.info[3].wrapping_add(u.decrypt_size) as usize;
let len = u.info[5].wrapping_sub(u.decrypt_size) as usize;
u.decompressed[dst_start..dst_start + len]
.copy_from_slice(&u.file_data[src_start..src_start + len]);
}
write_u32(&mut u.decompressed, u.info[3], 4096);
u.decompressed[..4096].copy_from_slice(&u.file_data[..4096]);
// PE32 (32-bit) images use an entirely different config layout and // PE32 (32-bit) images use an entirely different config layout and
// final-output transform than PE32+ (64-bit). Dispatch here, after the // final-output transform than PE32+ (64-bit). Dispatch here, after the
// shared header/payload setup (the PE32 branch follows the common // shared header/payload setup (the PE32 branch follows the common
// Stage 1/2 work). pe_magic at pe_off+24: 0x10B = PE32, 0x20B = PE32+. // Stage 1/2 work). pe_magic at pe_off+24: 0x10B = PE32, 0x20B = PE32+.
if get_u16(u.file_data, pe_off.wrapping_add(24)) == 0x10B { if get_u16(u.file_data, pe_off.wrapping_add(24)) == 0x10B {
return u.run_pe32(pe_off, verbose); let table = pe32_table.ok_or(UnpackError::Pe32TblNotFound)?;
return u.run_pe32(pe_off, table, verbose);
} }
// The config block layout in the decrypted region varies between Crackproof // The config block layout in the decrypted region varies between Crackproof
@@ -1992,6 +2021,146 @@ impl<'a> Unpacker<'a> {
} }
} }
#[cfg(test)]
mod payload_tests {
use super::*;
fn check_payload(full: bool, valid_table: bool) {
let info = [
0x1234_5678,
0x4E4E_4F4B,
0,
0x2000,
0x200,
0x7000,
0x2400,
0,
];
let mut plain = vec![0u8; 0x10000];
let table = 0x2800;
let (shell_field, header_field) = if full { (0x78, 0x50) } else { (0x88, 0x58) };
write_u32(&mut plain, table + shell_field, info[6]);
write_u32(&mut plain, table + shell_field + 4, 0x1800);
write_u32(&mut plain, table + header_field, 0x80);
write_u32(
&mut plain,
table + header_field + 4,
if valid_table { 0x58 } else { 0 },
);
let ss_size = if full { 0xAEC } else { 0xBC0 };
write_u32(&mut plain, table + shell_field + 0x10, 0x6000);
write_u32(&mut plain, table + shell_field + 0x14, ss_size);
write_u32(&mut plain, table + shell_field + 0x20, info[6]);
write_u32(&mut plain, table + shell_field + 0x24, 0x1800);
write_u32(&mut plain, table + 0x40, 0x9ABC_DEF0);
let third_pair = 0x6000 + if full { 0xAB4 } else { 0xB8C };
write_u32(&mut plain, third_pair, 0x8000);
write_u32(&mut plain, third_pair + 4, 0x20);
if full {
// This earlier table has plausible descriptors but a wrong stage key.
for (field, value) in [
(0x40, 0x1122_3344),
(0x50, 0x80),
(0x54, 0x58),
(0x78, info[6]),
(0x7C, 0x1800),
(0x88, 0x7000),
(0x8C, 0xAEC),
(0x98, info[6]),
(0x9C, 0x1800),
] {
write_u32(&mut plain, 0x2500 + field, value);
}
}
write_u32(&mut plain, 0x8100, 0xAABB_CCDD);
let encrypted_size = if full {
info[5]
} else {
info[6] - info[3] + 8192
};
let src = 4096 + info[4];
let mut input = vec![0u8; (src + info[5]) as usize];
write_u32(&mut input, 0x3C, 0x80);
write_u16(&mut input, 0x98, 0x10B);
plain[..4096].copy_from_slice(&input[..4096]);
let mut key = primitives::calculate_checksum(&plain, table + header_field)
^ primitives::calculate_checksum(&plain, table + shell_field + 0x20)
^ get_u32(&plain, table + 0x40);
for i in 0..ss_size / 4 {
let word = get_u32(&plain, 0x6000 + i * 4);
write_u32(
&mut plain,
0x6000 + i * 4,
word.wrapping_add(i).rotate_left(21) ^ key,
);
key = key.wrapping_add(i);
}
let mut key = info[0].wrapping_add(!encrypted_size);
for i in 0..info[5] / 4 {
let word = get_u32(&plain, info[3] + i * 4);
let cell = if i * 4 < encrypted_size {
word ^ key
} else {
word
};
write_u32(&mut input, src + i * 4, cell);
key = i.wrapping_mul(i) ^ key.wrapping_add(cell).wrapping_add(i);
}
let mut unpacker = Unpacker {
file_data: &input,
decompressed: vec![0u8; plain.len()],
info,
key_offsets: [0; 4],
decrypt_size: 0,
};
let selected = unpacker.load_payload();
assert_eq!(selected, valid_table.then_some(table));
if !valid_table {
let prefix_size = info[6] - info[3] + 8192;
assert_eq!(unpacker.decrypt_size, prefix_size);
assert_eq!(
get_u32(&unpacker.decompressed, info[3] + prefix_size - 4),
{
let mut key = info[0].wrapping_add(!prefix_size);
let mut word = 0;
for i in 0..prefix_size / 4 {
let cell = get_u32(&input, src + i * 4);
word = key ^ cell;
key = i.wrapping_mul(i) ^ key.wrapping_add(cell).wrapping_add(i);
}
word
}
);
assert_eq!(
&unpacker.decompressed
[(info[3] + prefix_size) as usize..(info[3] + info[5]) as usize],
&input[(src + prefix_size) as usize..(src + info[5]) as usize]
);
return;
}
assert_eq!(unpacker.decrypt_size, encrypted_size);
assert_eq!(
&unpacker.decompressed[info[3] as usize + 4..(info[3] + info[5]) as usize],
&plain[info[3] as usize + 4..(info[3] + info[5]) as usize]
);
}
#[test]
fn pe32_full_payload_encryption_is_trial_validated() {
check_payload(true, true);
}
#[test]
fn pe32_prefix_payload_encryption_preserves_plaintext_tail() {
check_payload(false, true);
}
#[test]
fn pe32_invalid_full_payload_trial_restores_prefix() {
check_payload(true, false);
}
}
#[cfg(test)] #[cfg(test)]
mod error_tests { mod error_tests {
use super::*; use super::*;
+220 -112
View File
@@ -23,10 +23,153 @@ fn stage_key_rounds(data: &[u8], table: u32, slots: usize) -> Result<u32, Unpack
} }
impl<'a> Unpacker<'a> { impl<'a> Unpacker<'a> {
pub(super) fn find_pe32_table(&mut self) -> Option<u32> {
let pe_off = get_u32(self.file_data, 60);
let header = self.decompressed[..4096].to_vec();
for tbl in layout::find_tbl_pe32_candidates(&self.decompressed, &self.info) {
let compact = get_u32(&self.decompressed, tbl + 0x78) == self.info[6];
let pair = tbl + if compact { 0x88 } else { 0x98 };
let start = get_u32(&self.decompressed, pair) as usize;
let size = get_u32(&self.decompressed, pair + 4) as usize;
let Some(end) = start
.checked_add(size)
.filter(|&end| end <= self.decompressed.len())
else {
continue;
};
let ciphertext = self.decompressed[start..end].to_vec();
let valid = self.decrypt_pe32_second_stage(tbl, pe_off).is_ok();
self.decompressed[start..end].copy_from_slice(&ciphertext);
self.decompressed[..4096].copy_from_slice(&header);
if valid {
return Some(tbl);
}
}
None
}
fn decrypt_pe32_second_stage(&mut self, tbl: u32, pe_off: u32) -> Result<u32, UnpackError> {
let compact = get_u32(&self.decompressed, tbl.wrapping_add(0x78)) == self.info[6];
let header_cs_field = if compact { 0x50 } else { 0x58 };
let stage_field = if compact { 0x88 } else { 0x98 };
let resource_field = if compact { 0xB0 } else { 0xC8 };
if (pe_off as usize)
.checked_add(0xB8)
.is_none_or(|end| end > 0x1000)
{
return Err(UnpackError::Pe32RelocationDataNotFound);
}
let import_rva = get_u32(&self.decompressed, tbl + stage_field + 0x24);
let resource_rva = get_u32(&self.decompressed, tbl + resource_field);
let resource_size = get_u32(&self.decompressed, tbl + resource_field + 4);
write_u32(&mut self.decompressed, pe_off + 0x80, import_rva);
write_u32(&mut self.decompressed, pe_off + 0x88, resource_rva);
write_u32(&mut self.decompressed, pe_off + 0x8C, resource_size);
write_u32(&mut self.decompressed, pe_off + 0xB0, 0);
write_u32(&mut self.decompressed, pe_off + 0xB4, 0);
let cs = tbl + stage_field + 0x10;
let cs_start = get_u32(&self.decompressed, cs) as usize;
let cs_size = get_u32(&self.decompressed, cs + 4) as usize;
if cs_size == 0
|| cs_start
.checked_add(cs_size)
.is_none_or(|end| end > self.decompressed.len())
{
return Err(UnpackError::Pe32RelocationDataNotFound);
}
let first_stage_cs = self.calculate_checksum(cs);
let second_stage_key = get_u32(&self.decompressed, tbl + 0x40);
let ss_pair = tbl + stage_field;
let ss = get_u32(&self.decompressed, ss_pair);
let ss_size = get_u32(&self.decompressed, ss_pair + 4);
let ss_shift = ss_size.wrapping_sub(if compact { 0xAEC } else { 0xBC0 });
let third_pair_off = (if compact { 0xAB4u32 } else { 0xB8C }).wrapping_add(ss_shift);
let ss_lo = ss as usize;
let ss_hi = ss_lo.saturating_add(ss_size as usize);
if ss_hi > self.decompressed.len() || third_pair_off.saturating_add(8) > ss_size {
return Err(UnpackError::Pe32SecondStageRangeInvalid {
offset: ss,
size: ss_size,
image_len: self.decompressed.len(),
});
}
let ss_ct = self.decompressed[ss_lo..ss_hi].to_vec();
// Header checksums refer to the original PE header. Compact EXEs and
// native DLLs can carry a packer-added BaseReloc entry: trial both keys.
for zero_reloc in [false, true] {
if zero_reloc {
write_u32(&mut self.decompressed, pe_off + 0xA0, 0);
write_u32(&mut self.decompressed, pe_off + 0xA4, 0);
}
let mut hcs_addr = tbl + header_cs_field;
let mut header_checksum = 0;
loop {
if hcs_addr + 8 > tbl + stage_field - 0x10 {
return Err(UnpackError::Pe32RelocationDataNotFound);
}
let size = get_u32(&self.decompressed, hcs_addr + 4) as usize;
if size == 0 {
break;
}
let start = get_u32(&self.decompressed, hcs_addr) as usize;
if start.checked_add(size).is_none_or(|end| end > 0x1000) {
return Err(UnpackError::Pe32RelocationDataNotFound);
}
header_checksum ^= self.calculate_checksum(hcs_addr);
hcs_addr += 8;
}
self.decompressed[ss_lo..ss_hi].copy_from_slice(&ss_ct);
self.decrypt_data3(
ss_pair,
header_checksum ^ first_stage_cs ^ second_stage_key,
21,
);
let pair = ss + third_pair_off;
let off = get_u32(&self.decompressed, pair) as usize;
let size = get_u32(&self.decompressed, pair + 4) as usize;
if off > 0x1000
&& size >= 4
&& off
.checked_add(size)
.is_some_and(|end| end <= self.decompressed.len())
{
return Ok(header_checksum);
}
}
Err(UnpackError::Pe32RelocationDataNotFound)
}
fn read_pe32_metadata(&mut self, compact: bool) -> (u32, [u8; 128]) {
let base = self.info[3];
let layout_b = get_u32(&self.decompressed, base.wrapping_add(0x10)) > 0x10000;
let (offset, size, ep_offset, dirs_offset) = if layout_b {
// Compact metadata has an extra 16-byte prefix before the EP/dirs.
let shift = if compact { 0x10 } else { 0 };
(0x10, 0x290, 0x20 + shift, 0x30 + shift)
} else {
(0x40, 144, 0x40, 0x50)
};
let start = base.wrapping_add(offset) as usize;
let backup = self.decompressed[start..start + size].to_vec();
self.decrypt_data5(base.wrapping_add(offset), size as u32);
let ep = get_u32(&self.decompressed, base.wrapping_add(ep_offset));
let mut dirs = [0u8; 128];
let dirs_start = base.wrapping_add(dirs_offset) as usize;
dirs.copy_from_slice(&self.decompressed[dirs_start..dirs_start + 128]);
self.decompressed[start..start + size].copy_from_slice(&backup);
(ep, dirs)
}
/// PE32 (32-bit) unpack pipeline. The shared Stage 1/2 setup (info decrypt, /// PE32 (32-bit) unpack pipeline. The shared Stage 1/2 setup (info decrypt,
/// payload decrypt, raw copy, header restore) has already run in `run()` /// payload decrypt, raw copy, header restore) has already run in `run()`
/// before dispatch; this takes over from "Locating shell offsets". /// before dispatch; this takes over from "Locating shell offsets".
pub(super) fn run_pe32(&mut self, pe_off: u32, verbose: bool) -> Result<Vec<u8>, UnpackError> { pub(super) fn run_pe32(
&mut self,
pe_off: u32,
tbl: u32,
verbose: bool,
) -> Result<Vec<u8>, UnpackError> {
let info = self.info; let info = self.info;
let info3 = info[3]; let info3 = info[3];
@@ -43,94 +186,21 @@ impl<'a> Unpacker<'a> {
key key
}; };
// ---- Locate tbl in shell ---- // ---- Validated shell table ----
let tbl = let compact = get_u32(&self.decompressed, tbl.wrapping_add(0x78)) == info[6];
layout::find_tbl_pe32(&self.decompressed, &info).ok_or(UnpackError::Pe32TblNotFound)?; let stage_field = if compact { 0x88 } else { 0x98 };
if verbose { if verbose {
println!("[3/9] Locating config layout (PE32)..."); println!("[3/9] Locating config layout (PE32)...");
println!(" tbl = 0x{:X}", tbl); println!(" tbl = 0x{:X}, compact = {}", tbl, compact);
} }
// ---- PE header restore ----
let val_bc = get_u32(&self.decompressed, tbl.wrapping_add(0xBC));
let val_c8 = get_u32(&self.decompressed, tbl.wrapping_add(0xC8));
let val_cc = get_u32(&self.decompressed, tbl.wrapping_add(0xCC));
write_u32(&mut self.decompressed, pe_off.wrapping_add(0x80), val_bc);
write_u32(&mut self.decompressed, pe_off.wrapping_add(0x88), val_c8);
write_u32(&mut self.decompressed, pe_off.wrapping_add(0x8C), val_cc);
write_u32(&mut self.decompressed, pe_off.wrapping_add(0xB0), 0);
write_u32(&mut self.decompressed, pe_off.wrapping_add(0xB4), 0);
// ---- Header-independent checksum inputs ----
let first_stage_cs = self.calculate_checksum(tbl.wrapping_add(0xA8));
let second_stage_key = get_u32(&self.decompressed, tbl.wrapping_add(0x40));
// ---- Stage 3: SecondStage ---- // ---- Stage 3: SecondStage ----
// let ss_pair = tbl.wrapping_add(stage_field);
// ss_key = headerChecksum ^ firstStageCS ^ secondStageKey, where the
// header checksum (a XOR of crc32(region)^size over the sub-regions at
// tbl+0x58) is taken over the *original* pre-pack PE header. For EXEs the
// import/resource restore above reconstructs that header exactly. Native
// DLLs additionally carry a packer-added BaseReloc data-directory entry
// (dir 5) that was absent from the checksummed original, so the header
// checksum only matches once that entry is treated as zero. EXEs have no
// dir-5 entry, so zeroing it is a no-op for them.
//
// Rather than branch on EXE-vs-DLL, try the header as-is and, on failure,
// with the BaseReloc entry zeroed; keep whichever ss_key decrypts a
// SecondStage whose ThirdStage (off,size) pair lands inside the image.
// This uses the same shift/key trial-and-validate the later stages
// already use, and keeps EXE output byte-identical (the as-is variant
// wins first).
let ss_pair = tbl.wrapping_add(0x98);
let ss = get_u32(&self.decompressed, ss_pair); let ss = get_u32(&self.decompressed, ss_pair);
let ss_size = get_u32(&self.decompressed, ss_pair.wrapping_add(4)); let ss_size = get_u32(&self.decompressed, ss_pair.wrapping_add(4));
let ss_shift = ss_size.wrapping_sub(0xBC0); let ss_shift = ss_size.wrapping_sub(if compact { 0xAEC } else { 0xBC0 });
// Back up the SecondStage ciphertext so a failed trial can be retried. let third_pair_off = (if compact { 0xAB4u32 } else { 0xB8C }).wrapping_add(ss_shift);
let ss_lo = ss as usize; let header_checksum = self.decrypt_pe32_second_stage(tbl, pe_off)?;
let ss_hi = ss_lo.wrapping_add(ss_size as usize);
if ss_hi < ss_lo || ss_hi > self.decompressed.len() {
return Err(UnpackError::Pe32SecondStageRangeInvalid {
offset: ss,
size: ss_size,
image_len: self.decompressed.len(),
});
}
let ss_ct: Vec<u8> = self.decompressed[ss_lo..ss_hi].to_vec();
// PE32 data dir 5 (BaseReloc) = optional_header(pe+24) + 0x60 + 5*8 = pe+0xA0.
let reloc_dir = pe_off.wrapping_add(0xA0);
let len = self.decompressed.len() as u64;
let pair_off = 0xB8Cu32.wrapping_add(ss_shift);
let mut found = false;
// Holds the winning variant's header checksum; the later stages
// (Forth/Fifth/Seven/Eighth) reuse it as a key component.
let mut header_checksum: u32 = 0;
for zero_reloc in [false, true] {
if zero_reloc {
write_u32(&mut self.decompressed, reloc_dir, 0);
write_u32(&mut self.decompressed, reloc_dir.wrapping_add(4), 0);
}
let mut hcs_addr = tbl.wrapping_add(0x58);
header_checksum = 0;
while get_u32(&self.decompressed, hcs_addr.wrapping_add(4)) != 0 {
header_checksum ^= self.calculate_checksum(hcs_addr);
hcs_addr = hcs_addr.wrapping_add(8);
}
let ss_key = header_checksum ^ first_stage_cs ^ second_stage_key;
self.decompressed[ss_lo..ss_hi].copy_from_slice(&ss_ct);
self.decrypt_data3(ss_pair, ss_key, 21);
// Validate: the ThirdStage (off,size) pair must reference the image.
let pair = ss.wrapping_add(pair_off);
let off = get_u32(&self.decompressed, pair) as u64;
let sz = get_u32(&self.decompressed, pair.wrapping_add(4)) as u64;
if off > 0x1000 && off < len && sz >= 4 && off.saturating_add(sz) <= len {
found = true;
break;
}
}
if !found {
return Err(UnpackError::Pe32RelocationDataNotFound);
}
if verbose { if verbose {
println!( println!(
" ss = 0x{:08X}, size = 0x{:X}, shift = 0x{:X}", " ss = 0x{:08X}, size = 0x{:X}, shift = 0x{:X}",
@@ -139,13 +209,12 @@ impl<'a> Unpacker<'a> {
} }
// ---- PE32 fixed offsets ---- // ---- PE32 fixed offsets ----
let third_key_off = 0x968u32.wrapping_add(ss_shift); let third_key_off = (if compact { 0x950u32 } else { 0x968 }).wrapping_add(ss_shift);
let forth_key_off = 0x964u32.wrapping_add(ss_shift); let forth_key_off = (if compact { 0x94Cu32 } else { 0x964 }).wrapping_add(ss_shift);
let cs_base_off = 0x96Cu32.wrapping_add(ss_shift); let cs_base_off = (if compact { 0x954u32 } else { 0x96C }).wrapping_add(ss_shift);
let dp_base_off = 0xA9Cu32.wrapping_add(ss_shift); let dp_base_off = (if compact { 0x9C4u32 } else { 0xA9C }).wrapping_add(ss_shift);
// ---- Stage 4: ThirdStage (brute-force the rotate shift) ---- // ---- Stage 4: ThirdStage (brute-force the rotate shift) ----
let third_pair_off = 0xB8Cu32.wrapping_add(ss_shift);
let key = get_u32(&self.decompressed, ss.wrapping_add(third_key_off)); let key = get_u32(&self.decompressed, ss.wrapping_add(third_key_off));
let pair_addr = ss.wrapping_add(third_pair_off); let pair_addr = ss.wrapping_add(third_pair_off);
let ts_addr = get_u32(&self.decompressed, pair_addr); let ts_addr = get_u32(&self.decompressed, pair_addr);
@@ -238,7 +307,7 @@ impl<'a> Unpacker<'a> {
} }
// ---- Checksum addresses ---- // ---- Checksum addresses ----
let second_stage_cs_addr = tbl.wrapping_add(0xB0); let second_stage_cs_addr = tbl.wrapping_add(stage_field + 0x18);
let forth_stage_cs_addr = ss.wrapping_add(cs_base_off); let forth_stage_cs_addr = ss.wrapping_add(cs_base_off);
let fifth_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x08); let fifth_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x08);
let seven_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x10); let seven_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x10);
@@ -298,7 +367,9 @@ impl<'a> Unpacker<'a> {
); );
let seven_key = !get_u32( let seven_key = !get_u32(
&self.decompressed, &self.decompressed,
cs1_addr.wrapping_add(cs1_size).wrapping_sub(0x10), cs1_addr
.wrapping_add(cs1_size)
.wrapping_sub(if compact { 0xC } else { 0x10 }),
); );
let fk7 = header_checksum ^ fifth_cs ^ seven_key; let fk7 = header_checksum ^ fifth_cs ^ seven_key;
if let Err(reason) = self.decrypt_and_decompress_data(seven_addr, fk7, None) { if let Err(reason) = self.decrypt_and_decompress_data(seven_addr, fk7, None) {
@@ -640,28 +711,7 @@ impl<'a> Unpacker<'a> {
)?; )?;
// ---- PE32 metadata: EP and data dirs from info[3] ---- // ---- PE32 metadata: EP and data dirs from info[3] ----
let test_val = get_u32(&self.decompressed, info3.wrapping_add(0x10)); let (metadata_ep, metadata_dirs) = self.read_pe32_metadata(compact);
let metadata_ep: u32;
let mut metadata_dirs = [0u8; 128];
if test_val > 0x10000 {
// Layout B
let s = info3.wrapping_add(0x10) as usize;
let backup_meta = self.decompressed[s..s + 0x290].to_vec();
self.decrypt_data5(info3.wrapping_add(0x10), 0x290);
metadata_ep = get_u32(&self.decompressed, info3.wrapping_add(0x20));
let d = info3.wrapping_add(0x30) as usize;
metadata_dirs.copy_from_slice(&self.decompressed[d..d + 128]);
self.decompressed[s..s + 0x290].copy_from_slice(&backup_meta);
} else {
// Layout A
let s = info3.wrapping_add(0x40) as usize;
let backup_meta = self.decompressed[s..s + 144].to_vec();
self.decrypt_data5(info3.wrapping_add(0x40), 144);
metadata_ep = get_u32(&self.decompressed, info3.wrapping_add(0x40));
let d = info3.wrapping_add(0x50) as usize;
metadata_dirs.copy_from_slice(&self.decompressed[d..d + 128]);
self.decompressed[s..s + 144].copy_from_slice(&backup_meta);
}
// ---- Zero-out list (runs BEFORE decompression) ---- // ---- Zero-out list (runs BEFORE decompression) ----
let zero_list_addr = eighth_start.wrapping_add(off_zero_list); let zero_list_addr = eighth_start.wrapping_add(off_zero_list);
@@ -823,7 +873,15 @@ impl<'a> Unpacker<'a> {
let d = export_va as usize; let d = export_va as usize;
let s = export_file_off as usize; let s = export_file_off as usize;
let n = export_size as usize; let n = export_size as usize;
self.decompressed[d..d + n].copy_from_slice(&self.file_data[s..s + n]); // Spliced companions may omit the stub's plaintext exports;
// the I/O adapter overlays those from the original stub later.
if s.checked_add(n)
.is_some_and(|end| end <= self.file_data.len())
&& d.checked_add(n)
.is_some_and(|end| end <= self.decompressed.len())
{
self.decompressed[d..d + n].copy_from_slice(&self.file_data[s..s + n]);
}
} }
// ---- .text decrypt with decrypt_data8 (PE32 auto-detected formula) ---- // ---- .text decrypt with decrypt_data8 (PE32 auto-detected formula) ----
@@ -1149,6 +1207,56 @@ impl<'a> Unpacker<'a> {
mod tests { mod tests {
use super::*; use super::*;
fn check_metadata(compact: bool, layout_b: bool) {
let base = 0x1000u32;
let (offset, size, ep_offset, dirs_offset) = if layout_b {
if compact {
(0x10, 0x290, 0x30, 0x40)
} else {
(0x10, 0x290, 0x20, 0x30)
}
} else {
(0x40, 144, 0x40, 0x50)
};
let mut data = vec![0u8; 0x2000];
let ep = 0x123456;
let dirs = std::array::from_fn::<_, 128, _>(|i| i as u8 ^ 0x5A);
write_u32(&mut data, base + ep_offset, ep);
data[(base + dirs_offset) as usize..(base + dirs_offset + 128) as usize]
.copy_from_slice(&dirs);
for i in 0..size {
let key = (base + offset + i) as u8;
let byte = &mut data[(base + offset + i) as usize];
let mixed = (byte.rotate_right(2) ^ key).rotate_right(2) ^ key.wrapping_add(1);
*byte = mixed.rotate_right(2);
}
assert_eq!(get_u32(&data, base + 0x10) > 0x10000, layout_b);
let backup = data.clone();
let mut unpacker = Unpacker {
file_data: &[],
decompressed: data,
info: [0, 0, 0, base, 0, 0, 0, 0],
key_offsets: [0; 4],
decrypt_size: 0,
};
assert_eq!(unpacker.read_pe32_metadata(compact), (ep, dirs));
assert_eq!(
unpacker.decompressed, backup,
"metadata reads must be nondestructive"
);
}
#[test]
fn pe32_compact_metadata_restores_entry_point_and_directories() {
check_metadata(true, true);
}
#[test]
fn pe32_standard_metadata_preserves_both_layouts() {
check_metadata(false, false);
check_metadata(false, true);
}
#[test] #[test]
fn stage_key_rounds_follow_active_descriptor_prefix() { fn stage_key_rounds_follow_active_descriptor_prefix() {
for (sizes, expected) in [ for (sizes, expected) in [
+3 -2
View File
@@ -6,8 +6,9 @@ mod image;
pub(super) use dd8::{select_dd8_formula_pe32, select_dd8_shift}; pub(super) use dd8::{select_dd8_formula_pe32, select_dd8_shift};
pub(super) use discovery::{ pub(super) use discovery::{
discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos, find_tbl_pe32, discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos,
find_v_after_pad, find_v4_offset, get_string_to_null, section_name, trial_decrypt5_u32, find_tbl_pe32_candidates, find_v_after_pad, find_v4_offset, get_string_to_null, section_name,
trial_decrypt5_u32,
}; };
pub(super) use image::{ pub(super) use image::{
compact_memory_image_to_pe, move_pe32_imports_to_kmiat, pe32_imports_already_match_idata_layout, compact_memory_image_to_pe, move_pe32_imports_to_kmiat, pe32_imports_already_match_idata_layout,
+96 -28
View File
@@ -212,37 +212,54 @@ pub fn section_name(data: &[u8], offset: u32) -> String {
// PE32 (32-bit) helpers // PE32 (32-bit) helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// PE32 shell-table locator. Walks the shell region (`info[6]`) for a dword /// PE32 shell-table locator. The compact layout moves the shell descriptor
/// equal to `info[6]` followed by a plausible shell size, returning the table /// from +0x88 to +0x78 and the header-checksum list from +0x58 to +0x50.
/// base (`candidate = off - 0x88`) when `candidate+0x58` holds a valid pointer. /// Return structurally plausible candidates; the pipeline must trial-decrypt
pub fn find_tbl_pe32(data: &[u8], info: &[u32; 8]) -> Option<u32> { /// their second stages before selecting a table.
let shell = info[6]; pub fn find_tbl_pe32_candidates(data: &[u8], info: &[u32; 8]) -> Vec<u32> {
if (data.len() as u64) < 0x100 { let mut tables = Vec::new();
return None; let shell = info[6] as usize;
} let hi = shell.saturating_add(0x3000).min(data.len());
let hi = (shell as u64) for off in (shell..hi.saturating_sub(8)).step_by(4) {
.saturating_add(0x3000) if get_u32(data, off as u32) != info[6] {
.min(data.len() as u64 - 0x100) as u32; continue;
let mut off = shell; }
while off < hi { let shell_size = get_u32(data, off as u32 + 4) as usize;
if off as usize + 8 <= data.len() { if !(0x1001..0x100000).contains(&shell_size)
let candidate = off.wrapping_sub(0x88); || shell
if candidate >= shell && get_u32(data, off) == info[6] { .checked_add(shell_size)
let shell_size_val = get_u32(data, off.wrapping_add(4)); .is_none_or(|end| end > data.len())
if shell_size_val > 0x1000 && shell_size_val < 0x100000 { {
let v58_off = candidate.wrapping_add(0x58); continue;
if (v58_off as usize + 4) <= data.len() { }
let v58 = get_u32(data, v58_off); for (shell_field, header_field) in [(0x88, 0x58), (0x78, 0x50)] {
if v58 > 0 && (v58 as usize) < data.len() { let Some(candidate) = off.checked_sub(shell_field).filter(|&base| base >= shell) else {
return Some(candidate); continue;
} };
} let Some(table) = data.get(candidate..candidate + 0xD0) else {
} continue;
};
let header = get_u32(table, header_field) as usize;
let header_size = get_u32(table, header_field + 4) as usize;
let stage = get_u32(table, shell_field as u32 + 0x10) as usize;
let stage_size = get_u32(table, shell_field as u32 + 0x14) as usize;
if header > 0
&& header_size > 0
&& header
.checked_add(header_size)
.is_some_and(|end| end <= 0x1000)
&& stage >= info[3] as usize
&& stage_size >= 4
&& stage
.checked_add(stage_size)
.is_some_and(|end| end <= data.len())
&& !tables.contains(&(candidate as u32))
{
tables.push(candidate as u32);
} }
} }
off = off.wrapping_add(4);
} }
None tables
} }
/// Locate an LFSR-encrypted bytecode block (decrypt_data6 form) in a region. /// Locate an LFSR-encrypted bytecode block (decrypt_data6 form) in a region.
@@ -449,6 +466,57 @@ pub fn discover_eighth_slots(
mod tests { mod tests {
use super::*; use super::*;
fn pe32_table_fixture(compact: bool) -> (Vec<u8>, [u32; 8], u32) {
let mut data = vec![0u8; 0x10000];
let info = [0, 0, 0, 0x2000, 0, 0x7000, 0x2400, 0];
let table = 0x2800;
let (shell, header) = if compact { (0x78, 0x50) } else { (0x88, 0x58) };
for (field, value) in [
(header, 0x80),
(header + 4, 0x58),
(shell, info[6]),
(shell + 4, 0x1800),
(shell + 0x10, 0x6000),
(shell + 0x14, 0xAEC),
] {
senbei_crypto::primitives::write_u32(&mut data, table + field, value);
}
(data, info, table)
}
#[test]
fn pe32_table_supports_compact_header_descriptors() {
for compact in [false, true] {
let (data, info, table) = pe32_table_fixture(compact);
assert_eq!(find_tbl_pe32_candidates(&data, &info), vec![table]);
}
}
#[test]
fn pe32_table_rejects_a_shell_reference_without_valid_descriptors() {
let (mut data, info, table) = pe32_table_fixture(true);
for (field, value) in [(0x58, 0x80), (0x5C, 0x58), (0x88, info[6]), (0x8C, 0x1800)] {
senbei_crypto::primitives::write_u32(&mut data, 0x2500 + field, value);
}
assert_eq!(find_tbl_pe32_candidates(&data, &info), vec![table]);
senbei_crypto::primitives::write_u32(&mut data, table + 0x54, 0);
assert_eq!(find_tbl_pe32_candidates(&data, &info), Vec::<u32>::new());
}
#[test]
fn pe32_table_rejects_truncated_and_out_of_range_regions() {
let (mut data, mut info, table) = pe32_table_fixture(true);
assert_eq!(
find_tbl_pe32_candidates(&data[..0x28C0], &info),
Vec::<u32>::new()
);
senbei_crypto::primitives::write_u32(&mut data, table + 0x50, u32::MAX - 15);
assert_eq!(find_tbl_pe32_candidates(&data, &info), Vec::<u32>::new());
info[6] = u32::MAX;
assert_eq!(find_tbl_pe32_candidates(&data, &info), Vec::<u32>::new());
assert_eq!(find_tbl_pe32_candidates(&[], &info), Vec::<u32>::new());
}
/// Task 4.1 regression: build a synthetic buffer whose valid bytecode block /// Task 4.1 regression: build a synthetic buffer whose valid bytecode block
/// sits PAST `len` but within `len*2`. Assert that the smaller window misses /// sits PAST `len` but within `len*2`. Assert that the smaller window misses
/// it and the doubled window finds it. /// it and the doubled window finds it.
+57 -1
View File
@@ -331,6 +331,13 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
let opt_hdr = pe_header.wrapping_add(24); let opt_hdr = pe_header.wrapping_add(24);
let sec_table = opt_hdr.wrapping_add(opt_hdr_size); let sec_table = opt_hdr.wrapping_add(opt_hdr_size);
let num_sections = get_u16(data, pe_header.wrapping_add(6)) as u32; let num_sections = get_u16(data, pe_header.wrapping_add(6)) as u32;
let export_dir = opt_hdr.wrapping_add(if get_u16(data, opt_hdr) == 0x20B {
112
} else {
96
});
let export_rva = get_u32(data, export_dir);
let export_size = get_u32(data, export_dir.wrapping_add(4));
struct SecLayout { struct SecLayout {
sec_off: u32, sec_off: u32,
@@ -365,11 +372,21 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
break; break;
} }
} }
let meaningful = if last_nonzero >= 0 { let mut meaningful = if last_nonzero >= 0 {
(last_nonzero + 1) as u32 (last_nonzero + 1) as u32
} else { } else {
0 0
}; };
// Companion exports are overlaid after compaction. Keep their whole
// range file-backed even when the payload leaves it zero-filled.
if export_size != 0
&& let Some(end) = export_rva
.checked_sub(va)
.and_then(|delta| delta.checked_add(export_size))
&& end as usize <= section_data.len()
{
meaningful = meaningful.max(end);
}
let mut raw_size = if meaningful != 0 { let mut raw_size = if meaningful != 0 {
align_up_u32(meaningful, FILE_ALIGNMENT) align_up_u32(meaningful, FILE_ALIGNMENT)
} else { } else {
@@ -433,6 +450,45 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn compaction_keeps_export_space_without_changing_the_next_section() {
for (rva, size, expected_raw_size) in [
(0, 0, 0x200),
(0x1280, 0x181, 0x600),
(0x1f00, 0x200, 0x200),
(0xffff_fff0, 0x40, 0x200),
] {
let mut data = vec![0; 0x3000];
data[..2].copy_from_slice(b"MZ");
data[0x80..0x84].copy_from_slice(b"PE\0\0");
write_u16(&mut data, 0x86, 2);
write_u16(&mut data, 0x94, 0xe0);
write_u16(&mut data, 0x98, 0x10b);
for (offset, value) in [
(0x3c, 0x80),
(0xf8, rva),
(0xfc, size),
(0x180, 0x1000),
(0x184, 0x1000),
(0x1a8, 0x1000),
(0x1ac, 0x2000),
] {
write_u32(&mut data, offset, value);
}
data[0x1000] = 0xaa;
data[0x2000..0x200c].copy_from_slice(b"next section");
let out = compact_memory_image_to_pe(&data, 0x80).unwrap();
assert_eq!(get_u32(&out, 0x188), expected_raw_size);
let headers = senbei_pe::parse(&out).unwrap();
let next = senbei_pe::rva_range(&out, headers, 0x2000, 12).unwrap();
assert_eq!(&out[next], b"next section");
if rva == 0x1280 {
let exports = senbei_pe::rva_range(&out, headers, rva, size).unwrap();
assert!(out[exports].iter().all(|&byte| byte == 0));
}
}
}
/// Review regression: a zero last-section VA (corrupt section table) must /// Review regression: a zero last-section VA (corrupt section table) must
/// bail instead of building .kmiat at RVA 0 — the old code zeroed /// bail instead of building .kmiat at RVA 0 — the old code zeroed
/// `[0, 0x7000)`, wiping the DOS/PE headers, and returned the broken image /// `[0, 0x7000)`, wiping the DOS/PE headers, and returned the broken image
+192 -50
View File
@@ -1,6 +1,6 @@
//! Android target orchestration: protected AArch64 shared libraries (`.so`), //! Android target orchestration: protected AArch64 shared libraries (`.so`),
//! app packages (`.apk` / `.apks` / `.xapk`), and the Android variant of the //! app packages (`.apk` / `.apks` / `.xapk`), and the Android variant of the
//! il2cpp method-token obfuscation. //! il2cpp method-token/method-index obfuscation.
//! //!
//! The protection scheme hollows out an ELF64/AArch64 shared object and moves //! The protection scheme hollows out an ELF64/AArch64 shared object and moves
//! the original bytes into an encrypted payload appended as a `SHT_LOUSER` //! the original bytes into an encrypted payload appended as a `SHT_LOUSER`
@@ -100,14 +100,32 @@ pub fn file_content_identity(path: &Path) -> std::io::Result<String> {
/// implementation detail of the two-phase restore, not user-facing output). /// implementation detail of the two-phase restore, not user-facing output).
/// Returns the unwrapped embedded metadata blob when the restored image /// Returns the unwrapped embedded metadata blob when the restored image
/// carries one (see the module docs); the caller decides where to write it. /// carries one (see the module docs); the caller decides where to write it.
pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Option<Vec<u8>>> { struct SoRestoreContext {
embedded_metadata: Option<Vec<u8>>,
method_index_module: Option<Vec<u8>>,
}
fn restore_so_file_with_context(
input: &Path,
dest: &Path,
verbose: bool,
) -> Result<SoRestoreContext> {
let temporary = tempfile::tempdir().context("create stage-2 workspace")?; let temporary = tempfile::tempdir().context("create stage-2 workspace")?;
let stage2_dir = temporary.path().join("stage2"); let stage2_dir = temporary.path().join("stage2");
extract_stage2(&ExtractOptions::with_defaults( let extraction = extract_stage2(&ExtractOptions::with_defaults(
input.to_path_buf(), input.to_path_buf(),
stage2_dir.clone(), stage2_dir.clone(),
)) ))
.context("extract stage-1/stage-2 payload")?; .context("extract stage-1/stage-2 payload")?;
let method_index_module = extraction
.module_registry
.iter()
.find(|module| module.command_id == 0x0c)
.map(|module| {
std::fs::read(stage2_dir.join(&module.image_path))
.with_context(|| format!("read decoded module 0x0C `{}`", module.image_path))
})
.transpose()?;
restore_libil2cpp(&RestoreOptions { restore_libil2cpp(&RestoreOptions {
input: input.to_path_buf(), input: input.to_path_buf(),
output: dest.to_path_buf(), output: dest.to_path_buf(),
@@ -120,9 +138,28 @@ pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Optio
.context("restore protected library")?; .context("restore protected library")?;
let restored = let restored =
std::fs::read(dest).with_context(|| format!("read restored `{}`", dest.display()))?; std::fs::read(dest).with_context(|| format!("read restored `{}`", dest.display()))?;
Ok(senbei_metadata::android::extract_embedded_metadata( Ok(SoRestoreContext {
&restored, embedded_metadata: senbei_metadata::android::extract_embedded_metadata(&restored),
)) method_index_module,
})
}
pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Option<Vec<u8>>> {
let mut method_index_module = None;
restore_so_file_with_method_index_module(input, dest, verbose, &mut method_index_module)
}
pub(crate) fn restore_so_file_with_method_index_module(
input: &Path,
dest: &Path,
verbose: bool,
method_index_module: &mut Option<Vec<u8>>,
) -> Result<Option<Vec<u8>>> {
let restored = restore_so_file_with_context(input, dest, verbose)?;
if let Some(module) = restored.method_index_module {
*method_index_module = Some(module);
}
Ok(restored.embedded_metadata)
} }
/// Content identity for cross-source deduplication: the same library may /// Content identity for cross-source deduplication: the same library may
@@ -134,8 +171,10 @@ pub fn content_identity(data: &[u8]) -> String {
hex_digest(&digest.finalize()) hex_digest(&digest.finalize())
} }
/// Restore an il2cpp metadata blob (Android seeded permutation first, then the /// Restore an il2cpp metadata blob. Paired v24.1 Android packages use the
/// structural remap used by the Windows builds). /// method-index profile recovered from module 0x0C; later Android layouts use
/// the seeded MethodDef RID permutation before falling back to the structural
/// remap used by the Windows builds.
/// ///
/// The Android variant obfuscates MethodDef RIDs with a keyed five-round /// The Android variant obfuscates MethodDef RIDs with a keyed five-round
/// permutation; the correct seed is recovered by intersecting per-image key /// permutation; the correct seed is recovered by intersecting per-image key
@@ -145,6 +184,32 @@ pub fn content_identity(data: &[u8]) -> String {
/// canonical form. Both paths are no-ops (`remapped == 0`) on an /// canonical form. Both paths are no-ops (`remapped == 0`) on an
/// already-clean blob. /// already-clean blob.
pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> { pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
restore_metadata_bytes_with_module(data, None)
}
pub(crate) fn restore_metadata_bytes_with_module(
data: &[u8],
method_index_module: Option<&[u8]>,
) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
let version = data
.get(4..8)
.and_then(|bytes| <[u8; 4]>::try_from(bytes).ok())
.map(u32::from_le_bytes);
if version == Some(24)
&& let Some(module) = method_index_module
{
let (out, report) = senbei_metadata::android::restore_method_indices_v24_1(data, module)
.map_err(anyhow::Error::new)?;
return Ok((
out,
senbei_metadata::Report {
version: report.version,
methods: report.methods,
remapped: report.changed_indices,
modules: 0,
},
));
}
if let Ok(discovery) = senbei_metadata::android::discover_method_token_seeds(data) if let Ok(discovery) = senbei_metadata::android::discover_method_token_seeds(data)
&& matches!(discovery.version, 29 | 31 | 39) && matches!(discovery.version, 29 | 31 | 39)
{ {
@@ -190,7 +255,7 @@ pub struct EntryOutcome {
pub enum EntryKind { pub enum EntryKind {
/// A protected shared library, restored. /// A protected shared library, restored.
So, So,
/// An il2cpp metadata blob, de-obfuscated (`remapped` tokens changed). /// An il2cpp metadata blob, de-obfuscated (`remapped` method fields changed).
Metadata { remapped: usize }, Metadata { remapped: usize },
/// A metadata blob unwrapped from a restored library's data section. /// A metadata blob unwrapped from a restored library's data section.
EmbeddedMetadata, EmbeddedMetadata,
@@ -204,12 +269,30 @@ pub enum EntryStatus {
Duplicate, Duplicate,
/// Content-probed but not a target (unprotected library). /// Content-probed but not a target (unprotected library).
NotTarget, NotTarget,
/// A metadata blob whose tokens were already canonical; no copy written. /// A metadata blob whose protected method fields were already canonical; no copy written.
Unchanged, Unchanged,
/// Recognised as a target but the restore failed. /// Recognised as a target but the restore failed.
Failed(anyhow::Error), Failed(anyhow::Error),
} }
struct PackageRestoreContext<'a> {
method_index_module: &'a mut Option<Vec<u8>>,
verbose: bool,
}
impl<'a> PackageRestoreContext<'a> {
fn new(verbose: bool, method_index_module: &'a mut Option<Vec<u8>>) -> Self {
Self {
method_index_module,
verbose,
}
}
}
fn package_entry_priority(path: &Path) -> u8 {
if is_so_name(path) { 0 } else { 1 }
}
/// Restore every protected library and metadata blob inside one app package. /// Restore every protected library and metadata blob inside one app package.
/// ///
/// `rel` is the package's path relative to the scanned root (or its bare file /// `rel` is the package's path relative to the scanned root (or its bare file
@@ -223,6 +306,25 @@ pub fn restore_package(
out_root: &Path, out_root: &Path,
seen: &mut HashSet<String>, seen: &mut HashSet<String>,
verbose: bool, verbose: bool,
) -> Result<Vec<EntryOutcome>> {
let mut method_index_module = None;
restore_package_with_method_index_module(
package,
rel,
out_root,
seen,
verbose,
&mut method_index_module,
)
}
pub(crate) fn restore_package_with_method_index_module(
package: &Path,
rel: &Path,
out_root: &Path,
seen: &mut HashSet<String>,
verbose: bool,
method_index_module: &mut Option<Vec<u8>>,
) -> Result<Vec<EntryOutcome>> { ) -> Result<Vec<EntryOutcome>> {
let bundle = package let bundle = package
.extension() .extension()
@@ -233,6 +335,7 @@ pub fn restore_package(
let mut archive = open_package(package)?; let mut archive = open_package(package)?;
let temporary = tempfile::tempdir().context("create package workspace")?; let temporary = tempfile::tempdir().context("create package workspace")?;
let mut outcomes = Vec::new(); let mut outcomes = Vec::new();
let mut context = PackageRestoreContext::new(verbose, method_index_module);
let mut direct = Vec::new(); let mut direct = Vec::new();
let mut nested = Vec::new(); let mut nested = Vec::new();
@@ -261,6 +364,7 @@ pub fn restore_package(
} }
} }
} }
direct.sort_by_key(|(_, name)| package_entry_priority(name));
for (index, name) in direct { for (index, name) in direct {
let label = format!("{}::{}", rel.display(), name.display()); let label = format!("{}::{}", rel.display(), name.display());
let dest = out_root.join(rel).join(crate::job::out_name(&name)); let dest = out_root.join(rel).join(crate::job::out_name(&name));
@@ -271,11 +375,19 @@ pub fn restore_package(
&dest, &dest,
&temporary, &temporary,
seen, seen,
verbose, &mut context,
) )
.with_context(|| format!("extract `{label}`"))?; .with_context(|| format!("extract `{label}`"))?;
outcomes.append(&mut entry_outcomes); outcomes.append(&mut entry_outcomes);
} }
struct NestedPackage {
label: PathBuf,
path: PathBuf,
base: PathBuf,
entries: Vec<(usize, PathBuf)>,
}
let mut nested_packages = Vec::with_capacity(nested.len());
for (index, name) in nested { for (index, name) in nested {
let nested_label = rel.join(&name); let nested_label = rel.join(&name);
let nested_path = extract_entry(&mut archive, index, &temporary, &nested_label) let nested_path = extract_entry(&mut archive, index, &temporary, &nested_label)
@@ -296,23 +408,45 @@ pub fn restore_package(
} }
} }
} }
// Keep the nested package's stem in the output layout so two splits nested_packages.push(NestedPackage {
// carrying same-named entries cannot collide. label: nested_label,
let base = rel.join(name.with_extension("")); path: nested_path,
for (nested_index, entry_name) in entries { base: rel.join(name.with_extension("")),
let label = format!("{}::{}", nested_label.display(), entry_name.display()); entries,
let dest = out_root.join(&base).join(crate::job::out_name(&entry_name)); });
let mut entry_outcomes = restore_package_entry( }
&mut nested_archive,
nested_index, // A bundle can put libil2cpp.so in an ABI split while metadata stays in
&label, // base.apk. Share one context across the whole bundle and process all SOs
&dest, // before any metadata, regardless of which nested APK owns each entry.
&temporary, for priority in [0_u8, 1_u8] {
seen, for nested_package in &nested_packages {
verbose, let mut nested_archive = open_package(&nested_package.path)?;
) for (nested_index, entry_name) in nested_package
.with_context(|| format!("extract `{label}`"))?; .entries
outcomes.append(&mut entry_outcomes); .iter()
.filter(|(_, entry_name)| package_entry_priority(entry_name) == priority)
{
let label = format!(
"{}::{}",
nested_package.label.display(),
entry_name.display()
);
let dest = out_root
.join(&nested_package.base)
.join(crate::job::out_name(entry_name));
let mut entry_outcomes = restore_package_entry(
&mut nested_archive,
*nested_index,
&label,
&dest,
&temporary,
seen,
&mut context,
)
.with_context(|| format!("extract `{label}`"))?;
outcomes.append(&mut entry_outcomes);
}
} }
} }
Ok(outcomes) Ok(outcomes)
@@ -328,7 +462,7 @@ fn restore_package_entry<R: Read + Seek>(
dest: &Path, dest: &Path,
temporary: &tempfile::TempDir, temporary: &tempfile::TempDir,
seen: &mut HashSet<String>, seen: &mut HashSet<String>,
verbose: bool, context: &mut PackageRestoreContext<'_>,
) -> Result<Vec<EntryOutcome>> { ) -> Result<Vec<EntryOutcome>> {
let entry_path = extract_entry(archive, index, temporary, Path::new(label))?; let entry_path = extract_entry(archive, index, temporary, Path::new(label))?;
let entry_file = let entry_file =
@@ -358,31 +492,39 @@ fn restore_package_entry<R: Read + Seek>(
if is_so { if is_so {
drop(entry_data); drop(entry_data);
drop(entry_file); drop(entry_file);
return Ok(match restore_so_file(&entry_path, dest, verbose) { return Ok(
Ok(embedded) => { match restore_so_file_with_context(&entry_path, dest, context.verbose) {
let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)]; Ok(restored) => {
if let Some(blob) = embedded { if let Some(module) = restored.method_index_module {
let meta_dest = embedded_metadata_dest(dest); *context.method_index_module = Some(module);
let status = match write_metadata_blob(&meta_dest, &blob) { }
Ok(()) => EntryStatus::Restored, let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)];
Err(error) => EntryStatus::Failed(error), if let Some(blob) = restored.embedded_metadata {
}; let meta_dest = embedded_metadata_dest(dest);
outcomes.push(EntryOutcome { let status = match write_metadata_blob(&meta_dest, &blob) {
label: format!("{label} (embedded metadata)"), Ok(()) => EntryStatus::Restored,
dest: meta_dest, Err(error) => EntryStatus::Failed(error),
kind: EntryKind::EmbeddedMetadata, };
status, outcomes.push(EntryOutcome {
}); label: format!("{label} (embedded metadata)"),
dest: meta_dest,
kind: EntryKind::EmbeddedMetadata,
status,
});
}
outcomes
} }
outcomes Err(error) => vec![outcome(EntryKind::So, EntryStatus::Failed(error))],
} },
Err(error) => vec![outcome(EntryKind::So, EntryStatus::Failed(error))], );
});
} }
// Metadata entry: write only when the restore actually changed tokens — // Metadata entry: write only when the restore actually changed protected method fields —
// a clean blob needs no copy (same contract as loose metadata files). // a clean blob needs no copy (same contract as loose metadata files).
let kind_and_status = match restore_metadata_bytes(&entry_data) { let kind_and_status = match restore_metadata_bytes_with_module(
&entry_data,
context.method_index_module.as_deref(),
) {
Ok((out, report)) if report.remapped > 0 => { Ok((out, report)) if report.remapped > 0 => {
let kind = EntryKind::Metadata { let kind = EntryKind::Metadata {
remapped: report.remapped, remapped: report.remapped,
+31 -9
View File
@@ -15,7 +15,7 @@ pub struct Summary {
/// — likely to crash at runtime (e.g. 0xC0000005). Counted in addition to /// — likely to crash at runtime (e.g. 0xC0000005). Counted in addition to
/// `unpacked` (a suspect file is still written). /// `unpacked` (a suspect file is still written).
pub suspect: usize, pub suspect: usize,
/// il2cpp `global-metadata.dat` files de-obfuscated (method tokens remapped), /// il2cpp `global-metadata.dat` files de-obfuscated (protected method fields remapped),
/// including blobs unwrapped from restored Android libraries. /// including blobs unwrapped from restored Android libraries.
pub metadata: usize, pub metadata: usize,
/// Android app packages (`.apk`/`.apks`/`.xapk`) opened and searched. /// Android app packages (`.apk`/`.apks`/`.xapk`) opened and searched.
@@ -221,6 +221,7 @@ pub fn run_folder_opts(
// files restore first so the cross-source dedup keeps them over a copy // files restore first so the cross-source dedup keeps them over a copy
// inside a package (loose beats `.apk` beats `.apks`/`.xapk` bundle). // inside a package (loose beats `.apk` beats `.apks`/`.xapk` bundle).
let mut android_seen = std::collections::HashSet::new(); let mut android_seen = std::collections::HashSet::new();
let mut android_method_index_module = None;
// Hashing a protected library costs a full read, so only pay it when a // Hashing a protected library costs a full read, so only pay it when a
// duplicate source can actually exist in this run. // duplicate source can actually exist in this run.
let android_dedup = scan.android_so.len() > 1 || !scan.android_packages.is_empty(); let android_dedup = scan.android_so.len() > 1 || !scan.android_packages.is_empty();
@@ -242,7 +243,12 @@ pub fn run_folder_opts(
let input_owned = input.clone(); let input_owned = input.clone();
let dest_owned = dest.clone(); let dest_owned = dest.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
crate::android::restore_so_file(&input_owned, &dest_owned, verbose_steps) crate::android::restore_so_file_with_method_index_module(
&input_owned,
&dest_owned,
verbose_steps,
&mut android_method_index_module,
)
})); }));
match result { match result {
Ok(Ok(embedded)) => { Ok(Ok(embedded)) => {
@@ -310,12 +316,13 @@ pub fn run_folder_opts(
let out_root_owned = out_root.clone(); let out_root_owned = out_root.clone();
let mut seen_taken = std::mem::take(&mut android_seen); let mut seen_taken = std::mem::take(&mut android_seen);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let outcomes = crate::android::restore_package( let outcomes = crate::android::restore_package_with_method_index_module(
&package_owned, &package_owned,
&rel_owned, &rel_owned,
&out_root_owned, &out_root_owned,
&mut seen_taken, &mut seen_taken,
verbose_steps, verbose_steps,
&mut android_method_index_module,
); );
(outcomes, seen_taken) (outcomes, seen_taken)
})); }));
@@ -361,7 +368,12 @@ pub fn run_folder_opts(
let meta_owned = meta.clone(); let meta_owned = meta.clone();
let dest_owned = dest.clone(); let dest_owned = dest.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
deobfuscate_metadata_to(&meta_owned, &dest_owned, verbose_steps) deobfuscate_metadata_to_with_module(
&meta_owned,
&dest_owned,
verbose_steps,
android_method_index_module.as_deref(),
)
})); }));
match result { match result {
Ok(Ok(report)) if report.remapped > 0 => { Ok(Ok(report)) if report.remapped > 0 => {
@@ -369,7 +381,7 @@ pub fn run_folder_opts(
crate::ui::metadata(&bar, suppress_file_lines, &rel, report.remapped, &dest); crate::ui::metadata(&bar, suppress_file_lines, &rel, report.remapped, &dest);
if let Some(log) = &log { if let Some(log) = &log {
log.step(&format!( log.step(&format!(
"META {rel:?} -> {dest:?}: v{} remapped {} method tokens", "META {rel:?} -> {dest:?}: v{} remapped {} method fields",
report.version, report.remapped report.version, report.remapped
)); ));
} }
@@ -506,13 +518,13 @@ pub fn run_file_v(
s.metadata = 1; s.metadata = 1;
if let Some(log) = &log { if let Some(log) = &log {
log.step(&format!( log.step(&format!(
"META {:?} -> {:?}: v{} remapped {} method tokens", "META {:?} -> {:?}: v{} remapped {} method fields",
input, dest, report.version, report.remapped input, dest, report.version, report.remapped
)); ));
} }
if quiet == 0 { if quiet == 0 {
println!( println!(
"✓ metadata v{} -> {:?} ({} method tokens remapped)", "✓ metadata v{} -> {:?} ({} method fields remapped)",
report.version, dest, report.remapped report.version, dest, report.remapped
); );
} }
@@ -735,6 +747,15 @@ pub fn deobfuscate_metadata_to(
input: &Path, input: &Path,
dest: &Path, dest: &Path,
verbose: bool, verbose: bool,
) -> anyhow::Result<senbei_metadata::Report> {
deobfuscate_metadata_to_with_module(input, dest, verbose, None)
}
fn deobfuscate_metadata_to_with_module(
input: &Path,
dest: &Path,
verbose: bool,
method_index_module: Option<&[u8]>,
) -> anyhow::Result<senbei_metadata::Report> { ) -> anyhow::Result<senbei_metadata::Report> {
let data = std::fs::read(input)?; let data = std::fs::read(input)?;
// The Android seeded-permutation variant is tried first (it validates // The Android seeded-permutation variant is tried first (it validates
@@ -742,8 +763,9 @@ pub fn deobfuscate_metadata_to(
// Windows path. The [`senbei_metadata::Error`] is preserved in the chain // Windows path. The [`senbei_metadata::Error`] is preserved in the chain
// (rather than stringified) so the folder driver can apply its // (rather than stringified) so the folder driver can apply its
// unsupported-version policy. // unsupported-version policy.
let (out, report) = crate::android::restore_metadata_bytes(&data) let (out, report) =
.map_err(|e| e.context(format!("{input:?}")))?; crate::android::restore_metadata_bytes_with_module(&data, method_index_module)
.map_err(|e| e.context(format!("{input:?}")))?;
if report.remapped > 0 { if report.remapped > 0 {
if let Some(parent) = dest.parent() { if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
+2 -2
View File
@@ -39,13 +39,13 @@ pub fn ok_label(bar: &ProgressBar, quiet: bool, rel: &str, label: &str, dest: &P
} }
/// Print a green success line for a de-obfuscated il2cpp `global-metadata.dat`, /// Print a green success line for a de-obfuscated il2cpp `global-metadata.dat`,
/// reporting how many method tokens were remapped. /// reporting how many protected method fields were remapped.
pub fn metadata(bar: &ProgressBar, quiet: bool, rel: &Path, remapped: usize, dest: &Path) { pub fn metadata(bar: &ProgressBar, quiet: bool, rel: &Path, remapped: usize, dest: &Path) {
if quiet { if quiet {
return; return;
} }
let msg = format!( let msg = format!(
"{} metadata {} -> {} ({} method tokens remapped)", "{} metadata {} -> {} ({} method fields remapped)",
"✓".green(), "✓".green(),
rel.display(), rel.display(),
dest.display(), dest.display(),
+39 -10
View File
@@ -128,18 +128,19 @@ pub(crate) fn overlay_exports_from_stub(out: &mut [u8], stub: &[u8]) {
Some(v) if v.1 != 0 => v, Some(v) if v.1 != 0 => v,
_ => return, _ => return,
}; };
let dst = match rva_to_file_off(out, export_rva) { let dst = match senbei_pe::parse(out)
Some(o) => o, .and_then(|headers| senbei_pe::rva_range(out, headers, export_rva, export_size))
None => return, {
Ok(range) => range,
Err(_) => return,
}; };
let src = match rva_to_file_off(stub, export_rva) { let src = match senbei_pe::parse(stub)
Some(o) => o, .and_then(|headers| senbei_pe::rva_range(stub, headers, export_rva, export_size))
None => return, {
Ok(range) => range,
Err(_) => return,
}; };
let n = export_size as usize; out[dst].copy_from_slice(&stub[src]);
if dst + n <= out.len() && src + n <= stub.len() {
out[dst..dst + n].copy_from_slice(&stub[src..src + n]);
}
} }
/// Restore the CLR regions retained by an external-companion loader stub. /// Restore the CLR regions retained by an external-companion loader stub.
@@ -633,6 +634,34 @@ mod tests {
data data
} }
#[test]
fn export_overlay_requires_the_entire_range_to_be_file_backed() {
for is_pe32_plus in [false, true] {
let dirs = 0x98 + if is_pe32_plus { 112 } else { 96 };
let section = 0x98 + if is_pe32_plus { 0xf0 } else { 0xe0 };
let mut stub = managed_fixture(is_pe32_plus, 0x600);
stub[0x780..0x880].fill(0x5a);
for (stub_raw_size, out_raw_size) in
[(0x600u32, 0x600u32), (0x200, 0x600), (0x600, 0x200)]
{
let mut out = managed_fixture(is_pe32_plus, 0x400);
out[dirs..dirs + 4].copy_from_slice(&0x2180u32.to_le_bytes());
out[dirs + 4..dirs + 8].copy_from_slice(&0x100u32.to_le_bytes());
out[section + 16..section + 20].copy_from_slice(&out_raw_size.to_le_bytes());
stub[section + 16..section + 20].copy_from_slice(&stub_raw_size.to_le_bytes());
let before = out.clone();
overlay_exports_from_stub(&mut out, &stub);
if stub_raw_size == 0x600 && out_raw_size == 0x600 {
assert_eq!(&out[0x580..0x680], &[0x5a; 0x100]);
assert_eq!(&out[..0x580], &before[..0x580]);
assert_eq!(&out[0x680..], &before[0x680..]);
} else {
assert_eq!(out, before);
}
}
}
}
#[test] #[test]
fn managed_companion_restores_rva_mapped_regions_without_overwriting_il() { fn managed_companion_restores_rva_mapped_regions_without_overwriting_il() {
for is_pe32_plus in [false, true] { for is_pe32_plus in [false, true] {
@@ -0,0 +1,467 @@
//! Restoration of IL2CPP v24.1 `Il2CppMethodDefinition.methodIndex` values.
//!
//! CrackProof's v24.1 Android module rewrites the global method-index permutation
//! at runtime. The transform parameters are carried by the decoded module `0x0C`,
//! so the restore derives them from that module instead of hard-coding a
//! game-specific table.
use serde::Serialize;
use crate::common::MAGIC;
const RAW_VERSION_24: u32 = 24;
const HDR_METHODS: usize = 0x30;
const V24_1_METHOD_STRIDE: usize = 0x34;
const V24_1_METHOD_INDEX_OFFSET: usize = 0x14;
const V24_1_SELECTOR: u32 = 1;
const EXCEPTION_COUNT: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MethodIndexReport {
pub version: u32,
pub methods: usize,
pub active_methods: usize,
pub changed_indices: usize,
pub exception_count: usize,
pub seed: String,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MethodIndexError {
#[error("not an IL2CPP global-metadata.dat")]
NotMetadata,
#[error("unsupported metadata version {0}")]
UnsupportedVersion(u32),
#[error("malformed v24.1 metadata: {0}")]
Malformed(String),
#[error("v24.1 method-index profile was not found in module 0x0C")]
ProfileNotFound,
#[error("multiple v24.1 method-index profiles matched module 0x0C")]
AmbiguousProfile,
#[error("v24.1 method-index restoration failed validation: {0}")]
Validation(String),
}
type Result<T> = std::result::Result<T, MethodIndexError>;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Profile {
seed: u32,
exception_values: [u32; EXCEPTION_COUNT],
}
fn bytes(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
let end = offset
.checked_add(size)
.ok_or_else(|| MethodIndexError::Malformed("byte range overflow".to_owned()))?;
data.get(offset..end).ok_or_else(|| {
MethodIndexError::Malformed(format!(
"byte range 0x{offset:x}..0x{end:x} is out of bounds"
))
})
}
fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
let value: [u8; 4] = bytes(data, offset, 4)?
.try_into()
.map_err(|_| MethodIndexError::Malformed("invalid u32 range".to_owned()))?;
Ok(u32::from_le_bytes(value))
}
fn read_i32(data: &[u8], offset: usize) -> Result<i32> {
let value: [u8; 4] = bytes(data, offset, 4)?
.try_into()
.map_err(|_| MethodIndexError::Malformed("invalid i32 range".to_owned()))?;
Ok(i32::from_le_bytes(value))
}
fn write_i32(data: &mut [u8], offset: usize, value: i32) -> Result<()> {
let destination = data.get_mut(offset..offset + 4).ok_or_else(|| {
MethodIndexError::Malformed("method-index write is out of bounds".to_owned())
})?;
destination.copy_from_slice(&value.to_le_bytes());
Ok(())
}
fn method_table(data: &[u8]) -> Result<(usize, usize)> {
if read_u32(data, 0)? != MAGIC {
return Err(MethodIndexError::NotMetadata);
}
let version = read_u32(data, 4)?;
if version != RAW_VERSION_24 {
return Err(MethodIndexError::UnsupportedVersion(version));
}
let offset = read_u32(data, HDR_METHODS)? as usize;
let size = read_u32(data, HDR_METHODS + 4)? as usize;
bytes(data, offset, size)?;
if !size.is_multiple_of(V24_1_METHOD_STRIDE) {
return Err(MethodIndexError::Malformed(format!(
"method table size 0x{size:x} is not divisible by v24.1 stride 0x{V24_1_METHOD_STRIDE:x}"
)));
}
Ok((offset, size / V24_1_METHOD_STRIDE))
}
fn method_indices(data: &[u8]) -> Result<(usize, Vec<Option<u32>>)> {
let (offset, count) = method_table(data)?;
let mut indices = Vec::with_capacity(count);
for method in 0..count {
let value = read_i32(
data,
offset + method * V24_1_METHOD_STRIDE + V24_1_METHOD_INDEX_OFFSET,
)?;
indices.push((value >= 0).then_some(value as u32));
}
Ok((offset, indices))
}
fn is_complete_permutation(indices: &[Option<u32>]) -> bool {
let active_count = indices.iter().filter(|value| value.is_some()).count();
if active_count == 0 || active_count > u32::MAX as usize {
return false;
}
let mut seen = vec![false; active_count];
for value in indices.iter().flatten().copied() {
let Ok(index) = usize::try_from(value) else {
return false;
};
if index >= active_count || seen[index] {
return false;
}
seen[index] = true;
}
seen.into_iter().all(|value| value)
}
fn permutation_key(seed: u32, count: u32) -> Result<u32> {
if count < 2 {
return Err(MethodIndexError::Validation(
"method-index permutation requires at least two active methods".to_owned(),
));
}
if count > u32::MAX / 2 {
return Err(MethodIndexError::Validation(
"active method count is too large for the permutation mirror".to_owned(),
));
}
let half = count / 2;
if half == 0 {
return Err(MethodIndexError::Validation(
"method-index permutation has a zero divisor".to_owned(),
));
}
Ok(seed % half + count / 4)
}
fn permutation_round(mut value: u32, count: u32, key: u32) -> u32 {
let mirror = count * 2 - 1;
if value & 1 != 0 {
value = mirror - value;
}
value >>= 1;
if value >= count {
value = mirror - value;
}
let adjusted = i64::from(value) - i64::from(key);
if adjusted < 0 {
(adjusted + i64::from(count)) as u32
} else {
adjusted as u32
}
}
fn transform_index(mut value: u32, count: u32, seed: u32) -> Result<u32> {
let key = permutation_key(seed, count)?;
for _ in 0..5 {
value = permutation_round(value, count, key);
}
Ok(value)
}
fn transformed_values(indices: &[Option<u32>], seed: u32) -> Result<Vec<Option<u32>>> {
let active_count = indices.iter().filter(|value| value.is_some()).count();
let count = u32::try_from(active_count)
.map_err(|_| MethodIndexError::Validation("active method count exceeds u32".to_owned()))?;
indices
.iter()
.map(|value| {
value
.map(|value| transform_index(value, count, seed))
.transpose()
})
.collect()
}
fn missing_values(indices: &[Option<u32>]) -> Option<Vec<u32>> {
let active_count = indices.iter().filter(|value| value.is_some()).count();
let mut seen = vec![false; active_count];
for value in indices.iter().flatten().copied() {
let index = usize::try_from(value).ok()?;
if index >= active_count {
return None;
}
seen[index] = true;
}
Some(
seen.into_iter()
.enumerate()
.filter_map(|(index, present)| (!present).then_some(index as u32))
.collect(),
)
}
fn find_exception_table(
module: &[u8],
count: u32,
missing: &[u32],
) -> Option<[u32; EXCEPTION_COUNT]> {
if missing.len() != EXCEPTION_COUNT || count == 0 {
return None;
}
let mut wanted = vec![false; count as usize];
for &value in missing {
let slot = wanted.get_mut(value as usize)?;
*slot = true;
}
let table_bytes = EXCEPTION_COUNT * 4;
for offset in (8..=module.len().checked_sub(table_bytes)?).step_by(4) {
if read_u32(module, offset - 8).ok()? != V24_1_SELECTOR
|| read_u32(module, offset - 4).ok()? != count - 1
{
continue;
}
let mut values = [0_u32; EXCEPTION_COUNT];
let mut seen = vec![false; count as usize];
let mut matches = true;
for (index, slot) in values.iter_mut().enumerate() {
let value = read_u32(module, offset + index * 4).ok()?;
let Some(is_wanted) = wanted.get(value as usize) else {
matches = false;
break;
};
if !*is_wanted || seen[value as usize] {
matches = false;
break;
}
seen[value as usize] = true;
*slot = value;
}
if matches {
return Some(values);
}
}
None
}
fn profile_candidates(module: &[u8], indices: &[Option<u32>]) -> Result<Vec<Profile>> {
let active_count = indices.iter().filter(|value| value.is_some()).count();
let count = u32::try_from(active_count)
.map_err(|_| MethodIndexError::Validation("active method count exceeds u32".to_owned()))?;
if count <= EXCEPTION_COUNT as u32 {
return Err(MethodIndexError::Validation(format!(
"active method count {count} is too small for the v24.1 exception table"
)));
}
let mut profiles = Vec::new();
for offset in (0..module.len().saturating_sub(8)).step_by(4) {
let Ok(version) = read_u32(module, offset + 4) else {
continue;
};
if version != RAW_VERSION_24 {
continue;
}
let seed = read_u32(module, offset)?;
let transformed = transformed_values(indices, seed)?;
let Some(missing) = missing_values(&transformed) else {
continue;
};
let Some(exception_values) = find_exception_table(module, count, &missing) else {
continue;
};
let mut candidate = transformed;
let mut exception = exception_values.iter().copied();
for value in candidate.iter_mut().flatten() {
if let Some(replacement) = exception.next() {
*value = replacement;
} else {
break;
}
}
if exception.next().is_none() && is_complete_permutation(&candidate) {
profiles.push(Profile {
seed,
exception_values,
});
}
}
profiles.sort_by_key(|profile| profile.seed);
profiles.dedup();
Ok(profiles)
}
/// Restore the v24.1 `methodIndex` permutation using the decoded CrackProof
/// module `0x0C` that accompanied the protected library.
///
/// The profile is accepted only when the module supplies a seed plus a
/// 256-entry exception table that turns the restored non-negative indices into
/// the exact permutation `0..active_method_count`. This makes a wrong module or
/// incompatible v24 sub-layout fail without mutating the metadata.
pub fn restore_method_indices_v24_1(
data: &[u8],
module_0c: &[u8],
) -> Result<(Vec<u8>, MethodIndexReport)> {
let (method_offset, indices) = method_indices(data)?;
let active_count = indices.iter().filter(|value| value.is_some()).count();
if is_complete_permutation(&indices) {
return Ok((
data.to_vec(),
MethodIndexReport {
version: RAW_VERSION_24,
methods: indices.len(),
active_methods: active_count,
changed_indices: 0,
exception_count: 0,
seed: "clean".to_owned(),
},
));
}
let profiles = profile_candidates(module_0c, &indices)?;
let profile = match profiles.as_slice() {
[] => return Err(MethodIndexError::ProfileNotFound),
[profile] => profile,
_ => return Err(MethodIndexError::AmbiguousProfile),
};
let mut restored = transformed_values(&indices, profile.seed)?;
let mut exception = profile.exception_values.iter().copied();
for value in restored.iter_mut().flatten() {
if let Some(replacement) = exception.next() {
*value = replacement;
} else {
break;
}
}
if exception.next().is_some() {
return Err(MethodIndexError::Validation(
"metadata has fewer active methods than the exception table".to_owned(),
));
}
if !is_complete_permutation(&restored) {
return Err(MethodIndexError::Validation(
"restored methodIndex values are not a complete permutation".to_owned(),
));
}
let mut output = data.to_vec();
let mut changed_indices = 0_usize;
for (method, value) in restored.iter().enumerate() {
let Some(value) = value else {
continue;
};
let offset = method_offset + method * V24_1_METHOD_STRIDE + V24_1_METHOD_INDEX_OFFSET;
let old = read_i32(data, offset)?;
let new = i32::try_from(*value).map_err(|_| {
MethodIndexError::Validation("restored methodIndex exceeds i32".to_owned())
})?;
if old != new {
write_i32(&mut output, offset, new)?;
changed_indices += 1;
}
}
Ok((
output,
MethodIndexReport {
version: RAW_VERSION_24,
methods: indices.len(),
active_methods: active_count,
changed_indices,
exception_count: EXCEPTION_COUNT,
seed: format!("0x{:08X}", profile.seed),
},
))
}
#[cfg(test)]
mod tests {
use super::*;
fn inverse_map(count: u32, seed: u32) -> Vec<u32> {
let mut inverse = vec![u32::MAX; count as usize];
for value in 0..count {
let transformed = transform_index(value, count, seed).unwrap();
inverse[transformed as usize] = value;
}
assert!(inverse.iter().all(|value| *value != u32::MAX));
inverse
}
fn fixture() -> (Vec<u8>, Vec<u8>) {
let active_count = 512_u32;
let seed = 0x7770_0dcc;
let inverse = inverse_map(active_count, seed);
let header_size = 0x100usize;
let method_size = active_count as usize * V24_1_METHOD_STRIDE;
let mut metadata = vec![0_u8; header_size + method_size];
metadata[0..4].copy_from_slice(&MAGIC.to_le_bytes());
metadata[4..8].copy_from_slice(&RAW_VERSION_24.to_le_bytes());
metadata[HDR_METHODS..HDR_METHODS + 4].copy_from_slice(&(header_size as u32).to_le_bytes());
metadata[HDR_METHODS + 4..HDR_METHODS + 8]
.copy_from_slice(&(method_size as u32).to_le_bytes());
// Before the exception patch the first half duplicates the second half,
// leaving 0..255 missing. The module's 256-entry table fills exactly
// those first active methods after the five-round transform.
for method in 0..active_count as usize {
let transformed = 256 + (method % 256) as u32;
let protected = inverse[transformed as usize];
let offset = header_size + method * V24_1_METHOD_STRIDE + V24_1_METHOD_INDEX_OFFSET;
metadata[offset..offset + 4].copy_from_slice(&(protected as i32).to_le_bytes());
}
let mut module = vec![0_u8; 0x1000];
module[0x100..0x104].copy_from_slice(&seed.to_le_bytes());
module[0x104..0x108].copy_from_slice(&RAW_VERSION_24.to_le_bytes());
module[0x3f8..0x3fc].copy_from_slice(&V24_1_SELECTOR.to_le_bytes());
module[0x3fc..0x400].copy_from_slice(&(active_count - 1).to_le_bytes());
for value in 0..EXCEPTION_COUNT as u32 {
let offset = 0x400 + value as usize * 4;
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
(metadata, module)
}
#[test]
fn restores_v24_1_method_index_permutation() {
let (metadata, module) = fixture();
let (restored, report) = restore_method_indices_v24_1(&metadata, &module).unwrap();
assert_eq!(report.active_methods, 512);
assert_eq!(report.exception_count, 256);
assert_eq!(report.seed, "0x77700DCC");
let (offset, indices) = method_indices(&restored).unwrap();
assert_eq!(offset, 0x100);
assert!(is_complete_permutation(&indices));
for (method, value) in indices.into_iter().enumerate() {
assert_eq!(value, Some(method as u32));
}
}
#[test]
fn clean_v24_1_metadata_is_idempotent() {
let (metadata, module) = fixture();
let (restored, _) = restore_method_indices_v24_1(&metadata, &module).unwrap();
let (again, report) = restore_method_indices_v24_1(&restored, &module).unwrap();
assert_eq!(again, restored);
assert_eq!(report.changed_indices, 0);
assert_eq!(report.seed, "clean");
}
#[test]
fn rejects_unrelated_module() {
let (metadata, _) = fixture();
let error = restore_method_indices_v24_1(&metadata, &[0_u8; 0x1000]).unwrap_err();
assert_eq!(error, MethodIndexError::ProfileNotFound);
}
}
+2
View File
@@ -2,9 +2,11 @@
mod embedded; mod embedded;
mod keystream; mod keystream;
mod method_indices;
mod method_tokens; mod method_tokens;
pub use embedded::{embedded_metadata_size, extract_embedded_metadata}; pub use embedded::{embedded_metadata_size, extract_embedded_metadata};
pub use method_indices::{MethodIndexError, MethodIndexReport, restore_method_indices_v24_1};
pub use method_tokens::{ pub use method_tokens::{
DEFAULT_METHOD_TOKEN_SEED, Error, ImageKeyDiscovery, Report, SeedDiscoveryReport, DEFAULT_METHOD_TOKEN_SEED, Error, ImageKeyDiscovery, Report, SeedDiscoveryReport,
discover_method_token_seeds, restore_method_tokens, discover_method_token_seeds, restore_method_tokens,
+7 -7
View File
@@ -412,7 +412,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-crypto" name = "senbei-crypto"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"aes", "aes",
"thiserror", "thiserror",
@@ -420,7 +420,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-elf" name = "senbei-elf"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"goblin", "goblin",
"thiserror", "thiserror",
@@ -428,7 +428,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-engine" name = "senbei-engine"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"memmap2", "memmap2",
"senbei-crypto", "senbei-crypto",
@@ -443,7 +443,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-io" name = "senbei-io"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"indicatif", "indicatif",
@@ -464,7 +464,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-metadata" name = "senbei-metadata"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@@ -472,14 +472,14 @@ dependencies = [
[[package]] [[package]]
name = "senbei-pe" name = "senbei-pe"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
[[package]] [[package]]
name = "senbei-wasm" name = "senbei-wasm"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"senbei-engine", "senbei-engine",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "senbei-wasm" name = "senbei-wasm"
version = "1.3.1" version = "1.3.3"
edition = "2024" edition = "2024"
rust-version = "1.98.1" rust-version = "1.98.1"
description = "WebAssembly bindings for senbei (browser frontend assets live in web/)" description = "WebAssembly bindings for senbei (browser frontend assets live in web/)"