mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
Merge Android (AArch64) shared-library restoration, bump to 1.2.0
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).
This commit is contained in:
+339
-39
@@ -323,6 +323,7 @@ fn splice_companion(stub: &[u8], comp: &[u8]) -> Option<Vec<u8>> {
|
||||
}
|
||||
|
||||
/// Summary of a folder-mode run.
|
||||
#[derive(Default)]
|
||||
pub struct Summary {
|
||||
pub unpacked: usize,
|
||||
pub skipped: usize,
|
||||
@@ -331,12 +332,29 @@ pub struct Summary {
|
||||
/// — likely to crash at runtime (e.g. 0xC0000005). Counted in addition to
|
||||
/// `unpacked` (a suspect file is still written).
|
||||
pub suspect: usize,
|
||||
/// il2cpp `global-metadata.dat` files de-obfuscated (method tokens remapped).
|
||||
/// il2cpp `global-metadata.dat` files de-obfuscated (method tokens remapped),
|
||||
/// including blobs unwrapped from restored Android libraries.
|
||||
pub metadata: usize,
|
||||
/// Android app packages (`.apk`/`.apks`/`.xapk`) opened and searched.
|
||||
pub packages: usize,
|
||||
/// Wall-clock duration of the folder run in milliseconds.
|
||||
pub duration_ms: u128,
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
/// The summary line shared by CLI output and the log file.
|
||||
pub fn line(&self) -> String {
|
||||
let mut line = format!(
|
||||
"{} unpacked · {} skipped · {} errors · {} suspect · {} metadata",
|
||||
self.unpacked, self.skipped, self.errors, self.suspect, self.metadata
|
||||
);
|
||||
if self.packages > 0 {
|
||||
line.push_str(&format!(" · {} packages", self.packages));
|
||||
}
|
||||
line
|
||||
}
|
||||
}
|
||||
|
||||
/// Default output root for a folder unpack: `<root>/unpack`.
|
||||
pub fn default_out_root_for_folder(root: &Path) -> PathBuf {
|
||||
root.join("unpack")
|
||||
@@ -416,12 +434,15 @@ pub fn run_folder_opts(
|
||||
log.step(&format!("out {}", out_root.display()));
|
||||
Some(log)
|
||||
};
|
||||
// Single merged directory walk: returns Crackproof unpack candidates and
|
||||
// il2cpp metadata blobs from one traversal (see
|
||||
// Single merged directory walk: returns Crackproof unpack candidates, il2cpp
|
||||
// metadata blobs, and Android targets from one traversal (see
|
||||
// [`crate::scan::find_targets_opts`]). Files the free directory metadata
|
||||
// already rules out are never opened — on asset-heavy trees the per-file
|
||||
// open+read latency, not the traversal, is the whole cost.
|
||||
let (candidates, metas, scan_stats) = crate::scan::find_targets_opts(root, scan_all);
|
||||
let scan = crate::scan::find_targets_opts(root, scan_all);
|
||||
let candidates = scan.crackproof.as_slice();
|
||||
let metas = scan.metadata.as_slice();
|
||||
let scan_stats = &scan.stats;
|
||||
// Files the scan could not classify are potential missed targets, not
|
||||
// clean skips: an unreadable directory or a locked il2cpp game assembly must
|
||||
// fail the run (exit 1) rather than report "0 errors" over a partial scan.
|
||||
@@ -453,21 +474,22 @@ pub fn run_folder_opts(
|
||||
// Verbose mode prints multi-line `[N/9]` step output per file straight to
|
||||
// stdout; an active progress bar would be clobbered by it, so hide the bar
|
||||
// (its per-file ok/err lines still print) when verbose is on.
|
||||
let bar = crate::ui::progress(candidates.len() as u64, quiet >= 1 || verbose);
|
||||
let android_targets = scan.android_so.len() + scan.android_packages.len();
|
||||
let bar = crate::ui::progress(
|
||||
(candidates.len() + android_targets) as u64,
|
||||
quiet >= 1 || verbose,
|
||||
);
|
||||
let mut s = Summary {
|
||||
unpacked: 0,
|
||||
skipped: scan_stats.skipped,
|
||||
errors: scan_failed,
|
||||
suspect: 0,
|
||||
metadata: 0,
|
||||
duration_ms: 0,
|
||||
..Summary::default()
|
||||
};
|
||||
|
||||
// Silence the default panic hook's stderr spew during per-file processing.
|
||||
let default_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(|_| {})); // suppress "thread panicked" messages
|
||||
|
||||
for input in &candidates {
|
||||
for input in candidates {
|
||||
let rel = rel_in_tree(root, input);
|
||||
let dest = out_root.join(out_name(&rel));
|
||||
|
||||
@@ -515,14 +537,146 @@ pub fn run_folder_opts(
|
||||
bar.inc(1);
|
||||
}
|
||||
|
||||
// Android pass: protected AArch64 libraries and app packages. Loose `.so`
|
||||
// files restore first so the cross-source dedup keeps them over a copy
|
||||
// inside a package (loose beats `.apk` beats `.apks`/`.xapk` bundle).
|
||||
let mut android_seen = std::collections::HashSet::new();
|
||||
// Hashing a protected library costs a full read, so only pay it when a
|
||||
// duplicate source can actually exist in this run.
|
||||
let android_dedup = scan.android_so.len() > 1 || !scan.android_packages.is_empty();
|
||||
for input in &scan.android_so {
|
||||
let rel = rel_in_tree(root, input);
|
||||
let dest = out_root.join(out_name(&rel));
|
||||
// Unreadable here is fine: the restore reports the same error.
|
||||
if android_dedup
|
||||
&& let Ok(bytes) = std::fs::read(input)
|
||||
&& !android_seen.insert(crate::android::content_identity(&bytes))
|
||||
{
|
||||
s.skipped += 1;
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("SKIP {rel:?}: duplicate of an earlier target"));
|
||||
}
|
||||
bar.inc(1);
|
||||
continue;
|
||||
}
|
||||
let input_owned = input.clone();
|
||||
let dest_owned = dest.clone();
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
crate::android::restore_so_file(&input_owned, &dest_owned, verbose_steps)
|
||||
}));
|
||||
match result {
|
||||
Ok(Ok(embedded)) => {
|
||||
s.unpacked += 1;
|
||||
crate::ui::ok_label(
|
||||
&bar,
|
||||
suppress_file_lines,
|
||||
&rel.display().to_string(),
|
||||
"So",
|
||||
&dest,
|
||||
);
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("OK {rel:?} -> {dest:?} (Android SO)"));
|
||||
}
|
||||
match write_embedded_metadata(embedded, &dest) {
|
||||
Ok(Some(meta_dest)) => {
|
||||
s.metadata += 1;
|
||||
crate::ui::ok_label(
|
||||
&bar,
|
||||
suppress_file_lines,
|
||||
&format!("{} (embedded metadata)", rel.display()),
|
||||
"metadata",
|
||||
&meta_dest,
|
||||
);
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("META {rel:?} (embedded) -> {meta_dest:?}"));
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
s.errors += 1;
|
||||
crate::ui::err(&bar, suppress_file_lines, &rel, &e);
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("ERR {rel:?}: embedded metadata: {e:#}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
s.errors += 1;
|
||||
crate::ui::err(&bar, suppress_file_lines, &rel, &e);
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("ERR {rel:?}: {e:#}"));
|
||||
}
|
||||
}
|
||||
Err(panic) => {
|
||||
s.errors += 1;
|
||||
let e = anyhow::anyhow!("unexpected panic: {}", panic_payload(&panic));
|
||||
crate::ui::err(&bar, suppress_file_lines, &rel, &e);
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!(
|
||||
"ERR {rel:?}: panic during restore: {}",
|
||||
panic_payload(&panic)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
bar.inc(1);
|
||||
}
|
||||
for package in &scan.android_packages {
|
||||
let rel = rel_in_tree(root, package);
|
||||
s.packages += 1;
|
||||
let package_owned = package.clone();
|
||||
let rel_owned = rel.clone().into_owned();
|
||||
let out_root_owned = out_root.clone();
|
||||
let mut seen_taken = std::mem::take(&mut android_seen);
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let outcomes = crate::android::restore_package(
|
||||
&package_owned,
|
||||
&rel_owned,
|
||||
&out_root_owned,
|
||||
&mut seen_taken,
|
||||
verbose_steps,
|
||||
);
|
||||
(outcomes, seen_taken)
|
||||
}));
|
||||
match result {
|
||||
Ok((Ok(outcomes), seen_back)) => {
|
||||
android_seen = seen_back;
|
||||
apply_package_outcomes(outcomes, &mut s, &bar, suppress_file_lines, &log);
|
||||
}
|
||||
Ok((Err(e), seen_back)) => {
|
||||
android_seen = seen_back;
|
||||
s.errors += 1;
|
||||
crate::ui::err(&bar, suppress_file_lines, &rel, &e);
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("ERR {rel:?}: {e:#}"));
|
||||
}
|
||||
}
|
||||
Err(panic) => {
|
||||
// The dedup set may be in an unknown state after a panic; a
|
||||
// re-scan costs a duplicate restore at worst, never corruption.
|
||||
let e = anyhow::anyhow!("unexpected panic: {}", panic_payload(&panic));
|
||||
s.errors += 1;
|
||||
crate::ui::err(&bar, suppress_file_lines, &rel, &e);
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!(
|
||||
"ERR {rel:?}: panic during package restore: {}",
|
||||
panic_payload(&panic)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
bar.inc(1);
|
||||
}
|
||||
|
||||
// il2cpp metadata pass. Crackproof's `-GMD` option obfuscates the method
|
||||
// tokens in `global-metadata.dat`; de-obfuscate any we find so the unpacked
|
||||
// il2cpp game assembly resolves methods instead of indexing its per-module
|
||||
// tables out of bounds (see [`senbei_metadata`]). This is additive to the
|
||||
// Crackproof module unpack above — the metadata blob is not itself a
|
||||
// Crackproof file.
|
||||
for meta in metas {
|
||||
let rel = rel_in_tree(root, &meta);
|
||||
for meta in metas.iter() {
|
||||
let rel = rel_in_tree(root, meta);
|
||||
let dest = out_root.join(out_name(&rel));
|
||||
let meta_owned = meta.clone();
|
||||
let dest_owned = dest.clone();
|
||||
@@ -604,10 +758,7 @@ pub fn run_folder_opts(
|
||||
s.duration_ms = t0.elapsed().as_millis();
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("done in {} ms", s.duration_ms));
|
||||
log.step(&format!(
|
||||
"summary: {} unpacked · {} skipped · {} errors · {} suspect · {} metadata",
|
||||
s.unpacked, s.skipped, s.errors, s.suspect, s.metadata
|
||||
));
|
||||
log.step(&format!("summary: {}", s.line()));
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
@@ -646,23 +797,30 @@ pub fn run_file_v(
|
||||
|
||||
let name = out_name(Path::new(input.file_name().unwrap_or_default()));
|
||||
let dest = out_root.join(name);
|
||||
let mut s = Summary {
|
||||
unpacked: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
suspect: 0,
|
||||
metadata: 0,
|
||||
duration_ms: 0,
|
||||
};
|
||||
let mut s = Summary::default();
|
||||
|
||||
let is_meta = {
|
||||
let prefix = {
|
||||
use std::io::Read;
|
||||
let mut buf = [0u8; 4];
|
||||
std::fs::File::open(input)
|
||||
.and_then(|mut f| f.read_exact(&mut buf))
|
||||
.map(|_| senbei_metadata::is_metadata(&buf))
|
||||
.unwrap_or(false)
|
||||
let mut buf = vec![0u8; 8 * 1024];
|
||||
match std::fs::File::open(input).and_then(|mut f| f.read(&mut buf).map(|n| (buf, n))) {
|
||||
Ok((buf, n)) => {
|
||||
let mut b = buf;
|
||||
b.truncate(n);
|
||||
b
|
||||
}
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
};
|
||||
let is_meta = senbei_metadata::is_metadata(&prefix);
|
||||
// Android single-file targets are routed by content: a protected AArch64
|
||||
// library probe needs the whole file (its payload section is found through
|
||||
// the section-header table at the end), while a package is a container
|
||||
// handled entry-by-entry. Anything else falls through to the PE pipeline.
|
||||
let is_android_so = crate::android::is_elf64_aarch64(&prefix)
|
||||
&& std::fs::read(input)
|
||||
.map(|bytes| senbei_android_engine::is_protected_libil2cpp(&bytes))
|
||||
.unwrap_or(false);
|
||||
let is_android_package = !is_android_so && crate::android::is_app_package(input, &prefix);
|
||||
|
||||
if is_meta {
|
||||
match deobfuscate_metadata_to(input, &dest, verbose && quiet == 0) {
|
||||
@@ -706,6 +864,77 @@ pub fn run_file_v(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if is_android_so {
|
||||
match crate::android::restore_so_file(input, &dest, verbose && quiet == 0) {
|
||||
Ok(embedded) => {
|
||||
s.unpacked = 1;
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("OK {:?} -> {:?} (Android SO)", input, dest));
|
||||
}
|
||||
if quiet == 0 {
|
||||
println!("✓ So {} -> {}", input.display(), dest.display());
|
||||
}
|
||||
match write_embedded_metadata(embedded, &dest) {
|
||||
Ok(Some(meta_dest)) => {
|
||||
s.metadata += 1;
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("META {:?} (embedded) -> {:?}", input, meta_dest));
|
||||
}
|
||||
if quiet == 0 {
|
||||
println!(
|
||||
"✓ metadata {} (embedded) -> {}",
|
||||
input.display(),
|
||||
meta_dest.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
s.errors += 1;
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("ERR {:?}: embedded metadata: {e:#}", input));
|
||||
}
|
||||
if quiet == 0 {
|
||||
eprintln!("error: embedded metadata: {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
s.errors = 1;
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("ERR {:?}: {e:#}", input));
|
||||
}
|
||||
if quiet == 0 {
|
||||
eprintln!("error: {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if is_android_package {
|
||||
s.packages = 1;
|
||||
let rel = PathBuf::from(input.file_name().unwrap_or_default());
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
match crate::android::restore_package(
|
||||
input,
|
||||
&rel,
|
||||
&out_root,
|
||||
&mut seen,
|
||||
verbose && quiet == 0,
|
||||
) {
|
||||
Ok(outcomes) => {
|
||||
let bar = crate::ui::progress(0, true);
|
||||
apply_package_outcomes(outcomes, &mut s, &bar, quiet >= 1, &log);
|
||||
}
|
||||
Err(e) => {
|
||||
s.errors = 1;
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("ERR {:?}: {e:#}", input));
|
||||
}
|
||||
if quiet == 0 {
|
||||
eprintln!("error: {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match unpack_one_v(input, &dest, verbose && quiet == 0) {
|
||||
Ok((kind, report)) => {
|
||||
@@ -748,10 +977,7 @@ pub fn run_file_v(
|
||||
s.duration_ms = t0.elapsed().as_millis();
|
||||
if let Some(log) = &log {
|
||||
log.step(&format!("done in {} ms", s.duration_ms));
|
||||
log.step(&format!(
|
||||
"summary: {} unpacked · {} skipped · {} errors · {} suspect · {} metadata",
|
||||
s.unpacked, s.skipped, s.errors, s.suspect, s.metadata
|
||||
));
|
||||
log.step(&format!("summary: {}", s.line()));
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
@@ -820,7 +1046,7 @@ fn unsupported_version(e: &anyhow::Error) -> Option<u32> {
|
||||
/// 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.
|
||||
fn write_atomic(dest: &Path, bytes: &[u8]) -> std::io::Result<()> {
|
||||
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);
|
||||
@@ -966,10 +1192,13 @@ pub fn deobfuscate_metadata_to(
|
||||
verbose: bool,
|
||||
) -> anyhow::Result<senbei_metadata::Report> {
|
||||
let data = std::fs::read(input)?;
|
||||
// Preserve the metadata::Error in the chain (rather than stringifying it)
|
||||
// so the folder driver can apply its unsupported-version policy.
|
||||
let (out, report) = senbei_metadata::deobfuscate(&data)
|
||||
.map_err(|e| anyhow::Error::new(e).context(format!("{input:?}")))?;
|
||||
// The Android seeded-permutation variant is tried first (it validates
|
||||
// every restored RID); the structural remap is the fallback and the
|
||||
// Windows path. The [`senbei_metadata::Error`] is preserved in the chain
|
||||
// (rather than stringified) so the folder driver can apply its
|
||||
// unsupported-version policy.
|
||||
let (out, report) = crate::android::restore_metadata_bytes(&data)
|
||||
.map_err(|e| e.context(format!("{input:?}")))?;
|
||||
if report.remapped > 0 {
|
||||
if let Some(parent) = dest.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
@@ -982,6 +1211,77 @@ pub fn deobfuscate_metadata_to(
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// Write an embedded metadata blob (unwrapped from a restored Android
|
||||
/// library) next to the restored library. Returns the destination when a
|
||||
/// blob was written.
|
||||
fn write_embedded_metadata(
|
||||
embedded: Option<Vec<u8>>,
|
||||
so_dest: &Path,
|
||||
) -> anyhow::Result<Option<PathBuf>> {
|
||||
let Some(blob) = embedded else {
|
||||
return Ok(None);
|
||||
};
|
||||
let dest = crate::android::embedded_metadata_dest(so_dest);
|
||||
if let Some(parent) = dest.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
write_atomic(&dest, &blob)?;
|
||||
Ok(Some(dest))
|
||||
}
|
||||
|
||||
/// Fold one package's per-entry outcomes into the run summary, UI, and log.
|
||||
fn apply_package_outcomes(
|
||||
outcomes: Vec<crate::android::EntryOutcome>,
|
||||
s: &mut Summary,
|
||||
bar: &indicatif::ProgressBar,
|
||||
quiet: bool,
|
||||
log: &Option<crate::logfile::Log>,
|
||||
) {
|
||||
use crate::android::{EntryKind, EntryStatus};
|
||||
for outcome in outcomes {
|
||||
match outcome.status {
|
||||
EntryStatus::Restored => {
|
||||
match outcome.kind {
|
||||
EntryKind::So => {
|
||||
s.unpacked += 1;
|
||||
crate::ui::ok_label(bar, quiet, &outcome.label, "So", &outcome.dest);
|
||||
}
|
||||
EntryKind::Metadata { remapped } => {
|
||||
s.metadata += 1;
|
||||
crate::ui::metadata(
|
||||
bar,
|
||||
quiet,
|
||||
Path::new(&outcome.label),
|
||||
remapped,
|
||||
&outcome.dest,
|
||||
);
|
||||
}
|
||||
EntryKind::EmbeddedMetadata => {
|
||||
s.metadata += 1;
|
||||
crate::ui::ok_label(bar, quiet, &outcome.label, "metadata", &outcome.dest);
|
||||
}
|
||||
}
|
||||
if let Some(log) = log {
|
||||
log.step(&format!("OK {} -> {:?}", outcome.label, outcome.dest));
|
||||
}
|
||||
}
|
||||
EntryStatus::Duplicate | EntryStatus::NotTarget | EntryStatus::Unchanged => {
|
||||
s.skipped += 1;
|
||||
if let Some(log) = log {
|
||||
log.step(&format!("SKIP {} ({:?})", outcome.label, outcome.kind));
|
||||
}
|
||||
}
|
||||
EntryStatus::Failed(e) => {
|
||||
s.errors += 1;
|
||||
crate::ui::err(bar, quiet, Path::new(&outcome.label), &e);
|
||||
if let Some(log) = log {
|
||||
log.step(&format!("ERR {}: {e:#}", outcome.label));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user