mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-03 10:31:28 -05:00
Compare commits
14
Commits
ba380a5774
...
v1.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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]]
|
[[package]]
|
||||||
name = "senbei-cli"
|
name = "senbei-cli"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"senbei-engine",
|
"senbei-engine",
|
||||||
"senbei-io",
|
"senbei-io",
|
||||||
@@ -413,7 +413,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-crypto"
|
name = "senbei-crypto"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -421,7 +421,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-elf"
|
name = "senbei-elf"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"goblin",
|
"goblin",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -429,7 +429,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-engine"
|
name = "senbei-engine"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"senbei-crypto",
|
"senbei-crypto",
|
||||||
@@ -444,7 +444,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-io"
|
name = "senbei-io"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
@@ -465,7 +465,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-metadata"
|
name = "senbei-metadata"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -473,7 +473,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-pe"
|
name = "senbei-pe"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ exclude = ["senbei-wasm"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.98.1"
|
rust-version = "1.98.1"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|||||||
+354
-25
@@ -5,6 +5,8 @@ pub const SHT_STRTAB: u32 = 3;
|
|||||||
pub const SHT_LOUSER: u32 = 0x8000_0000;
|
pub const SHT_LOUSER: u32 = 0x8000_0000;
|
||||||
pub const SHF_ALLOC: u64 = 2;
|
pub const SHF_ALLOC: u64 = 2;
|
||||||
const PT_LOAD: u32 = 1;
|
const PT_LOAD: u32 = 1;
|
||||||
|
const PT_NOTE: u32 = 4;
|
||||||
|
const PT_PHDR: u32 = 6;
|
||||||
pub const PF_R: u32 = 4;
|
pub const PF_R: u32 = 4;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -220,7 +222,89 @@ impl ElfLayout {
|
|||||||
Ok(alignment)
|
Ok(alignment)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append_load_segment(&self, output: &mut [u8], segment: LoadSegment) -> Result<Self> {
|
pub fn additional_program_header_reservation(&self) -> Result<usize> {
|
||||||
|
if self.program_header_size != 0x38 {
|
||||||
|
return invalid("unexpected ELF program header size");
|
||||||
|
}
|
||||||
|
let new_count = self
|
||||||
|
.program_header_count
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or_else(|| Error::Invalid("program header count overflow".to_owned()))?;
|
||||||
|
let header_offset = checked_index(
|
||||||
|
self.program_header_offset,
|
||||||
|
self.program_header_count,
|
||||||
|
self.program_header_size,
|
||||||
|
)?;
|
||||||
|
let header_end = header_offset
|
||||||
|
.checked_add(self.program_header_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("new program header range overflow".to_owned()))?;
|
||||||
|
let first_file_section = self
|
||||||
|
.section_headers
|
||||||
|
.iter()
|
||||||
|
.filter(|section| section.section_type != SHT_NOBITS && section.size != 0)
|
||||||
|
.map(|section| section.offset)
|
||||||
|
.min();
|
||||||
|
if first_file_section.is_none_or(|offset| header_end as u64 <= offset) {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
new_count
|
||||||
|
.checked_mul(self.program_header_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("relocated program header size overflow".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_program_header_type(&self, data: &[u8], program_type: u32) -> Result<bool> {
|
||||||
|
for index in 0..self.program_header_count {
|
||||||
|
let offset =
|
||||||
|
checked_index(self.program_header_offset, index, self.program_header_size)?;
|
||||||
|
if read_u32(data, offset)? == program_type {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reusable_note_program_header(&self, data: &[u8]) -> Result<Option<usize>> {
|
||||||
|
let mut note_count = 0_usize;
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
for index in 0..self.program_header_count {
|
||||||
|
let offset =
|
||||||
|
checked_index(self.program_header_offset, index, self.program_header_size)?;
|
||||||
|
if read_u32(data, offset)? != PT_NOTE {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
note_count += 1;
|
||||||
|
let file_offset = read_u64(data, offset + 8)?;
|
||||||
|
let file_size = read_u64(data, offset + 0x20)?;
|
||||||
|
let file_end = file_offset
|
||||||
|
.checked_add(file_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("PT_NOTE file range overflow".to_owned()))?;
|
||||||
|
let contained = self.program_headers.iter().any(|segment| {
|
||||||
|
segment
|
||||||
|
.offset
|
||||||
|
.checked_add(segment.file_size)
|
||||||
|
.is_some_and(|segment_end| {
|
||||||
|
segment.offset <= file_offset && file_end <= segment_end
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if contained {
|
||||||
|
candidates.push((file_offset, index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if note_count < 2 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(candidates
|
||||||
|
.into_iter()
|
||||||
|
.max_by_key(|(file_offset, _)| *file_offset)
|
||||||
|
.map(|(_, index)| index))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append_load_segment(
|
||||||
|
&self,
|
||||||
|
output: &mut [u8],
|
||||||
|
segment: LoadSegment,
|
||||||
|
reserved_program_header_bytes: usize,
|
||||||
|
) -> Result<Self> {
|
||||||
if self.program_header_size != 0x38 {
|
if self.program_header_size != 0x38 {
|
||||||
return invalid("unexpected ELF program header size");
|
return invalid("unexpected ELF program header size");
|
||||||
}
|
}
|
||||||
@@ -274,23 +358,11 @@ impl ElfLayout {
|
|||||||
.ok_or_else(|| Error::Invalid("program header count overflow".to_owned()))?;
|
.ok_or_else(|| Error::Invalid("program header count overflow".to_owned()))?;
|
||||||
let new_count_u16 = u16::try_from(new_count)
|
let new_count_u16 = u16::try_from(new_count)
|
||||||
.map_err(|_| Error::Invalid("program header count exceeds u16".to_owned()))?;
|
.map_err(|_| Error::Invalid("program header count exceeds u16".to_owned()))?;
|
||||||
let header_offset = checked_index(
|
let reservation = self.additional_program_header_reservation()?;
|
||||||
self.program_header_offset,
|
if reserved_program_header_bytes != reservation {
|
||||||
self.program_header_count,
|
return invalid(format!(
|
||||||
self.program_header_size,
|
"program-header reservation mismatch: expected 0x{reservation:x}, got 0x{reserved_program_header_bytes:x}"
|
||||||
)?;
|
));
|
||||||
let header_end = header_offset
|
|
||||||
.checked_add(self.program_header_size)
|
|
||||||
.ok_or_else(|| Error::Invalid("new program header range overflow".to_owned()))?;
|
|
||||||
slice(output, header_offset, self.program_header_size)?;
|
|
||||||
let first_file_section = self
|
|
||||||
.section_headers
|
|
||||||
.iter()
|
|
||||||
.filter(|section| section.section_type != SHT_NOBITS && section.size != 0)
|
|
||||||
.map(|section| section.offset)
|
|
||||||
.min();
|
|
||||||
if first_file_section.is_some_and(|offset| header_end as u64 > offset) {
|
|
||||||
return invalid("no space for an additional program header");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut header = [0_u8; 0x38];
|
let mut header = [0_u8; 0x38];
|
||||||
@@ -302,17 +374,95 @@ impl ElfLayout {
|
|||||||
header[0x20..0x28].copy_from_slice(&segment.file_size.to_le_bytes());
|
header[0x20..0x28].copy_from_slice(&segment.file_size.to_le_bytes());
|
||||||
header[0x28..0x30].copy_from_slice(&segment.memory_size.to_le_bytes());
|
header[0x28..0x30].copy_from_slice(&segment.memory_size.to_le_bytes());
|
||||||
header[0x30..0x38].copy_from_slice(&segment.alignment.to_le_bytes());
|
header[0x30..0x38].copy_from_slice(&segment.alignment.to_le_bytes());
|
||||||
|
|
||||||
|
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
|
output
|
||||||
.get_mut(header_offset..header_end)
|
.get_mut(header_offset..header_end)
|
||||||
.ok_or_else(|| Error::Invalid("new program header exceeds output".to_owned()))?
|
.ok_or_else(|| Error::Invalid("new program header exceeds output".to_owned()))?
|
||||||
.copy_from_slice(&header);
|
.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
|
output
|
||||||
.get_mut(0x38..0x3a)
|
.get_mut(0x38..0x3a)
|
||||||
.ok_or_else(|| Error::Invalid("ELF header is truncated".to_owned()))?
|
.ok_or_else(|| Error::Invalid("ELF header is truncated".to_owned()))?
|
||||||
.copy_from_slice(&new_count_u16.to_le_bytes());
|
.copy_from_slice(&new_count_u16.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
let mut updated = self.clone();
|
let mut updated = self.clone();
|
||||||
updated.program_header_count = new_count;
|
updated.program_header_offset = updated_program_header_offset;
|
||||||
|
updated.program_header_count = updated_program_header_count;
|
||||||
updated.program_headers.push(segment);
|
updated.program_headers.push(segment);
|
||||||
Ok(updated)
|
Ok(updated)
|
||||||
}
|
}
|
||||||
@@ -394,6 +544,47 @@ impl ElfLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_pt_phdr(
|
||||||
|
output: &mut [u8],
|
||||||
|
table_offset: usize,
|
||||||
|
old_count: usize,
|
||||||
|
entry_size: usize,
|
||||||
|
relocated: Option<(u64, u64)>,
|
||||||
|
table_size: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
for index in 0..old_count {
|
||||||
|
let offset = checked_index(table_offset, index, entry_size)?;
|
||||||
|
if read_u32(output, offset)? != PT_PHDR {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some((file_offset, virtual_address)) = relocated {
|
||||||
|
output
|
||||||
|
.get_mut(offset + 8..offset + 0x10)
|
||||||
|
.ok_or_else(|| Error::Invalid("PT_PHDR file offset exceeds output".to_owned()))?
|
||||||
|
.copy_from_slice(&file_offset.to_le_bytes());
|
||||||
|
output
|
||||||
|
.get_mut(offset + 0x10..offset + 0x18)
|
||||||
|
.ok_or_else(|| Error::Invalid("PT_PHDR virtual address exceeds output".to_owned()))?
|
||||||
|
.copy_from_slice(&virtual_address.to_le_bytes());
|
||||||
|
output
|
||||||
|
.get_mut(offset + 0x18..offset + 0x20)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::Invalid("PT_PHDR physical address exceeds output".to_owned())
|
||||||
|
})?
|
||||||
|
.copy_from_slice(&virtual_address.to_le_bytes());
|
||||||
|
}
|
||||||
|
output
|
||||||
|
.get_mut(offset + 0x20..offset + 0x28)
|
||||||
|
.ok_or_else(|| Error::Invalid("PT_PHDR file size exceeds output".to_owned()))?
|
||||||
|
.copy_from_slice(&table_size.to_le_bytes());
|
||||||
|
output
|
||||||
|
.get_mut(offset + 0x28..offset + 0x30)
|
||||||
|
.ok_or_else(|| Error::Invalid("PT_PHDR memory size exceeds output".to_owned()))?
|
||||||
|
.copy_from_slice(&table_size.to_le_bytes());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
|
pub fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
|
||||||
let end = offset
|
let end = offset
|
||||||
.checked_add(size)
|
.checked_add(size)
|
||||||
@@ -585,6 +776,7 @@ mod tests {
|
|||||||
flags: PF_R,
|
flags: PF_R,
|
||||||
alignment: 0x1000,
|
alignment: 0x1000,
|
||||||
},
|
},
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
.expect("append segment");
|
.expect("append segment");
|
||||||
assert_eq!(updated.program_header_count, 1);
|
assert_eq!(updated.program_header_count, 1);
|
||||||
@@ -594,7 +786,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn append_load_segment_rejects_program_header_overlap() {
|
fn append_load_segment_fails_closed_when_no_safe_phdr_slot() {
|
||||||
let mut elf_layout = ElfLayout {
|
let mut elf_layout = ElfLayout {
|
||||||
entrypoint: 0,
|
entrypoint: 0,
|
||||||
program_header_offset: 0,
|
program_header_offset: 0,
|
||||||
@@ -617,20 +809,157 @@ mod tests {
|
|||||||
alignment: 1,
|
alignment: 1,
|
||||||
entry_size: 0,
|
entry_size: 0,
|
||||||
});
|
});
|
||||||
let mut output = vec![0_u8; 0x100];
|
let reservation = elf_layout
|
||||||
|
.additional_program_header_reservation()
|
||||||
|
.expect("reservation");
|
||||||
|
assert_eq!(reservation, 0x38);
|
||||||
|
let mut output = vec![0_u8; 0x200];
|
||||||
let error = elf_layout
|
let error = elf_layout
|
||||||
.append_load_segment(
|
.append_load_segment(
|
||||||
&mut output,
|
&mut output,
|
||||||
LoadSegment {
|
LoadSegment {
|
||||||
offset: 0x80,
|
offset: 0x80,
|
||||||
virtual_address: 0x1080,
|
virtual_address: 0x1080,
|
||||||
file_size: 0x20,
|
file_size: 0x80,
|
||||||
memory_size: 0x20,
|
memory_size: 0x80,
|
||||||
flags: PF_R,
|
flags: PF_R,
|
||||||
alignment: 0x1000,
|
alignment: 0x1000,
|
||||||
},
|
},
|
||||||
|
reservation,
|
||||||
)
|
)
|
||||||
.expect_err("overlapping program header");
|
.expect_err("unsafe relocation must fail");
|
||||||
assert!(error.to_string().contains("additional program header"));
|
assert!(error.to_string().contains("cannot safely relocate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn append_load_segment_reuses_redundant_note_when_no_slack() {
|
||||||
|
let mut elf_layout = ElfLayout {
|
||||||
|
entrypoint: 0,
|
||||||
|
program_header_offset: 0x40,
|
||||||
|
program_header_size: 0x38,
|
||||||
|
program_header_count: 3,
|
||||||
|
program_headers: vec![LoadSegment {
|
||||||
|
offset: 0,
|
||||||
|
virtual_address: 0,
|
||||||
|
file_size: 0x400,
|
||||||
|
memory_size: 0x400,
|
||||||
|
flags: PF_R,
|
||||||
|
alignment: 0x1000,
|
||||||
|
}],
|
||||||
|
section_headers: Vec::new(),
|
||||||
|
section_name_index: 0,
|
||||||
|
private_section_index: usize::MAX,
|
||||||
|
};
|
||||||
|
elf_layout.section_headers.push(SectionHeader {
|
||||||
|
name: 0,
|
||||||
|
section_type: 1,
|
||||||
|
flags: 0,
|
||||||
|
address: 0,
|
||||||
|
offset: 0xe8,
|
||||||
|
size: 1,
|
||||||
|
link: 0,
|
||||||
|
info: 0,
|
||||||
|
alignment: 1,
|
||||||
|
entry_size: 0,
|
||||||
|
});
|
||||||
|
let mut output = vec![0_u8; 0x2000];
|
||||||
|
output[0x38..0x3a].copy_from_slice(&3_u16.to_le_bytes());
|
||||||
|
output[0x40..0x44].copy_from_slice(&PT_LOAD.to_le_bytes());
|
||||||
|
output[0x78..0x7c].copy_from_slice(&PT_NOTE.to_le_bytes());
|
||||||
|
output[0x80..0x88].copy_from_slice(&0x100_u64.to_le_bytes());
|
||||||
|
output[0x98..0xa0].copy_from_slice(&0x20_u64.to_le_bytes());
|
||||||
|
output[0xb0..0xb4].copy_from_slice(&PT_NOTE.to_le_bytes());
|
||||||
|
output[0xb8..0xc0].copy_from_slice(&0x200_u64.to_le_bytes());
|
||||||
|
output[0xd0..0xd8].copy_from_slice(&0x20_u64.to_le_bytes());
|
||||||
|
|
||||||
|
let reservation = elf_layout
|
||||||
|
.additional_program_header_reservation()
|
||||||
|
.expect("reservation");
|
||||||
|
assert_eq!(reservation, 0xe0);
|
||||||
|
let updated = elf_layout
|
||||||
|
.append_load_segment(
|
||||||
|
&mut output,
|
||||||
|
LoadSegment {
|
||||||
|
offset: 0x1000,
|
||||||
|
virtual_address: 0x2000,
|
||||||
|
file_size: 0x100,
|
||||||
|
memory_size: 0x100,
|
||||||
|
flags: PF_R,
|
||||||
|
alignment: 0x1000,
|
||||||
|
},
|
||||||
|
reservation,
|
||||||
|
)
|
||||||
|
.expect("reuse redundant PT_NOTE");
|
||||||
|
|
||||||
|
assert_eq!(updated.program_header_offset, 0x40);
|
||||||
|
assert_eq!(updated.program_header_count, 3);
|
||||||
|
assert_eq!(updated.program_headers.len(), 2);
|
||||||
|
assert_eq!(&output[0x38..0x3a], &3_u16.to_le_bytes());
|
||||||
|
assert_eq!(&output[0x78..0x7c], &PT_NOTE.to_le_bytes());
|
||||||
|
assert_eq!(&output[0xb0..0xb4], &PT_LOAD.to_le_bytes());
|
||||||
|
assert_eq!(&output[0xb8..0xc0], &0x1000_u64.to_le_bytes());
|
||||||
|
assert_eq!(&output[0xc0..0xc8], &0x2000_u64.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relocated_program_headers_update_pt_phdr() {
|
||||||
|
let mut elf_layout = ElfLayout {
|
||||||
|
entrypoint: 0,
|
||||||
|
program_header_offset: 0x40,
|
||||||
|
program_header_size: 0x38,
|
||||||
|
program_header_count: 1,
|
||||||
|
program_headers: Vec::new(),
|
||||||
|
section_headers: Vec::new(),
|
||||||
|
section_name_index: 0,
|
||||||
|
private_section_index: usize::MAX,
|
||||||
|
};
|
||||||
|
elf_layout.section_headers.push(SectionHeader {
|
||||||
|
name: 0,
|
||||||
|
section_type: 1,
|
||||||
|
flags: 0,
|
||||||
|
address: 0,
|
||||||
|
offset: 0x78,
|
||||||
|
size: 1,
|
||||||
|
link: 0,
|
||||||
|
info: 0,
|
||||||
|
alignment: 1,
|
||||||
|
entry_size: 0,
|
||||||
|
});
|
||||||
|
let mut output = vec![0_u8; 0x2000];
|
||||||
|
output[0x20..0x28].copy_from_slice(&0x40_u64.to_le_bytes());
|
||||||
|
output[0x40..0x44].copy_from_slice(&PT_PHDR.to_le_bytes());
|
||||||
|
output[0x48..0x50].copy_from_slice(&0x40_u64.to_le_bytes());
|
||||||
|
output[0x50..0x58].copy_from_slice(&0x40_u64.to_le_bytes());
|
||||||
|
output[0x58..0x60].copy_from_slice(&0x40_u64.to_le_bytes());
|
||||||
|
output[0x60..0x68].copy_from_slice(&0x38_u64.to_le_bytes());
|
||||||
|
output[0x68..0x70].copy_from_slice(&0x38_u64.to_le_bytes());
|
||||||
|
|
||||||
|
let reservation = elf_layout
|
||||||
|
.additional_program_header_reservation()
|
||||||
|
.expect("reservation");
|
||||||
|
assert_eq!(reservation, 0x70);
|
||||||
|
let updated = elf_layout
|
||||||
|
.append_load_segment(
|
||||||
|
&mut output,
|
||||||
|
LoadSegment {
|
||||||
|
offset: 0x1000,
|
||||||
|
virtual_address: 0x2000,
|
||||||
|
file_size: 0x100,
|
||||||
|
memory_size: 0x100,
|
||||||
|
flags: PF_R,
|
||||||
|
alignment: 0x1000,
|
||||||
|
},
|
||||||
|
reservation,
|
||||||
|
)
|
||||||
|
.expect("relocate program headers");
|
||||||
|
|
||||||
|
assert_eq!(updated.program_header_offset, 0x1000);
|
||||||
|
assert_eq!(&output[0x20..0x28], &0x1000_u64.to_le_bytes());
|
||||||
|
assert_eq!(&output[0x1000..0x1004], &PT_PHDR.to_le_bytes());
|
||||||
|
assert_eq!(&output[0x1008..0x1010], &0x1000_u64.to_le_bytes());
|
||||||
|
assert_eq!(&output[0x1010..0x1018], &0x2000_u64.to_le_bytes());
|
||||||
|
assert_eq!(&output[0x1020..0x1028], &0x70_u64.to_le_bytes());
|
||||||
|
assert_eq!(&output[0x1028..0x1030], &0x70_u64.to_le_bytes());
|
||||||
|
assert_eq!(&output[0x1038..0x103c], &PT_LOAD.to_le_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-10
@@ -15,26 +15,23 @@ pub use layout::{
|
|||||||
/// ELF machine identifier for AArch64.
|
/// ELF machine identifier for AArch64.
|
||||||
pub const AARCH64_MACHINE: u16 = EM_AARCH64;
|
pub const AARCH64_MACHINE: u16 = EM_AARCH64;
|
||||||
|
|
||||||
/// Dynamic sections required by the restored AArch64 loader image.
|
/// Dynamic sections required by every restored AArch64 loader image.
|
||||||
pub const DYNAMIC_SECTION_NAMES: [&str; 8] = [
|
pub const DYNAMIC_SECTION_NAMES: [&str; 7] = [
|
||||||
".dynsym",
|
".dynsym",
|
||||||
".gnu.version",
|
".gnu.version",
|
||||||
".gnu.version_r",
|
".gnu.version_r",
|
||||||
".gnu.hash",
|
|
||||||
".dynstr",
|
".dynstr",
|
||||||
".rela.dyn",
|
".rela.dyn",
|
||||||
".rela.plt",
|
".rela.plt",
|
||||||
".dynamic",
|
".dynamic",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Supported dynamic symbol-hash sections. At least one must be present.
|
||||||
|
pub const SYMBOL_HASH_SECTION_NAMES: [&str; 2] = [".gnu.hash", ".hash"];
|
||||||
|
|
||||||
/// Dynamic sections needed to identify a protected image before extraction.
|
/// Dynamic sections needed to identify a protected image before extraction.
|
||||||
pub const PROBE_SECTION_NAMES: [&str; 5] = [
|
/// Hash tables are checked separately because either GNU or SysV hashing is valid.
|
||||||
".dynsym",
|
pub const PROBE_SECTION_NAMES: [&str; 4] = [".dynsym", ".dynstr", ".gnu.version", ".gnu.version_r"];
|
||||||
".dynstr",
|
|
||||||
".gnu.hash",
|
|
||||||
".gnu.version",
|
|
||||||
".gnu.version_r",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// ELF64 dynamic table record sizes.
|
/// ELF64 dynamic table record sizes.
|
||||||
pub const ELF64_SYMBOL_SIZE: usize = 0x18;
|
pub const ELF64_SYMBOL_SIZE: usize = 0x18;
|
||||||
|
|||||||
@@ -70,6 +70,16 @@ pub(crate) fn inspect(
|
|||||||
return invalid(format!("protected ELF lacks required section {wanted}"));
|
return invalid(format!("protected ELF lacks required section {wanted}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let has_symbol_hash = senbei_elf::SYMBOL_HASH_SECTION_NAMES.iter().any(|wanted| {
|
||||||
|
elf.section_headers.iter().any(|section| {
|
||||||
|
elf.shdr_strtab
|
||||||
|
.get_at(section.sh_name)
|
||||||
|
.is_some_and(|name| name == *wanted)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !has_symbol_hash {
|
||||||
|
return invalid("protected ELF lacks a .gnu.hash or .hash symbol-hash section");
|
||||||
|
}
|
||||||
let (section_index, section) = matches[0];
|
let (section_index, section) = matches[0];
|
||||||
let section_offset = usize::try_from(section.sh_offset)
|
let section_offset = usize::try_from(section.sh_offset)
|
||||||
.map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?;
|
.map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?;
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ use serde_json::Value;
|
|||||||
use super::error::{Error, Result, invalid};
|
use super::error::{Error, Result, invalid};
|
||||||
|
|
||||||
const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e];
|
const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e];
|
||||||
|
const OPTIONAL_IDS: [u32; 2] = [0x96, 0x98];
|
||||||
|
|
||||||
|
fn wanted_id(command_id: u32) -> bool {
|
||||||
|
REQUIRED_IDS.contains(&command_id) || OPTIONAL_IDS.contains(&command_id)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(crate) struct Artifact {
|
pub(crate) struct Artifact {
|
||||||
@@ -27,7 +32,7 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
|
|||||||
};
|
};
|
||||||
let command_id = u32::try_from(command_id)
|
let command_id = u32::try_from(command_id)
|
||||||
.map_err(|_| Error::Invalid("module command ID exceeds u32".to_owned()))?;
|
.map_err(|_| Error::Invalid("module command ID exceeds u32".to_owned()))?;
|
||||||
if !REQUIRED_IDS.contains(&command_id) {
|
if !wanted_id(command_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(path) = item.get("image_path").and_then(Value::as_str) else {
|
let Some(path) = item.get("image_path").and_then(Value::as_str) else {
|
||||||
@@ -57,7 +62,7 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
|
|||||||
};
|
};
|
||||||
let command_id = u32::try_from(command_id)
|
let command_id = u32::try_from(command_id)
|
||||||
.map_err(|_| Error::Invalid("record command ID exceeds u32".to_owned()))?;
|
.map_err(|_| Error::Invalid("record command ID exceeds u32".to_owned()))?;
|
||||||
if !REQUIRED_IDS.contains(&command_id) {
|
if !wanted_id(command_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(image) = record.get("image") else {
|
let Some(image) = record.get("image") else {
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
+168
-26
@@ -1,6 +1,6 @@
|
|||||||
//! Android target orchestration: protected AArch64 shared libraries (`.so`),
|
//! Android target orchestration: protected AArch64 shared libraries (`.so`),
|
||||||
//! app packages (`.apk` / `.apks` / `.xapk`), and the Android variant of the
|
//! app packages (`.apk` / `.apks` / `.xapk`), and the Android variant of the
|
||||||
//! il2cpp method-token obfuscation.
|
//! il2cpp method-token/method-index obfuscation.
|
||||||
//!
|
//!
|
||||||
//! The protection scheme hollows out an ELF64/AArch64 shared object and moves
|
//! The protection scheme hollows out an ELF64/AArch64 shared object and moves
|
||||||
//! the original bytes into an encrypted payload appended as a `SHT_LOUSER`
|
//! the original bytes into an encrypted payload appended as a `SHT_LOUSER`
|
||||||
@@ -100,14 +100,32 @@ pub fn file_content_identity(path: &Path) -> std::io::Result<String> {
|
|||||||
/// implementation detail of the two-phase restore, not user-facing output).
|
/// implementation detail of the two-phase restore, not user-facing output).
|
||||||
/// Returns the unwrapped embedded metadata blob when the restored image
|
/// Returns the unwrapped embedded metadata blob when the restored image
|
||||||
/// carries one (see the module docs); the caller decides where to write it.
|
/// carries one (see the module docs); the caller decides where to write it.
|
||||||
pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Option<Vec<u8>>> {
|
struct SoRestoreContext {
|
||||||
|
embedded_metadata: Option<Vec<u8>>,
|
||||||
|
method_index_module: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_so_file_with_context(
|
||||||
|
input: &Path,
|
||||||
|
dest: &Path,
|
||||||
|
verbose: bool,
|
||||||
|
) -> Result<SoRestoreContext> {
|
||||||
let temporary = tempfile::tempdir().context("create stage-2 workspace")?;
|
let temporary = tempfile::tempdir().context("create stage-2 workspace")?;
|
||||||
let stage2_dir = temporary.path().join("stage2");
|
let stage2_dir = temporary.path().join("stage2");
|
||||||
extract_stage2(&ExtractOptions::with_defaults(
|
let extraction = extract_stage2(&ExtractOptions::with_defaults(
|
||||||
input.to_path_buf(),
|
input.to_path_buf(),
|
||||||
stage2_dir.clone(),
|
stage2_dir.clone(),
|
||||||
))
|
))
|
||||||
.context("extract stage-1/stage-2 payload")?;
|
.context("extract stage-1/stage-2 payload")?;
|
||||||
|
let method_index_module = extraction
|
||||||
|
.module_registry
|
||||||
|
.iter()
|
||||||
|
.find(|module| module.command_id == 0x0c)
|
||||||
|
.map(|module| {
|
||||||
|
std::fs::read(stage2_dir.join(&module.image_path))
|
||||||
|
.with_context(|| format!("read decoded module 0x0C `{}`", module.image_path))
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
restore_libil2cpp(&RestoreOptions {
|
restore_libil2cpp(&RestoreOptions {
|
||||||
input: input.to_path_buf(),
|
input: input.to_path_buf(),
|
||||||
output: dest.to_path_buf(),
|
output: dest.to_path_buf(),
|
||||||
@@ -120,9 +138,28 @@ pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Optio
|
|||||||
.context("restore protected library")?;
|
.context("restore protected library")?;
|
||||||
let restored =
|
let restored =
|
||||||
std::fs::read(dest).with_context(|| format!("read restored `{}`", dest.display()))?;
|
std::fs::read(dest).with_context(|| format!("read restored `{}`", dest.display()))?;
|
||||||
Ok(senbei_metadata::android::extract_embedded_metadata(
|
Ok(SoRestoreContext {
|
||||||
&restored,
|
embedded_metadata: senbei_metadata::android::extract_embedded_metadata(&restored),
|
||||||
))
|
method_index_module,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Option<Vec<u8>>> {
|
||||||
|
let mut method_index_module = None;
|
||||||
|
restore_so_file_with_method_index_module(input, dest, verbose, &mut method_index_module)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn restore_so_file_with_method_index_module(
|
||||||
|
input: &Path,
|
||||||
|
dest: &Path,
|
||||||
|
verbose: bool,
|
||||||
|
method_index_module: &mut Option<Vec<u8>>,
|
||||||
|
) -> Result<Option<Vec<u8>>> {
|
||||||
|
let restored = restore_so_file_with_context(input, dest, verbose)?;
|
||||||
|
if let Some(module) = restored.method_index_module {
|
||||||
|
*method_index_module = Some(module);
|
||||||
|
}
|
||||||
|
Ok(restored.embedded_metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Content identity for cross-source deduplication: the same library may
|
/// Content identity for cross-source deduplication: the same library may
|
||||||
@@ -134,8 +171,10 @@ pub fn content_identity(data: &[u8]) -> String {
|
|||||||
hex_digest(&digest.finalize())
|
hex_digest(&digest.finalize())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore an il2cpp metadata blob (Android seeded permutation first, then the
|
/// Restore an il2cpp metadata blob. Paired v24.1 Android packages use the
|
||||||
/// structural remap used by the Windows builds).
|
/// method-index profile recovered from module 0x0C; later Android layouts use
|
||||||
|
/// the seeded MethodDef RID permutation before falling back to the structural
|
||||||
|
/// remap used by the Windows builds.
|
||||||
///
|
///
|
||||||
/// The Android variant obfuscates MethodDef RIDs with a keyed five-round
|
/// The Android variant obfuscates MethodDef RIDs with a keyed five-round
|
||||||
/// permutation; the correct seed is recovered by intersecting per-image key
|
/// permutation; the correct seed is recovered by intersecting per-image key
|
||||||
@@ -145,6 +184,32 @@ pub fn content_identity(data: &[u8]) -> String {
|
|||||||
/// canonical form. Both paths are no-ops (`remapped == 0`) on an
|
/// canonical form. Both paths are no-ops (`remapped == 0`) on an
|
||||||
/// already-clean blob.
|
/// already-clean blob.
|
||||||
pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
|
pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
|
||||||
|
restore_metadata_bytes_with_module(data, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn restore_metadata_bytes_with_module(
|
||||||
|
data: &[u8],
|
||||||
|
method_index_module: Option<&[u8]>,
|
||||||
|
) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
|
||||||
|
let version = data
|
||||||
|
.get(4..8)
|
||||||
|
.and_then(|bytes| <[u8; 4]>::try_from(bytes).ok())
|
||||||
|
.map(u32::from_le_bytes);
|
||||||
|
if version == Some(24)
|
||||||
|
&& let Some(module) = method_index_module
|
||||||
|
{
|
||||||
|
let (out, report) = senbei_metadata::android::restore_method_indices_v24_1(data, module)
|
||||||
|
.map_err(anyhow::Error::new)?;
|
||||||
|
return Ok((
|
||||||
|
out,
|
||||||
|
senbei_metadata::Report {
|
||||||
|
version: report.version,
|
||||||
|
methods: report.methods,
|
||||||
|
remapped: report.changed_indices,
|
||||||
|
modules: 0,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
if let Ok(discovery) = senbei_metadata::android::discover_method_token_seeds(data)
|
if let Ok(discovery) = senbei_metadata::android::discover_method_token_seeds(data)
|
||||||
&& matches!(discovery.version, 29 | 31 | 39)
|
&& matches!(discovery.version, 29 | 31 | 39)
|
||||||
{
|
{
|
||||||
@@ -190,7 +255,7 @@ pub struct EntryOutcome {
|
|||||||
pub enum EntryKind {
|
pub enum EntryKind {
|
||||||
/// A protected shared library, restored.
|
/// A protected shared library, restored.
|
||||||
So,
|
So,
|
||||||
/// An il2cpp metadata blob, de-obfuscated (`remapped` tokens changed).
|
/// An il2cpp metadata blob, de-obfuscated (`remapped` method fields changed).
|
||||||
Metadata { remapped: usize },
|
Metadata { remapped: usize },
|
||||||
/// A metadata blob unwrapped from a restored library's data section.
|
/// A metadata blob unwrapped from a restored library's data section.
|
||||||
EmbeddedMetadata,
|
EmbeddedMetadata,
|
||||||
@@ -204,12 +269,30 @@ pub enum EntryStatus {
|
|||||||
Duplicate,
|
Duplicate,
|
||||||
/// Content-probed but not a target (unprotected library).
|
/// Content-probed but not a target (unprotected library).
|
||||||
NotTarget,
|
NotTarget,
|
||||||
/// A metadata blob whose tokens were already canonical; no copy written.
|
/// A metadata blob whose protected method fields were already canonical; no copy written.
|
||||||
Unchanged,
|
Unchanged,
|
||||||
/// Recognised as a target but the restore failed.
|
/// Recognised as a target but the restore failed.
|
||||||
Failed(anyhow::Error),
|
Failed(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PackageRestoreContext<'a> {
|
||||||
|
method_index_module: &'a mut Option<Vec<u8>>,
|
||||||
|
verbose: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PackageRestoreContext<'a> {
|
||||||
|
fn new(verbose: bool, method_index_module: &'a mut Option<Vec<u8>>) -> Self {
|
||||||
|
Self {
|
||||||
|
method_index_module,
|
||||||
|
verbose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn package_entry_priority(path: &Path) -> u8 {
|
||||||
|
if is_so_name(path) { 0 } else { 1 }
|
||||||
|
}
|
||||||
|
|
||||||
/// Restore every protected library and metadata blob inside one app package.
|
/// Restore every protected library and metadata blob inside one app package.
|
||||||
///
|
///
|
||||||
/// `rel` is the package's path relative to the scanned root (or its bare file
|
/// `rel` is the package's path relative to the scanned root (or its bare file
|
||||||
@@ -223,6 +306,25 @@ pub fn restore_package(
|
|||||||
out_root: &Path,
|
out_root: &Path,
|
||||||
seen: &mut HashSet<String>,
|
seen: &mut HashSet<String>,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
) -> Result<Vec<EntryOutcome>> {
|
||||||
|
let mut method_index_module = None;
|
||||||
|
restore_package_with_method_index_module(
|
||||||
|
package,
|
||||||
|
rel,
|
||||||
|
out_root,
|
||||||
|
seen,
|
||||||
|
verbose,
|
||||||
|
&mut method_index_module,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn restore_package_with_method_index_module(
|
||||||
|
package: &Path,
|
||||||
|
rel: &Path,
|
||||||
|
out_root: &Path,
|
||||||
|
seen: &mut HashSet<String>,
|
||||||
|
verbose: bool,
|
||||||
|
method_index_module: &mut Option<Vec<u8>>,
|
||||||
) -> Result<Vec<EntryOutcome>> {
|
) -> Result<Vec<EntryOutcome>> {
|
||||||
let bundle = package
|
let bundle = package
|
||||||
.extension()
|
.extension()
|
||||||
@@ -233,6 +335,7 @@ pub fn restore_package(
|
|||||||
let mut archive = open_package(package)?;
|
let mut archive = open_package(package)?;
|
||||||
let temporary = tempfile::tempdir().context("create package workspace")?;
|
let temporary = tempfile::tempdir().context("create package workspace")?;
|
||||||
let mut outcomes = Vec::new();
|
let mut outcomes = Vec::new();
|
||||||
|
let mut context = PackageRestoreContext::new(verbose, method_index_module);
|
||||||
|
|
||||||
let mut direct = Vec::new();
|
let mut direct = Vec::new();
|
||||||
let mut nested = Vec::new();
|
let mut nested = Vec::new();
|
||||||
@@ -261,6 +364,7 @@ pub fn restore_package(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
direct.sort_by_key(|(_, name)| package_entry_priority(name));
|
||||||
for (index, name) in direct {
|
for (index, name) in direct {
|
||||||
let label = format!("{}::{}", rel.display(), name.display());
|
let label = format!("{}::{}", rel.display(), name.display());
|
||||||
let dest = out_root.join(rel).join(crate::job::out_name(&name));
|
let dest = out_root.join(rel).join(crate::job::out_name(&name));
|
||||||
@@ -271,11 +375,19 @@ pub fn restore_package(
|
|||||||
&dest,
|
&dest,
|
||||||
&temporary,
|
&temporary,
|
||||||
seen,
|
seen,
|
||||||
verbose,
|
&mut context,
|
||||||
)
|
)
|
||||||
.with_context(|| format!("extract `{label}`"))?;
|
.with_context(|| format!("extract `{label}`"))?;
|
||||||
outcomes.append(&mut entry_outcomes);
|
outcomes.append(&mut entry_outcomes);
|
||||||
}
|
}
|
||||||
|
struct NestedPackage {
|
||||||
|
label: PathBuf,
|
||||||
|
path: PathBuf,
|
||||||
|
base: PathBuf,
|
||||||
|
entries: Vec<(usize, PathBuf)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut nested_packages = Vec::with_capacity(nested.len());
|
||||||
for (index, name) in nested {
|
for (index, name) in nested {
|
||||||
let nested_label = rel.join(&name);
|
let nested_label = rel.join(&name);
|
||||||
let nested_path = extract_entry(&mut archive, index, &temporary, &nested_label)
|
let nested_path = extract_entry(&mut archive, index, &temporary, &nested_label)
|
||||||
@@ -296,25 +408,47 @@ pub fn restore_package(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Keep the nested package's stem in the output layout so two splits
|
nested_packages.push(NestedPackage {
|
||||||
// carrying same-named entries cannot collide.
|
label: nested_label,
|
||||||
let base = rel.join(name.with_extension(""));
|
path: nested_path,
|
||||||
for (nested_index, entry_name) in entries {
|
base: rel.join(name.with_extension("")),
|
||||||
let label = format!("{}::{}", nested_label.display(), entry_name.display());
|
entries,
|
||||||
let dest = out_root.join(&base).join(crate::job::out_name(&entry_name));
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
let mut entry_outcomes = restore_package_entry(
|
||||||
&mut nested_archive,
|
&mut nested_archive,
|
||||||
nested_index,
|
*nested_index,
|
||||||
&label,
|
&label,
|
||||||
&dest,
|
&dest,
|
||||||
&temporary,
|
&temporary,
|
||||||
seen,
|
seen,
|
||||||
verbose,
|
&mut context,
|
||||||
)
|
)
|
||||||
.with_context(|| format!("extract `{label}`"))?;
|
.with_context(|| format!("extract `{label}`"))?;
|
||||||
outcomes.append(&mut entry_outcomes);
|
outcomes.append(&mut entry_outcomes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(outcomes)
|
Ok(outcomes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +462,7 @@ fn restore_package_entry<R: Read + Seek>(
|
|||||||
dest: &Path,
|
dest: &Path,
|
||||||
temporary: &tempfile::TempDir,
|
temporary: &tempfile::TempDir,
|
||||||
seen: &mut HashSet<String>,
|
seen: &mut HashSet<String>,
|
||||||
verbose: bool,
|
context: &mut PackageRestoreContext<'_>,
|
||||||
) -> Result<Vec<EntryOutcome>> {
|
) -> Result<Vec<EntryOutcome>> {
|
||||||
let entry_path = extract_entry(archive, index, temporary, Path::new(label))?;
|
let entry_path = extract_entry(archive, index, temporary, Path::new(label))?;
|
||||||
let entry_file =
|
let entry_file =
|
||||||
@@ -358,10 +492,14 @@ fn restore_package_entry<R: Read + Seek>(
|
|||||||
if is_so {
|
if is_so {
|
||||||
drop(entry_data);
|
drop(entry_data);
|
||||||
drop(entry_file);
|
drop(entry_file);
|
||||||
return Ok(match restore_so_file(&entry_path, dest, verbose) {
|
return Ok(
|
||||||
Ok(embedded) => {
|
match restore_so_file_with_context(&entry_path, dest, context.verbose) {
|
||||||
|
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)];
|
let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)];
|
||||||
if let Some(blob) = embedded {
|
if let Some(blob) = restored.embedded_metadata {
|
||||||
let meta_dest = embedded_metadata_dest(dest);
|
let meta_dest = embedded_metadata_dest(dest);
|
||||||
let status = match write_metadata_blob(&meta_dest, &blob) {
|
let status = match write_metadata_blob(&meta_dest, &blob) {
|
||||||
Ok(()) => EntryStatus::Restored,
|
Ok(()) => EntryStatus::Restored,
|
||||||
@@ -377,12 +515,16 @@ fn restore_package_entry<R: Read + Seek>(
|
|||||||
outcomes
|
outcomes
|
||||||
}
|
}
|
||||||
Err(error) => vec![outcome(EntryKind::So, EntryStatus::Failed(error))],
|
Err(error) => vec![outcome(EntryKind::So, EntryStatus::Failed(error))],
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata entry: write only when the restore actually changed tokens —
|
// Metadata entry: write only when the restore actually changed protected method fields —
|
||||||
// a clean blob needs no copy (same contract as loose metadata files).
|
// a clean blob needs no copy (same contract as loose metadata files).
|
||||||
let kind_and_status = match restore_metadata_bytes(&entry_data) {
|
let kind_and_status = match restore_metadata_bytes_with_module(
|
||||||
|
&entry_data,
|
||||||
|
context.method_index_module.as_deref(),
|
||||||
|
) {
|
||||||
Ok((out, report)) if report.remapped > 0 => {
|
Ok((out, report)) if report.remapped > 0 => {
|
||||||
let kind = EntryKind::Metadata {
|
let kind = EntryKind::Metadata {
|
||||||
remapped: report.remapped,
|
remapped: report.remapped,
|
||||||
|
|||||||
+30
-8
@@ -15,7 +15,7 @@ pub struct Summary {
|
|||||||
/// — likely to crash at runtime (e.g. 0xC0000005). Counted in addition to
|
/// — likely to crash at runtime (e.g. 0xC0000005). Counted in addition to
|
||||||
/// `unpacked` (a suspect file is still written).
|
/// `unpacked` (a suspect file is still written).
|
||||||
pub suspect: usize,
|
pub suspect: usize,
|
||||||
/// il2cpp `global-metadata.dat` files de-obfuscated (method tokens remapped),
|
/// il2cpp `global-metadata.dat` files de-obfuscated (protected method fields remapped),
|
||||||
/// including blobs unwrapped from restored Android libraries.
|
/// including blobs unwrapped from restored Android libraries.
|
||||||
pub metadata: usize,
|
pub metadata: usize,
|
||||||
/// Android app packages (`.apk`/`.apks`/`.xapk`) opened and searched.
|
/// Android app packages (`.apk`/`.apks`/`.xapk`) opened and searched.
|
||||||
@@ -221,6 +221,7 @@ pub fn run_folder_opts(
|
|||||||
// files restore first so the cross-source dedup keeps them over a copy
|
// files restore first so the cross-source dedup keeps them over a copy
|
||||||
// inside a package (loose beats `.apk` beats `.apks`/`.xapk` bundle).
|
// inside a package (loose beats `.apk` beats `.apks`/`.xapk` bundle).
|
||||||
let mut android_seen = std::collections::HashSet::new();
|
let mut android_seen = std::collections::HashSet::new();
|
||||||
|
let mut android_method_index_module = None;
|
||||||
// Hashing a protected library costs a full read, so only pay it when a
|
// Hashing a protected library costs a full read, so only pay it when a
|
||||||
// duplicate source can actually exist in this run.
|
// duplicate source can actually exist in this run.
|
||||||
let android_dedup = scan.android_so.len() > 1 || !scan.android_packages.is_empty();
|
let android_dedup = scan.android_so.len() > 1 || !scan.android_packages.is_empty();
|
||||||
@@ -242,7 +243,12 @@ pub fn run_folder_opts(
|
|||||||
let input_owned = input.clone();
|
let input_owned = input.clone();
|
||||||
let dest_owned = dest.clone();
|
let dest_owned = dest.clone();
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
crate::android::restore_so_file(&input_owned, &dest_owned, verbose_steps)
|
crate::android::restore_so_file_with_method_index_module(
|
||||||
|
&input_owned,
|
||||||
|
&dest_owned,
|
||||||
|
verbose_steps,
|
||||||
|
&mut android_method_index_module,
|
||||||
|
)
|
||||||
}));
|
}));
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(embedded)) => {
|
Ok(Ok(embedded)) => {
|
||||||
@@ -310,12 +316,13 @@ pub fn run_folder_opts(
|
|||||||
let out_root_owned = out_root.clone();
|
let out_root_owned = out_root.clone();
|
||||||
let mut seen_taken = std::mem::take(&mut android_seen);
|
let mut seen_taken = std::mem::take(&mut android_seen);
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
let outcomes = crate::android::restore_package(
|
let outcomes = crate::android::restore_package_with_method_index_module(
|
||||||
&package_owned,
|
&package_owned,
|
||||||
&rel_owned,
|
&rel_owned,
|
||||||
&out_root_owned,
|
&out_root_owned,
|
||||||
&mut seen_taken,
|
&mut seen_taken,
|
||||||
verbose_steps,
|
verbose_steps,
|
||||||
|
&mut android_method_index_module,
|
||||||
);
|
);
|
||||||
(outcomes, seen_taken)
|
(outcomes, seen_taken)
|
||||||
}));
|
}));
|
||||||
@@ -361,7 +368,12 @@ pub fn run_folder_opts(
|
|||||||
let meta_owned = meta.clone();
|
let meta_owned = meta.clone();
|
||||||
let dest_owned = dest.clone();
|
let dest_owned = dest.clone();
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
deobfuscate_metadata_to(&meta_owned, &dest_owned, verbose_steps)
|
deobfuscate_metadata_to_with_module(
|
||||||
|
&meta_owned,
|
||||||
|
&dest_owned,
|
||||||
|
verbose_steps,
|
||||||
|
android_method_index_module.as_deref(),
|
||||||
|
)
|
||||||
}));
|
}));
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(report)) if report.remapped > 0 => {
|
Ok(Ok(report)) if report.remapped > 0 => {
|
||||||
@@ -369,7 +381,7 @@ pub fn run_folder_opts(
|
|||||||
crate::ui::metadata(&bar, suppress_file_lines, &rel, report.remapped, &dest);
|
crate::ui::metadata(&bar, suppress_file_lines, &rel, report.remapped, &dest);
|
||||||
if let Some(log) = &log {
|
if let Some(log) = &log {
|
||||||
log.step(&format!(
|
log.step(&format!(
|
||||||
"META {rel:?} -> {dest:?}: v{} remapped {} method tokens",
|
"META {rel:?} -> {dest:?}: v{} remapped {} method fields",
|
||||||
report.version, report.remapped
|
report.version, report.remapped
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -506,13 +518,13 @@ pub fn run_file_v(
|
|||||||
s.metadata = 1;
|
s.metadata = 1;
|
||||||
if let Some(log) = &log {
|
if let Some(log) = &log {
|
||||||
log.step(&format!(
|
log.step(&format!(
|
||||||
"META {:?} -> {:?}: v{} remapped {} method tokens",
|
"META {:?} -> {:?}: v{} remapped {} method fields",
|
||||||
input, dest, report.version, report.remapped
|
input, dest, report.version, report.remapped
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if quiet == 0 {
|
if quiet == 0 {
|
||||||
println!(
|
println!(
|
||||||
"✓ metadata v{} -> {:?} ({} method tokens remapped)",
|
"✓ metadata v{} -> {:?} ({} method fields remapped)",
|
||||||
report.version, dest, report.remapped
|
report.version, dest, report.remapped
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -735,6 +747,15 @@ pub fn deobfuscate_metadata_to(
|
|||||||
input: &Path,
|
input: &Path,
|
||||||
dest: &Path,
|
dest: &Path,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
) -> anyhow::Result<senbei_metadata::Report> {
|
||||||
|
deobfuscate_metadata_to_with_module(input, dest, verbose, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deobfuscate_metadata_to_with_module(
|
||||||
|
input: &Path,
|
||||||
|
dest: &Path,
|
||||||
|
verbose: bool,
|
||||||
|
method_index_module: Option<&[u8]>,
|
||||||
) -> anyhow::Result<senbei_metadata::Report> {
|
) -> anyhow::Result<senbei_metadata::Report> {
|
||||||
let data = std::fs::read(input)?;
|
let data = std::fs::read(input)?;
|
||||||
// The Android seeded-permutation variant is tried first (it validates
|
// The Android seeded-permutation variant is tried first (it validates
|
||||||
@@ -742,7 +763,8 @@ pub fn deobfuscate_metadata_to(
|
|||||||
// Windows path. The [`senbei_metadata::Error`] is preserved in the chain
|
// Windows path. The [`senbei_metadata::Error`] is preserved in the chain
|
||||||
// (rather than stringified) so the folder driver can apply its
|
// (rather than stringified) so the folder driver can apply its
|
||||||
// unsupported-version policy.
|
// unsupported-version policy.
|
||||||
let (out, report) = crate::android::restore_metadata_bytes(&data)
|
let (out, report) =
|
||||||
|
crate::android::restore_metadata_bytes_with_module(&data, method_index_module)
|
||||||
.map_err(|e| e.context(format!("{input:?}")))?;
|
.map_err(|e| e.context(format!("{input:?}")))?;
|
||||||
if report.remapped > 0 {
|
if report.remapped > 0 {
|
||||||
if let Some(parent) = dest.parent() {
|
if let Some(parent) = dest.parent() {
|
||||||
|
|||||||
+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`,
|
/// Print a green success line for a de-obfuscated il2cpp `global-metadata.dat`,
|
||||||
/// reporting how many method tokens were remapped.
|
/// reporting how many protected method fields were remapped.
|
||||||
pub fn metadata(bar: &ProgressBar, quiet: bool, rel: &Path, remapped: usize, dest: &Path) {
|
pub fn metadata(bar: &ProgressBar, quiet: bool, rel: &Path, remapped: usize, dest: &Path) {
|
||||||
if quiet {
|
if quiet {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"{} metadata {} -> {} ({} method tokens remapped)",
|
"{} metadata {} -> {} ({} method fields remapped)",
|
||||||
"✓".green(),
|
"✓".green(),
|
||||||
rel.display(),
|
rel.display(),
|
||||||
dest.display(),
|
dest.display(),
|
||||||
|
|||||||
@@ -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 embedded;
|
||||||
mod keystream;
|
mod keystream;
|
||||||
|
mod method_indices;
|
||||||
mod method_tokens;
|
mod method_tokens;
|
||||||
|
|
||||||
pub use embedded::{embedded_metadata_size, extract_embedded_metadata};
|
pub use embedded::{embedded_metadata_size, extract_embedded_metadata};
|
||||||
|
pub use method_indices::{MethodIndexError, MethodIndexReport, restore_method_indices_v24_1};
|
||||||
pub use method_tokens::{
|
pub use method_tokens::{
|
||||||
DEFAULT_METHOD_TOKEN_SEED, Error, ImageKeyDiscovery, Report, SeedDiscoveryReport,
|
DEFAULT_METHOD_TOKEN_SEED, Error, ImageKeyDiscovery, Report, SeedDiscoveryReport,
|
||||||
discover_method_token_seeds, restore_method_tokens,
|
discover_method_token_seeds, restore_method_tokens,
|
||||||
|
|||||||
Generated
+7
-7
@@ -412,7 +412,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-crypto"
|
name = "senbei-crypto"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -420,7 +420,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-elf"
|
name = "senbei-elf"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"goblin",
|
"goblin",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -428,7 +428,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-engine"
|
name = "senbei-engine"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"senbei-crypto",
|
"senbei-crypto",
|
||||||
@@ -443,7 +443,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-io"
|
name = "senbei-io"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
@@ -464,7 +464,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-metadata"
|
name = "senbei-metadata"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -472,14 +472,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-pe"
|
name = "senbei-pe"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-wasm"
|
name = "senbei-wasm"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"senbei-engine",
|
"senbei-engine",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "senbei-wasm"
|
name = "senbei-wasm"
|
||||||
version = "1.3.1"
|
version = "1.3.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.98.1"
|
rust-version = "1.98.1"
|
||||||
description = "WebAssembly bindings for senbei (browser frontend assets live in web/)"
|
description = "WebAssembly bindings for senbei (browser frontend assets live in web/)"
|
||||||
|
|||||||
Reference in new issue
Block a user