fix(android): carry v24 runtime context across package scans

This commit is contained in:
y52
2026-09-19 06:10:41 +00:00
parent a45cc8aa90
commit b355d8ea8f
2 changed files with 117 additions and 36 deletions
+90 -31
View File
@@ -145,7 +145,21 @@ fn restore_so_file_with_context(
} }
pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Option<Vec<u8>>> { pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Option<Vec<u8>>> {
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<Vec<u8>>,
) -> Result<Option<Vec<u8>>> {
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 /// Content identity for cross-source deduplication: the same library may
@@ -173,7 +187,7 @@ pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec<u8>, senbei_me
restore_metadata_bytes_with_module(data, None) restore_metadata_bytes_with_module(data, None)
} }
fn restore_metadata_bytes_with_module( pub(crate) fn restore_metadata_bytes_with_module(
data: &[u8], data: &[u8],
method_index_module: Option<&[u8]>, method_index_module: Option<&[u8]>,
) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> { ) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
@@ -261,15 +275,15 @@ pub enum EntryStatus {
Failed(anyhow::Error), Failed(anyhow::Error),
} }
struct PackageRestoreContext { struct PackageRestoreContext<'a> {
method_index_module: Option<Vec<u8>>, method_index_module: &'a mut Option<Vec<u8>>,
verbose: bool, verbose: bool,
} }
impl PackageRestoreContext { impl<'a> PackageRestoreContext<'a> {
fn new(verbose: bool) -> Self { fn new(verbose: bool, method_index_module: &'a mut Option<Vec<u8>>) -> Self {
Self { Self {
method_index_module: None, method_index_module,
verbose, verbose,
} }
} }
@@ -292,6 +306,25 @@ pub fn restore_package(
out_root: &Path, out_root: &Path,
seen: &mut HashSet<String>, seen: &mut HashSet<String>,
verbose: bool, verbose: bool,
) -> Result<Vec<EntryOutcome>> {
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<String>,
verbose: bool,
method_index_module: &mut Option<Vec<u8>>,
) -> Result<Vec<EntryOutcome>> { ) -> Result<Vec<EntryOutcome>> {
let bundle = package let bundle = package
.extension() .extension()
@@ -302,7 +335,7 @@ pub fn restore_package(
let mut archive = open_package(package)?; let mut archive = open_package(package)?;
let temporary = tempfile::tempdir().context("create package workspace")?; let temporary = tempfile::tempdir().context("create package workspace")?;
let mut outcomes = Vec::new(); 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 direct = Vec::new();
let mut nested = Vec::new(); let mut nested = Vec::new();
@@ -347,8 +380,15 @@ pub fn restore_package(
.with_context(|| format!("extract `{label}`"))?; .with_context(|| format!("extract `{label}`"))?;
outcomes.append(&mut entry_outcomes); 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 { for (index, name) in nested {
let mut nested_context = PackageRestoreContext::new(verbose);
let nested_label = rel.join(&name); let nested_label = rel.join(&name);
let nested_path = extract_entry(&mut archive, index, &temporary, &nested_label) let nested_path = extract_entry(&mut archive, index, &temporary, &nested_label)
.with_context(|| format!("extract `{}`", nested_label.display()))?; .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 nested_packages.push(NestedPackage {
// consume the runtime profile recovered from module 0x0C. label: nested_label,
entries.sort_by_key(|(_, entry_name)| package_entry_priority(entry_name)); path: nested_path,
// Keep the nested package's stem in the output layout so two splits base: rel.join(name.with_extension("")),
// carrying same-named entries cannot collide. entries,
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)); // A bundle can put libil2cpp.so in an ABI split while metadata stays in
let mut entry_outcomes = restore_package_entry( // base.apk. Share one context across the whole bundle and process all SOs
&mut nested_archive, // before any metadata, regardless of which nested APK owns each entry.
nested_index, for priority in [0_u8, 1_u8] {
&label, for nested_package in &nested_packages {
&dest, let mut nested_archive = open_package(&nested_package.path)?;
&temporary, for (nested_index, entry_name) in nested_package
seen, .entries
&mut nested_context, .iter()
) .filter(|(_, entry_name)| package_entry_priority(entry_name) == priority)
.with_context(|| format!("extract `{label}`"))?; {
outcomes.append(&mut entry_outcomes); 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) Ok(outcomes)
@@ -403,7 +462,7 @@ fn restore_package_entry<R: Read + Seek>(
dest: &Path, dest: &Path,
temporary: &tempfile::TempDir, temporary: &tempfile::TempDir,
seen: &mut HashSet<String>, seen: &mut HashSet<String>,
context: &mut PackageRestoreContext, context: &mut PackageRestoreContext<'_>,
) -> Result<Vec<EntryOutcome>> { ) -> Result<Vec<EntryOutcome>> {
let entry_path = extract_entry(archive, index, temporary, Path::new(label))?; let entry_path = extract_entry(archive, index, temporary, Path::new(label))?;
let entry_file = let entry_file =
@@ -437,7 +496,7 @@ fn restore_package_entry<R: Read + Seek>(
match restore_so_file_with_context(&entry_path, dest, context.verbose) { match restore_so_file_with_context(&entry_path, dest, context.verbose) {
Ok(restored) => { Ok(restored) => {
if let Some(module) = restored.method_index_module { 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)]; let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)];
if let Some(blob) = restored.embedded_metadata { if let Some(blob) = restored.embedded_metadata {
+27 -5
View File
@@ -221,6 +221,7 @@ pub fn run_folder_opts(
// files restore first so the cross-source dedup keeps them over a copy // files restore first so the cross-source dedup keeps them over a copy
// inside a package (loose beats `.apk` beats `.apks`/`.xapk` bundle). // inside a package (loose beats `.apk` beats `.apks`/`.xapk` bundle).
let mut android_seen = std::collections::HashSet::new(); 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 // Hashing a protected library costs a full read, so only pay it when a
// duplicate source can actually exist in this run. // duplicate source can actually exist in this run.
let android_dedup = scan.android_so.len() > 1 || !scan.android_packages.is_empty(); 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 input_owned = input.clone();
let dest_owned = dest.clone(); let dest_owned = dest.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 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 { match result {
Ok(Ok(embedded)) => { Ok(Ok(embedded)) => {
@@ -310,12 +316,13 @@ pub fn run_folder_opts(
let out_root_owned = out_root.clone(); let out_root_owned = out_root.clone();
let mut seen_taken = std::mem::take(&mut android_seen); let mut seen_taken = std::mem::take(&mut android_seen);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 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, &package_owned,
&rel_owned, &rel_owned,
&out_root_owned, &out_root_owned,
&mut seen_taken, &mut seen_taken,
verbose_steps, verbose_steps,
&mut android_method_index_module,
); );
(outcomes, seen_taken) (outcomes, seen_taken)
})); }));
@@ -361,7 +368,12 @@ pub fn run_folder_opts(
let meta_owned = meta.clone(); let meta_owned = meta.clone();
let dest_owned = dest.clone(); let dest_owned = dest.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 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 { match result {
Ok(Ok(report)) if report.remapped > 0 => { Ok(Ok(report)) if report.remapped > 0 => {
@@ -735,6 +747,15 @@ pub fn deobfuscate_metadata_to(
input: &Path, input: &Path,
dest: &Path, dest: &Path,
verbose: bool, verbose: bool,
) -> anyhow::Result<senbei_metadata::Report> {
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<senbei_metadata::Report> { ) -> anyhow::Result<senbei_metadata::Report> {
let data = std::fs::read(input)?; let data = std::fs::read(input)?;
// The Android seeded-permutation variant is tried first (it validates // 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 // Windows path. The [`senbei_metadata::Error`] is preserved in the chain
// (rather than stringified) so the folder driver can apply its // (rather than stringified) so the folder driver can apply its
// unsupported-version policy. // unsupported-version policy.
let (out, report) = crate::android::restore_metadata_bytes(&data) let (out, report) =
.map_err(|e| e.context(format!("{input:?}")))?; crate::android::restore_metadata_bytes_with_module(&data, method_index_module)
.map_err(|e| e.context(format!("{input:?}")))?;
if report.remapped > 0 { if report.remapped > 0 {
if let Some(parent) = dest.parent() { if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;