diff --git a/senbei-io/src/android/mod.rs b/senbei-io/src/android/mod.rs index 8f216b9..e151abe 100644 --- a/senbei-io/src/android/mod.rs +++ b/senbei-io/src/android/mod.rs @@ -145,7 +145,21 @@ fn restore_so_file_with_context( } pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result>> { - Ok(restore_so_file_with_context(input, dest, verbose)?.embedded_metadata) + let mut method_index_module = None; + restore_so_file_with_method_index_module(input, dest, verbose, &mut method_index_module) +} + +pub(crate) fn restore_so_file_with_method_index_module( + input: &Path, + dest: &Path, + verbose: bool, + method_index_module: &mut Option>, +) -> Result>> { + let restored = restore_so_file_with_context(input, dest, verbose)?; + if let Some(module) = restored.method_index_module { + *method_index_module = Some(module); + } + Ok(restored.embedded_metadata) } /// Content identity for cross-source deduplication: the same library may @@ -173,7 +187,7 @@ pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec, senbei_me restore_metadata_bytes_with_module(data, None) } -fn restore_metadata_bytes_with_module( +pub(crate) fn restore_metadata_bytes_with_module( data: &[u8], method_index_module: Option<&[u8]>, ) -> anyhow::Result<(Vec, senbei_metadata::Report)> { @@ -261,15 +275,15 @@ pub enum EntryStatus { Failed(anyhow::Error), } -struct PackageRestoreContext { - method_index_module: Option>, +struct PackageRestoreContext<'a> { + method_index_module: &'a mut Option>, verbose: bool, } -impl PackageRestoreContext { - fn new(verbose: bool) -> Self { +impl<'a> PackageRestoreContext<'a> { + fn new(verbose: bool, method_index_module: &'a mut Option>) -> Self { Self { - method_index_module: None, + method_index_module, verbose, } } @@ -292,6 +306,25 @@ pub fn restore_package( out_root: &Path, seen: &mut HashSet, verbose: bool, +) -> Result> { + let mut method_index_module = None; + restore_package_with_method_index_module( + package, + rel, + out_root, + seen, + verbose, + &mut method_index_module, + ) +} + +pub(crate) fn restore_package_with_method_index_module( + package: &Path, + rel: &Path, + out_root: &Path, + seen: &mut HashSet, + verbose: bool, + method_index_module: &mut Option>, ) -> Result> { let bundle = package .extension() @@ -302,7 +335,7 @@ pub fn restore_package( let mut archive = open_package(package)?; let temporary = tempfile::tempdir().context("create package workspace")?; let mut outcomes = Vec::new(); - let mut context = PackageRestoreContext::new(verbose); + let mut context = PackageRestoreContext::new(verbose, method_index_module); let mut direct = Vec::new(); let mut nested = Vec::new(); @@ -347,8 +380,15 @@ pub fn restore_package( .with_context(|| format!("extract `{label}`"))?; outcomes.append(&mut entry_outcomes); } + struct NestedPackage { + label: PathBuf, + path: PathBuf, + base: PathBuf, + entries: Vec<(usize, PathBuf)>, + } + + let mut nested_packages = Vec::with_capacity(nested.len()); for (index, name) in nested { - let mut nested_context = PackageRestoreContext::new(verbose); let nested_label = rel.join(&name); let nested_path = extract_entry(&mut archive, index, &temporary, &nested_label) .with_context(|| format!("extract `{}`", nested_label.display()))?; @@ -368,26 +408,45 @@ pub fn restore_package( } } } - // Restore the protected library before metadata so v24.1 metadata can - // consume the runtime profile recovered from module 0x0C. - entries.sort_by_key(|(_, entry_name)| package_entry_priority(entry_name)); - // Keep the nested package's stem in the output layout so two splits - // carrying same-named entries cannot collide. - let base = rel.join(name.with_extension("")); - for (nested_index, entry_name) in entries { - let label = format!("{}::{}", nested_label.display(), entry_name.display()); - let dest = out_root.join(&base).join(crate::job::out_name(&entry_name)); - let mut entry_outcomes = restore_package_entry( - &mut nested_archive, - nested_index, - &label, - &dest, - &temporary, - seen, - &mut nested_context, - ) - .with_context(|| format!("extract `{label}`"))?; - outcomes.append(&mut entry_outcomes); + nested_packages.push(NestedPackage { + label: nested_label, + path: nested_path, + base: rel.join(name.with_extension("")), + entries, + }); + } + + // A bundle can put libil2cpp.so in an ABI split while metadata stays in + // base.apk. Share one context across the whole bundle and process all SOs + // before any metadata, regardless of which nested APK owns each entry. + for priority in [0_u8, 1_u8] { + for nested_package in &nested_packages { + let mut nested_archive = open_package(&nested_package.path)?; + for (nested_index, entry_name) in nested_package + .entries + .iter() + .filter(|(_, entry_name)| package_entry_priority(entry_name) == priority) + { + let label = format!( + "{}::{}", + nested_package.label.display(), + entry_name.display() + ); + let dest = out_root + .join(&nested_package.base) + .join(crate::job::out_name(entry_name)); + let mut entry_outcomes = restore_package_entry( + &mut nested_archive, + *nested_index, + &label, + &dest, + &temporary, + seen, + &mut context, + ) + .with_context(|| format!("extract `{label}`"))?; + outcomes.append(&mut entry_outcomes); + } } } Ok(outcomes) @@ -403,7 +462,7 @@ fn restore_package_entry( dest: &Path, temporary: &tempfile::TempDir, seen: &mut HashSet, - context: &mut PackageRestoreContext, + context: &mut PackageRestoreContext<'_>, ) -> Result> { let entry_path = extract_entry(archive, index, temporary, Path::new(label))?; let entry_file = @@ -437,7 +496,7 @@ fn restore_package_entry( match restore_so_file_with_context(&entry_path, dest, context.verbose) { Ok(restored) => { if let Some(module) = restored.method_index_module { - context.method_index_module = Some(module); + *context.method_index_module = Some(module); } let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)]; if let Some(blob) = restored.embedded_metadata { diff --git a/senbei-io/src/job.rs b/senbei-io/src/job.rs index 01ecb66..0990228 100644 --- a/senbei-io/src/job.rs +++ b/senbei-io/src/job.rs @@ -221,6 +221,7 @@ pub fn run_folder_opts( // 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(); + let mut android_method_index_module = None; // 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(); @@ -242,7 +243,12 @@ pub fn run_folder_opts( 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) + crate::android::restore_so_file_with_method_index_module( + &input_owned, + &dest_owned, + verbose_steps, + &mut android_method_index_module, + ) })); match result { Ok(Ok(embedded)) => { @@ -310,12 +316,13 @@ pub fn run_folder_opts( 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( + let outcomes = crate::android::restore_package_with_method_index_module( &package_owned, &rel_owned, &out_root_owned, &mut seen_taken, verbose_steps, + &mut android_method_index_module, ); (outcomes, seen_taken) })); @@ -361,7 +368,12 @@ pub fn run_folder_opts( let meta_owned = meta.clone(); let dest_owned = dest.clone(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - deobfuscate_metadata_to(&meta_owned, &dest_owned, verbose_steps) + deobfuscate_metadata_to_with_module( + &meta_owned, + &dest_owned, + verbose_steps, + android_method_index_module.as_deref(), + ) })); match result { Ok(Ok(report)) if report.remapped > 0 => { @@ -735,6 +747,15 @@ pub fn deobfuscate_metadata_to( input: &Path, dest: &Path, verbose: bool, +) -> anyhow::Result { + deobfuscate_metadata_to_with_module(input, dest, verbose, None) +} + +fn deobfuscate_metadata_to_with_module( + input: &Path, + dest: &Path, + verbose: bool, + method_index_module: Option<&[u8]>, ) -> anyhow::Result { let data = std::fs::read(input)?; // The Android seeded-permutation variant is tried first (it validates @@ -742,8 +763,9 @@ pub fn deobfuscate_metadata_to( // 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:?}")))?; + let (out, report) = + crate::android::restore_metadata_bytes_with_module(&data, method_index_module) + .map_err(|e| e.context(format!("{input:?}")))?; if report.remapped > 0 { if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent)?;