Merge PR #7: restore IL2CPP 24.1 method indices

This commit is contained in:
2026-09-20 10:04:49 +08:00
5 changed files with 694 additions and 61 deletions
+168 -26
View File
@@ -1,6 +1,6 @@
//! Android target orchestration: protected AArch64 shared libraries (`.so`), //! Android target orchestration: protected AArch64 shared libraries (`.so`),
//! app packages (`.apk` / `.apks` / `.xapk`), and the Android variant of the //! app packages (`.apk` / `.apks` / `.xapk`), and the Android variant of the
//! il2cpp method-token obfuscation. //! il2cpp method-token/method-index obfuscation.
//! //!
//! The protection scheme hollows out an ELF64/AArch64 shared object and moves //! The protection scheme hollows out an ELF64/AArch64 shared object and moves
//! the original bytes into an encrypted payload appended as a `SHT_LOUSER` //! the original bytes into an encrypted payload appended as a `SHT_LOUSER`
@@ -100,14 +100,32 @@ pub fn file_content_identity(path: &Path) -> std::io::Result<String> {
/// implementation detail of the two-phase restore, not user-facing output). /// implementation detail of the two-phase restore, not user-facing output).
/// Returns the unwrapped embedded metadata blob when the restored image /// Returns the unwrapped embedded metadata blob when the restored image
/// carries one (see the module docs); the caller decides where to write it. /// carries one (see the module docs); the caller decides where to write it.
pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Option<Vec<u8>>> { struct SoRestoreContext {
embedded_metadata: Option<Vec<u8>>,
method_index_module: Option<Vec<u8>>,
}
fn restore_so_file_with_context(
input: &Path,
dest: &Path,
verbose: bool,
) -> Result<SoRestoreContext> {
let temporary = tempfile::tempdir().context("create stage-2 workspace")?; let temporary = tempfile::tempdir().context("create stage-2 workspace")?;
let stage2_dir = temporary.path().join("stage2"); let stage2_dir = temporary.path().join("stage2");
extract_stage2(&ExtractOptions::with_defaults( let extraction = extract_stage2(&ExtractOptions::with_defaults(
input.to_path_buf(), input.to_path_buf(),
stage2_dir.clone(), stage2_dir.clone(),
)) ))
.context("extract stage-1/stage-2 payload")?; .context("extract stage-1/stage-2 payload")?;
let method_index_module = extraction
.module_registry
.iter()
.find(|module| module.command_id == 0x0c)
.map(|module| {
std::fs::read(stage2_dir.join(&module.image_path))
.with_context(|| format!("read decoded module 0x0C `{}`", module.image_path))
})
.transpose()?;
restore_libil2cpp(&RestoreOptions { restore_libil2cpp(&RestoreOptions {
input: input.to_path_buf(), input: input.to_path_buf(),
output: dest.to_path_buf(), output: dest.to_path_buf(),
@@ -120,9 +138,28 @@ pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Optio
.context("restore protected library")?; .context("restore protected library")?;
let restored = let restored =
std::fs::read(dest).with_context(|| format!("read restored `{}`", dest.display()))?; std::fs::read(dest).with_context(|| format!("read restored `{}`", dest.display()))?;
Ok(senbei_metadata::android::extract_embedded_metadata( Ok(SoRestoreContext {
&restored, embedded_metadata: senbei_metadata::android::extract_embedded_metadata(&restored),
)) method_index_module,
})
}
pub fn restore_so_file(input: &Path, dest: &Path, verbose: bool) -> Result<Option<Vec<u8>>> {
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
@@ -134,8 +171,10 @@ pub fn content_identity(data: &[u8]) -> String {
hex_digest(&digest.finalize()) hex_digest(&digest.finalize())
} }
/// Restore an il2cpp metadata blob (Android seeded permutation first, then the /// Restore an il2cpp metadata blob. Paired v24.1 Android packages use the
/// structural remap used by the Windows builds). /// method-index profile recovered from module 0x0C; later Android layouts use
/// the seeded MethodDef RID permutation before falling back to the structural
/// remap used by the Windows builds.
/// ///
/// The Android variant obfuscates MethodDef RIDs with a keyed five-round /// The Android variant obfuscates MethodDef RIDs with a keyed five-round
/// permutation; the correct seed is recovered by intersecting per-image key /// permutation; the correct seed is recovered by intersecting per-image key
@@ -145,6 +184,32 @@ pub fn content_identity(data: &[u8]) -> String {
/// canonical form. Both paths are no-ops (`remapped == 0`) on an /// canonical form. Both paths are no-ops (`remapped == 0`) on an
/// already-clean blob. /// already-clean blob.
pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> { pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
restore_metadata_bytes_with_module(data, None)
}
pub(crate) fn restore_metadata_bytes_with_module(
data: &[u8],
method_index_module: Option<&[u8]>,
) -> anyhow::Result<(Vec<u8>, senbei_metadata::Report)> {
let version = data
.get(4..8)
.and_then(|bytes| <[u8; 4]>::try_from(bytes).ok())
.map(u32::from_le_bytes);
if version == Some(24)
&& let Some(module) = method_index_module
{
let (out, report) = senbei_metadata::android::restore_method_indices_v24_1(data, module)
.map_err(anyhow::Error::new)?;
return Ok((
out,
senbei_metadata::Report {
version: report.version,
methods: report.methods,
remapped: report.changed_indices,
modules: 0,
},
));
}
if let Ok(discovery) = senbei_metadata::android::discover_method_token_seeds(data) if let Ok(discovery) = senbei_metadata::android::discover_method_token_seeds(data)
&& matches!(discovery.version, 29 | 31 | 39) && matches!(discovery.version, 29 | 31 | 39)
{ {
@@ -190,7 +255,7 @@ pub struct EntryOutcome {
pub enum EntryKind { pub enum EntryKind {
/// A protected shared library, restored. /// A protected shared library, restored.
So, So,
/// An il2cpp metadata blob, de-obfuscated (`remapped` tokens changed). /// An il2cpp metadata blob, de-obfuscated (`remapped` method fields changed).
Metadata { remapped: usize }, Metadata { remapped: usize },
/// A metadata blob unwrapped from a restored library's data section. /// A metadata blob unwrapped from a restored library's data section.
EmbeddedMetadata, EmbeddedMetadata,
@@ -204,12 +269,30 @@ pub enum EntryStatus {
Duplicate, Duplicate,
/// Content-probed but not a target (unprotected library). /// Content-probed but not a target (unprotected library).
NotTarget, NotTarget,
/// A metadata blob whose tokens were already canonical; no copy written. /// A metadata blob whose protected method fields were already canonical; no copy written.
Unchanged, Unchanged,
/// Recognised as a target but the restore failed. /// Recognised as a target but the restore failed.
Failed(anyhow::Error), Failed(anyhow::Error),
} }
struct PackageRestoreContext<'a> {
method_index_module: &'a mut Option<Vec<u8>>,
verbose: bool,
}
impl<'a> PackageRestoreContext<'a> {
fn new(verbose: bool, method_index_module: &'a mut Option<Vec<u8>>) -> Self {
Self {
method_index_module,
verbose,
}
}
}
fn package_entry_priority(path: &Path) -> u8 {
if is_so_name(path) { 0 } else { 1 }
}
/// Restore every protected library and metadata blob inside one app package. /// Restore every protected library and metadata blob inside one app package.
/// ///
/// `rel` is the package's path relative to the scanned root (or its bare file /// `rel` is the package's path relative to the scanned root (or its bare file
@@ -223,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()
@@ -233,6 +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, method_index_module);
let mut direct = Vec::new(); let mut direct = Vec::new();
let mut nested = Vec::new(); let mut nested = Vec::new();
@@ -261,6 +364,7 @@ pub fn restore_package(
} }
} }
} }
direct.sort_by_key(|(_, name)| package_entry_priority(name));
for (index, name) in direct { for (index, name) in direct {
let label = format!("{}::{}", rel.display(), name.display()); let label = format!("{}::{}", rel.display(), name.display());
let dest = out_root.join(rel).join(crate::job::out_name(&name)); let dest = out_root.join(rel).join(crate::job::out_name(&name));
@@ -271,11 +375,19 @@ pub fn restore_package(
&dest, &dest,
&temporary, &temporary,
seen, seen,
verbose, &mut context,
) )
.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 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)
@@ -296,25 +408,47 @@ pub fn restore_package(
} }
} }
} }
// Keep the nested package's stem in the output layout so two splits nested_packages.push(NestedPackage {
// carrying same-named entries cannot collide. label: nested_label,
let base = rel.join(name.with_extension("")); path: nested_path,
for (nested_index, entry_name) in entries { base: rel.join(name.with_extension("")),
let label = format!("{}::{}", nested_label.display(), entry_name.display()); entries,
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
// 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( let mut entry_outcomes = restore_package_entry(
&mut nested_archive, &mut nested_archive,
nested_index, *nested_index,
&label, &label,
&dest, &dest,
&temporary, &temporary,
seen, seen,
verbose, &mut context,
) )
.with_context(|| format!("extract `{label}`"))?; .with_context(|| format!("extract `{label}`"))?;
outcomes.append(&mut entry_outcomes); outcomes.append(&mut entry_outcomes);
} }
} }
}
Ok(outcomes) Ok(outcomes)
} }
@@ -328,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>,
verbose: bool, 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 =
@@ -358,10 +492,14 @@ fn restore_package_entry<R: Read + Seek>(
if is_so { if is_so {
drop(entry_data); drop(entry_data);
drop(entry_file); drop(entry_file);
return Ok(match restore_so_file(&entry_path, dest, verbose) { return Ok(
Ok(embedded) => { 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);
}
let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)]; let mut outcomes = vec![outcome(EntryKind::So, EntryStatus::Restored)];
if let Some(blob) = embedded { if let Some(blob) = restored.embedded_metadata {
let meta_dest = embedded_metadata_dest(dest); let meta_dest = embedded_metadata_dest(dest);
let status = match write_metadata_blob(&meta_dest, &blob) { let status = match write_metadata_blob(&meta_dest, &blob) {
Ok(()) => EntryStatus::Restored, Ok(()) => EntryStatus::Restored,
@@ -377,12 +515,16 @@ fn restore_package_entry<R: Read + Seek>(
outcomes outcomes
} }
Err(error) => vec![outcome(EntryKind::So, EntryStatus::Failed(error))], Err(error) => vec![outcome(EntryKind::So, EntryStatus::Failed(error))],
}); },
);
} }
// Metadata entry: write only when the restore actually changed tokens — // Metadata entry: write only when the restore actually changed protected method fields —
// a clean blob needs no copy (same contract as loose metadata files). // a clean blob needs no copy (same contract as loose metadata files).
let kind_and_status = match restore_metadata_bytes(&entry_data) { let kind_and_status = match restore_metadata_bytes_with_module(
&entry_data,
context.method_index_module.as_deref(),
) {
Ok((out, report)) if report.remapped > 0 => { Ok((out, report)) if report.remapped > 0 => {
let kind = EntryKind::Metadata { let kind = EntryKind::Metadata {
remapped: report.remapped, remapped: report.remapped,
+30 -8
View File
@@ -15,7 +15,7 @@ pub struct Summary {
/// — likely to crash at runtime (e.g. 0xC0000005). Counted in addition to /// — likely to crash at runtime (e.g. 0xC0000005). Counted in addition to
/// `unpacked` (a suspect file is still written). /// `unpacked` (a suspect file is still written).
pub suspect: usize, pub suspect: usize,
/// il2cpp `global-metadata.dat` files de-obfuscated (method tokens remapped), /// il2cpp `global-metadata.dat` files de-obfuscated (protected method fields remapped),
/// including blobs unwrapped from restored Android libraries. /// including blobs unwrapped from restored Android libraries.
pub metadata: usize, pub metadata: usize,
/// Android app packages (`.apk`/`.apks`/`.xapk`) opened and searched. /// Android app packages (`.apk`/`.apks`/`.xapk`) opened and searched.
@@ -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 => {
@@ -369,7 +381,7 @@ pub fn run_folder_opts(
crate::ui::metadata(&bar, suppress_file_lines, &rel, report.remapped, &dest); crate::ui::metadata(&bar, suppress_file_lines, &rel, report.remapped, &dest);
if let Some(log) = &log { if let Some(log) = &log {
log.step(&format!( log.step(&format!(
"META {rel:?} -> {dest:?}: v{} remapped {} method tokens", "META {rel:?} -> {dest:?}: v{} remapped {} method fields",
report.version, report.remapped report.version, report.remapped
)); ));
} }
@@ -506,13 +518,13 @@ pub fn run_file_v(
s.metadata = 1; s.metadata = 1;
if let Some(log) = &log { if let Some(log) = &log {
log.step(&format!( log.step(&format!(
"META {:?} -> {:?}: v{} remapped {} method tokens", "META {:?} -> {:?}: v{} remapped {} method fields",
input, dest, report.version, report.remapped input, dest, report.version, report.remapped
)); ));
} }
if quiet == 0 { if quiet == 0 {
println!( println!(
"✓ metadata v{} -> {:?} ({} method tokens remapped)", "✓ metadata v{} -> {:?} ({} method fields remapped)",
report.version, dest, report.remapped report.version, dest, report.remapped
); );
} }
@@ -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,7 +763,8 @@ 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) =
crate::android::restore_metadata_bytes_with_module(&data, method_index_module)
.map_err(|e| e.context(format!("{input:?}")))?; .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() {
+2 -2
View File
@@ -39,13 +39,13 @@ pub fn ok_label(bar: &ProgressBar, quiet: bool, rel: &str, label: &str, dest: &P
} }
/// Print a green success line for a de-obfuscated il2cpp `global-metadata.dat`, /// Print a green success line for a de-obfuscated il2cpp `global-metadata.dat`,
/// reporting how many method tokens were remapped. /// reporting how many protected method fields were remapped.
pub fn metadata(bar: &ProgressBar, quiet: bool, rel: &Path, remapped: usize, dest: &Path) { pub fn metadata(bar: &ProgressBar, quiet: bool, rel: &Path, remapped: usize, dest: &Path) {
if quiet { if quiet {
return; return;
} }
let msg = format!( let msg = format!(
"{} metadata {} -> {} ({} method tokens remapped)", "{} metadata {} -> {} ({} method fields remapped)",
"".green(), "".green(),
rel.display(), rel.display(),
dest.display(), dest.display(),
@@ -0,0 +1,467 @@
//! Restoration of IL2CPP v24.1 `Il2CppMethodDefinition.methodIndex` values.
//!
//! CrackProof's v24.1 Android module rewrites the global method-index permutation
//! at runtime. The transform parameters are carried by the decoded module `0x0C`,
//! so the restore derives them from that module instead of hard-coding a
//! game-specific table.
use serde::Serialize;
use crate::common::MAGIC;
const RAW_VERSION_24: u32 = 24;
const HDR_METHODS: usize = 0x30;
const V24_1_METHOD_STRIDE: usize = 0x34;
const V24_1_METHOD_INDEX_OFFSET: usize = 0x14;
const V24_1_SELECTOR: u32 = 1;
const EXCEPTION_COUNT: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MethodIndexReport {
pub version: u32,
pub methods: usize,
pub active_methods: usize,
pub changed_indices: usize,
pub exception_count: usize,
pub seed: String,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MethodIndexError {
#[error("not an IL2CPP global-metadata.dat")]
NotMetadata,
#[error("unsupported metadata version {0}")]
UnsupportedVersion(u32),
#[error("malformed v24.1 metadata: {0}")]
Malformed(String),
#[error("v24.1 method-index profile was not found in module 0x0C")]
ProfileNotFound,
#[error("multiple v24.1 method-index profiles matched module 0x0C")]
AmbiguousProfile,
#[error("v24.1 method-index restoration failed validation: {0}")]
Validation(String),
}
type Result<T> = std::result::Result<T, MethodIndexError>;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Profile {
seed: u32,
exception_values: [u32; EXCEPTION_COUNT],
}
fn bytes(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
let end = offset
.checked_add(size)
.ok_or_else(|| MethodIndexError::Malformed("byte range overflow".to_owned()))?;
data.get(offset..end).ok_or_else(|| {
MethodIndexError::Malformed(format!(
"byte range 0x{offset:x}..0x{end:x} is out of bounds"
))
})
}
fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
let value: [u8; 4] = bytes(data, offset, 4)?
.try_into()
.map_err(|_| MethodIndexError::Malformed("invalid u32 range".to_owned()))?;
Ok(u32::from_le_bytes(value))
}
fn read_i32(data: &[u8], offset: usize) -> Result<i32> {
let value: [u8; 4] = bytes(data, offset, 4)?
.try_into()
.map_err(|_| MethodIndexError::Malformed("invalid i32 range".to_owned()))?;
Ok(i32::from_le_bytes(value))
}
fn write_i32(data: &mut [u8], offset: usize, value: i32) -> Result<()> {
let destination = data.get_mut(offset..offset + 4).ok_or_else(|| {
MethodIndexError::Malformed("method-index write is out of bounds".to_owned())
})?;
destination.copy_from_slice(&value.to_le_bytes());
Ok(())
}
fn method_table(data: &[u8]) -> Result<(usize, usize)> {
if read_u32(data, 0)? != MAGIC {
return Err(MethodIndexError::NotMetadata);
}
let version = read_u32(data, 4)?;
if version != RAW_VERSION_24 {
return Err(MethodIndexError::UnsupportedVersion(version));
}
let offset = read_u32(data, HDR_METHODS)? as usize;
let size = read_u32(data, HDR_METHODS + 4)? as usize;
bytes(data, offset, size)?;
if !size.is_multiple_of(V24_1_METHOD_STRIDE) {
return Err(MethodIndexError::Malformed(format!(
"method table size 0x{size:x} is not divisible by v24.1 stride 0x{V24_1_METHOD_STRIDE:x}"
)));
}
Ok((offset, size / V24_1_METHOD_STRIDE))
}
fn method_indices(data: &[u8]) -> Result<(usize, Vec<Option<u32>>)> {
let (offset, count) = method_table(data)?;
let mut indices = Vec::with_capacity(count);
for method in 0..count {
let value = read_i32(
data,
offset + method * V24_1_METHOD_STRIDE + V24_1_METHOD_INDEX_OFFSET,
)?;
indices.push((value >= 0).then_some(value as u32));
}
Ok((offset, indices))
}
fn is_complete_permutation(indices: &[Option<u32>]) -> bool {
let active_count = indices.iter().filter(|value| value.is_some()).count();
if active_count == 0 || active_count > u32::MAX as usize {
return false;
}
let mut seen = vec![false; active_count];
for value in indices.iter().flatten().copied() {
let Ok(index) = usize::try_from(value) else {
return false;
};
if index >= active_count || seen[index] {
return false;
}
seen[index] = true;
}
seen.into_iter().all(|value| value)
}
fn permutation_key(seed: u32, count: u32) -> Result<u32> {
if count < 2 {
return Err(MethodIndexError::Validation(
"method-index permutation requires at least two active methods".to_owned(),
));
}
if count > u32::MAX / 2 {
return Err(MethodIndexError::Validation(
"active method count is too large for the permutation mirror".to_owned(),
));
}
let half = count / 2;
if half == 0 {
return Err(MethodIndexError::Validation(
"method-index permutation has a zero divisor".to_owned(),
));
}
Ok(seed % half + count / 4)
}
fn permutation_round(mut value: u32, count: u32, key: u32) -> u32 {
let mirror = count * 2 - 1;
if value & 1 != 0 {
value = mirror - value;
}
value >>= 1;
if value >= count {
value = mirror - value;
}
let adjusted = i64::from(value) - i64::from(key);
if adjusted < 0 {
(adjusted + i64::from(count)) as u32
} else {
adjusted as u32
}
}
fn transform_index(mut value: u32, count: u32, seed: u32) -> Result<u32> {
let key = permutation_key(seed, count)?;
for _ in 0..5 {
value = permutation_round(value, count, key);
}
Ok(value)
}
fn transformed_values(indices: &[Option<u32>], seed: u32) -> Result<Vec<Option<u32>>> {
let active_count = indices.iter().filter(|value| value.is_some()).count();
let count = u32::try_from(active_count)
.map_err(|_| MethodIndexError::Validation("active method count exceeds u32".to_owned()))?;
indices
.iter()
.map(|value| {
value
.map(|value| transform_index(value, count, seed))
.transpose()
})
.collect()
}
fn missing_values(indices: &[Option<u32>]) -> Option<Vec<u32>> {
let active_count = indices.iter().filter(|value| value.is_some()).count();
let mut seen = vec![false; active_count];
for value in indices.iter().flatten().copied() {
let index = usize::try_from(value).ok()?;
if index >= active_count {
return None;
}
seen[index] = true;
}
Some(
seen.into_iter()
.enumerate()
.filter_map(|(index, present)| (!present).then_some(index as u32))
.collect(),
)
}
fn find_exception_table(
module: &[u8],
count: u32,
missing: &[u32],
) -> Option<[u32; EXCEPTION_COUNT]> {
if missing.len() != EXCEPTION_COUNT || count == 0 {
return None;
}
let mut wanted = vec![false; count as usize];
for &value in missing {
let slot = wanted.get_mut(value as usize)?;
*slot = true;
}
let table_bytes = EXCEPTION_COUNT * 4;
for offset in (8..=module.len().checked_sub(table_bytes)?).step_by(4) {
if read_u32(module, offset - 8).ok()? != V24_1_SELECTOR
|| read_u32(module, offset - 4).ok()? != count - 1
{
continue;
}
let mut values = [0_u32; EXCEPTION_COUNT];
let mut seen = vec![false; count as usize];
let mut matches = true;
for (index, slot) in values.iter_mut().enumerate() {
let value = read_u32(module, offset + index * 4).ok()?;
let Some(is_wanted) = wanted.get(value as usize) else {
matches = false;
break;
};
if !*is_wanted || seen[value as usize] {
matches = false;
break;
}
seen[value as usize] = true;
*slot = value;
}
if matches {
return Some(values);
}
}
None
}
fn profile_candidates(module: &[u8], indices: &[Option<u32>]) -> Result<Vec<Profile>> {
let active_count = indices.iter().filter(|value| value.is_some()).count();
let count = u32::try_from(active_count)
.map_err(|_| MethodIndexError::Validation("active method count exceeds u32".to_owned()))?;
if count <= EXCEPTION_COUNT as u32 {
return Err(MethodIndexError::Validation(format!(
"active method count {count} is too small for the v24.1 exception table"
)));
}
let mut profiles = Vec::new();
for offset in (0..module.len().saturating_sub(8)).step_by(4) {
let Ok(version) = read_u32(module, offset + 4) else {
continue;
};
if version != RAW_VERSION_24 {
continue;
}
let seed = read_u32(module, offset)?;
let transformed = transformed_values(indices, seed)?;
let Some(missing) = missing_values(&transformed) else {
continue;
};
let Some(exception_values) = find_exception_table(module, count, &missing) else {
continue;
};
let mut candidate = transformed;
let mut exception = exception_values.iter().copied();
for value in candidate.iter_mut().flatten() {
if let Some(replacement) = exception.next() {
*value = replacement;
} else {
break;
}
}
if exception.next().is_none() && is_complete_permutation(&candidate) {
profiles.push(Profile {
seed,
exception_values,
});
}
}
profiles.sort_by_key(|profile| profile.seed);
profiles.dedup();
Ok(profiles)
}
/// Restore the v24.1 `methodIndex` permutation using the decoded CrackProof
/// module `0x0C` that accompanied the protected library.
///
/// The profile is accepted only when the module supplies a seed plus a
/// 256-entry exception table that turns the restored non-negative indices into
/// the exact permutation `0..active_method_count`. This makes a wrong module or
/// incompatible v24 sub-layout fail without mutating the metadata.
pub fn restore_method_indices_v24_1(
data: &[u8],
module_0c: &[u8],
) -> Result<(Vec<u8>, MethodIndexReport)> {
let (method_offset, indices) = method_indices(data)?;
let active_count = indices.iter().filter(|value| value.is_some()).count();
if is_complete_permutation(&indices) {
return Ok((
data.to_vec(),
MethodIndexReport {
version: RAW_VERSION_24,
methods: indices.len(),
active_methods: active_count,
changed_indices: 0,
exception_count: 0,
seed: "clean".to_owned(),
},
));
}
let profiles = profile_candidates(module_0c, &indices)?;
let profile = match profiles.as_slice() {
[] => return Err(MethodIndexError::ProfileNotFound),
[profile] => profile,
_ => return Err(MethodIndexError::AmbiguousProfile),
};
let mut restored = transformed_values(&indices, profile.seed)?;
let mut exception = profile.exception_values.iter().copied();
for value in restored.iter_mut().flatten() {
if let Some(replacement) = exception.next() {
*value = replacement;
} else {
break;
}
}
if exception.next().is_some() {
return Err(MethodIndexError::Validation(
"metadata has fewer active methods than the exception table".to_owned(),
));
}
if !is_complete_permutation(&restored) {
return Err(MethodIndexError::Validation(
"restored methodIndex values are not a complete permutation".to_owned(),
));
}
let mut output = data.to_vec();
let mut changed_indices = 0_usize;
for (method, value) in restored.iter().enumerate() {
let Some(value) = value else {
continue;
};
let offset = method_offset + method * V24_1_METHOD_STRIDE + V24_1_METHOD_INDEX_OFFSET;
let old = read_i32(data, offset)?;
let new = i32::try_from(*value).map_err(|_| {
MethodIndexError::Validation("restored methodIndex exceeds i32".to_owned())
})?;
if old != new {
write_i32(&mut output, offset, new)?;
changed_indices += 1;
}
}
Ok((
output,
MethodIndexReport {
version: RAW_VERSION_24,
methods: indices.len(),
active_methods: active_count,
changed_indices,
exception_count: EXCEPTION_COUNT,
seed: format!("0x{:08X}", profile.seed),
},
))
}
#[cfg(test)]
mod tests {
use super::*;
fn inverse_map(count: u32, seed: u32) -> Vec<u32> {
let mut inverse = vec![u32::MAX; count as usize];
for value in 0..count {
let transformed = transform_index(value, count, seed).unwrap();
inverse[transformed as usize] = value;
}
assert!(inverse.iter().all(|value| *value != u32::MAX));
inverse
}
fn fixture() -> (Vec<u8>, Vec<u8>) {
let active_count = 512_u32;
let seed = 0x7770_0dcc;
let inverse = inverse_map(active_count, seed);
let header_size = 0x100usize;
let method_size = active_count as usize * V24_1_METHOD_STRIDE;
let mut metadata = vec![0_u8; header_size + method_size];
metadata[0..4].copy_from_slice(&MAGIC.to_le_bytes());
metadata[4..8].copy_from_slice(&RAW_VERSION_24.to_le_bytes());
metadata[HDR_METHODS..HDR_METHODS + 4].copy_from_slice(&(header_size as u32).to_le_bytes());
metadata[HDR_METHODS + 4..HDR_METHODS + 8]
.copy_from_slice(&(method_size as u32).to_le_bytes());
// Before the exception patch the first half duplicates the second half,
// leaving 0..255 missing. The module's 256-entry table fills exactly
// those first active methods after the five-round transform.
for method in 0..active_count as usize {
let transformed = 256 + (method % 256) as u32;
let protected = inverse[transformed as usize];
let offset = header_size + method * V24_1_METHOD_STRIDE + V24_1_METHOD_INDEX_OFFSET;
metadata[offset..offset + 4].copy_from_slice(&(protected as i32).to_le_bytes());
}
let mut module = vec![0_u8; 0x1000];
module[0x100..0x104].copy_from_slice(&seed.to_le_bytes());
module[0x104..0x108].copy_from_slice(&RAW_VERSION_24.to_le_bytes());
module[0x3f8..0x3fc].copy_from_slice(&V24_1_SELECTOR.to_le_bytes());
module[0x3fc..0x400].copy_from_slice(&(active_count - 1).to_le_bytes());
for value in 0..EXCEPTION_COUNT as u32 {
let offset = 0x400 + value as usize * 4;
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
(metadata, module)
}
#[test]
fn restores_v24_1_method_index_permutation() {
let (metadata, module) = fixture();
let (restored, report) = restore_method_indices_v24_1(&metadata, &module).unwrap();
assert_eq!(report.active_methods, 512);
assert_eq!(report.exception_count, 256);
assert_eq!(report.seed, "0x77700DCC");
let (offset, indices) = method_indices(&restored).unwrap();
assert_eq!(offset, 0x100);
assert!(is_complete_permutation(&indices));
for (method, value) in indices.into_iter().enumerate() {
assert_eq!(value, Some(method as u32));
}
}
#[test]
fn clean_v24_1_metadata_is_idempotent() {
let (metadata, module) = fixture();
let (restored, _) = restore_method_indices_v24_1(&metadata, &module).unwrap();
let (again, report) = restore_method_indices_v24_1(&restored, &module).unwrap();
assert_eq!(again, restored);
assert_eq!(report.changed_indices, 0);
assert_eq!(report.seed, "clean");
}
#[test]
fn rejects_unrelated_module() {
let (metadata, _) = fixture();
let error = restore_method_indices_v24_1(&metadata, &[0_u8; 0x1000]).unwrap_err();
assert_eq!(error, MethodIndexError::ProfileNotFound);
}
}
+2
View File
@@ -2,9 +2,11 @@
mod embedded; mod embedded;
mod keystream; mod keystream;
mod method_indices;
mod method_tokens; mod method_tokens;
pub use embedded::{embedded_metadata_size, extract_embedded_metadata}; pub use embedded::{embedded_metadata_size, extract_embedded_metadata};
pub use method_indices::{MethodIndexError, MethodIndexReport, restore_method_indices_v24_1};
pub use method_tokens::{ pub use method_tokens::{
DEFAULT_METHOD_TOKEN_SEED, Error, ImageKeyDiscovery, Report, SeedDiscoveryReport, DEFAULT_METHOD_TOKEN_SEED, Error, ImageKeyDiscovery, Report, SeedDiscoveryReport,
discover_method_token_seeds, restore_method_tokens, discover_method_token_seeds, restore_method_tokens,