mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-03 10:31:28 -05:00
Compare commits
19
Commits
ba380a5774
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
366a19c2a1 | ||
|
|
ba1dacc0b2 | ||
|
|
31a0c4139c | ||
|
|
ea92d26d28 | ||
|
|
6e233bbf18 | ||
|
|
5bd1b0b07d | ||
|
|
01fdc58d2b | ||
|
|
f7d2d8b7e1 | ||
|
|
7728c6e7c2 | ||
|
|
921cd4bfb4 | ||
|
|
53f31d2210 | ||
|
|
9083c60d45 | ||
|
|
25939c9738 | ||
|
|
2501aabc57 | ||
|
|
da06b4f5fd | ||
|
|
b355d8ea8f | ||
|
|
539861acbd | ||
|
|
a45cc8aa90 | ||
|
|
d4613305fd |
No files matched your search
Generated
+7
-7
@@ -402,7 +402,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-cli"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"senbei-engine",
|
||||
"senbei-io",
|
||||
@@ -413,7 +413,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-crypto"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"thiserror",
|
||||
@@ -421,7 +421,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-elf"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"goblin",
|
||||
"thiserror",
|
||||
@@ -429,7 +429,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-engine"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"memmap2",
|
||||
"senbei-crypto",
|
||||
@@ -444,7 +444,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-io"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indicatif",
|
||||
@@ -465,7 +465,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-metadata"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
@@ -473,7 +473,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-pe"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ exclude = ["senbei-wasm"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.98.1"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
+362
-33
@@ -5,6 +5,8 @@ pub const SHT_STRTAB: u32 = 3;
|
||||
pub const SHT_LOUSER: u32 = 0x8000_0000;
|
||||
pub const SHF_ALLOC: u64 = 2;
|
||||
const PT_LOAD: u32 = 1;
|
||||
const PT_NOTE: u32 = 4;
|
||||
const PT_PHDR: u32 = 6;
|
||||
pub const PF_R: u32 = 4;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -220,7 +222,89 @@ impl ElfLayout {
|
||||
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 {
|
||||
return invalid("unexpected ELF program header size");
|
||||
}
|
||||
@@ -274,23 +358,11 @@ impl ElfLayout {
|
||||
.ok_or_else(|| Error::Invalid("program header count overflow".to_owned()))?;
|
||||
let new_count_u16 = u16::try_from(new_count)
|
||||
.map_err(|_| Error::Invalid("program header count exceeds u16".to_owned()))?;
|
||||
let header_offset = checked_index(
|
||||
self.program_header_offset,
|
||||
self.program_header_count,
|
||||
self.program_header_size,
|
||||
)?;
|
||||
let header_end = header_offset
|
||||
.checked_add(self.program_header_size)
|
||||
.ok_or_else(|| Error::Invalid("new program header range overflow".to_owned()))?;
|
||||
slice(output, header_offset, self.program_header_size)?;
|
||||
let first_file_section = self
|
||||
.section_headers
|
||||
.iter()
|
||||
.filter(|section| section.section_type != SHT_NOBITS && section.size != 0)
|
||||
.map(|section| section.offset)
|
||||
.min();
|
||||
if first_file_section.is_some_and(|offset| header_end as u64 > offset) {
|
||||
return invalid("no space for an additional program header");
|
||||
let reservation = self.additional_program_header_reservation()?;
|
||||
if reserved_program_header_bytes != reservation {
|
||||
return invalid(format!(
|
||||
"program-header reservation mismatch: expected 0x{reservation:x}, got 0x{reserved_program_header_bytes:x}"
|
||||
));
|
||||
}
|
||||
|
||||
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[0x28..0x30].copy_from_slice(&segment.memory_size.to_le_bytes());
|
||||
header[0x30..0x38].copy_from_slice(&segment.alignment.to_le_bytes());
|
||||
output
|
||||
.get_mut(header_offset..header_end)
|
||||
.ok_or_else(|| Error::Invalid("new program header exceeds output".to_owned()))?
|
||||
.copy_from_slice(&header);
|
||||
output
|
||||
.get_mut(0x38..0x3a)
|
||||
.ok_or_else(|| Error::Invalid("ELF header is truncated".to_owned()))?
|
||||
.copy_from_slice(&new_count_u16.to_le_bytes());
|
||||
|
||||
let old_table_size = self
|
||||
.program_header_count
|
||||
.checked_mul(self.program_header_size)
|
||||
.ok_or_else(|| Error::Invalid("program header table size overflow".to_owned()))?;
|
||||
let new_table_size = new_count
|
||||
.checked_mul(self.program_header_size)
|
||||
.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();
|
||||
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);
|
||||
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]> {
|
||||
let end = offset
|
||||
.checked_add(size)
|
||||
@@ -585,6 +776,7 @@ mod tests {
|
||||
flags: PF_R,
|
||||
alignment: 0x1000,
|
||||
},
|
||||
0,
|
||||
)
|
||||
.expect("append segment");
|
||||
assert_eq!(updated.program_header_count, 1);
|
||||
@@ -594,7 +786,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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 {
|
||||
entrypoint: 0,
|
||||
program_header_offset: 0,
|
||||
@@ -617,20 +809,157 @@ mod tests {
|
||||
alignment: 1,
|
||||
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
|
||||
.append_load_segment(
|
||||
&mut output,
|
||||
LoadSegment {
|
||||
offset: 0x80,
|
||||
virtual_address: 0x1080,
|
||||
file_size: 0x20,
|
||||
memory_size: 0x20,
|
||||
file_size: 0x80,
|
||||
memory_size: 0x80,
|
||||
flags: PF_R,
|
||||
alignment: 0x1000,
|
||||
},
|
||||
reservation,
|
||||
)
|
||||
.expect_err("overlapping program header");
|
||||
assert!(error.to_string().contains("additional program header"));
|
||||
.expect_err("unsafe relocation must fail");
|
||||
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
@@ -15,26 +15,23 @@ pub use layout::{
|
||||
/// ELF machine identifier for AArch64.
|
||||
pub const AARCH64_MACHINE: u16 = EM_AARCH64;
|
||||
|
||||
/// Dynamic sections required by the restored AArch64 loader image.
|
||||
pub const DYNAMIC_SECTION_NAMES: [&str; 8] = [
|
||||
/// Dynamic sections required by every restored AArch64 loader image.
|
||||
pub const DYNAMIC_SECTION_NAMES: [&str; 7] = [
|
||||
".dynsym",
|
||||
".gnu.version",
|
||||
".gnu.version_r",
|
||||
".gnu.hash",
|
||||
".dynstr",
|
||||
".rela.dyn",
|
||||
".rela.plt",
|
||||
".dynamic",
|
||||
];
|
||||
|
||||
/// Supported dynamic symbol-hash sections. At least one must be present.
|
||||
pub const SYMBOL_HASH_SECTION_NAMES: [&str; 2] = [".gnu.hash", ".hash"];
|
||||
|
||||
/// Dynamic sections needed to identify a protected image before extraction.
|
||||
pub const PROBE_SECTION_NAMES: [&str; 5] = [
|
||||
".dynsym",
|
||||
".dynstr",
|
||||
".gnu.hash",
|
||||
".gnu.version",
|
||||
".gnu.version_r",
|
||||
];
|
||||
/// Hash tables are checked separately because either GNU or SysV hashing is valid.
|
||||
pub const PROBE_SECTION_NAMES: [&str; 4] = [".dynsym", ".dynstr", ".gnu.version", ".gnu.version_r"];
|
||||
|
||||
/// ELF64 dynamic table record sizes.
|
||||
pub const ELF64_SYMBOL_SIZE: usize = 0x18;
|
||||
|
||||
@@ -70,6 +70,16 @@ pub(crate) fn inspect(
|
||||
return invalid(format!("protected ELF lacks required section {wanted}"));
|
||||
}
|
||||
}
|
||||
let has_symbol_hash = senbei_elf::SYMBOL_HASH_SECTION_NAMES.iter().any(|wanted| {
|
||||
elf.section_headers.iter().any(|section| {
|
||||
elf.shdr_strtab
|
||||
.get_at(section.sh_name)
|
||||
.is_some_and(|name| name == *wanted)
|
||||
})
|
||||
});
|
||||
if !has_symbol_hash {
|
||||
return invalid("protected ELF lacks a .gnu.hash or .hash symbol-hash section");
|
||||
}
|
||||
let (section_index, section) = matches[0];
|
||||
let section_offset = usize::try_from(section.sh_offset)
|
||||
.map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?;
|
||||
|
||||
@@ -6,11 +6,17 @@ use serde_json::Value;
|
||||
use super::error::{Error, Result, invalid};
|
||||
|
||||
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)]
|
||||
pub(crate) struct Artifact {
|
||||
pub path: PathBuf,
|
||||
pub size: u64,
|
||||
pub entry_offset: Option<u32>,
|
||||
}
|
||||
|
||||
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)
|
||||
.map_err(|_| Error::Invalid("module command ID exceeds u32".to_owned()))?;
|
||||
if !REQUIRED_IDS.contains(&command_id) {
|
||||
if !wanted_id(command_id) {
|
||||
continue;
|
||||
}
|
||||
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 {
|
||||
path: root.join(path),
|
||||
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)
|
||||
.map_err(|_| Error::Invalid("record command ID exceeds u32".to_owned()))?;
|
||||
if !REQUIRED_IDS.contains(&command_id) {
|
||||
if !wanted_id(command_id) {
|
||||
continue;
|
||||
}
|
||||
let Some(image) = record.get("image") else {
|
||||
@@ -74,6 +84,10 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
|
||||
Artifact {
|
||||
path: root.join(path),
|
||||
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,4 +1,5 @@
|
||||
mod artifact;
|
||||
mod constructors;
|
||||
mod error;
|
||||
mod pipeline;
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
fn decrypt_data3(&mut self, pos: u32, key: u32, shift: u32) {
|
||||
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.decrypt_size = u.info[6].wrapping_sub(u.info[3]).wrapping_add(8192);
|
||||
if verbose {
|
||||
println!("[2/9] Decrypting payload...");
|
||||
}
|
||||
u.decrypt_data2();
|
||||
|
||||
{
|
||||
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]);
|
||||
let pe32_table = u.load_payload();
|
||||
|
||||
// PE32 (32-bit) images use an entirely different config layout and
|
||||
// final-output transform than PE32+ (64-bit). Dispatch here, after the
|
||||
// shared header/payload setup (the PE32 branch follows the common
|
||||
// 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 {
|
||||
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
|
||||
@@ -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)]
|
||||
mod error_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -23,10 +23,153 @@ fn stage_key_rounds(data: &[u8], table: u32, slots: usize) -> Result<u32, Unpack
|
||||
}
|
||||
|
||||
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,
|
||||
/// payload decrypt, raw copy, header restore) has already run in `run()`
|
||||
/// 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 info3 = info[3];
|
||||
|
||||
@@ -43,94 +186,21 @@ impl<'a> Unpacker<'a> {
|
||||
key
|
||||
};
|
||||
|
||||
// ---- Locate tbl in shell ----
|
||||
let tbl =
|
||||
layout::find_tbl_pe32(&self.decompressed, &info).ok_or(UnpackError::Pe32TblNotFound)?;
|
||||
// ---- Validated shell table ----
|
||||
let compact = get_u32(&self.decompressed, tbl.wrapping_add(0x78)) == info[6];
|
||||
let stage_field = if compact { 0x88 } else { 0x98 };
|
||||
if verbose {
|
||||
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 ----
|
||||
//
|
||||
// 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_pair = tbl.wrapping_add(stage_field);
|
||||
let ss = get_u32(&self.decompressed, ss_pair);
|
||||
let ss_size = get_u32(&self.decompressed, ss_pair.wrapping_add(4));
|
||||
let ss_shift = ss_size.wrapping_sub(0xBC0);
|
||||
// Back up the SecondStage ciphertext so a failed trial can be retried.
|
||||
let ss_lo = ss as usize;
|
||||
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);
|
||||
}
|
||||
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 header_checksum = self.decrypt_pe32_second_stage(tbl, pe_off)?;
|
||||
if verbose {
|
||||
println!(
|
||||
" ss = 0x{:08X}, size = 0x{:X}, shift = 0x{:X}",
|
||||
@@ -139,13 +209,12 @@ impl<'a> Unpacker<'a> {
|
||||
}
|
||||
|
||||
// ---- PE32 fixed offsets ----
|
||||
let third_key_off = 0x968u32.wrapping_add(ss_shift);
|
||||
let forth_key_off = 0x964u32.wrapping_add(ss_shift);
|
||||
let cs_base_off = 0x96Cu32.wrapping_add(ss_shift);
|
||||
let dp_base_off = 0xA9Cu32.wrapping_add(ss_shift);
|
||||
let third_key_off = (if compact { 0x950u32 } else { 0x968 }).wrapping_add(ss_shift);
|
||||
let forth_key_off = (if compact { 0x94Cu32 } else { 0x964 }).wrapping_add(ss_shift);
|
||||
let cs_base_off = (if compact { 0x954u32 } else { 0x96C }).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) ----
|
||||
let third_pair_off = 0xB8Cu32.wrapping_add(ss_shift);
|
||||
let key = get_u32(&self.decompressed, ss.wrapping_add(third_key_off));
|
||||
let pair_addr = ss.wrapping_add(third_pair_off);
|
||||
let ts_addr = get_u32(&self.decompressed, pair_addr);
|
||||
@@ -238,7 +307,7 @@ impl<'a> Unpacker<'a> {
|
||||
}
|
||||
|
||||
// ---- 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 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);
|
||||
@@ -298,7 +367,9 @@ impl<'a> Unpacker<'a> {
|
||||
);
|
||||
let seven_key = !get_u32(
|
||||
&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;
|
||||
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] ----
|
||||
let test_val = get_u32(&self.decompressed, info3.wrapping_add(0x10));
|
||||
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);
|
||||
}
|
||||
let (metadata_ep, metadata_dirs) = self.read_pe32_metadata(compact);
|
||||
|
||||
// ---- Zero-out list (runs BEFORE decompression) ----
|
||||
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 s = export_file_off 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) ----
|
||||
@@ -1149,6 +1207,56 @@ impl<'a> Unpacker<'a> {
|
||||
mod tests {
|
||||
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]
|
||||
fn stage_key_rounds_follow_active_descriptor_prefix() {
|
||||
for (sizes, expected) in [
|
||||
|
||||
@@ -6,8 +6,9 @@ mod image;
|
||||
|
||||
pub(super) use dd8::{select_dd8_formula_pe32, select_dd8_shift};
|
||||
pub(super) use discovery::{
|
||||
discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos, find_tbl_pe32,
|
||||
find_v_after_pad, find_v4_offset, get_string_to_null, section_name, trial_decrypt5_u32,
|
||||
discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos,
|
||||
find_tbl_pe32_candidates, find_v_after_pad, find_v4_offset, get_string_to_null, section_name,
|
||||
trial_decrypt5_u32,
|
||||
};
|
||||
pub(super) use image::{
|
||||
compact_memory_image_to_pe, move_pe32_imports_to_kmiat, pe32_imports_already_match_idata_layout,
|
||||
|
||||
@@ -212,37 +212,54 @@ pub fn section_name(data: &[u8], offset: u32) -> String {
|
||||
// PE32 (32-bit) helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// PE32 shell-table locator. Walks the shell region (`info[6]`) for a dword
|
||||
/// equal to `info[6]` followed by a plausible shell size, returning the table
|
||||
/// base (`candidate = off - 0x88`) when `candidate+0x58` holds a valid pointer.
|
||||
pub fn find_tbl_pe32(data: &[u8], info: &[u32; 8]) -> Option<u32> {
|
||||
let shell = info[6];
|
||||
if (data.len() as u64) < 0x100 {
|
||||
return None;
|
||||
}
|
||||
let hi = (shell as u64)
|
||||
.saturating_add(0x3000)
|
||||
.min(data.len() as u64 - 0x100) as u32;
|
||||
let mut off = shell;
|
||||
while off < hi {
|
||||
if off as usize + 8 <= data.len() {
|
||||
let candidate = off.wrapping_sub(0x88);
|
||||
if candidate >= shell && get_u32(data, off) == info[6] {
|
||||
let shell_size_val = get_u32(data, off.wrapping_add(4));
|
||||
if shell_size_val > 0x1000 && shell_size_val < 0x100000 {
|
||||
let v58_off = candidate.wrapping_add(0x58);
|
||||
if (v58_off as usize + 4) <= data.len() {
|
||||
let v58 = get_u32(data, v58_off);
|
||||
if v58 > 0 && (v58 as usize) < data.len() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// PE32 shell-table locator. The compact layout moves the shell descriptor
|
||||
/// from +0x88 to +0x78 and the header-checksum list from +0x58 to +0x50.
|
||||
/// Return structurally plausible candidates; the pipeline must trial-decrypt
|
||||
/// their second stages before selecting a table.
|
||||
pub fn find_tbl_pe32_candidates(data: &[u8], info: &[u32; 8]) -> Vec<u32> {
|
||||
let mut tables = Vec::new();
|
||||
let shell = info[6] as usize;
|
||||
let hi = shell.saturating_add(0x3000).min(data.len());
|
||||
for off in (shell..hi.saturating_sub(8)).step_by(4) {
|
||||
if get_u32(data, off as u32) != info[6] {
|
||||
continue;
|
||||
}
|
||||
let shell_size = get_u32(data, off as u32 + 4) as usize;
|
||||
if !(0x1001..0x100000).contains(&shell_size)
|
||||
|| shell
|
||||
.checked_add(shell_size)
|
||||
.is_none_or(|end| end > data.len())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (shell_field, header_field) in [(0x88, 0x58), (0x78, 0x50)] {
|
||||
let Some(candidate) = off.checked_sub(shell_field).filter(|&base| base >= shell) else {
|
||||
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.
|
||||
@@ -449,6 +466,57 @@ pub fn discover_eighth_slots(
|
||||
mod tests {
|
||||
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
|
||||
/// sits PAST `len` but within `len*2`. Assert that the smaller window misses
|
||||
/// it and the doubled window finds it.
|
||||
|
||||
@@ -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 sec_table = opt_hdr.wrapping_add(opt_hdr_size);
|
||||
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 {
|
||||
sec_off: u32,
|
||||
@@ -365,11 +372,21 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
|
||||
break;
|
||||
}
|
||||
}
|
||||
let meaningful = if last_nonzero >= 0 {
|
||||
let mut meaningful = if last_nonzero >= 0 {
|
||||
(last_nonzero + 1) as u32
|
||||
} else {
|
||||
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 {
|
||||
align_up_u32(meaningful, FILE_ALIGNMENT)
|
||||
} else {
|
||||
@@ -433,6 +450,45 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
|
||||
mod tests {
|
||||
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
|
||||
/// bail instead of building .kmiat at RVA 0 — the old code zeroed
|
||||
/// `[0, 0x7000)`, wiping the DOS/PE headers, and returned the broken image
|
||||
|
||||
+192
-50
@@ -1,6 +1,6 @@
|
||||
//! Android target orchestration: protected AArch64 shared libraries (`.so`),
|
||||
//! 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 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).
|
||||
/// Returns the unwrapped embedded metadata blob when the restored image
|
||||
/// 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 stage2_dir = temporary.path().join("stage2");
|
||||
extract_stage2(&ExtractOptions::with_defaults(
|
||||
let extraction = extract_stage2(&ExtractOptions::with_defaults(
|
||||
input.to_path_buf(),
|
||||
stage2_dir.clone(),
|
||||
))
|
||||
.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 {
|
||||
input: input.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")?;
|
||||
let restored =
|
||||
std::fs::read(dest).with_context(|| format!("read restored `{}`", dest.display()))?;
|
||||
Ok(senbei_metadata::android::extract_embedded_metadata(
|
||||
&restored,
|
||||
))
|
||||
Ok(SoRestoreContext {
|
||||
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
|
||||
@@ -134,8 +171,10 @@ pub fn content_identity(data: &[u8]) -> String {
|
||||
hex_digest(&digest.finalize())
|
||||
}
|
||||
|
||||
/// Restore an il2cpp metadata blob (Android seeded permutation first, then the
|
||||
/// structural remap used by the Windows builds).
|
||||
/// Restore an il2cpp metadata blob. Paired v24.1 Android packages use the
|
||||
/// 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
|
||||
/// 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
|
||||
/// already-clean blob.
|
||||
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)
|
||||
&& matches!(discovery.version, 29 | 31 | 39)
|
||||
{
|
||||
@@ -190,7 +255,7 @@ pub struct EntryOutcome {
|
||||
pub enum EntryKind {
|
||||
/// A protected shared library, restored.
|
||||
So,
|
||||
/// An il2cpp metadata blob, de-obfuscated (`remapped` tokens changed).
|
||||
/// An il2cpp metadata blob, de-obfuscated (`remapped` method fields changed).
|
||||
Metadata { remapped: usize },
|
||||
/// A metadata blob unwrapped from a restored library's data section.
|
||||
EmbeddedMetadata,
|
||||
@@ -204,12 +269,30 @@ pub enum EntryStatus {
|
||||
Duplicate,
|
||||
/// Content-probed but not a target (unprotected library).
|
||||
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,
|
||||
/// Recognised as a target but the restore failed.
|
||||
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.
|
||||
///
|
||||
/// `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,
|
||||
seen: &mut HashSet<String>,
|
||||
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>> {
|
||||
let bundle = package
|
||||
.extension()
|
||||
@@ -233,6 +335,7 @@ pub fn restore_package(
|
||||
let mut archive = open_package(package)?;
|
||||
let temporary = tempfile::tempdir().context("create package workspace")?;
|
||||
let mut outcomes = Vec::new();
|
||||
let mut context = PackageRestoreContext::new(verbose, method_index_module);
|
||||
|
||||
let mut direct = 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 {
|
||||
let label = format!("{}::{}", rel.display(), name.display());
|
||||
let dest = out_root.join(rel).join(crate::job::out_name(&name));
|
||||
@@ -271,11 +375,19 @@ pub fn restore_package(
|
||||
&dest,
|
||||
&temporary,
|
||||
seen,
|
||||
verbose,
|
||||
&mut context,
|
||||
)
|
||||
.with_context(|| format!("extract `{label}`"))?;
|
||||
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 {
|
||||
let nested_label = rel.join(&name);
|
||||
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
|
||||
// carrying same-named entries cannot collide.
|
||||
let base = rel.join(name.with_extension(""));
|
||||
for (nested_index, entry_name) in entries {
|
||||
let label = format!("{}::{}", nested_label.display(), entry_name.display());
|
||||
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,
|
||||
&label,
|
||||
&dest,
|
||||
&temporary,
|
||||
seen,
|
||||
verbose,
|
||||
)
|
||||
.with_context(|| format!("extract `{label}`"))?;
|
||||
outcomes.append(&mut entry_outcomes);
|
||||
nested_packages.push(NestedPackage {
|
||||
label: nested_label,
|
||||
path: nested_path,
|
||||
base: rel.join(name.with_extension("")),
|
||||
entries,
|
||||
});
|
||||
}
|
||||
|
||||
// A bundle can put libil2cpp.so in an ABI split while metadata stays in
|
||||
// base.apk. Share one context across the whole bundle and process all SOs
|
||||
// before any metadata, regardless of which nested APK owns each entry.
|
||||
for priority in [0_u8, 1_u8] {
|
||||
for nested_package in &nested_packages {
|
||||
let mut nested_archive = open_package(&nested_package.path)?;
|
||||
for (nested_index, entry_name) in nested_package
|
||||
.entries
|
||||
.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)
|
||||
@@ -328,7 +462,7 @@ fn restore_package_entry<R: Read + Seek>(
|
||||
dest: &Path,
|
||||
temporary: &tempfile::TempDir,
|
||||
seen: &mut HashSet<String>,
|
||||
verbose: bool,
|
||||
context: &mut PackageRestoreContext<'_>,
|
||||
) -> Result<Vec<EntryOutcome>> {
|
||||
let entry_path = extract_entry(archive, index, temporary, Path::new(label))?;
|
||||
let entry_file =
|
||||
@@ -358,31 +492,39 @@ fn restore_package_entry<R: Read + Seek>(
|
||||
if is_so {
|
||||
drop(entry_data);
|
||||
drop(entry_file);
|
||||
return Ok(match restore_so_file(&entry_path, dest, verbose) {
|
||||
Ok(embedded) => {
|
||||
let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)];
|
||||
if let Some(blob) = embedded {
|
||||
let meta_dest = embedded_metadata_dest(dest);
|
||||
let status = match write_metadata_blob(&meta_dest, &blob) {
|
||||
Ok(()) => EntryStatus::Restored,
|
||||
Err(error) => EntryStatus::Failed(error),
|
||||
};
|
||||
outcomes.push(EntryOutcome {
|
||||
label: format!("{label} (embedded metadata)"),
|
||||
dest: meta_dest,
|
||||
kind: EntryKind::EmbeddedMetadata,
|
||||
status,
|
||||
});
|
||||
return Ok(
|
||||
match restore_so_file_with_context(&entry_path, dest, context.verbose) {
|
||||
Ok(restored) => {
|
||||
if let Some(module) = restored.method_index_module {
|
||||
*context.method_index_module = Some(module);
|
||||
}
|
||||
let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)];
|
||||
if let Some(blob) = restored.embedded_metadata {
|
||||
let meta_dest = embedded_metadata_dest(dest);
|
||||
let status = match write_metadata_blob(&meta_dest, &blob) {
|
||||
Ok(()) => EntryStatus::Restored,
|
||||
Err(error) => EntryStatus::Failed(error),
|
||||
};
|
||||
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).
|
||||
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 => {
|
||||
let kind = EntryKind::Metadata {
|
||||
remapped: report.remapped,
|
||||
|
||||
+31
-9
@@ -15,7 +15,7 @@ pub struct Summary {
|
||||
/// — likely to crash at runtime (e.g. 0xC0000005). Counted in addition to
|
||||
/// `unpacked` (a suspect file is still written).
|
||||
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.
|
||||
pub metadata: usize,
|
||||
/// 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
|
||||
// inside a package (loose beats `.apk` beats `.apks`/`.xapk` bundle).
|
||||
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
|
||||
// duplicate source can actually exist in this run.
|
||||
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 dest_owned = dest.clone();
|
||||
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 {
|
||||
Ok(Ok(embedded)) => {
|
||||
@@ -310,12 +316,13 @@ pub fn run_folder_opts(
|
||||
let out_root_owned = out_root.clone();
|
||||
let mut seen_taken = std::mem::take(&mut android_seen);
|
||||
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,
|
||||
&rel_owned,
|
||||
&out_root_owned,
|
||||
&mut seen_taken,
|
||||
verbose_steps,
|
||||
&mut android_method_index_module,
|
||||
);
|
||||
(outcomes, seen_taken)
|
||||
}));
|
||||
@@ -361,7 +368,12 @@ pub fn run_folder_opts(
|
||||
let meta_owned = meta.clone();
|
||||
let dest_owned = dest.clone();
|
||||
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 {
|
||||
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);
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!(
|
||||
"META {rel:?} -> {dest:?}: v{} remapped {} method tokens",
|
||||
"META {rel:?} -> {dest:?}: v{} remapped {} method fields",
|
||||
report.version, report.remapped
|
||||
));
|
||||
}
|
||||
@@ -506,13 +518,13 @@ pub fn run_file_v(
|
||||
s.metadata = 1;
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!(
|
||||
"META {:?} -> {:?}: v{} remapped {} method tokens",
|
||||
"META {:?} -> {:?}: v{} remapped {} method fields",
|
||||
input, dest, report.version, report.remapped
|
||||
));
|
||||
}
|
||||
if quiet == 0 {
|
||||
println!(
|
||||
"✓ metadata v{} -> {:?} ({} method tokens remapped)",
|
||||
"✓ metadata v{} -> {:?} ({} method fields remapped)",
|
||||
report.version, dest, report.remapped
|
||||
);
|
||||
}
|
||||
@@ -735,6 +747,15 @@ pub fn deobfuscate_metadata_to(
|
||||
input: &Path,
|
||||
dest: &Path,
|
||||
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> {
|
||||
let data = std::fs::read(input)?;
|
||||
// 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
|
||||
// (rather than stringified) so the folder driver can apply its
|
||||
// unsupported-version policy.
|
||||
let (out, report) = crate::android::restore_metadata_bytes(&data)
|
||||
.map_err(|e| e.context(format!("{input:?}")))?;
|
||||
let (out, report) =
|
||||
crate::android::restore_metadata_bytes_with_module(&data, method_index_module)
|
||||
.map_err(|e| e.context(format!("{input:?}")))?;
|
||||
if report.remapped > 0 {
|
||||
if let Some(parent) = dest.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
|
||||
+2
-2
@@ -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`,
|
||||
/// 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) {
|
||||
if quiet {
|
||||
return;
|
||||
}
|
||||
let msg = format!(
|
||||
"{} metadata {} -> {} ({} method tokens remapped)",
|
||||
"{} metadata {} -> {} ({} method fields remapped)",
|
||||
"✓".green(),
|
||||
rel.display(),
|
||||
dest.display(),
|
||||
|
||||
@@ -128,18 +128,19 @@ pub(crate) fn overlay_exports_from_stub(out: &mut [u8], stub: &[u8]) {
|
||||
Some(v) if v.1 != 0 => v,
|
||||
_ => return,
|
||||
};
|
||||
let dst = match rva_to_file_off(out, export_rva) {
|
||||
Some(o) => o,
|
||||
None => return,
|
||||
let dst = match senbei_pe::parse(out)
|
||||
.and_then(|headers| senbei_pe::rva_range(out, headers, export_rva, export_size))
|
||||
{
|
||||
Ok(range) => range,
|
||||
Err(_) => return,
|
||||
};
|
||||
let src = match rva_to_file_off(stub, export_rva) {
|
||||
Some(o) => o,
|
||||
None => return,
|
||||
let src = match senbei_pe::parse(stub)
|
||||
.and_then(|headers| senbei_pe::rva_range(stub, headers, export_rva, export_size))
|
||||
{
|
||||
Ok(range) => range,
|
||||
Err(_) => return,
|
||||
};
|
||||
let n = export_size as usize;
|
||||
if dst + n <= out.len() && src + n <= stub.len() {
|
||||
out[dst..dst + n].copy_from_slice(&stub[src..src + n]);
|
||||
}
|
||||
out[dst].copy_from_slice(&stub[src]);
|
||||
}
|
||||
|
||||
/// Restore the CLR regions retained by an external-companion loader stub.
|
||||
@@ -633,6 +634,34 @@ mod tests {
|
||||
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]
|
||||
fn managed_companion_restores_rva_mapped_regions_without_overwriting_il() {
|
||||
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,9 +2,11 @@
|
||||
|
||||
mod embedded;
|
||||
mod keystream;
|
||||
mod method_indices;
|
||||
mod method_tokens;
|
||||
|
||||
pub use embedded::{embedded_metadata_size, extract_embedded_metadata};
|
||||
pub use method_indices::{MethodIndexError, MethodIndexReport, restore_method_indices_v24_1};
|
||||
pub use method_tokens::{
|
||||
DEFAULT_METHOD_TOKEN_SEED, Error, ImageKeyDiscovery, Report, SeedDiscoveryReport,
|
||||
discover_method_token_seeds, restore_method_tokens,
|
||||
|
||||
Generated
+7
-7
@@ -412,7 +412,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-crypto"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"thiserror",
|
||||
@@ -420,7 +420,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-elf"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"goblin",
|
||||
"thiserror",
|
||||
@@ -428,7 +428,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-engine"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"memmap2",
|
||||
"senbei-crypto",
|
||||
@@ -443,7 +443,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-io"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indicatif",
|
||||
@@ -464,7 +464,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-metadata"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
@@ -472,14 +472,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-pe"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "senbei-wasm"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"senbei-engine",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "senbei-wasm"
|
||||
version = "1.3.1"
|
||||
version = "1.3.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.98.1"
|
||||
description = "WebAssembly bindings for senbei (browser frontend assets live in web/)"
|
||||
|
||||
Reference in new issue
Block a user