refactor: consolidate platform engines into senbei-engine

This commit is contained in:
bfloat16
2026-09-06 19:31:19 +08:00
parent cbfacbc31f
commit d436a200ba
66 changed files with 1148 additions and 1012 deletions
+31 -215
View File
@@ -1,238 +1,54 @@
# Design
Senbei is a fully static unpacker: it replays the unpacking algorithm on the
file bytes in memory and writes the recovered PE image. No code from the
protected binary is ever executed, no process is launched or attached to, and
no driver or proxy DLL is involved.
Senbei is a fully static unpacker. It reads protected bytes, replays the protection algorithm, validates the result, and writes a recovered image without launching or attaching to the protected program.
## Crate layout
## Crate Layout
Senbei is a Cargo workspace split into a pure core and thin shells around it:
The workspace is organized into eight crates. `senbei-cli` is the command-line entry point, `senbei-io` owns filesystem orchestration, `senbei-wasm` provides browser bindings, `senbei-pe` and `senbei-elf` provide basic format parsing, `senbei-crypto` provides shared primitives, `senbei-metadata` restores metadata, and `senbei-engine` owns protection-specific pipelines.
- **`senbei-pe/`** — the core. Pure functions over byte slices: no file I/O,
no environment access (beyond a few debugging overrides, see
[development.md](development.md)), panic-free at the public boundary (all
internal panics are trapped and converted to `UnpackError::Corrupt`). This
is what the WebAssembly build embeds.
- **`senbei-crypto/`** — cryptographic, checksum, compression, and bytecode
primitives the core is built from. Same purity rules as `senbei-pe`.
- **`senbei-metadata/`** — il2cpp `global-metadata.dat` method-token
de-obfuscation (format version 31; other versions are left untouched).
- **`senbei-android-crypto/`** — container primitives of the Android
(AArch64) protection scheme: the word/record ciphers, the GF(2³²)
transform, the AES-augmented segment transform, and the Huffman/LZ decoder.
- **`senbei-android-engine/`** — stage-1/stage-2 extraction: finds the
appended payload section, decrypts the stage-1 header and stage-2 payload,
and walks the recursive record streams to decode every module. Native-only
(memory-maps the input, writes the module set to a workspace directory).
- **`senbei-android-elf/`** — the restore: replays the decoded target-image
and fixup containers onto a hollowed ELF and rebuilds the dynamic-linker
tables (hash tables, symbols, relocations) the protector stripped.
Native-only.
- **`senbei-android-metadata/`** — the Android metadata variants: the seeded
five-round MethodDef-RID permutation restore (v31), seed discovery, and the
embedded-metadata XOR unwrap (`keystream.rs`).
- **`senbei-io/`** — filesystem and orchestration: recursive folder scanning,
per-run log file, progress bar, Explorer-friendly exit pause, the
single-file/folder orchestration in `job.rs` (incl. the wasm-safe in-memory
byte API used by the web frontend), and `android.rs` — the Android
single-library / folder / app-package orchestration.
- **`senbei-cli/`** — the `senbei` binary: argument parsing + dispatch. The
integration test suite (incl. the golden corpus test) lives in
`senbei-cli/tests/`.
Single-platform source stays directly under `src/`. Multi-platform crates keep platform code below `src/windows/` and `src/android/`, with shared code directly below `src/`.
```
senbei-cli/
└── src/main.rs argument parsing + dispatch
senbei-io/src/
├── job.rs single-file + folder orchestration, out-naming,
│ companion splice, stub overlay/TLS restore,
│ pipeline routing (incl. the wasm-safe byte API)
├── android.rs Android single-library / folder / package
│ orchestration, cross-source dedup
├── scan.rs recursive target discovery (PE + metadata + Android)
├── logfile.rs per-run timestamped log
├── ui.rs progress bar + status lines
└── pause.rs Explorer-friendly exit pause
senbei-metadata/src/
└── metadata.rs il2cpp global-metadata.dat de-obfuscation
```text
senbei-cli/src/main.rs
senbei-crypto/src/
├── primitives.rs decrypt_data* steps, key derivation
├── bytecode.rs bytecode VM
├── tables.rs constant tables
└── crc32.rs checksum
senbei-pe/src/engine/ pure, panic-free, no-I/O core
├── mod.rs detection + unpack_auto dispatch
├── error.rs structured error taxonomy
├── integrity.rs static post-unpack sanity check
├── parallel.rs deterministic block-parallel fan-out
├── layout/ layout discovery + validation
│ ├── dd8.rs .text dd8 key-formula + shift selection
│ ├── discovery.rs layout candidate discovery (trial-and-validate)
│ └── image.rs PE image reconstruction helpers
├── exe/
│ ├── pipeline.rs EXE pipeline (PE32+ and PE32 orchestration)
│ └── pipeline/pe32.rs PE32-specific EXE restore
└── dll/
└── pipeline.rs native + managed DLL pipeline
senbei-android-crypto/src/
└── protector.rs container ciphers, GF(2^32), Huffman/LZ decoder
senbei-android-engine/src/
├── stage1.rs payload-section discovery + stage-1 header/payload
├── stream.rs record-stream parsing
├── extract.rs recursive module extraction (writes the workspace)
├── probe.rs protected-library content probe
└── report.rs machine-readable extraction report
senbei-android-elf/src/
├── restore.rs image restore + dynamic-table rebuild
├── layout.rs ELF layout parsing
├── artifact.rs module-workspace index loading
└── hash.rs SysV/GNU hash table rebuild
senbei-android-metadata/src/
├── method_tokens.rs seeded RID permutation restore + seed discovery
├── embedded.rs embedded-metadata blob locate + XOR unwrap
└── keystream.rs recovered keystream table (one observed build)
senbei-crypto/src/android/
senbei-elf/src/
senbei-engine/src/windows/
senbei-engine/src/android/
senbei-io/src/
senbei-io/src/android/
senbei-metadata/src/windows/
senbei-metadata/src/android/
senbei-pe/src/
senbei-wasm/src/
```
## Detection and routing
`senbei-pe` and `senbei-elf` are format crates only. They do not depend on the unpacking engines, filesystem code, or platform protection logic.
Detection is content-based (`unpacker::detect`), never extension-based: the
key table is derived from the file header and checked against the format
magic, then the PE characteristics classify the input as EXE or DLL and the
CLR data directory splits each into native vs managed (`NativeExe` /
`ManagedExe` / `NativeDll` / `ManagedDll`). The folder scan additionally
classifies Android targets: an ELF64/AArch64 prefix promotes the file to a
full protection probe (`senbei_android_engine::is_protected_libil2cpp`), and a
package extension plus zip magic marks an app package for container
extraction.
## Windows Engine
`unpack_auto` then dispatches:
`senbei-engine/src/windows/` contains PE detection, layout discovery, EXE and DLL restoration, deterministic block parallelism, and structural integrity checks. Candidate layouts are trial-decrypted and validated before an output is accepted.
- `NativeExe` / `ManagedExe` → the EXE pipeline (handles both PE32+ and
PE32). Managed EXEs take the same path: their import-string table is null
(imports are the CLR bootstrap stub), the entry point comes from the
protected header (the config block stores 0 for managed images), and the
COR20 header, BSJB metadata stream, and CLR resources are restored verbatim
from the protected file, mirroring the managed-DLL restore.
- `NativeDll` / `ManagedDll` → the DLL pipeline first; on failure, the EXE
pipeline as a fallback. Two DLL layouts exist in the wild: an older layout
the DLL pipeline parses, and a newer one that protects DLLs with the
EXE-style shell layout instead. The DLL-first order keeps old-layout outputs
byte-identical (the EXE pipeline also "succeeds" on old-layout DLLs but
produces different bytes); the fallback handles the new layout (including
the managed-DLL .NET metadata restore).
External companion inputs are reconstructed as `stub[..4096]` followed by the matching `._` payload. The stub's export and TLS data is overlaid after unpacking because those regions are not present in the encrypted companion.
One routing shortcut bypasses `unpack_auto`: inputs spliced from an external
companion (`job.rs`, both the CLI and the wasm byte API) go **straight to the
EXE pipeline**. The companion layout is definitionally the EXE-style shell,
so the DLL probe can never be right for it — and the probe's rejection of
EXE-shell DLLs relies on a caught panic, which is a fatal trap on targets
without unwinding (WebAssembly). Output bytes are identical to the
probe-then-fallback route.
## Android Engine
## External-companion inputs
`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.
Some builds split a protected module into an on-disk loader stub plus an
encrypted `._` companion. When a `<name>._` sibling matches the stub's header
region, `job.rs` splices the two before unpacking and afterwards overlays the
export table and TLS directory from the stub — pieces the encrypted companion
does not carry. All overlay steps are best-effort no-ops when their inputs
can't be mapped, so a malformed stub can never corrupt an otherwise-good
unpack.
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/`.
## Pipelines
## Scanning and Packages
Both pipelines are **heuristic with trial-and-validate**: where a layout
leaves ambiguity (e.g. which block is the real file decryptor, or a page-XOR
shift), the pipeline tries candidates and validates the result structurally
(an entry-stub oracle, checksum stamps, cluster stamps) instead of trusting
the first match. A validation failure falls through to the next candidate
rather than producing silently wrong output.
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.
Several protected stages are themselves little bytecode programs. The core
includes a small VM (`bytecode.rs`) that generates and interprets those
programs rather than hardcoding each variant's constants.
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.
## The Android pipeline
## Validation
The Android scheme hollows an ELF64/AArch64 shared object: section bodies are
zeroed in the file and the original bytes move into an encrypted payload
appended as a `SHT_LOUSER` section (invisible to the dynamic loader). Restore
is two-phase:
Every heuristic layout uses trial-and-validate. A candidate that fails structural checks, checksums, or table bounds is rejected and the next candidate is tried. A failed restore is reported as an error rather than emitting a silently damaged binary.
1. **Extract** (`senbei-android-engine`): decrypt the stage-1 parameter block
and stage-2 payload from the payload section, then walk the recursive
record streams — each decoded module may interpret a further nested stream
— into a temporary module workspace with a JSON index.
2. **Restore** (`senbei-android-elf`): decode the target-image container onto
a copy of the hollowed file, apply the compact fixup database (the
relocations stripped from `.rela.dyn`), and rebuild the dynamic-linker
tables the loader needs (SysV/GNU hash, symbol and string tables,
`.rela.dyn`/`.rela.plt`). Validation is structural and total: mismatched
container sizes, descriptor bounds, or a rebuilt table overhanging its
section fail the restore rather than emit a broken image.
The PE integrity check verifies headers, section ranges, entry-point mapping, import names, relocation requirements, and managed metadata signatures. Android restoration validates ELF ranges, decoded container sizes, fixup bounds, and rebuilt dynamic tables.
il2cpp metadata comes in three shapes, all routed through
`job::deobfuscate_metadata_to` / `android::restore_metadata_bytes`:
## WebAssembly
- **structural (Windows `-GMD`)**: sparse method tokens remapped to the
contiguous per-module range, keyless, idempotent (`senbei-metadata`).
- **seeded permutation (Android v31)**: MethodDef RIDs permuted by a keyed
five-round transform; the seed is recovered by intersecting per-image key
residues, and the restore validates every RID — a wrong seed errors and the
structural remap takes over (`senbei-android-metadata`).
- **embedded blob**: no metadata file in the app at all; a slim blob sits in
the library's data section under a per-word XOR layer. After a restore the
blob is located by content (two known plaintext header words against the
embedded keystream) and unwrapped to a standalone `global-metadata.dat`.
Key derivation is untraced — the shipped keystream covers the one observed
build, and other builds simply never match the probe.
Packages (`.apk`/`.apks`/`.xapk`) are containers, not targets: entries are
extracted to a temporary workspace and content-probed like loose files.
Cross-source duplicates (a library loose in the tree *and* inside its
package) are restored once, preferring the loose file, then the `.apk`, then
bundle splits.
## Integrity check
Every produced image passes through `integrity::check` — a static, execution-
free sanity check that only flags defects impossible in a correctly unpacked
image (malformed headers, unmapped/non-executable/all-zero/all-int3 entry
point, a native DLL with no base-relocation directory, any import descriptor
whose DLL name is still ciphertext, a managed image whose COR20 header or BSJB
metadata did not survive). See [usage.md](usage.md#integrity-check).
A clean report is not a proof of correctness; a non-clean report is a reliable
"broken" signal.
## Parallelism
Section decrypt/decompress blocks write disjoint output spans and read only
immutable input plus snapshotted key tables, so `parallel.rs` fans them out
across worker threads with **byte-identical** output regardless of thread
count. There is no `unsafe`: the buffer is carved with safe `split_at_mut`
chains so the borrow checker proves spans never alias. Overlapping spans (only
possible on corrupt input) degrade to the sequential whole-buffer pass,
preserving the deterministic last-writer-wins behavior of the serial
pipeline. `SENBEI_THREADS=1` forces the sequential path; on targets without
threads (WebAssembly) the sequential path is used automatically.
## Error model
The public API never panics: every pipeline runs under a `catch_unwind`
wrapper (`catch_unpack`) that converts a trapped panic to
`UnpackError::Corrupt`, with the default panic hook transiently suppressed.
Size requests are bounds-checked against a 1 GiB `MAX_IMAGE_SIZE` before
allocation so a crafted header cannot abort the process with a huge
allocation. In folder mode each file is isolated: one file's failure is logged
and counted, never fatal to the run.
**WebAssembly caveat:** the prebuilt wasm std cannot unwind, so a caught
panic becomes a fatal `unreachable` trap there. The DLL-routing probe relies
on this mechanism to reject EXE-shell-layout DLLs, so the web build routes
around it instead of through it: spliced companion inputs skip the probe
entirely (see "Detection and routing"), and the web app isolates every unpack
in a disposable Web Worker — a trapped DLL is retried once in a fresh worker
with the forced-EXE pipeline (`job::unpack_bytes_force_exe`), reproducing the
probe-then-fallback outcome without a catchable panic. A trap on any other
input is reported as a clean error rather than freezing the page.
The browser binding depends on `senbei-engine` through the I/O byte API. Native filesystem and Android package orchestration remain outside the browser workflow. Each browser unpack runs in a disposable worker because WebAssembly cannot recover from a caught panic in the same way as native code.
+34 -99
View File
@@ -2,125 +2,60 @@
## Building
Requires a Rust toolchain (MSVC backend is the default on Windows;
`rustup-init.exe` from <https://rustup.rs> installs it). The pinned toolchain
and targets are in `rust-toolchain.toml`.
The pinned Rust toolchain is defined in `rust-toolchain.toml`. Build the CLI with `cargo build --release`; the binary is written to `target/release/senbei.exe` on Windows.
```cmd
cargo build --release
```
Output: `target\release\senbei.exe`. The binary is self-contained — no driver,
no proxy DLL, no external assets.
The library and CLI also build for Linux/macOS (`cfg`-gated platform code
only) and for `wasm32-unknown-unknown` (see the [web version](../web/README.md)).
The workspace crates are portable where their APIs are pure. The browser binding is outside the workspace and is checked with `cargo check --manifest-path senbei-wasm/Cargo.toml` or built with `wasm-pack`.
## Testing
```cmd
cargo test --release
cargo test --release --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
```
The suite covers CLI behavior, detection, the folder driver, the run log, and
byte-exact golden tests over `samples/` — a user-managed corpus (git-ignored,
see `samples/README.md`) of real Crackproof inputs plus `<base>.golden.<ext>`
reference outputs. Every input goes through `job::unpack_bytes` — the same
routing the CLI uses, so an `<input>._` companion in the corpus is spliced and
the stub export/TLS overlays run — and is gated on **two** checks: the static
integrity check (catches runtime-broken outputs even when a stale golden would
still byte-match) and, when a golden exists, a bit-for-bit comparison. il2cpp
`*.dat` inputs are routed through `metadata::deobfuscate` instead. An empty or
absent corpus is a no-op pass; set `SENBEI_REQUIRE_SAMPLES` to make it fail
instead (useful on a private CI that has the corpus — public CI never does,
since binaries are not committed).
The tracked test suite is safe without protected samples. The optional local `samples/` corpus is user-managed and the ignored `test/` folder can be used for real Windows and Android runs.
> **Note:** goldens encode expected *bytes*, not runtime behavior. A golden
> produced before a pipeline fix may byte-match while still being wrong — the
> integrity check is the second gate for exactly this reason. Re-verify
> goldens against real runs when touching the affected pipeline stages.
>
> **The corpus only protects what it contains.** Wire the test to the routing
> the CLI actually takes (it is), and keep a sample for every layout family —
> marker-based, marker-less, external-companion, PE32, PE32+, native, managed,
> metadata. An unrepresented family has no regression gate at all, which is
> how a "re-run the golden corpus" rule can pass while silently covering
> nothing.
For an Android package, use one command at a time because a protected `.so` can be hundreds of megabytes. APK, APKS, and XAPK tests read the ZIP manifest first and extract only `.so` and `global-metadata.dat` entries.
## Debugging levers (environment variables)
## Environment Variables
- `DD8_SHIFT` override the `decrypt_data8` page-XOR shift (`99` skips dd8
entirely).
- `SEL_DIAG` — print the dd8 selector's scores: the per-shift `0xCC` counts and
the plaintext baseline they are compared against (PE32+), and the per-formula
counts, baseline and net gain (PE32).
- `SENBEI_THREADS` — cap the block-parallel fan-out (`1` forces the fully
sequential path).
- `SENBEI_SCAN_ALL` — same as `--scan-all` (probe every file in a folder).
- `SENBEI_ANDROID_SAMPLES` — override the Android corpus location (default
`samples/android/`; see `samples/README.md`). The Android corpus test pins
restored outputs with SHA-256 sidecar files next to each protected input
and documents known restore gaps with empty `<base>.restore-fails` markers.
- `DD8_SHIFT` overrides the PE page-XOR shift; `99` skips that stage.
- `SEL_DIAG` prints PE layout-selector diagnostics.
- `SENBEI_THREADS` caps deterministic block fan-out; `1` forces the sequential reference path.
- `SENBEI_SCAN_ALL` enables the explicit scan-all mode for selected target names.
- `SENBEI_ANDROID_SAMPLES` overrides the Android sample corpus location.
## Conventions
- The `senbei-pe/` core (and its `senbei-crypto/` base) is pure: no file I/O,
no panics across the public boundary, no `unsafe`. Keep it that way — it is
what the WebAssembly build embeds.
- Layout heuristics must **trial-and-validate**: never pick a candidate offset
on shape alone and trust it; validate by decryption/checksum and fall
through to the next candidate on failure. A silent wrong offset produces a
silently broken output, which is worse than an error.
- Output must remain byte-identical against the golden corpus for every
supported layout. When fixing one build family, re-run the full golden
corpus to prove no other family regressed.
- Folder scanning uses a size floor plus an extension **deny**-list, never an
allow-list: targets are recognised by content, not extension, and can carry
arbitrary names, so only known bulk-asset extensions are excluded. The
pre-filter exists because folder-scan cost is per-file I/O latency, not the
walk — probe fewer files, don't parallelize the probe loop.
- `cargo fmt` and `cargo clippy` must stay clean (CI enforces both).
Format crates stay free of filesystem I/O and protection-specific logic. Windows engine code lives below `senbei-engine/src/windows/`, Android engine code below `senbei-engine/src/android/`, and shared code stays directly under each crate's `src/`.
## Repository layout
Layout heuristics must trial and validate every candidate. A failed validation is an error or a fall-through, never a silently accepted offset.
```
senbei/
├── Cargo.toml workspace root (members: the senbei-* crates)
├── rust-toolchain.toml pinned toolchain + targets
├── senbei-cli/ senbei binary (default member)
│ └── tests/ CLI, detection, golden, and folder tests
├── senbei-pe/ pure unpacker core (see docs/design.md)
├── senbei-crypto/ crypto/compression primitives
├── senbei-metadata/ il2cpp metadata de-obfuscation
├── senbei-io/ filesystem, scanning, CLI orchestration
├── senbei-wasm/ WebAssembly bindings crate (own Cargo.lock,
│ outside the workspace; builds into web/pkg/)
├── samples/ local-only test corpus (git-ignored)
├── web/ static browser frontend assets (+ built pkg/)
├── docs/ usage, design, and development documentation
└── .github/ CI workflows and issue templates
Outputs must remain byte-identical against the available golden corpus. Run the full workspace tests after changing a pipeline or a metadata layout.
Folder scanning uses explicit target names to avoid opening bulk assets. External `.exe._` and `.dll._` files are auxiliary data for their sibling stubs and are not independent scan targets.
## Repository Layout
```text
senbei-cli/ command-line binary and integration tests
senbei-crypto/ shared crypto and Android crypto primitives
senbei-elf/ basic ELF parsing
senbei-engine/ Windows and Android unpacking engines
senbei-io/ filesystem, package, scanning, and CLI orchestration
senbei-metadata/ Windows and Android metadata restoration
senbei-pe/ basic PE parsing
senbei-wasm/ browser bindings and its own lockfile
web/ static browser frontend
samples/ optional local corpus
```
## Web build
See [web/README.md](../web/README.md). In short:
## Web Build
```cmd
cd senbei-wasm
wasm-pack build --target web --release --out-dir ../web/pkg
```
then serve `web/` statically and open `index.html`. Everything runs
client-side; no file leaves the browser.
## Contributing
Issues and pull requests are welcome. A few ground rules:
- **Never commit binaries** (protected or decrypted) to the repository —
the only corpus is the local git-ignored `samples/`. Attaching a protected
input file to an issue is welcome if it helps diagnose the problem; only
attach files you are authorized to share.
- Run `cargo test --release`, `cargo clippy`, and `cargo fmt` before
submitting.
- Keep the unpacker core free of I/O, `unsafe`, and platform-specific code.
Serve `web/` with a static HTTP server after the build. The browser never uploads input files.
+25 -133
View File
@@ -1,163 +1,55 @@
# Usage
```
senbei <file|folder> [--out DIR] [-v|--verbose] [-q|--quiet]... [--scan-all]
[--no-log] [--no-pause] [-V|--version] [-h|--help]
```text
senbei <file|folder> [--out DIR] [-v|--verbose] [-q|--quiet]... [--scan-all] [--no-log] [--no-pause] [-V|--version] [-h|--help]
```
Real runs print `Senbei <version>` once at start. Use `-V` / `--version` to
print the version and exit.
## Single File
## Single file
The decrypted image is written under `<parent>/unpack/` with `.unpack` inserted
before the extension. A `senbei-<timestamp>.log` is written in the same
directory. With `--out DIR`, both the output and the log go into `DIR` instead:
The output is written below `<parent>/unpack/` with `.unpack` inserted before the extension. `--out DIR` changes both the output and log directory.
```cmd
senbei app.exe
:: -> unpack\app.unpack.exe
:: -> unpack\senbei-YYYYMMDD-HHMMSS.log
senbei app.exe --out C:\out
:: -> C:\out\app.unpack.exe
:: -> C:\out\senbei-YYYYMMDD-HHMMSS.log
```
Pointing senbei directly at an il2cpp `global-metadata.dat` rewrites its
obfuscated method tokens back to the contiguous per-module range il2cpp
expects; the output is `global-metadata.unpack.dat`, written only when tokens
actually changed. Only metadata format version 31 is rewritten; other versions
are reported and left untouched.
For `global-metadata.dat`, Senbei writes `global-metadata.unpack.dat` only when method tokens change. Unsupported metadata versions remain untouched and are reported as skipped.
## Android targets
## Android Targets
Senbei also restores Android (AArch64) protected shared libraries and app
packages:
Protected `.so` files are restored from their encrypted payload sections and written as `libil2cpp.unpack.so` or the corresponding input name. APK, APKS, and XAPK files are treated as containers: their manifests are read first, nested APKs are followed when necessary, and only `.so` and exact `global-metadata.dat` entries are extracted.
- **`.so`** — a protected library is hollowed out on disk: its original
sections live in an encrypted payload appended to the file, and senbei
rebuilds the static image from it. Output: `libil2cpp.unpack.so`.
- **`.apk`** — entries are extracted to a temporary workspace and
content-probed like loose files; protected libraries and metadata blobs
inside are restored to `<out>/<apk name>/<entry path>`.
- **`.apks` / `.xapk`** — split-package bundles; each nested `.apk` is opened
and searched the same way, under `<out>/<bundle name>/<split name>/...`.
If a restored library contains embedded metadata, the unwrapped blob is written beside it as `global-metadata.unpack.dat`. Identical loose and package entries are restored once, preferring the loose file.
When a restored il2cpp library carries its metadata embedded in its data
section (no standalone `global-metadata.dat` in the app at all), senbei
unwraps the blob and writes it next to the library as
`global-metadata.unpack.dat`. One observed packaging variant wraps the blob in
a per-word XOR layer whose keys are generated at runtime and stored nowhere;
senbei ships the keystream recovered from the one build known to use it and
content-probes for it — builds with a different keystream are silently
skipped (the library itself is still fully restored).
## Folder Mode
The same content may appear loose in a folder, in its `.apk`, and in a bundle
side by side: identical content is restored once, at the loose file's
destination. A restored library is validated structurally by the restore
itself (the rebuild refuses inconsistent layouts); a protected library that
fails validation counts as an error, not a suspect.
Folder mode walks recursively, skips directories named `unpack`, and mirrors recognized outputs below `<root>/unpack/` or `--out DIR`. Windows candidates are `.exe`, `.dll`, and `global-metadata.dat`; Android candidates are `.so` and `global-metadata.dat`. A matching `.exe._` or `.dll._` payload is consumed by its stub and is excluded from the skipped count.
## Folder mode
The summary has the form `12 unpacked · 3 skipped · 0 errors · 1 suspect · 2 metadata`; the package count is appended when packages were opened. Each file is isolated so one failed target does not stop the folder run.
Senbei walks the directory recursively, skips any subdirectory literally named
`unpack`, and unpacks every file it recognises as protected (by content, not
extension — renamed files and `.bak` backups are still found; packages are the
one exception, recognised by extension plus the zip magic because they are
containers). Results land under `<root>/unpack/` (or `--out DIR`), mirroring
the input tree's relative paths. The run log is written **in that same out
directory**:
## Integrity Check
```cmd
senbei "C:\Games\MyGame"
:: -> C:\Games\MyGame\unpack\...
:: -> C:\Games\MyGame\unpack\senbei-YYYYMMDD-HHMMSS.log
```
PE outputs are checked for valid headers, section ranges, entry-point mapping, readable import names, relocation requirements, and managed metadata signatures. Android outputs are validated during ELF restoration, including decoded container sizes, fixup bounds, and rebuilt dynamic tables.
Folder mode also picks up `global-metadata.dat` files and external-companion
`._` payloads: a module whose `<name>._` sibling matches its header region is
spliced with the companion automatically (no flag needed) and unpacked as one
image, with the output named for the stub.
Each file is processed in isolation: an error or panic on one file is caught,
counted, and logged, and the run continues. Folder mode finishes with a summary
line, then duration:
```
12 unpacked · 3 skipped · 0 errors · 1 suspect · 2 metadata
done in 1234 ms
```
The `packages` count appears (as `· N packages`) only when Android app
packages were processed.
## Integrity check
(PE outputs only — Android restores carry their own structural validation; see
[Android targets](#android-targets).)
A successful unpack is not always a runnable one: a layout heuristic can pick
the wrong offset and leave the entry-point stub or import strings encrypted, so
the pipeline reports success but the OS loader faults at runtime (typically
`0xC0000005`, STATUS_ACCESS_VIOLATION). To catch this, senbei runs a static
sanity check over every output it produces — inspecting the bytes alone, with
no reference image and no execution.
It flags only defects that cannot occur in a correctly unpacked image:
- malformed DOS/PE headers, bad optional-header magic, implausible section
count, zero `SizeOfImage`, or section raw-data ranges that run past EOF;
- an entry point that doesn't map into a section, isn't in an executable
section, or whose stub is all zeros or all `0xCC` int3 padding (the classic
left-encrypted symptom);
- a native (unmanaged) DLL with no base-relocation directory — it cannot
survive being mapped at a non-preferred base;
- **any** import descriptor whose DLL name doesn't resolve or isn't readable
ASCII (imports left encrypted) — the whole table is walked, not just the
first entry;
- for a managed assembly, a COR20 header whose `cb` isn't `0x48` or a
MetaData stream missing its `BSJB` signature (the CLR would reject the
image outright).
The entry-point and import checks are skipped for managed assemblies, whose
native EP and import stub are legitimately not what the native loader expects.
The check is deliberately conservative: a clean report is **not** a proof of
correctness, but a non-clean report is a reliable "this is broken" signal. A
suspect file is still written (the bytes are the best available) and flagged —
single-file mode prints a warning to stderr, folder mode prints a yellow `!`
line, adds a `SUSPECT` entry to the run log, and counts it in the summary's
`suspect` total (which is additive to `unpacked`).
A clean report is not a proof of correctness, but a non-clean report is a reliable broken-output signal. Suspect PE files are still written and counted separately.
## Flags
| Flag | Behavior |
| --- | --- |
| `--out DIR` | Write outputs (and the log, unless `--no-log`) under `DIR`. |
| `-v`, `--verbose` | Print detailed per-stage progress (and the destination path) for each file — `[N/9]` stages for PE targets, container/segment lines for Android libraries. In folder mode this replaces the progress bar. |
| `-q`, `--quiet` | Once: hide progress bar and per-file lines; keep banner, summary, and duration. Twice (`-q -q`): suppress all stdio (exit code only). |
| `--no-log` | Do not write `senbei-*.log`. Console output is unchanged by this flag alone. |
| `--scan-all` | Probe every file in a folder, including ones the scan pre-filter skips (under 4128 bytes, or a bulk-asset extension like `.ab`/`.xml`/`.acb`). Much slower on large game trees; finds the same targets in practice. |
| `--no-pause` | Skip the "Press Enter to exit" prompt (for scripted runs). |
| `-V`, `--version` | Print `Senbei <version>` and exit. |
| `--out DIR` | Write outputs and logs below `DIR`. |
| `-v`, `--verbose` | Print per-stage progress. |
| `-q`, `--quiet` | Hide progress and per-file lines; repeat to suppress all standard output. |
| `--no-log` | Do not write a run log. |
| `--scan-all` | Probe every selected target-name candidate, including files below the size floor. |
| `--no-pause` | Disable the Explorer-friendly Windows exit prompt. |
| `-V`, `--version` | Print the version and exit. |
| `-h`, `--help` | Show usage. |
On Windows, when launched from Explorer (the process owns its console) senbei
pauses for Enter before exiting so the window doesn't vanish. `--no-pause`
disables this; it has no effect when stdout is piped or run from another
process.
## Exit codes
## Exit Codes
| Code | Meaning |
| --- | --- |
| `0` | Success (single file restored, or folder run with no errors). |
| `1` | At least one file failed, a scan probe was unreadable, or a single-file unpack errored. |
| `2` | Usage error: no path given, unknown option, missing `--out` value, or multiple input paths (help printed). |
A folder run also fails with `1` when parts of the tree could not be scanned
(unreadable directory entries or files that failed the content probe) — those
are potential missed targets, not clean skips. An il2cpp metadata blob whose
format version senbei does not handle is *not* an error: it is reported, left
untouched, and counted as skipped.
| `0` | The requested restore completed without errors. |
| `1` | A target failed, a scan probe was unreadable, or a single-file restore errored. |
| `2` | The command line was invalid. |