mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
Compare commits
4
Commits
21cd151e15
..
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef13419b93 | ||
|
|
dc6e72a8bb | ||
|
|
fe4f904409 | ||
|
|
7f143b3b27 |
Generated
+1
-1
@@ -209,7 +209,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indicatif",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "senbei"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition = "2024"
|
||||
description = "Static unpacker for Crackproof-protected PE files"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
@@ -41,7 +41,8 @@ lives in [`web/`](web/).
|
||||
|
||||
| Kind | Description |
|
||||
| --- | --- |
|
||||
| `Exe` | Crackproof-protected executable (PE32+ and PE32). |
|
||||
| `NativeExe` | Crackproof-protected native executable (PE32+ and PE32). |
|
||||
| `ManagedExe` | Protected .NET executable (has a CLR data directory). |
|
||||
| `NativeDll` | Protected native (unmanaged) DLL. |
|
||||
| `ManagedDll` | Protected .NET assembly (has a CLR data directory). |
|
||||
| `._` companion | Stub + external encrypted payload layout, spliced automatically. |
|
||||
|
||||
+9
-3
@@ -48,12 +48,18 @@ src/
|
||||
|
||||
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.
|
||||
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`).
|
||||
|
||||
`unpack_auto` then dispatches:
|
||||
|
||||
- `Exe` → the EXE pipeline (handles both PE32+ and PE32).
|
||||
- `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
|
||||
|
||||
+68
-2
@@ -1040,8 +1040,13 @@ impl<'a> Unpacker<'a> {
|
||||
// layout has no such table — imports are rebuilt from the PE Import
|
||||
// Directory after the header is reconstructed (see `process_imports_idt`
|
||||
// below). Skip the walk5 pass entirely for the new layout.
|
||||
//
|
||||
// Managed assemblies leave the walk5 slot null as well: their imports
|
||||
// are just the CLR bootstrap stub, so there is no encrypted name table
|
||||
// to walk. Reading the table at address 0 would chase header garbage as
|
||||
// a pointer chain, so a null slot means "nothing to decrypt".
|
||||
let mut walk5 = get_u32(&u.decompressed, at7);
|
||||
if !new_layout {
|
||||
if !new_layout && walk5 != 0 {
|
||||
loop {
|
||||
let outer = get_u32(&u.decompressed, walk5.wrapping_add(12));
|
||||
if outer == 0 {
|
||||
@@ -1290,7 +1295,13 @@ impl<'a> Unpacker<'a> {
|
||||
let backup: Vec<u8> = u.decompressed[meta_start..meta_start + 144].to_vec();
|
||||
u.decrypt_data5(u.info[3].wrapping_add(ep_off), 144);
|
||||
let ep = get_u32(&u.decompressed, u.info[3].wrapping_add(ep_off));
|
||||
write_u32(&mut u.decompressed, pe_off2.wrapping_add(40), ep);
|
||||
// Managed assemblies store 0 as the entry point here (their EP is a
|
||||
// property of the CLR header, not the PE). Keep the protected
|
||||
// header's EP in that case — overwriting with 0 would produce an
|
||||
// image whose entry point is the DOS header.
|
||||
if ep != 0 {
|
||||
write_u32(&mut u.decompressed, pe_off2.wrapping_add(40), ep);
|
||||
}
|
||||
for n in 0..128 {
|
||||
u.decompressed[(pe_off2 + 136 + n) as usize] =
|
||||
u.decompressed[(u.info[3] + dd_off + n) as usize];
|
||||
@@ -1388,6 +1399,61 @@ impl<'a> Unpacker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Old-layout managed (CLR) restore: same verbatim regions as the
|
||||
// new-layout restore above (COR20 header + BSJB MetaData stream) plus
|
||||
// the COR20 resources blob. Crackproof preserves only these regions
|
||||
// verbatim in the protected file — the IL method bodies between the
|
||||
// COR20 header and the resources ARE packer-encrypted and arrive via
|
||||
// the section-block pass, so copying the whole section's raw data (as
|
||||
// the older-DLL pipeline does for its layout) would clobber them with
|
||||
// the placeholder zeros the protected file carries there. Runs after
|
||||
// the old-layout dd8 pass, so the restored bytes are final.
|
||||
if !new_layout {
|
||||
let clr_rva = get_u32(&u.decompressed, pe_off2.wrapping_add(0xF8));
|
||||
let clr_size = get_u32(&u.decompressed, pe_off2.wrapping_add(0xFC));
|
||||
if clr_rva != 0
|
||||
&& clr_size != 0
|
||||
&& (clr_rva as u64 + clr_size as u64) <= u.decompressed.len() as u64
|
||||
&& let Some(cor_off) = prot_rva_to_off(u.file_data, pe_off, clr_rva)
|
||||
&& (cor_off as u64 + 0x48) <= u.file_data.len() as u64
|
||||
&& get_u32(u.file_data, cor_off) == 0x48
|
||||
{
|
||||
let s = cor_off as usize;
|
||||
let d = clr_rva as usize;
|
||||
u.decompressed[d..d + 0x48].copy_from_slice(&u.file_data[s..s + 0x48]);
|
||||
restored_clr = true;
|
||||
// MetaData RVA/size from the just-restored COR20 header.
|
||||
let md_rva = get_u32(&u.decompressed, clr_rva + 0x08);
|
||||
let md_size = get_u32(&u.decompressed, clr_rva + 0x0C);
|
||||
if md_rva != 0
|
||||
&& md_size != 0
|
||||
&& (md_rva as u64 + md_size as u64) <= u.decompressed.len() as u64
|
||||
&& let Some(md_off) = prot_rva_to_off(u.file_data, pe_off, md_rva)
|
||||
&& (md_off as u64 + md_size as u64) <= u.file_data.len() as u64
|
||||
&& &u.file_data[md_off as usize..md_off as usize + 4] == b"BSJB"
|
||||
{
|
||||
let s = md_off as usize;
|
||||
let d = md_rva as usize;
|
||||
let n = md_size as usize;
|
||||
u.decompressed[d..d + n].copy_from_slice(&u.file_data[s..s + n]);
|
||||
}
|
||||
// COR20 resources (managed .resources blob), verbatim too.
|
||||
let res_rva = get_u32(&u.decompressed, clr_rva + 0x18);
|
||||
let res_size = get_u32(&u.decompressed, clr_rva + 0x1C);
|
||||
if res_rva != 0
|
||||
&& res_size != 0
|
||||
&& (res_rva as u64 + res_size as u64) <= u.decompressed.len() as u64
|
||||
&& let Some(res_off) = prot_rva_to_off(u.file_data, pe_off, res_rva)
|
||||
&& (res_off as u64 + res_size as u64) <= u.file_data.len() as u64
|
||||
{
|
||||
let s = res_off as usize;
|
||||
let d = res_rva as usize;
|
||||
let n = res_size as usize;
|
||||
u.decompressed[d..d + n].copy_from_slice(&u.file_data[s..s + n]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The payload's TLS directory (DD[9]) arrives blanked: Crackproof strips
|
||||
// the struct and re-installs TLS itself when it maps the module. Prefer
|
||||
// recovering the real one from the stub's plaintext `.rdata` — dropping
|
||||
|
||||
+12
-16
@@ -55,7 +55,8 @@ pub(crate) fn is_supported_magic(magic: u32) -> bool {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
Exe,
|
||||
NativeExe,
|
||||
ManagedExe,
|
||||
NativeDll,
|
||||
ManagedDll,
|
||||
}
|
||||
@@ -109,7 +110,7 @@ fn key_table(input: &[u8]) -> Option<[u32; 8]> {
|
||||
///
|
||||
/// Routing: `keys[1]` must be the Crackproof magic (`KONN`).
|
||||
/// The PE IMAGE_FILE_DLL characteristic distinguishes EXE vs DLL;
|
||||
/// the CLR data-directory RVA further distinguishes ManagedDll from NativeDll.
|
||||
/// the CLR data-directory RVA distinguishes managed from native for both.
|
||||
pub fn detect(input: &[u8]) -> Option<Detected> {
|
||||
let keys = key_table(input)?;
|
||||
let magic = keys[1];
|
||||
@@ -132,21 +133,15 @@ pub fn detect(input: &[u8]) -> Option<Detected> {
|
||||
let chars =
|
||||
(input[chars_offset as usize] as u16) | ((input[chars_offset as usize + 1] as u16) << 8);
|
||||
let is_dll = (chars & 0x2000) != 0;
|
||||
if !is_dll {
|
||||
return Some(Detected {
|
||||
kind: Kind::Exe,
|
||||
magic,
|
||||
});
|
||||
}
|
||||
// DLL: determine managed vs native via CLR data-directory RVA.
|
||||
// Managed vs native via the CLR data-directory RVA.
|
||||
// peOff + 24 = start of optional header. The data directories start at a
|
||||
// magic-dependent offset within it: PE32 (0x10B) at +96, PE32+ (0x20B) at
|
||||
// +112. Using the PE32+ offset on a PE32 image reads the wrong dword and
|
||||
// can mis-flag a native DLL as managed.
|
||||
// can mis-flag a native image as managed.
|
||||
//
|
||||
// `get_u16`/`get_u32` index unchecked, so every read past the already-
|
||||
// checked Characteristics word must be bounds-checked first: a truncated
|
||||
// DLL (e.g. `e_lfanew` pointing at len-24) would otherwise panic here,
|
||||
// file (e.g. `e_lfanew` pointing at len-24) would otherwise panic here,
|
||||
// and this detector runs on the folder scan threads where a panic aborts
|
||||
// the whole run.
|
||||
let opt_magic_off = pe_off.wrapping_add(24) as usize;
|
||||
@@ -165,10 +160,11 @@ pub fn detect(input: &[u8]) -> Option<Detected> {
|
||||
return None;
|
||||
}
|
||||
let clr_rva = primitives::get_u32(input, clr_rva_offset);
|
||||
let kind = if clr_rva != 0 {
|
||||
Kind::ManagedDll
|
||||
} else {
|
||||
Kind::NativeDll
|
||||
let kind = match (is_dll, clr_rva != 0) {
|
||||
(false, false) => Kind::NativeExe,
|
||||
(false, true) => Kind::ManagedExe,
|
||||
(true, false) => Kind::NativeDll,
|
||||
(true, true) => Kind::ManagedDll,
|
||||
};
|
||||
Some(Detected { kind, magic })
|
||||
}
|
||||
@@ -184,7 +180,7 @@ pub fn unpack_auto(input: &[u8]) -> Result<(Kind, Vec<u8>), UnpackError> {
|
||||
pub fn unpack_auto_v(input: &[u8], verbose: bool) -> Result<(Kind, Vec<u8>), UnpackError> {
|
||||
let detected = detect(input).ok_or(UnpackError::NotCrackproof)?;
|
||||
let out = match detected.kind {
|
||||
Kind::Exe => unpack_exe_v(input, verbose)?,
|
||||
Kind::NativeExe | Kind::ManagedExe => unpack_exe_v(input, verbose)?,
|
||||
Kind::NativeDll | Kind::ManagedDll => {
|
||||
// Two Crackproof DLL layouts exist. The older one (the byte-identical
|
||||
// DLL goldens) follows the pipeline in `dll.rs`. Newer builds protect
|
||||
|
||||
+171
-42
@@ -1874,70 +1874,199 @@ pub(crate) fn decrypt_and_decompress_data(
|
||||
// (0x40327253) yet require different shifts, so the only reliable discriminator
|
||||
// is the .text content itself.
|
||||
//
|
||||
// Detection scoring formula: for each candidate shift, replay decrypt_data8
|
||||
// across a few sample pages (25/50/75% of .text) and count how many of the 255
|
||||
// mutated positions become 0xCC — the MSVC int3 padding byte. The correct shift
|
||||
// hits int3 pads disproportionately often (~3-10x the baseline), so the
|
||||
// highest-scoring shift wins. If neither shift clears 2x the baseline, .text
|
||||
// is already plaintext → skip (return 99).
|
||||
// Detection replays the three candidate states — no dd8 (already plaintext),
|
||||
// shift 0, shift 15 — over a few sample pages (head/tail margin skipped:
|
||||
// entry/exit regions have atypical padding density) and picks the state whose
|
||||
// decoded pages look most like real x64 code. The primary signal is a
|
||||
// *structural* fingerprint: the MSVC function-end padding pattern, a 0xC3 RET
|
||||
// opcode followed by a run of >= 4 0xCC int3 bytes. dd8 XORs one pseudo-random
|
||||
// byte per 16-byte block, so an already-plaintext page keeps its padding runs
|
||||
// only under "no dd8", while a packer-encrypted page restores them only under
|
||||
// the correct shift — a wrong candidate destroys every run it touches and
|
||||
// essentially never manufactures a RET followed by a long int3 run by chance.
|
||||
// This separates the states far more cleanly than a bare 0xCC count, which a
|
||||
// wrong candidate inflates for free (~255 coincidences per page at p=1/256).
|
||||
//
|
||||
// When no candidate produces any RET-anchored padding (sampled pages with
|
||||
// dense code and no padded epilogues), the fingerprint is silent, so the
|
||||
// decision falls back to the older mutated-position 0xCC count. Both signals
|
||||
// use the same decision rule: a candidate must beat the no-dd8 baseline by a
|
||||
// clear 2x margin AND an absolute floor, otherwise dd8 is skipped — a wrongly
|
||||
// applied dd8 scrambles ~1 byte per 16 with no error surfaced downstream.
|
||||
//
|
||||
// This replaces an earlier entry-stub oracle that matched the 14 fixed CRT-stub
|
||||
// bytes at the AEP. That oracle false-positived on a newer EXE-64 build: dd8
|
||||
// corrupted only the call rel32 (bytes 5-8, the wildcard region), so the stub
|
||||
// matched under BOTH shifts and the selector defaulted to 0 when the truth was
|
||||
// 15. The 0xCC statistic samples hundreds of positions per page and is not
|
||||
// fooled by a stub whose fixed bytes happen to survive.
|
||||
// 15. A whole-page padding statistic samples hundreds of positions per page
|
||||
// and is not fooled by a stub whose fixed bytes happen to survive.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Minimum 0xCC run length after a RET for the run to count as MSVC
|
||||
/// function-end padding.
|
||||
const MIN_CC_RUN: u32 = 4;
|
||||
|
||||
/// Total length of MSVC function-end padding runs in a page: each 0xC3 byte
|
||||
/// followed by >= [`MIN_CC_RUN`] 0xCC bytes contributes the run length.
|
||||
fn ret_int3_score(page: &[u8]) -> u32 {
|
||||
let mut total = 0u32;
|
||||
let mut i = 0;
|
||||
while i < page.len() {
|
||||
if page[i] == 0xC3 {
|
||||
let mut j = i + 1;
|
||||
while j < page.len() && page[j] == 0xCC {
|
||||
j += 1;
|
||||
}
|
||||
let run = (j - i - 1) as u32;
|
||||
if run >= MIN_CC_RUN {
|
||||
total += run;
|
||||
}
|
||||
i = j;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// Replay the dd8 page-XOR in place on one sample page.
|
||||
fn dd8_apply(buf: &mut [u8; 0x1000], abs_page: u32, shift: u32) {
|
||||
let mut key = abs_page << shift;
|
||||
for bi in 0..256u32 {
|
||||
let mixed = key.rotate_right(15).wrapping_add(bi);
|
||||
key = mixed.wrapping_add(bi);
|
||||
// The packer's dd8 loop does not XOR block i=0 (see decrypt_data8).
|
||||
if bi == 0 {
|
||||
continue;
|
||||
}
|
||||
let tidx = (bi.wrapping_mul(16).wrapping_add(mixed & 0xF)) as usize;
|
||||
buf[tidx] ^= key as u8;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sum the RET+int3 fingerprint over the sample pages for one candidate
|
||||
/// (`None` = the no-dd8 baseline, page as-is).
|
||||
fn fingerprint_score(
|
||||
data: &[u8],
|
||||
text_off: usize,
|
||||
abs_base: u32,
|
||||
sample_pages: &[u32],
|
||||
shift: Option<u32>,
|
||||
) -> u32 {
|
||||
let mut total = 0u32;
|
||||
for &sp in sample_pages {
|
||||
let pg_off = text_off + (sp as usize) * 0x1000;
|
||||
if pg_off + 0x1000 > data.len() {
|
||||
continue;
|
||||
}
|
||||
let mut page = [0u8; 0x1000];
|
||||
page.copy_from_slice(&data[pg_off..pg_off + 0x1000]);
|
||||
if let Some(sh) = shift {
|
||||
dd8_apply(&mut page, abs_base.wrapping_add(sp), sh);
|
||||
}
|
||||
total += ret_int3_score(&page);
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
pub(crate) fn select_dd8_shift(data: &[u8], text_va: u32, text_size: u32, _info3: u32) -> u32 {
|
||||
if text_size < 0x1000 {
|
||||
let num_pages_total = text_size >> 12;
|
||||
// Fewer than two pages: nothing meaningful to sample; preserve the
|
||||
// historical behavior (shift 0 — the dd8 loop is empty or single-page).
|
||||
if num_pages_total < 2 {
|
||||
return 0;
|
||||
}
|
||||
let text_off = text_va as usize;
|
||||
let num_pages_total = text_size >> 12;
|
||||
|
||||
// Sample pages at 25/50/75% of .text, falling back to the midpoint for tiny
|
||||
// sections.
|
||||
// Sample up to 4 pages, skipping a head/tail margin. Small .text: sample
|
||||
// every page.
|
||||
let mut sample_pages: Vec<u32> = Vec::new();
|
||||
for frac in [0.25f64, 0.5, 0.75] {
|
||||
let pg = (num_pages_total as f64 * frac) as u32;
|
||||
if pg > 0 && pg < num_pages_total {
|
||||
sample_pages.push(pg);
|
||||
if num_pages_total <= 4 {
|
||||
sample_pages.extend(0..num_pages_total);
|
||||
} else {
|
||||
let margin = (num_pages_total / 8).max(1);
|
||||
let lo = margin;
|
||||
let hi = num_pages_total - margin;
|
||||
if hi <= lo {
|
||||
sample_pages.extend(0..num_pages_total);
|
||||
} else {
|
||||
let step = ((hi - lo) / 4).max(1);
|
||||
let mut i = 0;
|
||||
while i < 4 {
|
||||
let p = lo + i * step;
|
||||
if p < num_pages_total {
|
||||
sample_pages.push(p);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if sample_pages.is_empty() && num_pages_total > 1 {
|
||||
sample_pages.push(num_pages_total / 2);
|
||||
}
|
||||
if sample_pages.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let none_hits = score_dd8_baseline(data, text_off, &sample_pages);
|
||||
let s0 = score_dd8_shift(data, text_off, text_va, &sample_pages, 0);
|
||||
let s15 = score_dd8_shift(data, text_off, text_va, &sample_pages, 15);
|
||||
let mut best_score = none_hits;
|
||||
let mut best_shift = 99u32; // 99 == skip dd8
|
||||
for (shift, hits) in [(0u32, s0), (15u32, s15)] {
|
||||
if hits > best_score {
|
||||
best_score = hits;
|
||||
best_shift = shift;
|
||||
}
|
||||
}
|
||||
let abs_base = text_va >> 12;
|
||||
// Require a clear 2x margin over the already-plaintext baseline AND an
|
||||
// absolute floor. The 2x test alone
|
||||
// trips on noise when the counts are tiny: an external-companion DLL whose
|
||||
// .text is already plaintext scores s15=4 vs none=1 — a spurious 4x — and
|
||||
// gets dd8 wrongly applied, corrupting ~1 byte per 16. Across the whole
|
||||
// golden corpus every build that genuinely needs dd8 scores >= 10 (lowest
|
||||
// observed scores at 10-12; up to 107), so a floor of 8 rejects the noise
|
||||
// while keeping every golden's shift selection unchanged.
|
||||
// absolute floor. The 2x test alone trips on noise when the counts are
|
||||
// tiny: an external-companion DLL whose .text is already plaintext scores
|
||||
// s15=4 vs none=1 — a spurious 4x — and gets dd8 wrongly applied,
|
||||
// corrupting ~1 byte per 16. The floor rejects that noise while sitting
|
||||
// far below every genuinely-encrypted build's score.
|
||||
const MIN_DD8_HITS: u32 = 8;
|
||||
if best_shift != 99 && (best_score < none_hits * 2 || best_score < MIN_DD8_HITS) {
|
||||
best_shift = 99;
|
||||
}
|
||||
let margin_pick = |none: u32, s0: u32, s15: u32| -> u32 {
|
||||
let mut best_score = none;
|
||||
let mut best_shift = 99u32; // 99 == skip dd8
|
||||
for (shift, hits) in [(0u32, s0), (15u32, s15)] {
|
||||
if hits > best_score {
|
||||
best_score = hits;
|
||||
best_shift = shift;
|
||||
}
|
||||
}
|
||||
if best_shift != 99 && (best_score < none * 2 || best_score < MIN_DD8_HITS) {
|
||||
best_shift = 99;
|
||||
}
|
||||
best_shift
|
||||
};
|
||||
|
||||
// Primary: RET+int3 padding fingerprint. The fingerprint is diluted across
|
||||
// the whole page (dd8 touches only 255 of 4096 bytes, so even an encrypted
|
||||
// page keeps most of its padding runs), so instead of the fallback's 2x
|
||||
// margin the gate is a *positive delta* over the no-dd8 baseline: on an
|
||||
// already-plaintext .text each wrong shift destroys runs (scores below the
|
||||
// baseline), while the correct shift on an encrypted page restores them
|
||||
// (scores above it). The floor on the delta rejects noise-level gains.
|
||||
let r_none = fingerprint_score(data, text_off, abs_base, &sample_pages, None);
|
||||
let r0 = fingerprint_score(data, text_off, abs_base, &sample_pages, Some(0));
|
||||
let r15 = fingerprint_score(data, text_off, abs_base, &sample_pages, Some(15));
|
||||
// Fallback: mutated-position 0xCC count, for pages whose code has no
|
||||
// RET-anchored padding at all (the fingerprint is silent there).
|
||||
let (none_hits, s0, s15);
|
||||
let best_shift = if r_none != 0 || r0 != 0 || r15 != 0 {
|
||||
none_hits = 0;
|
||||
s0 = 0;
|
||||
s15 = 0;
|
||||
let mut best_score = r_none;
|
||||
let mut shift = 99u32;
|
||||
for (s, score) in [(0u32, r0), (15u32, r15)] {
|
||||
if score > best_score {
|
||||
best_score = score;
|
||||
shift = s;
|
||||
}
|
||||
}
|
||||
if shift != 99 && best_score.saturating_sub(r_none) < MIN_DD8_HITS {
|
||||
shift = 99;
|
||||
}
|
||||
shift
|
||||
} else {
|
||||
none_hits = score_dd8_baseline(data, text_off, &sample_pages);
|
||||
s0 = score_dd8_shift(data, text_off, text_va, &sample_pages, 0);
|
||||
s15 = score_dd8_shift(data, text_off, text_va, &sample_pages, 15);
|
||||
margin_pick(none_hits, s0, s15)
|
||||
};
|
||||
if std::env::var("SEL_DIAG").is_ok() {
|
||||
eprintln!(
|
||||
"SEL dd8 best_shift={} s0={} s15={} none_hits={} samples={:?}",
|
||||
best_shift, s0, s15, none_hits, sample_pages
|
||||
"SEL dd8 best_shift={} fp=({},{},{}) cc=({},{},{}) samples={:?}",
|
||||
best_shift, r_none, r0, r15, none_hits, s0, s15, sample_pages
|
||||
);
|
||||
}
|
||||
best_shift
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "senbei-web"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition = "2024"
|
||||
description = "WebAssembly browser frontend for senbei"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
+4
-2
@@ -45,7 +45,8 @@ const files = new Map();
|
||||
const rowEls = new Map();
|
||||
|
||||
const KIND_LABEL = {
|
||||
exe: 'protected EXE',
|
||||
'native-exe': 'protected native EXE',
|
||||
'managed-exe': 'protected managed EXE',
|
||||
'native-dll': 'protected native DLL',
|
||||
'managed-dll': 'protected managed DLL',
|
||||
metadata: 'il2cpp metadata',
|
||||
@@ -323,7 +324,8 @@ async function unpackModule(name, entry) {
|
||||
let comp = compEntry ? await read(compEntry.file) : undefined;
|
||||
let r = await runUnpack(input, comp, false);
|
||||
|
||||
if (!r.ok && r.trap && entry.kind !== 'exe') {
|
||||
const isExe = entry.kind === 'native-exe' || entry.kind === 'managed-exe';
|
||||
if (!r.ok && r.trap && !isExe) {
|
||||
// The DLL-routing probe trapped (panics can't be caught in wasm). Retry
|
||||
// once with the forced-EXE pipeline in a fresh worker — this mirrors the
|
||||
// CLI's dll-first/exe-fallback outcome for EXE-shell-layout DLLs.
|
||||
|
||||
+7
-4
@@ -24,7 +24,8 @@ pub struct UnpackResult {
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl UnpackResult {
|
||||
/// Detected module kind: `"exe"`, `"native-dll"`, or `"managed-dll"`.
|
||||
/// Detected module kind: `"native-exe"`, `"managed-exe"`, `"native-dll"`,
|
||||
/// or `"managed-dll"`.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn kind(&self) -> String {
|
||||
self.kind.clone()
|
||||
@@ -102,7 +103,8 @@ impl MetadataResult {
|
||||
|
||||
fn kind_str(kind: senbei::unpacker::Kind) -> &'static str {
|
||||
match kind {
|
||||
senbei::unpacker::Kind::Exe => "exe",
|
||||
senbei::unpacker::Kind::NativeExe => "native-exe",
|
||||
senbei::unpacker::Kind::ManagedExe => "managed-exe",
|
||||
senbei::unpacker::Kind::NativeDll => "native-dll",
|
||||
senbei::unpacker::Kind::ManagedDll => "managed-dll",
|
||||
}
|
||||
@@ -110,8 +112,9 @@ fn kind_str(kind: senbei::unpacker::Kind) -> &'static str {
|
||||
|
||||
/// Classify a file's bytes without unpacking.
|
||||
///
|
||||
/// Returns `"exe"`, `"native-dll"`, `"managed-dll"`, `"metadata"` (an il2cpp
|
||||
/// `global-metadata.dat`), or `undefined` for anything unrecognized.
|
||||
/// Returns `"native-exe"`, `"managed-exe"`, `"native-dll"`, `"managed-dll"`,
|
||||
/// `"metadata"` (an il2cpp `global-metadata.dat`), or `undefined` for
|
||||
/// anything unrecognized.
|
||||
#[wasm_bindgen]
|
||||
pub fn detect(input: &[u8]) -> Option<String> {
|
||||
if senbei::metadata::is_metadata(input) {
|
||||
|
||||
Reference in New Issue
Block a user