The packer demotes the four base-relocation entries covering the TLS
directory's VA fields to IMAGE_REL_BASED_ABSOLUTE padding, because its
own loader fixes TLS up by hand. Restored verbatim, a DLL mapped off its
preferred base keeps stale VAs in its TLS directory and the OS loader
faults in LdrpAllocateTlsEntry while writing the TLS slot index through
the unrelocated AddressOfIndex (observed as a 0xc0000005 startup
failure).
Walk the restored BaseReloc blocks and promote those entries back to
IMAGE_REL_BASED_HIGHLOW, gated on is_dll so EXE output stays
byte-identical.
Dropping an .apk/.apks/.xapk package or a .so library reported
"not recognized — will be skipped", which reads as "not protected"
when the real gap is that the Android pipeline (filesystem
orchestration in senbei-io) has no wasm build. These files now get an
explicit android pseudo-kind at staging time: the row explains that
the CLI handles them, and they neither enable the Unpack button nor
error out mid-run.
On phone-width viewports the right-aligned status column shared a flex
line with a long file name and wrapped one word per line. Below 560px
the row now wraps: name and action icons keep the first line and the
status takes a full-width line underneath.
The 1.98.1 pin installs without rustfmt and clippy, so the fmt and
clippy jobs fail at the shim before running. Listing the components in
the toolchain file makes rustup auto-install them on every host,
including the CI runners that invoke cargo with no setup action.
zip 0.6.6 -> 8.6.0 (the 0.6 line is unmaintained), aes 0.8 -> 0.9
(BlockCipherDecrypt trait replaces BlockDecrypt), sha2 0.10 -> 0.11
(Array no longer formats as hex; local hex_digest helpers). Outputs are
byte-identical across the upgrade: full golden corpus and Android corpus
sidecars all pass.
Adds the Android protection-scheme pipeline: hollowed ELF64/AArch64
libraries are restored statically (stage-1/stage-2 module extraction,
container decode, dynamic-linker table rebuild), with app-package
(.apk/.apks/.xapk) container handling, cross-source content dedup, and
il2cpp metadata support for the Android variants (seeded RID permutation;
embedded XOR-wrapped blob extraction).
The single senbei CLI now routes single .so files, packages, and folders
by content; outputs follow the existing .unpack-infix naming under
<root>/unpack or --out. PE behavior is unchanged (35/35 goldens).
The Rust bindings move from web/src to a top-level senbei-wasm crate (still
outside the workspace, own Cargo.lock), matching the other senbei-* crates.
web/ keeps only the static frontend; wasm-pack emits the JS/wasm package into
web/pkg/ via --out-dir. JS glue renamed senbei_web -> senbei_wasm with the
crate.
Adopts the fork's workspace split (senbei-cli / senbei-crypto / senbei-io /
senbei-metadata / senbei-pe), its structured error taxonomy, entry-transform
and layout validation, PE32 dd8 key-formula selection with a skip floor, the
CRT entry-stub dd8 oracle, and the extensionless-file scan skip.
Kept from senbei on top of the restructure:
- ManagedExe detection/routing and the CLR (COR20 + BSJB) metadata restore
in the EXE pipeline.
- The RET+int3 padding fingerprint as the primary dd8 padding signal, ahead
of the mutated-position 0xCC fallback.
- docs/, .github/, samples/, tests/ (moved to senbei-cli/tests), and the
web/ wasm frontend (rewired to the split crates), all of which the fork
had dropped.
- The fork's README compatibility matrix is not taken: it names real games,
which the public-repo hygiene rules forbid.
- The wasm32 localtime fallback in logfile and unpack_bytes_force_exe (the
web app's trap-recovery entry point), both lost in the restructure.
Golden corpus: 35/35 byte-identical. clippy -D warnings clean; wasm32 check
clean for the full workspace.
The detector already used the CLR data-directory RVA to split DLLs into
NativeDll / ManagedDll; EXEs were a single undifferentiated Exe kind.
Apply the same CLR check to EXEs so callers can tell a protected .NET
executable from a native one without unpacking. Routing is unchanged:
both EXE kinds go to the EXE pipeline.
- CLI per-file lines and the run log now print NativeExe / ManagedExe
(the kind comes from the same Debug formatting as the DLL variants).
- The web API's detect()/unpack_file() kind strings become
'native-exe' / 'managed-exe'; the web UI gains matching labels, and
the trap-retry guard (DLL-probe recovery) keys off both EXE kinds.
Golden corpus unchanged (35/35 byte-identical); kind is classification
only and never affects output bytes.
The selector replayed each candidate shift over three sample pages and
counted 0xCC bytes at dd8-mutated positions. That signal is biased
upward for wrong candidates (255 pseudo-random XORs manufacture ~1
spurious 0xCC per page for free) and cannot express 'this candidate
destroys real padding', so the decision leaned on a 2x-margin-plus-floor
rule tuned around the noise.
Score candidates instead by a structural fingerprint of real x64 code:
the MSVC function-end padding pattern (a 0xC3 RET followed by a run of
>= 4 0xCC bytes), summed over up to four sample pages taken with a
head/tail margin. Because dd8 touches only 255 of 4096 bytes per page,
an encrypted page keeps most runs under 'no dd8' and restores them only
under the correct shift, while an already-plaintext page loses runs
under any shift — wrong candidates score *below* the baseline, which the
old count could never say. The gate becomes a positive delta over the
baseline (floor 8) instead of the 2x margin.
When every candidate's fingerprint is silent (sampled pages with no
padded epilogues), fall back to the previous mutated-position count with
its 2x-margin-plus-floor rule, so pages without padding still resolve.
Across the 35-input golden corpus every decision now comes from the
fingerprint with wide, sign-correct margins; all outputs are unchanged
(byte-identical goldens).
Managed EXE builds differ from their native counterparts in the old
layout: the encrypted import-name table pointer is null (their imports
are just the CLR bootstrap stub), the config block's entry-point field
is 0, and the COR20 header / BSJB metadata stream / CLR resources are
stored verbatim in the protected file rather than arriving through the
section-block pass.
- Skip the import-string walk when the table pointer is null instead of
chasing header garbage as a pointer chain (previously a caught
out-of-bounds panic reported as corrupt input).
- Keep the protected header's entry point when the config block stores
0, instead of overwriting it with 0.
- Restore the COR20 header, BSJB metadata stream, and CLR resources
verbatim from the protected file after the .text dd8 pass, and
suppress the native COR20-directory clearing when the restore ran.
Validated by decompiling the unpacked managed EXEs with ilspycmd: full
assemblies (types, methods, IL bodies) decompile cleanly. Golden corpus
unchanged (35/35 byte-identical).