mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
refactor: align platform crate boundaries
This commit is contained in:
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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}"));
|
||||||
}
|
}
|
||||||
|
output
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"));
|
||||||
}
|
}
|
||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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>"),
|
||||||
|
|||||||
@@ -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,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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
for i in 0..nsec {
|
|
||||||
let s = tab + i * 40;
|
|
||||||
if (s as usize + 24) > file_data.len() {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let va = get_u32(file_data, s + 12);
|
let offset = senbei_pe::rva_to_offset(file_data, headers, rva).ok()?;
|
||||||
let vs = get_u32(file_data, s + 8);
|
u32::try_from(offset).ok()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
// If the table runs past EOF the image is structurally broken.
|
|
||||||
let (vsize, va, raw_size, raw_ptr, chars) = match (
|
|
||||||
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
|
r.issues
|
||||||
.push("section table extends past end of file".into());
|
.push("section table extends past end of file".into());
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Raw data must lie within the file for compacted (disk-layout) output.
|
let parsed_sections = match senbei_pe::sections(out, headers) {
|
||||||
if raw_size != 0 {
|
Ok(sections) => sections,
|
||||||
let end = raw_ptr.wrapping_add(raw_size) as usize;
|
Err(_) => {
|
||||||
|
r.issues
|
||||||
|
.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 {
|
if end > file_len {
|
||||||
r.issues.push(format!(
|
r.issues.push(format!(
|
||||||
"section #{i} raw data [0x{raw_ptr:X}..0x{end:X}] exceeds file size 0x{file_len:X}"
|
"section #{i} raw data [0x{:X}..0x{end:X}] exceeds file size 0x{file_len:X}",
|
||||||
|
section.raw_offset
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
secs.push(Section {
|
section
|
||||||
va,
|
})
|
||||||
vsize,
|
.collect();
|
||||||
raw_ptr,
|
|
||||||
raw_size,
|
|
||||||
chars,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 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,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+3
-493
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Windows metadata restoration.
|
//! Compatibility namespace for the shared structural metadata transform.
|
||||||
|
|
||||||
mod metadata;
|
pub use crate::structural::*;
|
||||||
|
|
||||||
pub use metadata::*;
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Generated
+20
-1
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user