refactor: align platform crate boundaries

This commit is contained in:
bfloat16
2026-09-07 19:29:54 +08:00
parent aa1bcaa2eb
commit 6250ca4e98
43 changed files with 1004 additions and 1004 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ The optional `samples/` corpus is user-managed and ignored by Git. The Android c
## Crate Boundaries ## Crate Boundaries
`senbei-pe` and `senbei-elf` contain basic format parsing and address mapping only. `senbei-engine/src/windows/` contains the PE unpacking pipeline; `senbei-engine/src/android/` contains Android extraction and ELF restoration. `senbei-crypto/src/android/` and `senbei-metadata/src/android/` contain Android-specific primitives; Windows metadata code is under `senbei-metadata/src/windows/`. Shared source stays directly under `src/`. `senbei-pe` and `senbei-elf` contain format parsing, address mapping, and ELF dynamic-table helpers only. `senbei-engine/src/windows/` contains the PE unpacking pipeline; `senbei-engine/src/android/` contains Android extraction and ELF restoration. `senbei-crypto/src/windows/` and `senbei-crypto/src/android/` contain platform-specific primitives; seeded Android metadata code is under `senbei-metadata/src/android/`, while the structural metadata transform is shared at the metadata crate root. Shared source stays directly under `src/`.
The format crates and PE engine remain free of filesystem I/O. Native Android extraction and restoration may memory-map inputs and write temporary workspaces. The browser binding must continue to compile for `wasm32-unknown-unknown`. The format crates and PE engine remain free of filesystem I/O. Native Android extraction and restoration may memory-map inputs and write temporary workspaces. The browser binding must continue to compile for `wasm32-unknown-unknown`.
Generated
+5 -1
View File
@@ -431,9 +431,10 @@ dependencies = [
name = "senbei-engine" name = "senbei-engine"
version = "1.2.0" version = "1.2.0"
dependencies = [ dependencies = [
"goblin",
"memmap2", "memmap2",
"senbei-crypto", "senbei-crypto",
"senbei-elf",
"senbei-pe",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@@ -450,8 +451,11 @@ dependencies = [
"libc", "libc",
"memmap2", "memmap2",
"owo-colors", "owo-colors",
"senbei-crypto",
"senbei-elf",
"senbei-engine", "senbei-engine",
"senbei-metadata", "senbei-metadata",
"senbei-pe",
"sha2", "sha2",
"tempfile", "tempfile",
"walkdir", "walkdir",
+1 -1
View File
@@ -8,7 +8,7 @@ Senbei reads protected input bytes and replays the unpacking algorithm staticall
The workspace contains eight crates: `senbei-cli`, `senbei-crypto`, `senbei-io`, `senbei-metadata`, `senbei-pe`, `senbei-elf`, `senbei-engine`, and `senbei-wasm`. The workspace contains eight crates: `senbei-cli`, `senbei-crypto`, `senbei-io`, `senbei-metadata`, `senbei-pe`, `senbei-elf`, `senbei-engine`, and `senbei-wasm`.
`senbei-pe` and `senbei-elf` contain only basic format parsing and address mapping. Protection-specific code is in `senbei-engine/src/windows/` and `senbei-engine/src/android/`. Platform-specific crypto and metadata code is grouped under `senbei-crypto/src/android/`, `senbei-metadata/src/windows/`, and `senbei-metadata/src/android/`. `senbei-pe` and `senbei-elf` contain validated format parsing, address mapping, and ELF dynamic-table helpers. Protection-specific code is in `senbei-engine/src/windows/` and `senbei-engine/src/android/`. Platform-specific crypto is grouped under `senbei-crypto/src/windows/` and `senbei-crypto/src/android/`; metadata code shared by both platforms stays at the `senbei-metadata` root, with seeded Android code under `src/android/`.
## Supported Inputs ## Supported Inputs
+6 -3
View File
@@ -12,18 +12,21 @@ Single-platform source stays directly under `src/`. Multi-platform crates keep p
senbei-cli/src/main.rs senbei-cli/src/main.rs
senbei-crypto/src/ senbei-crypto/src/
senbei-crypto/src/android/ senbei-crypto/src/android/
senbei-crypto/src/windows/
senbei-elf/src/ senbei-elf/src/
senbei-engine/src/windows/ senbei-engine/src/windows/
senbei-engine/src/android/ senbei-engine/src/android/
senbei-io/src/ senbei-io/src/
senbei-io/src/android/ senbei-io/src/android/
senbei-io/src/windows/
senbei-metadata/src/
senbei-metadata/src/windows/ senbei-metadata/src/windows/
senbei-metadata/src/android/ senbei-metadata/src/android/
senbei-pe/src/ senbei-pe/src/
senbei-wasm/src/ senbei-wasm/src/
``` ```
`senbei-pe` and `senbei-elf` are format crates only. They do not depend on the unpacking engines, filesystem code, or platform protection logic. `senbei-pe` and `senbei-elf` own validated format models, address mapping, and ELF dynamic hash helpers. They do not depend on the unpacking engines, filesystem code, or platform protection logic.
## Windows Engine ## Windows Engine
@@ -35,13 +38,13 @@ External companion inputs are reconstructed as `stub[..4096]` followed by the ma
`senbei-engine/src/android/extract/` decrypts the stage-1 header and stage-2 record streams and writes a temporary module workspace. `senbei-engine/src/android/restore/` applies decoded image and fixup containers to the hollowed ELF and rebuilds dynamic-linker tables. Both phases validate bounds and table placement before writing output. `senbei-engine/src/android/extract/` decrypts the stage-1 header and stage-2 record streams and writes a temporary module workspace. `senbei-engine/src/android/restore/` applies decoded image and fixup containers to the hollowed ELF and rebuilds dynamic-linker tables. Both phases validate bounds and table placement before writing output.
Android protection primitives are in `senbei-crypto/src/android/`. Android metadata restoration is in `senbei-metadata/src/android/` and only rewrites MethodDef token fields. The Windows structural metadata transform is in `senbei-metadata/src/windows/`. Windows protection primitives are in `senbei-crypto/src/windows/`, while Android protection primitives are in `senbei-crypto/src/android/`. Android seeded metadata restoration is in `senbei-metadata/src/android/`; the structural MethodDef transform is shared at the metadata crate root because both platform paths use it.
Android ELF dynamic tables are located from the input section table and its actual file ranges. When the original gap is too small, restoration adds a validated read-only `PT_LOAD` after the existing load image and updates the dynamic tags; it never overwrites an adjacent section or emits a partial image. Android ELF dynamic tables are located from the input section table and its actual file ranges. When the original gap is too small, restoration adds a validated read-only `PT_LOAD` after the existing load image and updates the dynamic tags; it never overwrites an adjacent section or emits a partial image.
## Scanning and Packages ## Scanning and Packages
Folder scanning uses platform target names to avoid opening bulk assets: Windows candidates are `.exe`, `.dll`, and `global-metadata.dat`; Android candidates are `.so` and `global-metadata.dat`. A Windows `.exe._` or `.dll._` companion is auxiliary input for its sibling stub and is excluded from the skipped count. Folder scanning uses platform target names to avoid opening bulk assets: Windows candidates are `.exe`, `.dll`, and `global-metadata.dat`; Android candidates are `.so` and `global-metadata.dat`. The shared walker is in `senbei-io/src/scan.rs`; platform name filters and PE companion byte adaptation are in `senbei-io/src/windows/`, and Android package adaptation is in `senbei-io/src/android/`. A Windows `.exe._` or `.dll._` companion is auxiliary input for its sibling stub and is excluded from the skipped count.
APK, APKS, and XAPK files are containers. Senbei reads their ZIP manifests first, follows nested APK entries when necessary, and extracts only `.so` and exact `global-metadata.dat` entries. Extraction streams directly to temporary files, so compressed and decompressed copies are not held in memory together. APK, APKS, and XAPK files are containers. Senbei reads their ZIP manifests first, follows nested APK entries when necessary, and extracts only `.so` and exact `global-metadata.dat` entries. Extraction streams directly to temporary files, so compressed and decompressed copies are not held in memory together.
+5 -5
View File
@@ -40,12 +40,12 @@ Folder scanning uses explicit target names to avoid opening bulk assets. Externa
```text ```text
senbei-cli/ command-line binary and integration tests senbei-cli/ command-line binary and integration tests
senbei-crypto/ shared crypto and Android crypto primitives senbei-crypto/ Windows and Android crypto primitives
senbei-elf/ basic ELF parsing senbei-elf/ ELF parsing, mapping, and dynamic-table helpers
senbei-engine/ Windows and Android unpacking engines senbei-engine/ Windows and Android unpacking engines
senbei-io/ filesystem, package, scanning, and CLI orchestration senbei-io/ filesystem, package, scanning, and platform adapters
senbei-metadata/ Windows and Android metadata restoration senbei-metadata/ shared, Windows, and Android metadata restoration
senbei-pe/ basic PE parsing senbei-pe/ PE parsing, data directories, and RVA mapping
senbei-wasm/ browser bindings and its own lockfile senbei-wasm/ browser bindings and its own lockfile
web/ static browser frontend web/ static browser frontend
samples/ optional local corpus samples/ optional local corpus
+14 -72
View File
@@ -1,78 +1,20 @@
//! Cryptographic, checksum, compression, and bytecode primitives. //! Cryptographic and compression primitives for the supported protection
//! formats.
pub mod android; pub mod android;
pub mod bytecode; pub mod windows;
pub mod crc32;
pub mod primitives;
mod tables;
/// Maximum buffer size accepted by allocation-sensitive transforms. // Keep the historical flat paths available to downstream callers while the
pub const MAX_IMAGE_SIZE: u64 = 1 << 30; // implementations themselves live under their platform boundary.
pub use windows::{BufferOperation, DecompressionFailure, Error, MAX_IMAGE_SIZE};
pub use windows::{bytecode, crc32, primitives};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Lowercase hexadecimal representation for digest and diagnostic bytes.
pub enum BufferOperation { #[must_use]
Read, pub fn hex_digest(data: &[u8]) -> String {
CopySource, let mut output = String::with_capacity(data.len() * 2);
CopyDestination, for byte in data {
ZeroFill, output.push_str(&format!("{byte:02x}"));
}
impl std::fmt::Display for BufferOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Read => "read",
Self::CopySource => "copy source",
Self::CopyDestination => "copy destination",
Self::ZeroFill => "zero-fill",
})
} }
} output
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error(
"{operation} range out of bounds (offset {offset}, size {size}, buffer length {buffer_len})"
)]
BufferRangeOutOfBounds {
operation: BufferOperation,
offset: usize,
size: usize,
buffer_len: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DecompressionFailure {
#[error("compressed source size {size} exceeds limit {max}")]
SourceTooLarge { size: u32, max: u64 },
#[error("Huffman code length {bits} is invalid")]
InvalidCodeLength { bits: u8 },
#[error("Huffman tree traversal exceeded 64 levels")]
HuffmanTraversalLimit,
#[error("pending length accumulator overflowed at {pending}")]
PendingLengthOverflow { pending: u32 },
#[error("output step {step} at byte {written} exceeds expected size {expected}")]
OutputOverflow {
written: u32,
step: u32,
expected: u32,
},
#[error("run-fill width {width} reads before output offset 0x{destination:08X}")]
RunFillBeforeOutput { width: u32, destination: u32 },
#[error("run-fill width {width} is unsupported")]
InvalidRunFillWidth { width: u32 },
#[error("back-reference distance {distance} exceeds {written} written bytes")]
InvalidBackReference { distance: u32, written: u32 },
#[error("Huffman symbol consumed no input and produced no output")]
NoProgress,
#[error(
"output size mismatch (wrote {written}/{expected} bytes after consuming {consumed}/{source_size})"
)]
OutputSizeMismatch {
written: u32,
expected: u32,
consumed: u32,
source_size: u32,
},
} }
+77
View File
@@ -0,0 +1,77 @@
//! Windows PE protection primitives.
pub mod bytecode;
pub mod crc32;
pub mod primitives;
mod tables;
/// Maximum buffer size accepted by allocation-sensitive PE transforms.
pub const MAX_IMAGE_SIZE: u64 = 1 << 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BufferOperation {
Read,
CopySource,
CopyDestination,
ZeroFill,
}
impl std::fmt::Display for BufferOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Read => "read",
Self::CopySource => "copy source",
Self::CopyDestination => "copy destination",
Self::ZeroFill => "zero-fill",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error(
"{operation} range out of bounds (offset {offset}, size {size}, buffer length {buffer_len})"
)]
BufferRangeOutOfBounds {
operation: BufferOperation,
offset: usize,
size: usize,
buffer_len: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DecompressionFailure {
#[error("compressed source size {size} exceeds limit {max}")]
SourceTooLarge { size: u32, max: u64 },
#[error("Huffman code length {bits} is invalid")]
InvalidCodeLength { bits: u8 },
#[error("Huffman tree traversal exceeded 64 levels")]
HuffmanTraversalLimit,
#[error("pending length accumulator overflowed at {pending}")]
PendingLengthOverflow { pending: u32 },
#[error("output step {step} at byte {written} exceeds expected size {expected}")]
OutputOverflow {
written: u32,
step: u32,
expected: u32,
},
#[error("run-fill width {width} reads before output offset 0x{destination:08X}")]
RunFillBeforeOutput { width: u32, destination: u32 },
#[error("run-fill width {width} is unsupported")]
InvalidRunFillWidth { width: u32 },
#[error("back-reference distance {distance} exceeds {written} written bytes")]
InvalidBackReference { distance: u32, written: u32 },
#[error("Huffman symbol consumed no input and produced no output")]
NoProgress,
#[error(
"output size mismatch (wrote {written}/{expected} bytes after consuming {consumed}/{source_size})"
)]
OutputSizeMismatch {
written: u32,
expected: u32,
consumed: u32,
source_size: u32,
},
}
@@ -4,9 +4,9 @@
//! Each free function is self-contained: it takes the relevant byte buffer(s) //! Each free function is self-contained: it takes the relevant byte buffer(s)
//! and parameters explicitly, with no coupling to the EXE `Unpacker` struct. //! and parameters explicitly, with no coupling to the EXE `Unpacker` struct.
use super::tables::{COLUMMIX1, COLUMMIX2, COLUMMIX3, COLUMMIX4, SBOX};
use crate::bytecode::{Op, OpsLut}; use crate::bytecode::{Op, OpsLut};
use crate::crc32; use crate::crc32;
use crate::tables::{COLUMMIX1, COLUMMIX2, COLUMMIX3, COLUMMIX4, SBOX};
use std::cell::RefCell; use std::cell::RefCell;
thread_local! { thread_local! {
@@ -1,7 +1,7 @@
use super::error::{Error, Result, invalid}; use crate::{Error, Result, invalid};
#[must_use] #[must_use]
pub(crate) fn elf_hash(name: &[u8]) -> u32 { pub fn elf_hash(name: &[u8]) -> u32 {
let mut value = 0_u32; let mut value = 0_u32;
for &byte in name { for &byte in name {
value = value.wrapping_shl(4).wrapping_add(u32::from(byte)); value = value.wrapping_shl(4).wrapping_add(u32::from(byte));
@@ -15,13 +15,13 @@ pub(crate) fn elf_hash(name: &[u8]) -> u32 {
} }
#[must_use] #[must_use]
pub(crate) fn gnu_hash(name: &[u8]) -> u32 { pub fn gnu_hash(name: &[u8]) -> u32 {
name.iter().fold(5381_u32, |value, &byte| { name.iter().fold(5381_u32, |value, &byte| {
value.wrapping_mul(33).wrapping_add(u32::from(byte)) value.wrapping_mul(33).wrapping_add(u32::from(byte))
}) })
} }
pub(crate) fn build_sysv_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> { pub fn build_sysv_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
if names.len() < 2 { if names.len() < 2 {
return invalid("dynamic symbol table is unexpectedly empty"); return invalid("dynamic symbol table is unexpectedly empty");
} }
@@ -60,7 +60,7 @@ pub(crate) fn build_sysv_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
Ok(output) Ok(output)
} }
pub(crate) fn build_gnu_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> { pub fn build_gnu_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
let hashes = names let hashes = names
.iter() .iter()
.skip(1) .skip(1)
@@ -1,14 +1,14 @@
use super::error::{Error, Result, invalid}; use crate::{Error, Result, invalid};
pub(crate) const SHT_NOBITS: u32 = 8; pub const SHT_NOBITS: u32 = 8;
pub(crate) const SHT_STRTAB: u32 = 3; pub const SHT_STRTAB: u32 = 3;
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000; pub const SHT_LOUSER: u32 = 0x8000_0000;
pub(crate) const SHF_ALLOC: u64 = 2; pub const SHF_ALLOC: u64 = 2;
const PT_LOAD: u32 = 1; const PT_LOAD: u32 = 1;
pub(crate) const PF_R: u32 = 4; pub const PF_R: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LoadSegment { pub struct LoadSegment {
pub offset: u64, pub offset: u64,
pub virtual_address: u64, pub virtual_address: u64,
pub file_size: u64, pub file_size: u64,
@@ -18,7 +18,7 @@ pub(crate) struct LoadSegment {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SectionHeader { pub struct SectionHeader {
pub name: u32, pub name: u32,
pub section_type: u32, pub section_type: u32,
pub flags: u64, pub flags: u64,
@@ -66,7 +66,7 @@ impl SectionHeader {
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ElfLayout { pub struct ElfLayout {
pub entrypoint: u64, pub entrypoint: u64,
pub program_header_offset: usize, pub program_header_offset: usize,
pub program_header_size: usize, pub program_header_size: usize,
@@ -83,7 +83,7 @@ impl ElfLayout {
if ident[..4] != *b"\x7fELF" || ident[4] != 2 || ident[5] != 1 { if ident[..4] != *b"\x7fELF" || ident[4] != 2 || ident[5] != 1 {
return invalid("input is not a little-endian ELF64 file"); return invalid("input is not a little-endian ELF64 file");
} }
if read_u16(data, 0x12)? != 0xb7 { if read_u16(data, 0x12)? != crate::AARCH64_MACHINE {
return invalid("input is not an AArch64 ELF"); return invalid("input is not an AArch64 ELF");
} }
let entrypoint = read_u64(data, 0x18)?; let entrypoint = read_u64(data, 0x18)?;
@@ -394,7 +394,7 @@ impl ElfLayout {
} }
} }
pub(crate) 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)
.ok_or_else(|| Error::Invalid("byte range overflow".to_owned()))?; .ok_or_else(|| Error::Invalid("byte range overflow".to_owned()))?;
@@ -405,7 +405,7 @@ pub(crate) fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
}) })
} }
pub(crate) fn slice_u64(data: &[u8], offset: u64, size: u64) -> Result<&[u8]> { pub fn slice_u64(data: &[u8], offset: u64, size: u64) -> Result<&[u8]> {
slice( slice(
data, data,
usize_from_u64(offset, "file offset")?, usize_from_u64(offset, "file offset")?,
@@ -413,46 +413,46 @@ pub(crate) fn slice_u64(data: &[u8], offset: u64, size: u64) -> Result<&[u8]> {
) )
} }
pub(crate) fn read_u16(data: &[u8], offset: usize) -> Result<u16> { pub fn read_u16(data: &[u8], offset: usize) -> Result<u16> {
let bytes: [u8; 2] = slice(data, offset, 2)? let bytes: [u8; 2] = slice(data, offset, 2)?
.try_into() .try_into()
.map_err(|_| Error::Invalid("invalid u16 range".to_owned()))?; .map_err(|_| Error::Invalid("invalid u16 range".to_owned()))?;
Ok(u16::from_le_bytes(bytes)) Ok(u16::from_le_bytes(bytes))
} }
pub(crate) fn read_u32(data: &[u8], offset: usize) -> Result<u32> { pub fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
let bytes: [u8; 4] = slice(data, offset, 4)? let bytes: [u8; 4] = slice(data, offset, 4)?
.try_into() .try_into()
.map_err(|_| Error::Invalid("invalid u32 range".to_owned()))?; .map_err(|_| Error::Invalid("invalid u32 range".to_owned()))?;
Ok(u32::from_le_bytes(bytes)) Ok(u32::from_le_bytes(bytes))
} }
pub(crate) fn read_u64(data: &[u8], offset: usize) -> Result<u64> { pub fn read_u64(data: &[u8], offset: usize) -> Result<u64> {
let bytes: [u8; 8] = slice(data, offset, 8)? let bytes: [u8; 8] = slice(data, offset, 8)?
.try_into() .try_into()
.map_err(|_| Error::Invalid("invalid u64 range".to_owned()))?; .map_err(|_| Error::Invalid("invalid u64 range".to_owned()))?;
Ok(u64::from_le_bytes(bytes)) Ok(u64::from_le_bytes(bytes))
} }
pub(crate) fn read_i64(data: &[u8], offset: usize) -> Result<i64> { pub fn read_i64(data: &[u8], offset: usize) -> Result<i64> {
let bytes: [u8; 8] = slice(data, offset, 8)? let bytes: [u8; 8] = slice(data, offset, 8)?
.try_into() .try_into()
.map_err(|_| Error::Invalid("invalid i64 range".to_owned()))?; .map_err(|_| Error::Invalid("invalid i64 range".to_owned()))?;
Ok(i64::from_le_bytes(bytes)) Ok(i64::from_le_bytes(bytes))
} }
pub(crate) fn usize_from_u64(value: u64, field: &str) -> Result<usize> { pub fn usize_from_u64(value: u64, field: &str) -> Result<usize> {
usize::try_from(value).map_err(|_| Error::Invalid(format!("{field} 0x{value:x} exceeds usize"))) usize::try_from(value).map_err(|_| Error::Invalid(format!("{field} 0x{value:x} exceeds usize")))
} }
pub(crate) fn checked_index(base: usize, index: usize, stride: usize) -> Result<usize> { pub fn checked_index(base: usize, index: usize, stride: usize) -> Result<usize> {
index index
.checked_mul(stride) .checked_mul(stride)
.and_then(|value| base.checked_add(value)) .and_then(|value| base.checked_add(value))
.ok_or_else(|| Error::Invalid("table index overflow".to_owned())) .ok_or_else(|| Error::Invalid("table index overflow".to_owned()))
} }
pub(crate) fn align_up(value: u64, alignment: u64) -> Result<u64> { pub fn align_up(value: u64, alignment: u64) -> Result<u64> {
if alignment == 0 || !alignment.is_power_of_two() { if alignment == 0 || !alignment.is_power_of_two() {
return invalid(format!("invalid alignment {alignment}")); return invalid(format!("invalid alignment {alignment}"));
} }
+75
View File
@@ -3,6 +3,64 @@
use goblin::elf::{Elf, header::EM_AARCH64, program_header::PT_LOAD}; use goblin::elf::{Elf, header::EM_AARCH64, program_header::PT_LOAD};
use thiserror::Error; use thiserror::Error;
pub mod hash;
pub mod layout;
pub use hash::{build_gnu_hash, build_sysv_hash};
pub use layout::{
ElfLayout, LoadSegment, PF_R, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, align_up,
checked_index, read_i64, read_u16, read_u32, read_u64, slice, slice_u64, usize_from_u64,
};
/// 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] = [
".dynsym",
".gnu.version",
".gnu.version_r",
".gnu.hash",
".dynstr",
".rela.dyn",
".rela.plt",
".dynamic",
];
/// 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",
];
/// ELF64 dynamic table record sizes.
pub const ELF64_SYMBOL_SIZE: usize = 0x18;
pub const ELF64_RELA_SIZE: usize = 0x18;
/// AArch64 relocation kinds used by the dynamic linker.
pub const R_AARCH64_ABS64: u32 = 0x101;
pub const R_AARCH64_GLOB_DAT: u32 = 0x401;
pub const R_AARCH64_JUMP_SLOT: u32 = 0x402;
pub const R_AARCH64_RELATIVE: u32 = 0x403;
pub const VER_NDX_GLOBAL: u16 = 1;
/// ELF dynamic-table tag identifiers used by restored images.
pub const DT_PLTRELSZ: u64 = 2;
pub const DT_HASH: u64 = 4;
pub const DT_STRTAB: u64 = 5;
pub const DT_SYMTAB: u64 = 6;
pub const DT_RELA: u64 = 7;
pub const DT_RELASZ: u64 = 8;
pub const DT_STRSZ: u64 = 10;
pub const DT_JMPREL: u64 = 23;
pub const DT_GNU_HASH: u64 = 0x6fff_fef5;
pub const DT_VERSYM: u64 = 0x6fff_fff0;
pub const DT_RELACOUNT: u64 = 0x6fff_fff9;
pub const DT_VERNEED: u64 = 0x6fff_fffe;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[error("ELF parse failed: {0}")] #[error("ELF parse failed: {0}")]
@@ -11,6 +69,8 @@ pub enum Error {
NotElf64, NotElf64,
#[error("input is not an AArch64 image")] #[error("input is not an AArch64 image")]
NotAarch64, NotAarch64,
#[error("invalid ELF layout: {0}")]
Invalid(String),
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@@ -31,6 +91,17 @@ pub fn is_aarch64(data: &[u8]) -> bool {
.unwrap_or(false) .unwrap_or(false)
} }
/// Return whether a short prefix identifies an ELF64 little-endian AArch64
/// image. This is intentionally a prefix-only check for filesystem scanners;
/// callers that need structural guarantees must use [`parse`].
#[must_use]
pub fn is_aarch64_prefix(data: &[u8]) -> bool {
data.get(0..6) == Some(b"\x7fELF\x02\x01")
&& data
.get(18..20)
.is_some_and(|bytes| u16::from_le_bytes([bytes[0], bytes[1]]) == EM_AARCH64)
}
/// Return the maximum file end among PT_LOAD segments. /// Return the maximum file end among PT_LOAD segments.
pub fn load_file_end(data: &[u8]) -> Result<u64> { pub fn load_file_end(data: &[u8]) -> Result<u64> {
let elf = parse(data)?; let elf = parse(data)?;
@@ -43,6 +114,10 @@ pub fn load_file_end(data: &[u8]) -> Result<u64> {
.unwrap_or(0)) .unwrap_or(0))
} }
pub(crate) fn invalid<T>(message: impl Into<String>) -> Result<T> {
Err(Error::Invalid(message.into()))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+2 -1
View File
@@ -7,7 +7,6 @@ license.workspace = true
description = "Platform unpacking engines for Senbei" description = "Platform unpacking engines for Senbei"
[dependencies] [dependencies]
goblin.workspace = true
memmap2.workspace = true memmap2.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
@@ -15,6 +14,8 @@ sha2.workspace = true
tempfile.workspace = true tempfile.workspace = true
thiserror.workspace = true thiserror.workspace = true
senbei-crypto.workspace = true senbei-crypto.workspace = true
senbei-elf.workspace = true
senbei-pe.workspace = true
[lints] [lints]
workspace = true workspace = true
+32
View File
@@ -0,0 +1,32 @@
//! Shared Android engine filesystem and digest helpers.
use std::io::Write;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
pub(crate) fn absolute(path: &Path) -> std::io::Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
std::env::current_dir().map(|current| current.join(path))
}
}
pub(crate) fn write_atomic(path: &Path, data: &[u8]) -> std::io::Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(parent)?;
let mut temporary = NamedTempFile::new_in(parent)?;
temporary.write_all(data)?;
temporary.as_file().sync_all()?;
temporary.persist(path).map_err(|error| error.error)?;
Ok(())
}
#[must_use]
pub(crate) fn sha256(data: &[u8]) -> String {
let mut digest = Sha256::new();
digest.update(data);
senbei_crypto::hex_digest(&digest.finalize())
}
+1 -1
View File
@@ -14,7 +14,7 @@ pub enum Error {
Elf { Elf {
path: PathBuf, path: PathBuf,
#[source] #[source]
source: goblin::error::Error, source: senbei_elf::Error,
}, },
#[error("serialize extraction index: {0}")] #[error("serialize extraction index: {0}")]
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
+7 -37
View File
@@ -1,14 +1,12 @@
use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fs::{File, create_dir_all}; use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use memmap2::MmapOptions; use memmap2::MmapOptions;
use senbei_crypto::android::{Module9bConfig, decode_container}; use senbei_crypto::android::{Module9bConfig, decode_container};
use serde_json::to_vec_pretty; use serde_json::to_vec_pretty;
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
use super::super::common;
use super::error::{Error, Result, invalid}; use super::error::{Error, Result, invalid};
use super::report::{ use super::report::{
ArtifactReport, DecoderReport, ExtractionReport, ModuleRegistryEntry, RecordReport, ArtifactReport, DecoderReport, ExtractionReport, ModuleRegistryEntry, RecordReport,
@@ -86,7 +84,7 @@ pub fn extract_stage2(options: &ExtractOptions) -> Result<ExtractionReport> {
return invalid("refusing to overwrite the protected ELF with Stage 2 output"); return invalid("refusing to overwrite the protected ELF with Stage 2 output");
} }
} }
create_dir_all(&output_dir) std::fs::create_dir_all(&output_dir)
.map_err(|source| Error::io("create Stage 2 output directory", &output_dir, source))?; .map_err(|source| Error::io("create Stage 2 output directory", &output_dir, source))?;
let file = File::open(&input_path) let file = File::open(&input_path)
@@ -497,42 +495,14 @@ fn write_json_atomic(path: &Path, value: &impl serde::Serialize) -> Result<()> {
} }
fn write_atomic(path: &Path, data: &[u8]) -> Result<()> { fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new(".")); common::write_atomic(path, data)
create_dir_all(parent) .map_err(|source| Error::io("write temporary output", path, source))
.map_err(|source| Error::io("create output directory", parent, source))?;
let mut temporary = NamedTempFile::new_in(parent)
.map_err(|source| Error::io("create temporary output", parent, source))?;
temporary
.write_all(data)
.and_then(|()| temporary.as_file().sync_all())
.map_err(|source| Error::io("write temporary output", temporary.path(), source))?;
temporary
.persist(path)
.map_err(|error| Error::io("replace output", path, error.error))?;
Ok(())
} }
fn absolute(path: &Path) -> Result<PathBuf> { fn absolute(path: &Path) -> Result<PathBuf> {
if path.is_absolute() { common::absolute(path).map_err(|source| Error::io("query current directory", path, source))
Ok(path.to_path_buf())
} else {
std::env::current_dir()
.map(|current| current.join(path))
.map_err(|source| Error::io("query current directory", path, source))
}
} }
fn sha256(data: &[u8]) -> String { fn sha256(data: &[u8]) -> String {
let mut digest = Sha256::new(); common::sha256(data)
digest.update(data);
hex_digest(&digest.finalize())
}
/// Lowercase hex of a digest output (sha2 0.11's `Array` no longer formats as
/// hex directly).
fn hex_digest(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len() * 2);
for byte in data {
out.push_str(&format!("{byte:02x}"));
}
out
} }
@@ -7,9 +7,6 @@ use super::stage1::{self, DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE};
/// Return whether `data` has a supported protected AArch64 IL2CPP layout. /// Return whether `data` has a supported protected AArch64 IL2CPP layout.
#[must_use] #[must_use]
pub fn is_protected_libil2cpp(data: &[u8]) -> bool { pub fn is_protected_libil2cpp(data: &[u8]) -> bool {
if !stage1::looks_protected(data) {
return false;
}
let Ok(stage1) = stage1::inspect( let Ok(stage1) = stage1::inspect(
data, data,
Path::new("<probe>"), Path::new("<probe>"),
+14 -36
View File
@@ -1,44 +1,13 @@
use std::path::Path; use std::path::Path;
use goblin::elf::{Elf, header::EM_AARCH64}; use senbei_elf::{AARCH64_MACHINE, Error as ElfError, parse};
use super::error::{Error, Result, invalid}; use super::error::{Error, Result, invalid};
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000; pub(crate) use senbei_elf::SHT_LOUSER;
pub const DEFAULT_CIPHER_CONSTANT: u32 = 0xbf20_165d; pub const DEFAULT_CIPHER_CONSTANT: u32 = 0xbf20_165d;
pub const DEFAULT_OUTER_SIZE: usize = 0x23c; pub const DEFAULT_OUTER_SIZE: usize = 0x23c;
pub(crate) fn looks_protected(data: &[u8]) -> bool {
let Ok(elf) = Elf::parse(data) else {
return false;
};
if elf.header.e_machine != EM_AARCH64
|| elf
.section_headers
.iter()
.filter(|section| section.sh_type == SHT_LOUSER)
.count()
!= 1
{
return false;
}
[
".dynsym",
".dynstr",
".gnu.hash",
".gnu.version",
".gnu.version_r",
]
.into_iter()
.all(|wanted| {
elf.section_headers.iter().any(|section| {
elf.shdr_strtab
.get_at(section.sh_name)
.is_some_and(|name| name == wanted)
})
})
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) struct Stage1Header { pub(crate) struct Stage1Header {
pub key: u32, pub key: u32,
@@ -70,13 +39,13 @@ pub(crate) fn inspect(
outer_size: usize, outer_size: usize,
cipher_constant: u32, cipher_constant: u32,
) -> Result<Stage1Result> { ) -> Result<Stage1Result> {
let elf = Elf::parse(data).map_err(|source| Error::Elf { let elf = parse(data).map_err(|source: ElfError| Error::Elf {
path: path.to_path_buf(), path: path.to_path_buf(),
source, source,
})?; })?;
if elf.header.e_machine != EM_AARCH64 { if elf.header.e_machine != AARCH64_MACHINE {
return invalid(format!( return invalid(format!(
"expected AArch64 ELF (machine 0x{EM_AARCH64:X}), got 0x{:X}", "expected AArch64 ELF (machine 0x{AARCH64_MACHINE:X}), got 0x{:X}",
elf.header.e_machine elf.header.e_machine
)); ));
} }
@@ -92,6 +61,15 @@ pub(crate) fn inspect(
matches.len() matches.len()
)); ));
} }
for wanted in senbei_elf::PROBE_SECTION_NAMES {
if !elf.section_headers.iter().any(|section| {
elf.shdr_strtab
.get_at(section.sh_name)
.is_some_and(|name| name == wanted)
}) {
return invalid(format!("protected ELF lacks required section {wanted}"));
}
}
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()))?;
+1
View File
@@ -1,5 +1,6 @@
//! Android AArch64 extraction and ELF restoration. //! Android AArch64 extraction and ELF restoration.
mod common;
mod extract; mod extract;
mod restore; mod restore;
@@ -14,6 +14,8 @@ pub enum Error {
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error(transparent)] #[error(transparent)]
Crypto(#[from] senbei_crypto::android::Error), Crypto(#[from] senbei_crypto::android::Error),
#[error(transparent)]
Elf(#[from] senbei_elf::Error),
#[error("{0}")] #[error("{0}")]
Invalid(String), Invalid(String),
} }
-2
View File
@@ -1,7 +1,5 @@
mod artifact; mod artifact;
mod error; mod error;
mod hash;
mod layout;
mod pipeline; mod pipeline;
pub use error::Error; pub use error::Error;
+16 -72
View File
@@ -12,35 +12,19 @@ use serde::Serialize;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use super::super::common;
use super::artifact::load_artifacts; use super::artifact::load_artifacts;
use super::error::{Error, Result, invalid}; use super::error::{Error, Result, invalid};
use super::hash::{build_gnu_hash, build_sysv_hash}; use senbei_elf::{
use super::layout::{ DT_GNU_HASH, DT_HASH, DT_JMPREL, DT_PLTRELSZ, DT_RELA, DT_RELACOUNT, DT_RELASZ, DT_STRSZ,
ElfLayout, LoadSegment, PF_R, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, align_up, DT_STRTAB, DT_SYMTAB, DT_VERNEED, DT_VERSYM, ELF64_RELA_SIZE, ELF64_SYMBOL_SIZE, ElfLayout,
read_i64, read_u32, read_u64, slice, slice_u64, usize_from_u64, LoadSegment, PF_R, R_AARCH64_ABS64, R_AARCH64_GLOB_DAT, R_AARCH64_JUMP_SLOT,
R_AARCH64_RELATIVE, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, VER_NDX_GLOBAL, align_up,
build_gnu_hash, build_sysv_hash, read_i64, read_u32, read_u64, slice, slice_u64,
usize_from_u64,
}; };
const CHUNK_SIZE: usize = 16 * 1024 * 1024; const CHUNK_SIZE: usize = 16 * 1024 * 1024;
const ELF64_SYMBOL_SIZE: usize = 0x18;
const ELF64_RELA_SIZE: usize = 0x18;
const R_AARCH64_ABS64: u32 = 0x101;
const R_AARCH64_GLOB_DAT: u32 = 0x401;
const R_AARCH64_JUMP_SLOT: u32 = 0x402;
const R_AARCH64_RELATIVE: u32 = 0x403;
const VER_NDX_GLOBAL: u16 = 1;
const DT_PLTRELSZ: u64 = 2;
const DT_HASH: u64 = 4;
const DT_STRTAB: u64 = 5;
const DT_SYMTAB: u64 = 6;
const DT_RELA: u64 = 7;
const DT_RELASZ: u64 = 8;
const DT_STRSZ: u64 = 10;
const DT_JMPREL: u64 = 23;
const DT_GNU_HASH: u64 = 0x6fff_fef5;
const DT_VERSYM: u64 = 0x6fff_fff0;
const DT_RELACOUNT: u64 = 0x6fff_fff9;
const DT_VERNEED: u64 = 0x6fff_fffe;
/// Inputs and optional diagnostics for one `libil2cpp.so` restoration. /// Inputs and optional diagnostics for one `libil2cpp.so` restoration.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -187,9 +171,7 @@ fn read_file(path: &Path) -> Result<Vec<u8>> {
} }
fn sha256_bytes(data: &[u8]) -> String { fn sha256_bytes(data: &[u8]) -> String {
let mut digest = Sha256::new(); common::sha256(data)
digest.update(data);
hex_digest(&digest.finalize())
} }
fn sha256_file(path: &Path) -> Result<String> { fn sha256_file(path: &Path) -> Result<String> {
@@ -205,7 +187,7 @@ fn sha256_file(path: &Path) -> Result<String> {
} }
digest.update(&buffer[..read]); digest.update(&buffer[..read]);
} }
Ok(hex_digest(&digest.finalize())) Ok(senbei_crypto::hex_digest(&digest.finalize()))
} }
fn copy_range(source: &[u8], output: &mut File, size: usize, path: &Path) -> Result<()> { fn copy_range(source: &[u8], output: &mut File, size: usize, path: &Path) -> Result<()> {
@@ -286,7 +268,7 @@ impl FileLayoutWriter<'_> {
"decoded write 0x{virtual_address:x}..0x{end:x} is not covered by PT_LOAD memory" "decoded write 0x{virtual_address:x}..0x{end:x} is not covered by PT_LOAD memory"
)); ));
} }
usize_from_u64(written, "written byte count") Ok(usize_from_u64(written, "written byte count")?)
} }
} }
@@ -772,18 +754,8 @@ fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> R
} }
fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, usize>> { fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, usize>> {
const REQUIRED: [&str; 8] = [ let mut result = HashMap::with_capacity(senbei_elf::DYNAMIC_SECTION_NAMES.len());
".dynsym", for required in senbei_elf::DYNAMIC_SECTION_NAMES {
".gnu.version",
".gnu.version_r",
".gnu.hash",
".dynstr",
".rela.dyn",
".rela.plt",
".dynamic",
];
let mut result = HashMap::with_capacity(REQUIRED.len());
for required in REQUIRED {
let indices = names let indices = names
.iter() .iter()
.enumerate() .enumerate()
@@ -952,7 +924,7 @@ fn metadata_mapping_length(
let end = extension_start let end = extension_start
.checked_add(cursor) .checked_add(cursor)
.ok_or_else(|| Error::Invalid("dynamic-table mapping end overflow".to_owned()))?; .ok_or_else(|| Error::Invalid("dynamic-table mapping end overflow".to_owned()))?;
usize_from_u64(end, "dynamic-table mapping length") Ok(usize_from_u64(end, "dynamic-table mapping length")?)
} }
fn table_placements( fn table_placements(
@@ -1467,29 +1439,11 @@ fn validate_restored_binary(
} }
fn absolute(path: &Path) -> Result<PathBuf> { fn absolute(path: &Path) -> Result<PathBuf> {
if path.is_absolute() { common::absolute(path).map_err(|error| Error::io("query current directory", path, error))
Ok(path.to_path_buf())
} else {
std::env::current_dir()
.map(|current| current.join(path))
.map_err(|error| Error::io("query current directory", path, error))
}
} }
fn write_atomic(path: &Path, data: &[u8]) -> Result<()> { fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new(".")); common::write_atomic(path, data).map_err(|error| Error::io("write temporary file", path, error))
std::fs::create_dir_all(parent)
.map_err(|error| Error::io("create output directory", parent, error))?;
let mut temporary = NamedTempFile::new_in(parent)
.map_err(|error| Error::io("create temporary file", parent, error))?;
temporary
.write_all(data)
.and_then(|_| temporary.as_file().sync_all())
.map_err(|error| Error::io("write temporary file", temporary.path(), error))?;
temporary
.persist(path)
.map_err(|error| Error::io("replace output", path, error.error))?;
Ok(())
} }
/// Restore the current protected `libil2cpp.so` without executing protector code. /// Restore the current protected `libil2cpp.so` without executing protector code.
@@ -1723,16 +1677,6 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
elapsed_seconds: started.elapsed().as_secs_f64(), elapsed_seconds: started.elapsed().as_secs_f64(),
}) })
} }
/// Lowercase hex of a digest output (sha2 0.11's `Array` no longer formats as
/// hex directly).
fn hex_digest(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len() * 2);
for byte in data {
out.push_str(&format!("{byte:02x}"));
}
out
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+9 -1
View File
@@ -10,5 +10,13 @@ pub use windows::{
/// Deterministic worker-thread cap shared by filesystem scanning and engines. /// Deterministic worker-thread cap shared by filesystem scanning and engines.
pub fn thread_cap() -> usize { pub fn thread_cap() -> usize {
windows::thread_cap() if let Ok(value) = std::env::var("SENBEI_THREADS")
&& let Ok(count) = value.trim().parse::<usize>()
&& count >= 1
{
return count;
}
std::thread::available_parallelism()
.map(|count| count.get())
.unwrap_or(1)
} }
+5 -16
View File
@@ -15,23 +15,12 @@ pub fn unpack(input: &[u8]) -> Result<Vec<u8>, UnpackError> {
/// Used by the new-layout managed (CLR) metadata restore to locate the COR20 /// Used by the new-layout managed (CLR) metadata restore to locate the COR20
/// header and BSJB MetaData stream in the original protected file. /// header and BSJB MetaData stream in the original protected file.
fn prot_rva_to_off(file_data: &[u8], pe_header: u32, rva: u32) -> Option<u32> { fn prot_rva_to_off(file_data: &[u8], pe_header: u32, rva: u32) -> Option<u32> {
let nsec = get_u16(file_data, pe_header + 6) as u32; let headers = senbei_pe::parse(file_data).ok()?;
let opt = get_u16(file_data, pe_header + 20) as u32; if headers.pe_offset != pe_header as usize {
let tab = pe_header + 24 + opt; return None;
for i in 0..nsec {
let s = tab + i * 40;
if (s as usize + 24) > file_data.len() {
return None;
}
let va = get_u32(file_data, s + 12);
let vs = get_u32(file_data, s + 8);
let rsz = get_u32(file_data, s + 16);
let rp = get_u32(file_data, s + 20);
if va <= rva && rva < va + vs.max(rsz) {
return Some(rp + (rva - va));
}
} }
None let offset = senbei_pe::rva_to_offset(file_data, headers, rva).ok()?;
u32::try_from(offset).ok()
} }
pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> { pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
+54 -78
View File
@@ -42,47 +42,26 @@ fn rd_u32(d: &[u8], off: u32) -> Option<u32> {
.map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]])) .map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
} }
/// A parsed section-table entry (only the fields we translate against). /// PE format section data used by the integrity policy.
struct Section { type Section = senbei_pe::Section;
va: u32,
vsize: u32,
raw_ptr: u32,
raw_size: u32,
chars: u32,
}
/// Walk the output's own section table and translate an RVA to a file offset. /// Walk the output's own section table and translate an RVA to a file offset.
/// Works for both memory-image output (raw_ptr == va) and compacted disk /// Works for both memory-image output (raw_ptr == va) and compacted disk
/// output (real raw pointers), because it consults whatever the output declares. /// output (real raw pointers), because it consults whatever the output declares.
/// Returns the offset only if the translated range `[off, off+need)` lies inside /// Returns the offset only if the translated range `[off, off+need)` lies inside
/// the file. /// the file.
fn rva_to_off(secs: &[Section], file_len: usize, rva: u32, need: u32) -> Option<u32> { fn rva_to_off(data: &[u8], headers: senbei_pe::Headers, rva: u32, need: u32) -> Option<u32> {
for s in secs { let offset = u32::try_from(senbei_pe::rva_to_offset(data, headers, rva).ok()?).ok()?;
// The mapped span is the larger of virtual and raw size, so an RVA that let end = offset.checked_add(need)?;
// falls in the virtual tail of a section still resolves. (usize::try_from(end).ok()? <= data.len()).then_some(offset)
let span = s.vsize.max(s.raw_size);
if span == 0 {
continue;
}
if rva >= s.va && rva < s.va.wrapping_add(span) {
let delta = rva - s.va;
let off = s.raw_ptr.checked_add(delta)?;
let end = off.checked_add(need)?;
if (end as usize) <= file_len {
return Some(off);
}
return None;
}
}
None
} }
fn is_executable_rva(secs: &[Section], rva: u32) -> bool { fn is_executable_rva(secs: &[Section], rva: u32) -> bool {
secs.iter().any(|section| { secs.iter().any(|section| {
let span = section.vsize.max(section.raw_size); let span = section.virtual_size.max(section.raw_size);
rva >= section.va rva >= section.virtual_address
&& rva < section.va.wrapping_add(span) && rva < section.virtual_address.wrapping_add(span)
&& (section.chars & 0x2000_0000) != 0 && (section.characteristics & 0x2000_0000) != 0
}) })
} }
@@ -151,7 +130,6 @@ pub fn check(out: &[u8]) -> IntegrityReport {
return r; return r;
} }
}; };
let opt_hdr_size = rd_u16(out, pe_off.wrapping_add(20)).unwrap_or(0) as u32;
let opt = pe_off.wrapping_add(24); let opt = pe_off.wrapping_add(24);
let magic = match rd_u16(out, opt) { let magic = match rd_u16(out, opt) {
Some(v) => v, Some(v) => v,
@@ -181,42 +159,38 @@ pub fn check(out: &[u8]) -> IntegrityReport {
} }
// --- Section table ------------------------------------------------------ // --- Section table ------------------------------------------------------
let sec_table = opt.wrapping_add(opt_hdr_size); let headers = match senbei_pe::parse(out) {
let mut secs: Vec<Section> = Vec::new(); Ok(headers) => headers,
for i in 0..num_sections { Err(_) => {
let base = sec_table.wrapping_add(i * 40); r.issues
// If the table runs past EOF the image is structurally broken. .push("section table extends past end of file".into());
let (vsize, va, raw_size, raw_ptr, chars) = match ( return r;
rd_u32(out, base.wrapping_add(8)),
rd_u32(out, base.wrapping_add(12)),
rd_u32(out, base.wrapping_add(16)),
rd_u32(out, base.wrapping_add(20)),
rd_u32(out, base.wrapping_add(36)),
) {
(Some(a), Some(b), Some(c), Some(d), Some(e)) => (a, b, c, d, e),
_ => {
r.issues
.push("section table extends past end of file".into());
return r;
}
};
// Raw data must lie within the file for compacted (disk-layout) output.
if raw_size != 0 {
let end = raw_ptr.wrapping_add(raw_size) as usize;
if end > file_len {
r.issues.push(format!(
"section #{i} raw data [0x{raw_ptr:X}..0x{end:X}] exceeds file size 0x{file_len:X}"
));
}
} }
secs.push(Section { };
va, let parsed_sections = match senbei_pe::sections(out, headers) {
vsize, Ok(sections) => sections,
raw_ptr, Err(_) => {
raw_size, r.issues
chars, .push("section table extends past end of file".into());
}); return r;
} }
};
let secs: Vec<Section> = parsed_sections
.into_iter()
.enumerate()
.map(|(i, section)| {
if section.raw_size != 0 {
let end = section.raw_offset.wrapping_add(section.raw_size) as usize;
if end > file_len {
r.issues.push(format!(
"section #{i} raw data [0x{:X}..0x{end:X}] exceeds file size 0x{file_len:X}",
section.raw_offset
));
}
}
section
})
.collect();
// --- Managed (CLR) detection ------------------------------------------ // --- Managed (CLR) detection ------------------------------------------
// The COR20 (CLR) data directory, when present and non-zero, marks a managed // The COR20 (CLR) data directory, when present and non-zero, marks a managed
@@ -266,7 +240,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
r.issues.push("entry point RVA is zero".into()); r.issues.push("entry point RVA is zero".into());
} }
} else if !is_managed { } else if !is_managed {
match rva_to_off(&secs, file_len, ep, 16) { match rva_to_off(out, headers, ep, 16) {
None => { None => {
r.issues.push(format!( r.issues.push(format!(
"entry point RVA 0x{ep:X} does not map into any section" "entry point RVA 0x{ep:X} does not map into any section"
@@ -288,8 +262,10 @@ pub fn check(out: &[u8]) -> IntegrityReport {
} }
// The entry must live in an executable section. // The entry must live in an executable section.
let exec = secs.iter().any(|s| { let exec = secs.iter().any(|s| {
let span = s.vsize.max(s.raw_size); let span = s.virtual_size.max(s.raw_size);
ep >= s.va && ep < s.va.wrapping_add(span) && (s.chars & 0x2000_0000) != 0 ep >= s.virtual_address
&& ep < s.virtual_address.wrapping_add(span)
&& (s.characteristics & 0x2000_0000) != 0
}); });
if !exec { if !exec {
r.issues.push(format!( r.issues.push(format!(
@@ -316,7 +292,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
if !is_managed { if !is_managed {
let imp_rva = rd_u32(out, dd_base.wrapping_add(8)).unwrap_or(0); let imp_rva = rd_u32(out, dd_base.wrapping_add(8)).unwrap_or(0);
if imp_rva != 0 { if imp_rva != 0 {
match rva_to_off(&secs, file_len, imp_rva, 20) { match rva_to_off(out, headers, imp_rva, 20) {
None => r.issues.push(format!( None => r.issues.push(format!(
"import directory RVA 0x{imp_rva:X} does not map into any section" "import directory RVA 0x{imp_rva:X} does not map into any section"
)), )),
@@ -331,7 +307,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
if name_rva == 0 { if name_rva == 0 {
break; break;
} }
match rva_to_off(&secs, file_len, name_rva, 1) { match rva_to_off(out, headers, name_rva, 1) {
None => r.issues.push(format!( None => r.issues.push(format!(
"import descriptor {i} DLL name RVA 0x{name_rva:X} does not map into any section" "import descriptor {i} DLL name RVA 0x{name_rva:X} does not map into any section"
)), )),
@@ -359,7 +335,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
// still refuses to load. Validate: COR20 cb == 0x48, and the MetaData stream // still refuses to load. Validate: COR20 cb == 0x48, and the MetaData stream
// begins with the "BSJB" signature. // begins with the "BSJB" signature.
if is_managed { if is_managed {
match rva_to_off(&secs, file_len, clr_rva, 0x48) { match rva_to_off(out, headers, clr_rva, 0x48) {
None => r.issues.push(format!( None => r.issues.push(format!(
"CLR (COR20) directory RVA 0x{clr_rva:X} does not map into any section" "CLR (COR20) directory RVA 0x{clr_rva:X} does not map into any section"
)), )),
@@ -373,7 +349,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
// MetaData RVA/size live at COR20 + 0x08 / + 0x0C. // MetaData RVA/size live at COR20 + 0x08 / + 0x0C.
let md_rva = rd_u32(out, coff.wrapping_add(8)).unwrap_or(0); let md_rva = rd_u32(out, coff.wrapping_add(8)).unwrap_or(0);
if md_rva != 0 { if md_rva != 0 {
match rva_to_off(&secs, file_len, md_rva, 4) { match rva_to_off(out, headers, md_rva, 4) {
None => r.issues.push(format!( None => r.issues.push(format!(
"CLR MetaData RVA 0x{md_rva:X} does not map into any section" "CLR MetaData RVA 0x{md_rva:X} does not map into any section"
)), )),
@@ -416,11 +392,11 @@ mod tests {
fn executable_text() -> Vec<Section> { fn executable_text() -> Vec<Section> {
vec![Section { vec![Section {
va: 0x1000, virtual_address: 0x1000,
vsize: 0x4000, virtual_size: 0x4000,
raw_ptr: 0x1000, raw_offset: 0x1000,
raw_size: 0x4000, raw_size: 0x4000,
chars: 0x6000_0020, characteristics: 0x6000_0020,
}] }]
} }
+1 -1
View File
@@ -11,11 +11,11 @@ use senbei_crypto::primitives;
use std::cell::RefCell; use std::cell::RefCell;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
pub use crate::thread_cap;
pub use dll::{unpack_dll, unpack_dll_v}; pub use dll::{unpack_dll, unpack_dll_v};
pub use error::*; pub use error::*;
pub use exe::{unpack as unpack_exe, unpack_v as unpack_exe_v}; pub use exe::{unpack as unpack_exe, unpack_v as unpack_exe_v};
pub use integrity::{IntegrityReport, check as check_integrity}; pub use integrity::{IntegrityReport, check as check_integrity};
pub use parallel::thread_cap;
/// Maximum plausible PE `SizeOfImage` we are willing to allocate a zero buffer /// Maximum plausible PE `SizeOfImage` we are willing to allocate a zero buffer
/// for. Guards against a corrupt/crafted header requesting a multi-gigabyte /// for. Guards against a corrupt/crafted header requesting a multi-gigabyte
+1 -15
View File
@@ -19,20 +19,6 @@
use std::sync::Mutex; use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
/// Worker-thread cap. `SENBEI_THREADS` overrides it (`1` forces the sequential
/// path); otherwise the host's available parallelism; otherwise 1.
pub fn thread_cap() -> usize {
if let Ok(v) = std::env::var("SENBEI_THREADS")
&& let Ok(n) = v.trim().parse::<usize>()
&& n >= 1
{
return n;
}
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
/// Run `f(i, span_base, span)` for every block `i`, fanning out across worker /// Run `f(i, span_base, span)` for every block `i`, fanning out across worker
/// threads when the spans are disjoint and worthwhile, else sequentially. /// threads when the spans are disjoint and worthwhile, else sequentially.
/// ///
@@ -102,7 +88,7 @@ where
} }
} }
let cap = thread_cap(); let cap = crate::thread_cap();
let per = min_per_thread.max(1); let per = min_per_thread.max(1);
let workers = if cap > 1 && n >= per.saturating_mul(2) { let workers = if cap > 1 && n >= per.saturating_mul(2) {
cap.min(n / per) cap.min(n / per)
+3
View File
@@ -7,10 +7,13 @@ description = "Filesystem, scanning, logging, and CLI orchestration for Senbei"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
senbei-crypto.workspace = true
indicatif.workspace = true indicatif.workspace = true
memmap2.workspace = true memmap2.workspace = true
owo-colors.workspace = true owo-colors.workspace = true
senbei-engine.workspace = true senbei-engine.workspace = true
senbei-elf.workspace = true
senbei-pe.workspace = true
senbei-metadata.workspace = true senbei-metadata.workspace = true
sha2.workspace = true sha2.workspace = true
tempfile.workspace = true tempfile.workspace = true
+37 -35
View File
@@ -22,43 +22,54 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use memmap2::{Mmap, MmapOptions}; use memmap2::{Mmap, MmapOptions};
use senbei_crypto::hex_digest;
use senbei_engine::android::{ExtractOptions, extract_stage2, is_protected_libil2cpp}; use senbei_engine::android::{ExtractOptions, extract_stage2, is_protected_libil2cpp};
use senbei_engine::android::{RestoreOptions, restore_libil2cpp}; use senbei_engine::android::{RestoreOptions, restore_libil2cpp};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use zip::ZipArchive; use zip::ZipArchive;
/// File name of an il2cpp metadata blob (a platform-standard technology name). pub use crate::METADATA_FILE_NAME;
pub const METADATA_FILE_NAME: &str = "global-metadata.dat";
/// Package extensions recognised as Android app packages. Packages are /// Package extensions recognised as Android app packages. Packages are
/// *containers*: membership is decided by extension plus the ZIP magic, while /// *containers*: membership is decided by extension plus the ZIP magic, while
/// only `.so` and `global-metadata.dat` entries are read. /// only `.so` and `global-metadata.dat` entries are read.
const PACKAGE_EXTENSIONS: [&str; 3] = ["apk", "apks", "xapk"]; const PACKAGE_EXTENSIONS: [&str; 3] = ["apk", "apks", "xapk"];
/// Whether `prefix` (the first bytes of a file) is an ELF64/AArch64 image. pub(crate) fn is_package_name(path: &Path) -> bool {
/// Only those can be protected Android libraries, so the folder scan uses this path.extension()
/// cheap check to decide when the full-file protection probe is worth its
/// read.
pub fn is_elf64_aarch64(prefix: &[u8]) -> bool {
prefix.len() >= 20
&& prefix[0..4] == [0x7f, b'E', b'L', b'F']
&& prefix[4] == 2 // ELFCLASS64
&& prefix[5] == 1 // ELFDATA2LSB
&& u16::from_le_bytes([prefix[18], prefix[19]]) == 0xB7 // EM_AARCH64
}
/// Whether `path` is an Android app package: a recognised package extension
/// and the local-file-header zip magic in `prefix`.
pub fn is_app_package(path: &Path, prefix: &[u8]) -> bool {
let is_package_ext = path
.extension()
.and_then(|value| value.to_str()) .and_then(|value| value.to_str())
.is_some_and(|value| { .is_some_and(|value| {
PACKAGE_EXTENSIONS PACKAGE_EXTENSIONS
.iter() .iter()
.any(|ext| value.eq_ignore_ascii_case(ext)) .any(|ext| value.eq_ignore_ascii_case(ext))
}); })
is_package_ext && prefix.starts_with(b"PK\x03\x04") }
pub(crate) fn is_so_name(path: &Path) -> bool {
path.extension()
.and_then(|value| value.to_str())
.is_some_and(|value| value.eq_ignore_ascii_case("so"))
}
pub(crate) fn is_android_entry_name(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.eq_ignore_ascii_case(METADATA_FILE_NAME))
|| is_so_name(path)
}
/// Whether `prefix` (the first bytes of a file) is an ELF64/AArch64 image.
/// Only those can be protected Android libraries, so the folder scan uses this
/// cheap check to decide when the full-file protection probe is worth its
/// read.
pub fn is_elf64_aarch64(prefix: &[u8]) -> bool {
senbei_elf::is_aarch64_prefix(prefix)
}
/// Whether `path` is an Android app package: a recognised package extension
/// and the local-file-header zip magic in `prefix`.
pub fn is_app_package(path: &Path, prefix: &[u8]) -> bool {
is_package_name(path) && prefix.starts_with(b"PK\x03\x04")
} }
/// Probe a file on disk: true when it is a protected AArch64 library. /// Probe a file on disk: true when it is a protected AArch64 library.
@@ -245,7 +256,7 @@ pub fn restore_package(
nested.push((index, name)); nested.push((index, name));
} }
} else { } else {
if crate::scan::is_android_entry_name(&name) { if is_android_entry_name(&name) {
direct.push((index, name)); direct.push((index, name));
} }
} }
@@ -280,7 +291,7 @@ pub fn restore_package(
let Some(entry_name) = entry_name else { let Some(entry_name) = entry_name else {
bail!("unsafe entry path in `{}`", nested_label.display()); bail!("unsafe entry path in `{}`", nested_label.display());
}; };
if crate::scan::is_android_entry_name(&entry_name) { if is_android_entry_name(&entry_name) {
entries.push((nested_index, entry_name)); entries.push((nested_index, entry_name));
} }
} }
@@ -398,14 +409,14 @@ pub fn embedded_metadata_dest(restored_so: &Path) -> PathBuf {
} }
/// Write a metadata blob, creating the parent directory. The restore writes /// Write a metadata blob, creating the parent directory. The restore writes
/// its own output atomically; metadata blobs go through the job layer's /// its own output atomically; metadata blobs use the shared orchestration
/// atomic write to share the mid-write failure semantics. /// atomic writer to keep the same mid-write failure semantics.
fn write_metadata_blob(dest: &Path, data: &[u8]) -> Result<()> { fn write_metadata_blob(dest: &Path, data: &[u8]) -> Result<()> {
if let Some(parent) = dest.parent() { if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent) std::fs::create_dir_all(parent)
.with_context(|| format!("create `{}`", parent.display()))?; .with_context(|| format!("create `{}`", parent.display()))?;
} }
crate::job::write_atomic(dest, data) crate::atomic::write_atomic(dest, data)
.map_err(anyhow::Error::from) .map_err(anyhow::Error::from)
.context("write metadata output") .context("write metadata output")
} }
@@ -449,12 +460,3 @@ fn map_read_only(file: &File, path: &Path) -> Result<Mmap> {
unsafe { MmapOptions::new().map(file) } unsafe { MmapOptions::new().map(file) }
.with_context(|| format!("map extracted `{}`", path.display())) .with_context(|| format!("map extracted `{}`", path.display()))
} }
/// Lowercase hex of a digest output (sha2 0.11's `Array` no longer formats as
/// hex directly).
fn hex_digest(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len() * 2);
for byte in data {
out.push_str(&format!("{byte:02x}"));
}
out
}
+16
View File
@@ -0,0 +1,16 @@
//! Shared atomic filesystem writes for native orchestration.
use std::path::{Path, PathBuf};
/// Write `bytes` through a sibling temporary file and replace `dest` only after
/// the complete write succeeds.
pub(crate) fn write_atomic(dest: &Path, bytes: &[u8]) -> std::io::Result<()> {
let mut temporary_name = dest.as_os_str().to_os_string();
temporary_name.push(".senbei-tmp");
let temporary = PathBuf::from(temporary_name);
let result = std::fs::write(&temporary, bytes).and_then(|()| std::fs::rename(&temporary, dest));
if result.is_err() {
let _ = std::fs::remove_file(&temporary);
}
result
}
+4 -494
View File
@@ -1,326 +1,9 @@
use senbei_engine as unpacker;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
/// Crackproof header key table lives at this fixed file offset. For the use crate::atomic::write_atomic;
/// external-companion layout, the companion payload aligns to the stub here. pub use crate::windows::{
const HEADER_OFF: usize = 4096; UnpackedImage, unpack_bytes, unpack_bytes_force_exe, unpack_one, unpack_one_v,
};
/// Build the unpacker input for `input`, transparently handling the
/// **external-companion** layout used by some il2cpp games.
///
/// In that layout a protected module is split into a thin on-disk loader stub
/// (`Foo.dll`, whose code sections are stripped to one page) plus an encrypted
/// `Foo.dll._` companion holding the real payload. The companion is byte-for-byte
/// the stub's payload region starting at the Crackproof header (offset 4096), so
/// `stub[..4096] ++ companion` reconstructs the ordinary embedded-payload file
/// the existing pipelines already unpack. The runtime loader does exactly this:
/// it maps `Foo.dll._` and feeds it through the standard Crackproof unpack.
///
/// The splice fires only when a sibling `<input>._` exists *and* its first 32
/// bytes equal the stub's header at offset 4096 — a precise signal that the
/// companion is this stub's payload. Otherwise the file is returned untouched,
/// so normal (embedded-payload) inputs are unaffected.
fn read_unpacker_input(input: &Path) -> std::io::Result<UnpackerInput> {
let stub = std::fs::read(input)?;
// Companion path: append "._" to the full file name (Foo.dll -> Foo.dll._).
let companion = match input.file_name() {
Some(name) => {
let mut n = name.to_os_string();
n.push("._");
input.with_file_name(n)
}
None => {
return Ok(UnpackerInput {
bytes: stub,
stub: None,
});
}
};
if !companion.is_file() {
return Ok(UnpackerInput {
bytes: stub,
stub: None,
});
}
let comp = std::fs::read(&companion)?;
match splice_companion(&stub, &comp) {
// A splice fired: keep the stub so its plaintext export table can be
// overlaid onto the unpacked image (the companion does not carry it).
Some(spliced) => Ok(UnpackerInput {
bytes: spliced,
stub: Some(stub),
}),
None => Ok(UnpackerInput {
bytes: stub,
stub: None,
}),
}
}
/// The bytes fed to the unpacker, plus the original loader stub when the input
/// was reconstructed from an external companion. The stub is retained because
/// the crackproof loader rebuilds the PE export table at runtime from data kept
/// in the stub — that table is *not* present in the encrypted companion, so the
/// unpacked image needs it overlaid from the stub afterwards
/// (see [`overlay_exports_from_stub`]).
struct UnpackerInput {
bytes: Vec<u8>,
stub: Option<Vec<u8>>,
}
/// Overlay the PE export table from the loader `stub` onto the unpacked image
/// `out`, for the external-companion layout.
///
/// In that layout the encrypted companion carries the real `.text`/`il2cpp`
/// payload but **not** a usable export directory: the crackproof loader rebuilds
/// exports at runtime from the plaintext copy retained in the stub's `.rdata`.
/// Statically, the spliced input therefore decrypts to a garbage export
/// directory (`NumberOfFunctions` etc. are ciphertext), which makes downstream
/// tools (IL2CppDumper, IDA) choke when they parse it. The fix does what the
/// loader does: copy the export-directory region byte-for-byte from the stub to
/// the same RVA in the unpacked image.
///
/// No-op (leaves `out` untouched) if there is no export directory, or if the
/// region cannot be mapped in either image — so a malformed stub can never
/// corrupt an otherwise-good unpack.
fn overlay_exports_from_stub(out: &mut [u8], stub: &[u8]) {
let (export_rva, export_size) = match pe_export_dir(out) {
Some(v) if v.1 != 0 => v,
_ => return,
};
let dst = match rva_to_file_off(out, export_rva) {
Some(o) => o,
None => return,
};
let src = match rva_to_file_off(stub, export_rva) {
Some(o) => o,
None => 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]);
}
}
/// Restore the TLS directory from the loader `stub` onto the unpacked image
/// `out`, for the external-companion layout.
///
/// Crackproof strips the whole `IMAGE_TLS_DIRECTORY` from the encrypted payload
/// — the data-directory entry, the directory struct, the raw-data template, and
/// the base relocations for the struct's four 64-bit pointer fields — and
/// re-installs TLS itself from data kept in the stub when it loads the module.
/// A statically-unpacked DLL is loaded by the ordinary Windows loader instead,
/// which needs a valid TLS directory or it never allocates a TLS slot for the
/// module nor writes `_tls_index`. The module's C++ `thread_local` accesses then
/// read a garbage TLS slot — observed as a `0xC0000005` deep in IL2CPP type
/// resolution (a TypeDef token used as a raw `s_TypeInfoTable` index).
///
/// The stub retains the full plaintext `.rdata` (only `.text`/`il2cpp` are
/// stripped to one page), so the directory struct and its raw-data template are
/// copied back byte-for-byte at their RVAs, the data-directory entry is taken
/// from the stub header (the unpacked image's was overwritten with the zeroed
/// saved-header blob), and four DIR64 relocations are appended to `.reloc`.
///
/// No-op if the stub declares no TLS directory or if any required region cannot
/// be mapped/relocated — so it can never corrupt an otherwise-good unpack.
fn restore_tls_from_stub(out: &mut [u8], stub: &[u8]) {
let pe = match read_u32(out, 0x3C) {
Some(v) => v as usize,
None => return,
};
if out.get(pe..pe + 4) != Some(&b"PE\0\0"[..]) {
return;
}
// This restore is PE32+-only: it copies a 40-byte IMAGE_TLS_DIRECTORY64,
// converts fields with a 64-bit image base, and appends DIR64 relocs. A
// PE32 module needs the 24-byte struct / DIR32 handling (the unpacker core
// does that itself — see `restore_pe32_tls_from_stub`), so bail rather than
// read the data directories at the wrong (PE32+) offset and write garbage.
if read_u16(out, pe + 24) != Some(0x20B) {
return;
}
// TLS is data-directory index 9 (PE32+ directories at optional header +112).
let tls_dd = match pe.checked_add(24 + 112 + 9 * 8) {
Some(v) => v,
None => return,
};
// The genuine entry survives in the stub header; the unpacked image's copy
// was clobbered by the (zeroed-TLS) saved-header blob.
let (tls_rva, tls_size) = match (read_u32(stub, tls_dd), read_u32(stub, tls_dd + 4)) {
(Some(r), Some(s)) if r != 0 && s != 0 => (r, s),
_ => return, // module has no TLS — nothing to restore
};
// Image base (PE32+, optional header +24) converts the struct's absolute VAs
// back to RVAs for the raw-data template overlay.
let image_base = match read_u64(out, pe + 24 + 24) {
Some(v) => v,
None => return,
};
// 1) Overlay the IMAGE_TLS_DIRECTORY struct from the stub at its RVA.
let dst = match rva_to_file_off(out, tls_rva) {
Some(o) => o,
None => return,
};
let src = match rva_to_file_off(stub, tls_rva) {
Some(o) => o,
None => return,
};
let n = tls_size as usize;
if dst.checked_add(n).is_none_or(|e| e > out.len())
|| src.checked_add(n).is_none_or(|e| e > stub.len())
{
return;
}
out[dst..dst + n].copy_from_slice(&stub[src..src + n]);
// 2) Restore the data-directory entry so the loader processes TLS at all.
write_u32_at(out, tls_dd, tls_rva);
write_u32_at(out, tls_dd + 4, tls_size);
// 3) Overlay the raw-data template [StartAddressOfRawData, EndAddressOfRawData).
if let (Some(start_va), Some(end_va)) = (read_u64(out, dst), read_u64(out, dst + 8))
&& end_va > start_va
&& start_va >= image_base
{
let tpl_rva = (start_va - image_base) as u32;
let tpl_len = (end_va - start_va) as usize;
if let (Some(td), Some(ts)) = (
rva_to_file_off(out, tpl_rva),
rva_to_file_off(stub, tpl_rva),
) && td.checked_add(tpl_len).is_some_and(|e| e <= out.len())
&& ts.checked_add(tpl_len).is_some_and(|e| e <= stub.len())
{
out[td..td + tpl_len].copy_from_slice(&stub[ts..ts + tpl_len]);
}
}
// 4) Append DIR64 relocations for the struct's four 64-bit pointer fields
// (Start/End/Index/CallBacks at +0/+8/+0x10/+0x18). Without them the
// loader would leave preferred-base VAs in a rebased image.
add_tls_relocs(out, pe, tls_rva);
}
/// Append a single base-relocation block covering the four 64-bit pointer fields
/// of the TLS directory struct at `tls_rva`. The block is written immediately
/// after the existing relocation table (which must be free space and in bounds)
/// and the BaseReloc directory size is grown to include it. No-op if the table
/// is absent, the fields straddle a relocation page, or the slot is not free.
fn add_tls_relocs(out: &mut [u8], pe: usize, tls_rva: u32) {
let reloc_dd = pe + 24 + 112 + 5 * 8; // BaseReloc = directory index 5
let (reloc_rva, reloc_size) = match (read_u32(out, reloc_dd), read_u32(out, reloc_dd + 4)) {
(Some(r), Some(s)) if r != 0 => (r, s),
_ => return,
};
// All four fields (last at +0x18) must share one 0x1000 relocation page.
let page = tls_rva & !0xFFF;
if (tls_rva.wrapping_add(0x18)) & !0xFFF != page {
return;
}
const BLOCK: usize = 8 + 4 * 2; // header + four DIR64 entries
let at = match rva_to_file_off(out, reloc_rva.wrapping_add(reloc_size)) {
Some(o) => o,
None => return,
};
if at.checked_add(BLOCK).is_none_or(|e| e > out.len()) {
return;
}
if out[at..at + BLOCK].iter().any(|&b| b != 0) {
return; // refuse to clobber existing data
}
write_u32_at(out, at, page);
write_u32_at(out, at + 4, BLOCK as u32);
for (i, off) in [0u32, 8, 0x10, 0x18].iter().enumerate() {
let entry = (10u16 << 12) | (((tls_rva.wrapping_add(*off)) & 0xFFF) as u16);
let p = at + 8 + i * 2;
out[p..p + 2].copy_from_slice(&entry.to_le_bytes());
}
write_u32_at(out, reloc_dd + 4, reloc_size.wrapping_add(BLOCK as u32));
}
/// Read the Export data-directory (RVA, size) from a PE image, or `None` if the
/// headers are too short/invalid to parse.
fn pe_export_dir(buf: &[u8]) -> Option<(u32, u32)> {
let pe = read_u32(buf, 0x3C)? as usize;
if buf.get(pe..pe + 4)? != b"PE\0\0" {
return None;
}
// Optional header at pe+24; data directories start at +96 on PE32 (0x10B)
// and +112 on PE32+ (0x20B); Export is index 0.
let dd_base = match read_u16(buf, pe + 24)? {
0x20B => 112,
0x10B => 96,
_ => return None,
};
let dd = pe.checked_add(24 + dd_base)?;
Some((read_u32(buf, dd)?, read_u32(buf, dd + 4)?))
}
/// Map an RVA to a file offset using the PE section table. Returns `None` if no
/// section contains the RVA or the headers cannot be parsed.
fn rva_to_file_off(buf: &[u8], rva: u32) -> Option<usize> {
let pe = read_u32(buf, 0x3C)? as usize;
if buf.get(pe..pe + 4)? != b"PE\0\0" {
return None;
}
let nsec = read_u16(buf, pe + 6)? as usize;
let opt_size = read_u16(buf, pe + 20)? as usize;
let sh = pe.checked_add(24)?.checked_add(opt_size)?;
for i in 0..nsec {
let o = sh.checked_add(i.checked_mul(40)?)?;
let vsz = read_u32(buf, o + 8)?;
let va = read_u32(buf, o + 12)?;
let raw = read_u32(buf, o + 20)?;
if rva >= va && rva < va.wrapping_add(vsz.max(1)) {
return Some((rva - va).wrapping_add(raw) as usize);
}
}
None
}
fn read_u32(buf: &[u8], off: usize) -> Option<u32> {
let b = buf.get(off..off + 4)?;
Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn read_u16(buf: &[u8], off: usize) -> Option<u16> {
let b = buf.get(off..off + 2)?;
Some(u16::from_le_bytes([b[0], b[1]]))
}
fn read_u64(buf: &[u8], off: usize) -> Option<u64> {
let b = buf.get(off..off + 8)?;
Some(u64::from_le_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
/// Write a little-endian `u32` at `off`, silently doing nothing if out of bounds.
fn write_u32_at(buf: &mut [u8], off: usize, val: u32) {
if let Some(slot) = buf.get_mut(off..off + 4) {
slot.copy_from_slice(&val.to_le_bytes());
}
}
/// Splice a stub and its external-companion payload into the embedded-payload
/// form the pipelines expect, or `None` if `comp` is not this stub's payload.
///
/// The companion is byte-for-byte the stub's payload region from the Crackproof
/// header (offset 4096) onward, so the result is `stub[..4096] ++ comp`. The
/// splice fires only when the first 32 bytes of `comp` equal the stub's header
/// at offset 4096 — a 32-byte match on the key-table/magic region that confirms
/// the pairing and leaves ordinary (non-companion) inputs untouched.
fn splice_companion(stub: &[u8], comp: &[u8]) -> Option<Vec<u8>> {
let hdr_end = HEADER_OFF + 32;
if stub.len() >= hdr_end && comp.len() >= 32 && stub[HEADER_OFF..hdr_end] == comp[..32] {
let mut spliced = Vec::with_capacity(HEADER_OFF + comp.len());
spliced.extend_from_slice(&stub[..HEADER_OFF]);
spliced.extend_from_slice(comp);
return Some(spliced);
}
None
}
/// Summary of a folder-mode run. /// Summary of a folder-mode run.
#[derive(Default)] #[derive(Default)]
@@ -1036,139 +719,6 @@ fn unsupported_version(e: &anyhow::Error) -> Option<u32> {
None None
} }
/// Write `bytes` to `dest` atomically: a sibling temp file, then a rename.
/// A direct `std::fs::write` truncates the destination first, so a mid-write
/// failure (disk full, AV lock, quota) destroys a previously good unpack at
/// the same path; the temp+rename keeps the old file until the new one is
/// complete. Best-effort temp cleanup on failure.
pub(crate) fn write_atomic(dest: &Path, bytes: &[u8]) -> std::io::Result<()> {
let mut tmp_name = dest.as_os_str().to_os_string();
tmp_name.push(".senbei-tmp");
let tmp = PathBuf::from(tmp_name);
let r = std::fs::write(&tmp, bytes).and_then(|()| std::fs::rename(&tmp, dest));
if r.is_err() {
let _ = std::fs::remove_file(&tmp);
}
r
}
/// Detect `bytes` and run the right pipeline. Spliced external companions use
/// the EXE pipeline directly because that layout is definitionally EXE-style.
///
/// Routing spliced inputs straight to the EXE pipeline is safe: the
/// companion layout is definitionally the EXE-style shell (the runtime
/// loader maps the companion and runs the standard shell unpack), so the DLL
/// pipeline probe can never be right for it. Output bytes are identical to the
/// DLL-first + EXE-fallback route for every input that route handles.
fn unpack_spliced_or_auto(
bytes: &[u8],
spliced: bool,
force_exe: bool,
verbose: bool,
) -> Result<(unpacker::Kind, Vec<u8>), unpacker::UnpackError> {
if spliced || force_exe {
let detected = unpacker::detect(bytes).ok_or(unpacker::UnpackError::NotCrackproof)?;
let out = unpacker::unpack_exe_v(bytes, verbose)?;
return Ok((detected.kind, out));
}
unpacker::unpack_auto_v(bytes, verbose)
}
/// Unpack a single file to `dest`. Returns the Kind and integrity report on success.
pub fn unpack_one(
input: &Path,
dest: &Path,
) -> anyhow::Result<(unpacker::Kind, unpacker::IntegrityReport)> {
unpack_one_v(input, dest, false)
}
/// Outcome of a byte-level unpack ([`unpack_bytes`]): the image, its detected
/// kind, and its integrity report. No file I/O is involved.
pub struct UnpackedImage {
pub kind: unpacker::Kind,
pub bytes: Vec<u8>,
pub integrity: unpacker::IntegrityReport,
/// True when the input was reconstructed from an external companion (the
/// `._` layout), i.e. the export/TLS overlays ran.
pub companion: bool,
}
/// Unpack in-memory `input` bytes, optionally paired with an external
/// companion payload `companion` (the `<input>._` file's contents).
///
/// This is the in-memory counterpart of [`unpack_one_v`]: splice a matching
/// companion, unpack, overlay the export table and TLS directory from the stub,
/// then run the static integrity check.
pub fn unpack_bytes(
input: &[u8],
companion: Option<&[u8]>,
) -> Result<UnpackedImage, unpacker::UnpackError> {
unpack_bytes_impl(input, companion, false)
}
/// Like [`unpack_bytes`], but forces the EXE pipeline (no DLL-pipeline
/// probe). This is the web app's recovery path: the DLL-first probe relies
/// on `catch_unwind` to reject EXE-shell-layout DLLs, and panics cannot be
/// caught on wasm — the probe traps the whole call. The web app runs each
/// unpack in a disposable Web Worker and retries trapped DLLs with this
/// entry point, reproducing the CLI's dll-first/exe-fallback routing.
pub fn unpack_bytes_force_exe(
input: &[u8],
companion: Option<&[u8]>,
) -> Result<UnpackedImage, unpacker::UnpackError> {
unpack_bytes_impl(input, companion, true)
}
fn unpack_bytes_impl(
input: &[u8],
companion: Option<&[u8]>,
force_exe: bool,
) -> Result<UnpackedImage, unpacker::UnpackError> {
let spliced = companion.and_then(|c| splice_companion(input, c));
let bytes: &[u8] = spliced.as_deref().unwrap_or(input);
let (kind, mut out) = unpack_spliced_or_auto(bytes, spliced.is_some(), force_exe, false)?;
if spliced.is_some() {
overlay_exports_from_stub(&mut out, input);
restore_tls_from_stub(&mut out, input);
}
let integrity = unpacker::check_integrity(&out);
Ok(UnpackedImage {
kind,
bytes: out,
integrity,
companion: spliced.is_some(),
})
}
/// Like [`unpack_one`], but prints detailed `[N/9]` step progress (and a final
/// `Write to <dest>` line) to stdout when `verbose` is true.
pub fn unpack_one_v(
input: &Path,
dest: &Path,
verbose: bool,
) -> anyhow::Result<(unpacker::Kind, unpacker::IntegrityReport)> {
let UnpackerInput { bytes, stub } = read_unpacker_input(input)?;
let (kind, mut out) = unpack_spliced_or_auto(&bytes, stub.is_some(), false, verbose)?;
// External-companion layout: restore the export table from the stub, which
// the encrypted companion does not carry (the loader rebuilds it at runtime).
if let Some(stub) = stub {
overlay_exports_from_stub(&mut out, &stub);
// ...and the TLS directory, which Crackproof strips from the payload and
// re-installs at runtime; the ordinary loader needs it or thread_local
// access crashes (see [`restore_tls_from_stub`]).
restore_tls_from_stub(&mut out, &stub);
}
let report = unpacker::check_integrity(&out);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
write_atomic(dest, &out)?;
if verbose {
println!("Write to {}", dest.display());
}
Ok((kind, report))
}
/// De-obfuscate an il2cpp `global-metadata.dat` to `dest`. /// De-obfuscate an il2cpp `global-metadata.dat` to `dest`.
/// ///
/// Crackproof's `-GMD` option scrambles each `Il2CppMethodDefinition`'s token /// Crackproof's `-GMD` option scrambles each `Il2CppMethodDefinition`'s token
@@ -1297,44 +847,4 @@ mod tests {
let rel = rel_in_tree(root, under); let rel = rel_in_tree(root, under);
assert_eq!(rel.as_ref(), Path::new(r"bin\app.exe")); assert_eq!(rel.as_ref(), Path::new(r"bin\app.exe"));
} }
fn stub_with_header(header: &[u8; 32], extra: usize) -> Vec<u8> {
let mut s = vec![0u8; HEADER_OFF];
s.extend_from_slice(header);
s.extend_from_slice(&vec![0xAAu8; extra]);
s
}
#[test]
fn splices_when_header_matches() {
let header = [7u8; 32];
let stub = stub_with_header(&header, 16);
// Companion: same 32-byte header, then the real (longer) payload.
let mut comp = header.to_vec();
comp.extend_from_slice(&[0x42u8; 1000]);
let out = splice_companion(&stub, &comp).expect("should splice");
assert_eq!(out.len(), HEADER_OFF + comp.len());
assert_eq!(&out[..HEADER_OFF], &stub[..HEADER_OFF]);
assert_eq!(&out[HEADER_OFF..], &comp[..]);
}
#[test]
fn no_splice_when_header_differs() {
let stub = stub_with_header(&[7u8; 32], 16);
let mut comp = vec![9u8; 32]; // different header
comp.extend_from_slice(&[0x42u8; 1000]);
assert!(splice_companion(&stub, &comp).is_none());
}
#[test]
fn no_splice_when_too_short() {
let short_stub = vec![0u8; HEADER_OFF + 8]; // < HEADER_OFF + 32
let comp = vec![0u8; 64];
assert!(splice_companion(&short_stub, &comp).is_none());
let stub = stub_with_header(&[1u8; 32], 0);
let short_comp = vec![1u8; 16]; // < 32
assert!(splice_companion(&stub, &short_comp).is_none());
}
} }
+5
View File
@@ -1,8 +1,13 @@
//! Filesystem and command-line orchestration. //! Filesystem and command-line orchestration.
/// File name of an IL2CPP metadata blob shared by both platform scanners.
pub const METADATA_FILE_NAME: &str = "global-metadata.dat";
pub mod android; pub mod android;
mod atomic;
pub mod job; pub mod job;
pub mod logfile; pub mod logfile;
pub mod pause; pub mod pause;
pub mod scan; pub mod scan;
pub mod ui; pub mod ui;
pub mod windows;
+17 -95
View File
@@ -1,6 +1,4 @@
use memmap2::MmapOptions;
use senbei_engine::detect; use senbei_engine::detect;
use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use walkdir::WalkDir; use walkdir::WalkDir;
@@ -27,56 +25,10 @@ const DETECT_PREFIX: u64 = 8 * 1024;
/// processable is lost. /// processable is lost.
const MIN_SIZE: u64 = 4128; const MIN_SIZE: u64 = 4128;
const METADATA_FILE_NAME: &str = "global-metadata.dat";
fn is_metadata_name(path: &Path) -> bool { fn is_metadata_name(path: &Path) -> bool {
path.file_name() path.file_name()
.and_then(|name| name.to_str()) .and_then(|name| name.to_str())
.is_some_and(|name| name.eq_ignore_ascii_case(METADATA_FILE_NAME)) .is_some_and(|name| name.eq_ignore_ascii_case(crate::METADATA_FILE_NAME))
}
fn is_target_extension(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| {
ext.eq_ignore_ascii_case("exe")
|| ext.eq_ignore_ascii_case("dll")
|| ext.eq_ignore_ascii_case("so")
})
}
fn is_android_package_name(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| {
ext.eq_ignore_ascii_case("apk")
|| ext.eq_ignore_ascii_case("apks")
|| ext.eq_ignore_ascii_case("xapk")
})
}
/// External Windows payloads are consumed through their sibling `.exe`/`.dll`
/// stub. They are valid input bytes, but are not independent unpack targets.
pub(crate) fn is_windows_companion(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
let Some(stub_name) = name.strip_suffix("._") else {
return false;
};
let stub_path = Path::new(stub_name);
stub_path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("exe") || ext.eq_ignore_ascii_case("dll"))
}
pub(crate) fn is_android_entry_name(path: &Path) -> bool {
is_metadata_name(path)
|| path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("so"))
} }
/// Content classification of a single file. /// Content classification of a single file.
@@ -182,7 +134,7 @@ pub fn find_targets_opts(root: &Path, scan_all: bool) -> ScanResult {
// Skip reparse-point directories (junctions, symlink-dirs): they point // Skip reparse-point directories (junctions, symlink-dirs): they point
// outside the scanned tree — walking one would silently unpack an // outside the scanned tree — walking one would silently unpack an
// entire foreign tree (e.g. a `samples` junction into the golden corpus). // entire foreign tree (e.g. a `samples` junction into the golden corpus).
!is_reparse_point(e) !crate::windows::is_reparse_point(e)
}) { }) {
let entry = match entry { let entry = match entry {
Ok(e) => e, Ok(e) => e,
@@ -194,12 +146,13 @@ pub fn find_targets_opts(root: &Path, scan_all: bool) -> ScanResult {
if !entry.file_type().is_file() { if !entry.file_type().is_file() {
continue; continue;
} }
if is_windows_companion(entry.path()) { if crate::windows::is_companion(entry.path()) {
continue; continue;
} }
if !is_metadata_name(entry.path()) if !is_metadata_name(entry.path())
&& !is_target_extension(entry.path()) && !crate::windows::is_pe_extension(entry.path())
&& !is_android_package_name(entry.path()) && !crate::android::is_so_name(entry.path())
&& !crate::android::is_package_name(entry.path())
{ {
continue; continue;
} }
@@ -258,26 +211,6 @@ pub fn find_targets_opts(root: &Path, scan_all: bool) -> ScanResult {
result result
} }
/// True if a walked directory entry is a reparse point (junction or symlink).
///
/// `DirEntry::file_type` only flags true symlinks; NTFS junctions report as
/// ordinary directories, so without this check the walker descends into them.
/// Off-Windows there are no junctions — symlink dirs are already excluded
/// because `follow_links` is off (their `file_type().is_dir()` is false).
#[cfg(windows)]
fn is_reparse_point(e: &walkdir::DirEntry) -> bool {
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
e.metadata()
.map(|m| m.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0)
.unwrap_or(false)
}
#[cfg(not(windows))]
fn is_reparse_point(_e: &walkdir::DirEntry) -> bool {
false
}
/// Whether the size pre-filter is disabled via `SENBEI_SCAN_ALL`. Any value /// Whether the size pre-filter is disabled via `SENBEI_SCAN_ALL`. Any value
/// other than `0`/empty enables probing small selected target names. It never /// other than `0`/empty enables probing small selected target names. It never
/// expands the platform filename boundary. /// expands the platform filename boundary.
@@ -309,33 +242,18 @@ pub fn scan_all_env() -> bool {
fn classify(path: &Path) -> Option<Class> { fn classify(path: &Path) -> Option<Class> {
let head = read_prefix(path, DETECT_PREFIX)?; let head = read_prefix(path, DETECT_PREFIX)?;
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if is_android_package_name(path) && crate::android::is_app_package(path, &head) { if crate::android::is_package_name(path) && crate::android::is_app_package(path, &head) {
return Class::AndroidPackage; return Class::AndroidPackage;
} }
if is_metadata_name(path) && senbei_metadata::is_metadata(&head) { if is_metadata_name(path) && senbei_metadata::is_metadata(&head) {
return Class::Metadata; return Class::Metadata;
} }
if path if crate::windows::is_pe_extension(path) && detect(&head).is_some() {
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("exe") || ext.eq_ignore_ascii_case("dll"))
&& detect(&head).is_some()
{
return Class::Crackproof; return Class::Crackproof;
} }
if path if crate::android::is_so_name(path)
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("so"))
&& crate::android::is_elf64_aarch64(&head) && crate::android::is_elf64_aarch64(&head)
&& File::open(path) && crate::android::is_protected_so_file(path)
.and_then(|file| {
// SAFETY: the file remains open for the mapping lifetime
// and the mapping is read-only.
unsafe { MmapOptions::new().map(&file) }
})
.map(|bytes| senbei_engine::android::is_protected_libil2cpp(&bytes))
.unwrap_or(false)
{ {
return Class::AndroidSo; return Class::AndroidSo;
} }
@@ -366,7 +284,9 @@ mod tests {
"global-metadata.dat", "global-metadata.dat",
] { ] {
assert!( assert!(
is_metadata_name(Path::new(p)) || is_target_extension(Path::new(p)), is_metadata_name(Path::new(p))
|| crate::windows::is_pe_extension(Path::new(p))
|| crate::android::is_so_name(Path::new(p)),
"{p} should be a candidate" "{p} should be a candidate"
); );
} }
@@ -379,7 +299,9 @@ mod tests {
"a.ab", "a.ab",
] { ] {
assert!( assert!(
!is_metadata_name(Path::new(p)) && !is_target_extension(Path::new(p)), !is_metadata_name(Path::new(p))
&& !crate::windows::is_pe_extension(Path::new(p))
&& !crate::android::is_so_name(Path::new(p)),
"{p} must not be a candidate" "{p} must not be a candidate"
); );
} }
@@ -472,7 +394,7 @@ mod tests {
let scan = find_targets_opts(root, false); let scan = find_targets_opts(root, false);
assert_eq!(scan.stats.skipped, 1, "only the stub was probed"); assert_eq!(scan.stats.skipped, 1, "only the stub was probed");
assert!(is_windows_companion(&root.join("app.exe._"))); assert!(crate::windows::is_companion(&root.join("app.exe._")));
} }
#[test] #[test]
+499
View File
@@ -0,0 +1,499 @@
//! Windows filesystem adapter for PE companion payloads and byte APIs.
use senbei_engine as unpacker;
use std::path::Path;
use crate::atomic::write_atomic;
pub(crate) fn is_pe_extension(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("exe") || ext.eq_ignore_ascii_case("dll"))
}
pub(crate) fn is_companion(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
let Some(stub_name) = name.strip_suffix("._") else {
return false;
};
is_pe_extension(Path::new(stub_name))
}
/// Return whether a directory entry is an NTFS reparse point. The scanner keeps
/// this host-specific check in the Windows adapter while the traversal itself
/// remains platform-neutral.
#[cfg(windows)]
pub(crate) fn is_reparse_point(entry: &walkdir::DirEntry) -> bool {
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
entry
.metadata()
.map(|metadata| metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0)
.unwrap_or(false)
}
#[cfg(not(windows))]
pub(crate) fn is_reparse_point(_entry: &walkdir::DirEntry) -> bool {
false
}
/// Crackproof header key table lives at this fixed file offset. For the
/// external-companion layout, the companion payload aligns to the stub here.
const HEADER_OFF: usize = 4096;
/// Build the unpacker input for `input`, transparently handling the
/// **external-companion** layout used by some il2cpp games.
///
/// In that layout a protected module is split into a thin on-disk loader stub
/// (`Foo.dll`, whose code sections are stripped to one page) plus an encrypted
/// `Foo.dll._` companion holding the real payload. The companion is byte-for-byte
/// the stub's payload region starting at the Crackproof header (offset 4096), so
/// `stub[..4096] ++ companion` reconstructs the ordinary embedded-payload file
/// the existing pipelines already unpack. The runtime loader does exactly this:
/// it maps `Foo.dll._` and feeds it through the standard Crackproof unpack.
///
/// The splice fires only when a sibling `<input>._` exists *and* its first 32
/// bytes equal the stub's header at offset 4096 — a precise signal that the
/// companion is this stub's payload. Otherwise the file is returned untouched,
/// so normal (embedded-payload) inputs are unaffected.
pub(crate) fn read_unpacker_input(input: &Path) -> std::io::Result<UnpackerInput> {
let stub = std::fs::read(input)?;
// Companion path: append "._" to the full file name (Foo.dll -> Foo.dll._).
let companion = match input.file_name() {
Some(name) => {
let mut n = name.to_os_string();
n.push("._");
input.with_file_name(n)
}
None => {
return Ok(UnpackerInput {
bytes: stub,
stub: None,
});
}
};
if !companion.is_file() {
return Ok(UnpackerInput {
bytes: stub,
stub: None,
});
}
let comp = std::fs::read(&companion)?;
match splice_companion(&stub, &comp) {
// A splice fired: keep the stub so its plaintext export table can be
// overlaid onto the unpacked image (the companion does not carry it).
Some(spliced) => Ok(UnpackerInput {
bytes: spliced,
stub: Some(stub),
}),
None => Ok(UnpackerInput {
bytes: stub,
stub: None,
}),
}
}
/// The bytes fed to the unpacker, plus the original loader stub when the input
/// was reconstructed from an external companion. The stub is retained because
/// the crackproof loader rebuilds the PE export table at runtime from data kept
/// in the stub — that table is *not* present in the encrypted companion, so the
/// unpacked image needs it overlaid from the stub afterwards
/// (see [`overlay_exports_from_stub`]).
pub(crate) struct UnpackerInput {
pub(crate) bytes: Vec<u8>,
pub(crate) stub: Option<Vec<u8>>,
}
/// Overlay the PE export table from the loader `stub` onto the unpacked image
/// `out`, for the external-companion layout.
///
/// In that layout the encrypted companion carries the real `.text`/`il2cpp`
/// payload but **not** a usable export directory: the crackproof loader rebuilds
/// exports at runtime from the plaintext copy retained in the stub's `.rdata`.
/// Statically, the spliced input therefore decrypts to a garbage export
/// directory (`NumberOfFunctions` etc. are ciphertext), which makes downstream
/// tools (IL2CppDumper, IDA) choke when they parse it. The fix does what the
/// loader does: copy the export-directory region byte-for-byte from the stub to
/// the same RVA in the unpacked image.
///
/// No-op (leaves `out` untouched) if there is no export directory, or if the
/// region cannot be mapped in either image — so a malformed stub can never
/// corrupt an otherwise-good unpack.
pub(crate) fn overlay_exports_from_stub(out: &mut [u8], stub: &[u8]) {
let (export_rva, export_size) = match pe_export_dir(out) {
Some(v) if v.1 != 0 => v,
_ => return,
};
let dst = match rva_to_file_off(out, export_rva) {
Some(o) => o,
None => return,
};
let src = match rva_to_file_off(stub, export_rva) {
Some(o) => o,
None => 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]);
}
}
/// Restore the TLS directory from the loader `stub` onto the unpacked image
/// `out`, for the external-companion layout.
///
/// Crackproof strips the whole `IMAGE_TLS_DIRECTORY` from the encrypted payload
/// — the data-directory entry, the directory struct, the raw-data template, and
/// the base relocations for the struct's four 64-bit pointer fields — and
/// re-installs TLS itself from data kept in the stub when it loads the module.
/// A statically-unpacked DLL is loaded by the ordinary Windows loader instead,
/// which needs a valid TLS directory or it never allocates a TLS slot for the
/// module nor writes `_tls_index`. The module's C++ `thread_local` accesses then
/// read a garbage TLS slot — observed as a `0xC0000005` deep in IL2CPP type
/// resolution (a TypeDef token used as a raw `s_TypeInfoTable` index).
///
/// The stub retains the full plaintext `.rdata` (only `.text`/`il2cpp` are
/// stripped to one page), so the directory struct and its raw-data template are
/// copied back byte-for-byte at their RVAs, the data-directory entry is taken
/// from the stub header (the unpacked image's was overwritten with the zeroed
/// saved-header blob), and four DIR64 relocations are appended to `.reloc`.
///
/// No-op if the stub declares no TLS directory or if any required region cannot
/// be mapped/relocated — so it can never corrupt an otherwise-good unpack.
pub(crate) fn restore_tls_from_stub(out: &mut [u8], stub: &[u8]) {
let pe = match read_u32(out, 0x3C) {
Some(v) => v as usize,
None => return,
};
if out.get(pe..pe + 4) != Some(&b"PE\0\0"[..]) {
return;
}
// This restore is PE32+-only: it copies a 40-byte IMAGE_TLS_DIRECTORY64,
// converts fields with a 64-bit image base, and appends DIR64 relocs. A
// PE32 module needs the 24-byte struct / DIR32 handling (the unpacker core
// does that itself — see `restore_pe32_tls_from_stub`), so bail rather than
// read the data directories at the wrong (PE32+) offset and write garbage.
if read_u16(out, pe + 24) != Some(0x20B) {
return;
}
// TLS is data-directory index 9 (PE32+ directories at optional header +112).
let tls_dd = match pe.checked_add(24 + 112 + 9 * 8) {
Some(v) => v,
None => return,
};
// The genuine entry survives in the stub header; the unpacked image's copy
// was clobbered by the (zeroed-TLS) saved-header blob.
let (tls_rva, tls_size) = match (read_u32(stub, tls_dd), read_u32(stub, tls_dd + 4)) {
(Some(r), Some(s)) if r != 0 && s != 0 => (r, s),
_ => return, // module has no TLS — nothing to restore
};
// Image base (PE32+, optional header +24) converts the struct's absolute VAs
// back to RVAs for the raw-data template overlay.
let image_base = match read_u64(out, pe + 24 + 24) {
Some(v) => v,
None => return,
};
// 1) Overlay the IMAGE_TLS_DIRECTORY struct from the stub at its RVA.
let dst = match rva_to_file_off(out, tls_rva) {
Some(o) => o,
None => return,
};
let src = match rva_to_file_off(stub, tls_rva) {
Some(o) => o,
None => return,
};
let n = tls_size as usize;
if dst.checked_add(n).is_none_or(|e| e > out.len())
|| src.checked_add(n).is_none_or(|e| e > stub.len())
{
return;
}
out[dst..dst + n].copy_from_slice(&stub[src..src + n]);
// 2) Restore the data-directory entry so the loader processes TLS at all.
write_u32_at(out, tls_dd, tls_rva);
write_u32_at(out, tls_dd + 4, tls_size);
// 3) Overlay the raw-data template [StartAddressOfRawData, EndAddressOfRawData).
if let (Some(start_va), Some(end_va)) = (read_u64(out, dst), read_u64(out, dst + 8))
&& end_va > start_va
&& start_va >= image_base
{
let tpl_rva = (start_va - image_base) as u32;
let tpl_len = (end_va - start_va) as usize;
if let (Some(td), Some(ts)) = (
rva_to_file_off(out, tpl_rva),
rva_to_file_off(stub, tpl_rva),
) && td.checked_add(tpl_len).is_some_and(|e| e <= out.len())
&& ts.checked_add(tpl_len).is_some_and(|e| e <= stub.len())
{
out[td..td + tpl_len].copy_from_slice(&stub[ts..ts + tpl_len]);
}
}
// 4) Append DIR64 relocations for the struct's four 64-bit pointer fields
// (Start/End/Index/CallBacks at +0/+8/+0x10/+0x18). Without them the
// loader would leave preferred-base VAs in a rebased image.
add_tls_relocs(out, pe, tls_rva);
}
/// Append a single base-relocation block covering the four 64-bit pointer fields
/// of the TLS directory struct at `tls_rva`. The block is written immediately
/// after the existing relocation table (which must be free space and in bounds)
/// and the BaseReloc directory size is grown to include it. No-op if the table
/// is absent, the fields straddle a relocation page, or the slot is not free.
fn add_tls_relocs(out: &mut [u8], pe: usize, tls_rva: u32) {
let reloc_dd = pe + 24 + 112 + 5 * 8; // BaseReloc = directory index 5
let (reloc_rva, reloc_size) = match (read_u32(out, reloc_dd), read_u32(out, reloc_dd + 4)) {
(Some(r), Some(s)) if r != 0 => (r, s),
_ => return,
};
// All four fields (last at +0x18) must share one 0x1000 relocation page.
let page = tls_rva & !0xFFF;
if (tls_rva.wrapping_add(0x18)) & !0xFFF != page {
return;
}
const BLOCK: usize = 8 + 4 * 2; // header + four DIR64 entries
let at = match rva_to_file_off(out, reloc_rva.wrapping_add(reloc_size)) {
Some(o) => o,
None => return,
};
if at.checked_add(BLOCK).is_none_or(|e| e > out.len()) {
return;
}
if out[at..at + BLOCK].iter().any(|&b| b != 0) {
return; // refuse to clobber existing data
}
write_u32_at(out, at, page);
write_u32_at(out, at + 4, BLOCK as u32);
for (i, off) in [0u32, 8, 0x10, 0x18].iter().enumerate() {
let entry = (10u16 << 12) | (((tls_rva.wrapping_add(*off)) & 0xFFF) as u16);
let p = at + 8 + i * 2;
out[p..p + 2].copy_from_slice(&entry.to_le_bytes());
}
write_u32_at(out, reloc_dd + 4, reloc_size.wrapping_add(BLOCK as u32));
}
/// Read the Export data-directory (RVA, size) from a PE image, or `None` if the
/// headers are too short/invalid to parse.
fn pe_export_dir(buf: &[u8]) -> Option<(u32, u32)> {
let headers = senbei_pe::parse(buf).ok()?;
senbei_pe::data_directory(buf, headers, 0).ok()
}
/// Map an RVA to a file offset using the PE section table. Returns `None` if no
/// section contains the RVA or the headers cannot be parsed.
fn rva_to_file_off(buf: &[u8], rva: u32) -> Option<usize> {
let headers = senbei_pe::parse(buf).ok()?;
senbei_pe::rva_to_offset(buf, headers, rva).ok()
}
fn read_u32(buf: &[u8], off: usize) -> Option<u32> {
let b = buf.get(off..off + 4)?;
Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn read_u16(buf: &[u8], off: usize) -> Option<u16> {
let b = buf.get(off..off + 2)?;
Some(u16::from_le_bytes([b[0], b[1]]))
}
fn read_u64(buf: &[u8], off: usize) -> Option<u64> {
let b = buf.get(off..off + 8)?;
Some(u64::from_le_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
/// Write a little-endian `u32` at `off`, silently doing nothing if out of bounds.
fn write_u32_at(buf: &mut [u8], off: usize, val: u32) {
if let Some(slot) = buf.get_mut(off..off + 4) {
slot.copy_from_slice(&val.to_le_bytes());
}
}
/// Splice a stub and its external-companion payload into the embedded-payload
/// form the pipelines expect, or `None` if `comp` is not this stub's payload.
///
/// The companion is byte-for-byte the stub's payload region from the Crackproof
/// header (offset 4096) onward, so the result is `stub[..4096] ++ comp`. The
/// splice fires only when the first 32 bytes of `comp` equal the stub's header
/// at offset 4096 — a 32-byte match on the key-table/magic region that confirms
/// the pairing and leaves ordinary (non-companion) inputs untouched.
pub(crate) fn splice_companion(stub: &[u8], comp: &[u8]) -> Option<Vec<u8>> {
let hdr_end = HEADER_OFF + 32;
if stub.len() >= hdr_end && comp.len() >= 32 && stub[HEADER_OFF..hdr_end] == comp[..32] {
let mut spliced = Vec::with_capacity(HEADER_OFF + comp.len());
spliced.extend_from_slice(&stub[..HEADER_OFF]);
spliced.extend_from_slice(comp);
return Some(spliced);
}
None
}
/// Detect `bytes` and run the right pipeline. Spliced external companions use
/// the EXE pipeline directly because that layout is definitionally EXE-style.
///
/// Routing spliced inputs straight to the EXE pipeline is safe: the
/// companion layout is definitionally the EXE-style shell (the runtime
/// loader maps the companion and runs the standard shell unpack), so the DLL
/// pipeline probe can never be right for it. Output bytes are identical to the
/// DLL-first + EXE-fallback route for every input that route handles.
pub(crate) fn unpack_spliced_or_auto(
bytes: &[u8],
spliced: bool,
force_exe: bool,
verbose: bool,
) -> Result<(unpacker::Kind, Vec<u8>), unpacker::UnpackError> {
if spliced || force_exe {
let detected = unpacker::detect(bytes).ok_or(unpacker::UnpackError::NotCrackproof)?;
let out = unpacker::unpack_exe_v(bytes, verbose)?;
return Ok((detected.kind, out));
}
unpacker::unpack_auto_v(bytes, verbose)
}
/// Unpack a single file to `dest`. Returns the Kind and integrity report on success.
pub fn unpack_one(
input: &Path,
dest: &Path,
) -> anyhow::Result<(unpacker::Kind, unpacker::IntegrityReport)> {
unpack_one_v(input, dest, false)
}
/// Outcome of a byte-level unpack ([`unpack_bytes`]): the image, its detected
/// kind, and its integrity report. No file I/O is involved.
pub struct UnpackedImage {
pub kind: unpacker::Kind,
pub bytes: Vec<u8>,
pub integrity: unpacker::IntegrityReport,
/// True when the input was reconstructed from an external companion (the
/// `._` layout), i.e. the export/TLS overlays ran.
pub companion: bool,
}
/// Unpack in-memory `input` bytes, optionally paired with an external
/// companion payload `companion` (the `<input>._` file's contents).
///
/// This is the in-memory counterpart of [`unpack_one_v`]: splice a matching
/// companion, unpack, overlay the export table and TLS directory from the stub,
/// then run the static integrity check.
pub fn unpack_bytes(
input: &[u8],
companion: Option<&[u8]>,
) -> Result<UnpackedImage, unpacker::UnpackError> {
unpack_bytes_impl(input, companion, false)
}
/// Like [`unpack_bytes`], but forces the EXE pipeline (no DLL-pipeline
/// probe). This is the web app's recovery path: the DLL-first probe relies
/// on `catch_unwind` to reject EXE-shell-layout DLLs, and panics cannot be
/// caught on wasm — the probe traps the whole call. The web app runs each
/// unpack in a disposable Web Worker and retries trapped DLLs with this
/// entry point, reproducing the CLI's dll-first/exe-fallback routing.
pub fn unpack_bytes_force_exe(
input: &[u8],
companion: Option<&[u8]>,
) -> Result<UnpackedImage, unpacker::UnpackError> {
unpack_bytes_impl(input, companion, true)
}
fn unpack_bytes_impl(
input: &[u8],
companion: Option<&[u8]>,
force_exe: bool,
) -> Result<UnpackedImage, unpacker::UnpackError> {
let spliced = companion.and_then(|c| splice_companion(input, c));
let bytes: &[u8] = spliced.as_deref().unwrap_or(input);
let (kind, mut out) = unpack_spliced_or_auto(bytes, spliced.is_some(), force_exe, false)?;
if spliced.is_some() {
overlay_exports_from_stub(&mut out, input);
restore_tls_from_stub(&mut out, input);
}
let integrity = unpacker::check_integrity(&out);
Ok(UnpackedImage {
kind,
bytes: out,
integrity,
companion: spliced.is_some(),
})
}
/// Like [`unpack_one`], but prints detailed `[N/9]` step progress (and a final
/// `Write to <dest>` line) to stdout when `verbose` is true.
pub fn unpack_one_v(
input: &Path,
dest: &Path,
verbose: bool,
) -> anyhow::Result<(unpacker::Kind, unpacker::IntegrityReport)> {
let UnpackerInput { bytes, stub } = read_unpacker_input(input)?;
let (kind, mut out) = unpack_spliced_or_auto(&bytes, stub.is_some(), false, verbose)?;
// External-companion layout: restore the export table from the stub, which
// the encrypted companion does not carry (the loader rebuilds it at runtime).
if let Some(stub) = stub {
overlay_exports_from_stub(&mut out, &stub);
// ...and the TLS directory, which Crackproof strips from the payload and
// re-installs at runtime; the ordinary loader needs it or thread_local
// access crashes (see [`restore_tls_from_stub`]).
restore_tls_from_stub(&mut out, &stub);
}
let report = unpacker::check_integrity(&out);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
write_atomic(dest, &out)?;
if verbose {
println!("Write to {}", dest.display());
}
Ok((kind, report))
}
#[cfg(test)]
mod tests {
use super::*;
const HEADER_OFF: usize = 4096;
fn stub_with_header(header: &[u8; 32], extra: usize) -> Vec<u8> {
let mut stub = vec![0_u8; HEADER_OFF];
stub.extend_from_slice(header);
stub.extend_from_slice(&vec![0xAA_u8; extra]);
stub
}
#[test]
fn splices_when_header_matches() {
let header = [7_u8; 32];
let stub = stub_with_header(&header, 16);
let mut companion = header.to_vec();
companion.extend_from_slice(&[0x42_u8; 1000]);
let output = splice_companion(&stub, &companion).expect("should splice");
assert_eq!(output.len(), HEADER_OFF + companion.len());
assert_eq!(&output[..HEADER_OFF], &stub[..HEADER_OFF]);
assert_eq!(&output[HEADER_OFF..], &companion[..]);
}
#[test]
fn no_splice_when_header_differs() {
let stub = stub_with_header(&[7_u8; 32], 16);
let mut companion = vec![9_u8; 32];
companion.extend_from_slice(&[0x42_u8; 1000]);
assert!(splice_companion(&stub, &companion).is_none());
}
#[test]
fn no_splice_when_too_short() {
let short_stub = vec![0_u8; HEADER_OFF + 8];
let companion = vec![0_u8; 64];
assert!(splice_companion(&short_stub, &companion).is_none());
let stub = stub_with_header(&[1_u8; 32], 0);
let short_companion = vec![1_u8; 16];
assert!(splice_companion(&stub, &short_companion).is_none());
}
}
+2 -1
View File
@@ -2,10 +2,11 @@
use serde::Serialize; use serde::Serialize;
use crate::common::MAGIC;
/// Seed embedded in the current `libil2cpp` module `0x0C`. /// Seed embedded in the current `libil2cpp` module `0x0C`.
pub const DEFAULT_METHOD_TOKEN_SEED: u32 = 0xa6fa_e968; pub const DEFAULT_METHOD_TOKEN_SEED: u32 = 0xa6fa_e968;
const MAGIC: u32 = 0xfab1_1baf;
const SUPPORTED_V29: u32 = 29; const SUPPORTED_V29: u32 = 29;
const SUPPORTED_V31: u32 = 31; const SUPPORTED_V31: u32 = 31;
const HDR_METHODS: usize = 0x30; const HDR_METHODS: usize = 0x30;
+12
View File
@@ -0,0 +1,12 @@
//! Shared IL2CPP metadata header primitives.
/// IL2CPP global-metadata sanity magic.
pub(crate) const MAGIC: u32 = 0xFAB1_1BAF;
/// Cheap check used by both platform scanners before opening a full metadata
/// file.
#[must_use]
pub fn is_metadata(data: &[u8]) -> bool {
data.get(0..4)
.is_some_and(|bytes| u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) == MAGIC)
}
+4 -1
View File
@@ -1,6 +1,9 @@
//! Unity il2cpp metadata restoration. //! Unity il2cpp metadata restoration.
pub mod android; pub mod android;
mod common;
mod structural;
pub mod windows; pub mod windows;
pub use windows::*; pub use common::is_metadata;
pub use structural::*;
@@ -27,8 +27,7 @@
//! metadata (its tokens already equal `local_index + 1`), so it is safe to run on //! metadata (its tokens already equal `local_index + 1`), so it is safe to run on
//! any il2cpp game — `remapped == 0` then reports that nothing changed. //! any il2cpp game — `remapped == 0` then reports that nothing changed.
/// il2cpp `global-metadata.dat` sanity magic (`Il2CppGlobalMetadataHeader.sanity`). use crate::common::MAGIC;
const MAGIC: u32 = 0xFAB1_1BAF;
/// Metadata format version this de-obfuscator understands. The struct strides /// Metadata format version this de-obfuscator understands. The struct strides
/// and header field offsets below are specific to it; other versions are left /// and header field offsets below are specific to it; other versions are left
+2 -4
View File
@@ -1,5 +1,3 @@
//! Windows metadata restoration. //! Compatibility namespace for the shared structural metadata transform.
mod metadata; pub use crate::structural::*;
pub use metadata::*;
+28
View File
@@ -92,6 +92,34 @@ pub fn sections(data: &[u8], headers: Headers) -> Result<Vec<Section>> {
.collect() .collect()
} }
/// Read one PE data-directory entry as `(RVA, size)`.
pub fn data_directory(data: &[u8], headers: Headers, index: u16) -> Result<(u32, u32)> {
let directory_base = headers
.pe_offset
.checked_add(24)
.and_then(|offset| offset.checked_add(if headers.is_pe32_plus { 112 } else { 96 }))
.ok_or(Error::OutOfBounds)?;
let offset = directory_base
.checked_add(
usize::from(index)
.checked_mul(8)
.ok_or(Error::OutOfBounds)?,
)
.ok_or(Error::OutOfBounds)?;
Ok((read_u32(data, offset)?, read_u32(data, offset + 4)?))
}
/// Return the COFF characteristics bit field.
pub fn characteristics(data: &[u8], headers: Headers) -> Result<u16> {
read_u16(
data,
headers
.pe_offset
.checked_add(22)
.ok_or(Error::OutOfBounds)?,
)
}
pub fn rva_to_offset(data: &[u8], headers: Headers, rva: u32) -> Result<usize> { pub fn rva_to_offset(data: &[u8], headers: Headers, rva: u32) -> Result<usize> {
if rva < headers.sections_offset as u32 { if rva < headers.sections_offset as u32 {
return Ok(rva as usize); return Ok(rva as usize);
+20 -1
View File
@@ -419,12 +419,21 @@ dependencies = [
] ]
[[package]] [[package]]
name = "senbei-engine" name = "senbei-elf"
version = "1.2.0" version = "1.2.0"
dependencies = [ dependencies = [
"goblin", "goblin",
"thiserror",
]
[[package]]
name = "senbei-engine"
version = "1.2.0"
dependencies = [
"memmap2", "memmap2",
"senbei-crypto", "senbei-crypto",
"senbei-elf",
"senbei-pe",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@@ -441,8 +450,11 @@ dependencies = [
"libc", "libc",
"memmap2", "memmap2",
"owo-colors", "owo-colors",
"senbei-crypto",
"senbei-elf",
"senbei-engine", "senbei-engine",
"senbei-metadata", "senbei-metadata",
"senbei-pe",
"sha2", "sha2",
"tempfile", "tempfile",
"walkdir", "walkdir",
@@ -458,6 +470,13 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "senbei-pe"
version = "1.2.0"
dependencies = [
"thiserror",
]
[[package]] [[package]]
name = "senbei-wasm" name = "senbei-wasm"
version = "1.2.0" version = "1.2.0"