mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
refactor: init
This commit is contained in:
-154
@@ -1,154 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## Crate layout
|
||||
|
||||
The crate is split into a pure core and a thin CLI shell:
|
||||
|
||||
- **`src/unpacker/`** — 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::InternalPanic`). This
|
||||
is what the WebAssembly build embeds.
|
||||
- **`src/` (top level)** — the CLI shell: argument parsing, recursive folder
|
||||
scanning, per-run log file, progress bar, Explorer-friendly exit pause, and
|
||||
the single-file/folder orchestration in `job.rs`.
|
||||
- **`src/metadata.rs`** — il2cpp `global-metadata.dat` method-token
|
||||
de-obfuscation (format version 31; other versions are left untouched).
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs argument parsing + dispatch
|
||||
├── lib.rs module roots
|
||||
├── job.rs single-file + folder orchestration, out-naming,
|
||||
│ companion splice, stub overlay/TLS restore,
|
||||
│ pipeline routing (incl. the wasm-safe byte API)
|
||||
├── scan.rs recursive Crackproof + metadata discovery
|
||||
├── metadata.rs il2cpp global-metadata.dat de-obfuscation
|
||||
├── logfile.rs per-run timestamped log
|
||||
├── ui.rs progress bar + status lines
|
||||
├── pause.rs Explorer-friendly exit pause
|
||||
└── unpacker/ pure, panic-free, no-I/O core
|
||||
├── mod.rs detection + unpack_auto dispatch
|
||||
├── exe.rs EXE pipeline (PE32+ and PE32)
|
||||
├── dll.rs native + managed DLL pipeline
|
||||
├── integrity.rs static post-unpack sanity check
|
||||
├── primitives.rs decrypt_data* steps, key/shift selection
|
||||
├── bytecode.rs bytecode VM
|
||||
├── parallel.rs deterministic block-parallel fan-out
|
||||
├── tables.rs constant tables
|
||||
└── crc32.rs checksum
|
||||
```
|
||||
|
||||
## Detection and routing
|
||||
|
||||
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, native DLL, or
|
||||
managed DLL.
|
||||
|
||||
`unpack_auto` then dispatches:
|
||||
|
||||
- `Exe` → the EXE pipeline (handles both PE32+ and PE32).
|
||||
- `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).
|
||||
|
||||
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.
|
||||
|
||||
## External-companion inputs
|
||||
|
||||
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.
|
||||
|
||||
## Pipelines
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
The PE32+ configuration block has two observed anchor-relative alignments.
|
||||
Senbei selects between them by validating the stage1 `(RVA, length)`
|
||||
descriptor against the image, rather than relying on a version-like word whose
|
||||
value is not stable across build families. Stage3 seed advancement also varies:
|
||||
the usual four-round result is tried first, then bounded alternatives are
|
||||
replayed from the untouched ciphertext and accepted only when decompression
|
||||
writes the exact target size and the recovered stage has its expected function
|
||||
tail structure.
|
||||
|
||||
## 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::InternalPanic`, including the Rust source location and panic
|
||||
payload. The capture context is propagated into section worker threads; panics
|
||||
outside an active unpack continue through the previously installed panic hook.
|
||||
Expected validation failures use structured variants carrying the failed stage,
|
||||
block index, table kind, or invalid range instead of collapsing unrelated causes
|
||||
into a generic corruption error. Huffman/LZ failures distinguish invalid code
|
||||
lengths, tree traversal, pending-length overflow, invalid back-references,
|
||||
output overflow, and size mismatch. When both DLL parsing and the EXE-layout
|
||||
fallback fail, the returned error retains both pipeline errors.
|
||||
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.
|
||||
@@ -1,116 +0,0 @@
|
||||
# Development
|
||||
|
||||
## 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`.
|
||||
|
||||
```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)).
|
||||
|
||||
## Testing
|
||||
|
||||
```cmd
|
||||
cargo test --release
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
> **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.
|
||||
|
||||
## Debugging levers (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).
|
||||
|
||||
## Conventions
|
||||
|
||||
- The `src/unpacker/` core 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).
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
senbei/
|
||||
├── Cargo.toml senbei lib + bin package
|
||||
├── rust-toolchain.toml pinned toolchain + targets
|
||||
├── src/ CLI shell + pure unpacker core (see docs/design.md)
|
||||
├── tests/ CLI, detection, golden, and folder tests
|
||||
├── samples/ local-only test corpus (git-ignored)
|
||||
├── web/ WebAssembly browser build
|
||||
├── docs/ usage, design, and development documentation
|
||||
└── .github/ CI workflows and issue templates
|
||||
```
|
||||
|
||||
## Web build
|
||||
|
||||
See [web/README.md](../web/README.md). In short:
|
||||
|
||||
```cmd
|
||||
cd web
|
||||
wasm-pack build --target web --release
|
||||
```
|
||||
|
||||
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.
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
# Usage
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
||||
## Folder mode
|
||||
|
||||
Senbei walks the directory recursively, skips any subdirectory literally named
|
||||
`unpack`, and unpacks every file it recognises as Crackproof-protected (by
|
||||
content, not extension — renamed files and `.bak` backups are still found).
|
||||
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**:
|
||||
|
||||
```cmd
|
||||
senbei "C:\Games\MyGame"
|
||||
:: -> C:\Games\MyGame\unpack\...
|
||||
:: -> C:\Games\MyGame\unpack\senbei-YYYYMMDD-HHMMSS.log
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Integrity check
|
||||
|
||||
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`).
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Behavior |
|
||||
| --- | --- |
|
||||
| `--out DIR` | Write outputs (and the log, unless `--no-log`) under `DIR`. |
|
||||
| `-v`, `--verbose` | Print detailed `[N/9]` per-stage unpack progress (and the destination path) for each file. 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. |
|
||||
| `-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
|
||||
|
||||
| Code | Meaning |
|
||||
| --- | --- |
|
||||
| `0` | Success (single file unpacked, 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.
|
||||
Reference in New Issue
Block a user