From 4d73406ab1bdeba09dd1d91978137004c1c255c0 Mon Sep 17 00:00:00 2001 From: bfloat16 Date: Mon, 7 Sep 2026 00:43:34 +0800 Subject: [PATCH] fix(metadata): support Android v29 method layouts --- senbei-io/src/android/mod.rs | 2 +- senbei-metadata/src/android/method_tokens.rs | 107 +++++++++++++++---- 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/senbei-io/src/android/mod.rs b/senbei-io/src/android/mod.rs index e28fa09..5200579 100644 --- a/senbei-io/src/android/mod.rs +++ b/senbei-io/src/android/mod.rs @@ -135,7 +135,7 @@ pub fn content_identity(data: &[u8]) -> String { /// already-clean blob. pub fn restore_metadata_bytes(data: &[u8]) -> anyhow::Result<(Vec, senbei_metadata::Report)> { if let Ok(discovery) = senbei_metadata::android::discover_method_token_seeds(data) - && matches!(discovery.version, 31 | 39) + && matches!(discovery.version, 29 | 31 | 39) { let mut seeds = discovery.seed_candidates.clone(); if seeds.is_empty() { diff --git a/senbei-metadata/src/android/method_tokens.rs b/senbei-metadata/src/android/method_tokens.rs index 0c1ce72..61833bd 100644 --- a/senbei-metadata/src/android/method_tokens.rs +++ b/senbei-metadata/src/android/method_tokens.rs @@ -1,4 +1,4 @@ -//! Static restoration of protected IL2CPP v31 method tokens. +//! Static restoration of protected IL2CPP method tokens for verified layouts. use serde::Serialize; @@ -6,12 +6,11 @@ use serde::Serialize; pub const DEFAULT_METHOD_TOKEN_SEED: u32 = 0xa6fa_e968; const MAGIC: u32 = 0xfab1_1baf; -const SUPPORTED_VERSION: u32 = 31; +const SUPPORTED_V29: u32 = 29; +const SUPPORTED_V31: u32 = 31; const HDR_METHODS: usize = 0x30; const HDR_TYPES: usize = 0xa0; const HDR_IMAGES: usize = 0xa8; -const METHOD_STRIDE: usize = 0x24; -const METHOD_TOKEN_OFFSET: usize = 0x18; const TYPE_STRIDE: usize = 0x58; const TYPE_METHOD_START_OFFSET: usize = 0x24; const TYPE_METHOD_COUNT_OFFSET: usize = 0x40; @@ -20,6 +19,33 @@ const IMAGE_TYPE_START_OFFSET: usize = 0x08; const IMAGE_TYPE_COUNT_OFFSET: usize = 0x0c; const METHOD_TOKEN_TABLE: u32 = 0x0600_0000; +// The aliases keep the v31 synthetic fixtures readable; production paths use +// the version-specific layout returned by `layout_for_version`. +#[cfg(test)] +const METHOD_STRIDE: usize = 0x24; +#[cfg(test)] +const METHOD_TOKEN_OFFSET: usize = 0x18; + +#[derive(Clone, Copy)] +struct Layout { + method_stride: usize, + method_token_offset: usize, +} + +fn layout_for_version(version: u32) -> Option { + match version { + SUPPORTED_V29 => Some(Layout { + method_stride: 0x20, + method_token_offset: 0x14, + }), + SUPPORTED_V31 => Some(Layout { + method_stride: 0x24, + method_token_offset: 0x18, + }), + _ => None, + } +} + /// Summary of one metadata restoration pass. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Report { @@ -174,20 +200,20 @@ pub fn restore_method_tokens(data: &[u8], seed: u32) -> Result<(Vec, Report) if version == 39 { return restore_v39(data, seed); } - if version != SUPPORTED_VERSION { + let Some(layout) = layout_for_version(version) else { return Err(Error::UnsupportedVersion(version)); - } + }; let (method_offset, method_size) = table(data, HDR_METHODS)?; let (type_offset, type_size) = table(data, HDR_TYPES)?; let (image_offset, image_size) = table(data, HDR_IMAGES)?; - if method_size % METHOD_STRIDE != 0 + if method_size % layout.method_stride != 0 || type_size % TYPE_STRIDE != 0 || image_size % IMAGE_STRIDE != 0 { - return malformed("v31 table size is not divisible by its entry stride"); + return malformed("method/type/image table size is not divisible by its entry stride"); } - let method_count = method_size / METHOD_STRIDE; + let method_count = method_size / layout.method_stride; let type_count = type_size / TYPE_STRIDE; let image_count = image_size / IMAGE_STRIDE; let mut owners = vec![u32::MAX; method_count]; @@ -267,7 +293,8 @@ pub fn restore_method_tokens(data: &[u8], seed: u32) -> Result<(Vec, Report) let mut tokens = Vec::with_capacity(methods.len()); let mut image_already_clean = true; for &method_index in &methods { - let token_offset = method_offset + method_index * METHOD_STRIDE + METHOD_TOKEN_OFFSET; + let token_offset = + method_offset + method_index * layout.method_stride + layout.method_token_offset; let token = read_u32(data, token_offset)?; if token & 0xff00_0000 != METHOD_TOKEN_TABLE { return malformed(format!( @@ -360,7 +387,7 @@ pub fn restore_method_tokens(data: &[u8], seed: u32) -> Result<(Vec, Report) )) } -/// Discover seeds compatible with the known v31 five-round RID permutation. +/// Discover seeds compatible with the known five-round RID permutation. /// /// This is diagnostic and does not modify metadata. It enumerates the only /// possible per-image key residues and intersects them over the 32-bit seed @@ -374,23 +401,23 @@ pub fn discover_method_token_seeds(data: &[u8]) -> Result { if version == 39 { return discover_v39(data); } - if version != SUPPORTED_VERSION { + let Some(layout) = layout_for_version(version) else { return Ok(SeedDiscoveryReport { version, images: Vec::new(), seed_candidates: Vec::new(), }); - } + }; let (method_offset, method_size) = table(data, HDR_METHODS)?; let (type_offset, type_size) = table(data, HDR_TYPES)?; let (image_offset, image_size) = table(data, HDR_IMAGES)?; - if method_size % METHOD_STRIDE != 0 + if method_size % layout.method_stride != 0 || type_size % TYPE_STRIDE != 0 || image_size % IMAGE_STRIDE != 0 { - return malformed("v31 table size is not divisible by its entry stride"); + return malformed("method/type/image table size is not divisible by its entry stride"); } - let method_count = method_size / METHOD_STRIDE; + let method_count = method_size / layout.method_stride; let type_count = type_size / TYPE_STRIDE; let image_count = image_size / IMAGE_STRIDE; let mut reports = Vec::with_capacity(image_count); @@ -452,7 +479,7 @@ pub fn discover_method_token_seeds(data: &[u8]) -> Result { for method_index in methods { let token = read_u32( data, - method_offset + method_index * METHOD_STRIDE + METHOD_TOKEN_OFFSET, + method_offset + method_index * layout.method_stride + layout.method_token_offset, )?; if token & 0xff00_0000 != METHOD_TOKEN_TABLE { return validation(format!( @@ -1033,7 +1060,7 @@ mod tests { let methods = types + 2 * TYPE_STRIDE; let mut data = vec![0_u8; methods + tokens.len() * METHOD_STRIDE]; put_u32(&mut data, 0, MAGIC); - put_u32(&mut data, 4, SUPPORTED_VERSION); + put_u32(&mut data, 4, SUPPORTED_V31); put_u32(&mut data, HDR_METHODS, methods as u32); put_u32( &mut data, @@ -1086,6 +1113,50 @@ mod tests { } } + #[test] + fn restores_v29_method_tokens_with_legacy_method_layout() { + let method_stride = 0x20; + let method_token_offset = 0x14; + let hdr = 0x100usize; + let images = hdr; + let types = images + IMAGE_STRIDE; + let methods = types + 2 * TYPE_STRIDE; + let mut data = vec![0_u8; methods + 7 * method_stride]; + put_u32(&mut data, 0, MAGIC); + put_u32(&mut data, 4, SUPPORTED_V29); + put_u32(&mut data, HDR_METHODS, methods as u32); + put_u32(&mut data, HDR_METHODS + 4, (7 * method_stride) as u32); + put_u32(&mut data, HDR_TYPES, types as u32); + put_u32(&mut data, HDR_TYPES + 4, (2 * TYPE_STRIDE) as u32); + put_u32(&mut data, HDR_IMAGES, images as u32); + put_u32(&mut data, HDR_IMAGES + 4, IMAGE_STRIDE as u32); + put_u32(&mut data, images + IMAGE_TYPE_START_OFFSET, 0); + put_u32(&mut data, images + IMAGE_TYPE_COUNT_OFFSET, 2); + put_u32(&mut data, types + TYPE_METHOD_START_OFFSET, 0); + put_u16(&mut data, types + TYPE_METHOD_COUNT_OFFSET, 3); + put_u32(&mut data, types + TYPE_STRIDE + TYPE_METHOD_START_OFFSET, 3); + put_u16(&mut data, types + TYPE_STRIDE + TYPE_METHOD_COUNT_OFFSET, 4); + for expected in 1..=7 { + let encrypted = encrypted_rid(expected, 7, DEFAULT_METHOD_TOKEN_SEED); + put_u32( + &mut data, + methods + (expected as usize - 1) * method_stride + method_token_offset, + METHOD_TOKEN_TABLE | encrypted, + ); + } + let (restored, report) = + restore_method_tokens(&data, DEFAULT_METHOD_TOKEN_SEED).expect("v29 restore"); + assert_eq!(report.version, SUPPORTED_V29); + assert_eq!(report.changed_tokens, 7); + for expected in 1..=7 { + let offset = methods + (expected as usize - 1) * method_stride + method_token_offset; + assert_eq!( + read_u32(&restored, offset).unwrap(), + METHOD_TOKEN_TABLE | expected + ); + } + } + #[test] fn clean_metadata_is_idempotent() { let tokens = (1..=7)