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:
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "senbei-android-metadata"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
description = "IL2CPP metadata restoration for Senbei Android"
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,137 @@
|
||||
//! Extraction of the embedded-metadata packaging variant.
|
||||
//!
|
||||
//! Some protected il2cpp builds ship no `global-metadata.dat` in the app's
|
||||
//! assets at all. Instead a slim metadata blob (an older header format with
|
||||
//! custom record layouts) is embedded in the protected library's data section
|
||||
//! and wrapped in a per-word XOR layer: a 0x100-byte header whose 64 words each
|
||||
//! carry their own key, followed by exactly 256 segments with one u32 key each
|
||||
//! at irregular boundaries. At runtime the protector's il2cpp-side modules
|
||||
//! regenerate the keys and unwrap the blob in place; the keys are stored
|
||||
//! nowhere in the image.
|
||||
//!
|
||||
//! For the one observed build using this variant the full keystream was
|
||||
//! recovered from a ciphertext/plaintext pair and is embedded in
|
||||
//! [`crate::keystream`]. Extraction is therefore content-gated: the wrapped
|
||||
//! header's first plaintext words are known constants, so a restored image that
|
||||
//! does not contain them (every other build) is skipped cheaply and nothing is
|
||||
//! written.
|
||||
//!
|
||||
//! The unwrapped blob stores its patched sanity/version fields byte-swapped;
|
||||
//! they are rewritten to the standard il2cpp metadata magic and version so the
|
||||
//! output is a well-formed `global-metadata.dat`.
|
||||
|
||||
use crate::keystream::{HEADER_KEYS, SEGMENTS};
|
||||
|
||||
/// Standard il2cpp metadata sanity magic written over the patched header.
|
||||
const STANDARD_MAGIC: u32 = 0xfab1_1baf;
|
||||
/// Standard header version matching the blob's record layout.
|
||||
const STANDARD_VERSION: u32 = 24;
|
||||
|
||||
/// Plaintext of the first two wrapped header words (the byte-swapped patched
|
||||
/// sanity/version pair). Also the probe pattern: a restored image contains the
|
||||
/// embedded blob iff `word[0] ^ HEADER_KEYS[0]` and `word[1] ^ HEADER_KEYS[1]`
|
||||
/// equal these constants at some 4-aligned offset.
|
||||
const PROBE_WORDS: [u32; 2] = [0x9732_ca38, 0xbac4_374f];
|
||||
|
||||
/// Size of the wrapped blob: the last segment's end offset.
|
||||
pub fn embedded_metadata_size() -> usize {
|
||||
SEGMENTS[SEGMENTS.len() - 1].0 as usize
|
||||
}
|
||||
|
||||
/// Locate and unwrap the embedded metadata blob in a restored library image.
|
||||
///
|
||||
/// Returns a standalone, well-formed `global-metadata.dat`, or `None` when the
|
||||
/// image carries no blob wrapped with the known keystream.
|
||||
pub fn extract_embedded_metadata(image: &[u8]) -> Option<Vec<u8>> {
|
||||
let total = embedded_metadata_size();
|
||||
let offset = find_wrapped_header(image)?;
|
||||
let blob = image.get(offset..offset.checked_add(total)?)?;
|
||||
|
||||
let mut out = blob.to_vec();
|
||||
for (i, &key) in HEADER_KEYS.iter().enumerate() {
|
||||
xor_word(&mut out, 4 * i, key);
|
||||
}
|
||||
let mut pos = 0x100_usize;
|
||||
for &(end, key) in &SEGMENTS {
|
||||
let end = end as usize;
|
||||
let mut o = pos;
|
||||
while o + 4 <= end {
|
||||
xor_word(&mut out, o, key);
|
||||
o += 4;
|
||||
}
|
||||
pos = end;
|
||||
}
|
||||
out[0..4].copy_from_slice(&STANDARD_MAGIC.to_le_bytes());
|
||||
out[4..8].copy_from_slice(&STANDARD_VERSION.to_le_bytes());
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Scan `image` for the wrapped header probe pattern (4-aligned).
|
||||
fn find_wrapped_header(image: &[u8]) -> Option<usize> {
|
||||
let mut off = 0;
|
||||
while off + 8 <= image.len() {
|
||||
let word = u32::from_le_bytes(image[off..off + 4].try_into().ok()?);
|
||||
if word ^ HEADER_KEYS[0] == PROBE_WORDS[0] {
|
||||
let next = u32::from_le_bytes(image[off + 4..off + 8].try_into().ok()?);
|
||||
if next ^ HEADER_KEYS[1] == PROBE_WORDS[1] {
|
||||
return Some(off);
|
||||
}
|
||||
}
|
||||
off += 4;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn xor_word(data: &mut [u8], offset: usize, key: u32) {
|
||||
let word = u32::from_le_bytes(data[offset..offset + 4].try_into().expect("word in bounds"));
|
||||
data[offset..offset + 4].copy_from_slice(&(word ^ key).to_le_bytes());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Wrap a synthetic blob with the keystream, then unwrap it back.
|
||||
#[test]
|
||||
fn roundtrip_wrapped_blob() {
|
||||
let total = embedded_metadata_size();
|
||||
let mut image = vec![0_u8; total + 0x40];
|
||||
// Plaintext blob: standard probe words, then a ramp.
|
||||
image[0..4].copy_from_slice(&PROBE_WORDS[0].to_le_bytes());
|
||||
image[4..8].copy_from_slice(&PROBE_WORDS[1].to_le_bytes());
|
||||
for o in (8..total).step_by(4) {
|
||||
let v = (o as u32).wrapping_mul(0x9e37_79b1);
|
||||
image[o..o + 4].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
// Wrap with the keystream.
|
||||
for (i, &key) in HEADER_KEYS.iter().enumerate() {
|
||||
xor_word(&mut image, 4 * i, key);
|
||||
}
|
||||
let mut pos = 0x100_usize;
|
||||
for &(end, key) in &SEGMENTS {
|
||||
let mut o = pos;
|
||||
while o + 4 <= end as usize {
|
||||
xor_word(&mut image, o, key);
|
||||
o += 4;
|
||||
}
|
||||
pos = end as usize;
|
||||
}
|
||||
|
||||
let out = extract_embedded_metadata(&image).expect("blob found");
|
||||
assert_eq!(out.len(), total);
|
||||
// Header rewritten to the standard magic/version…
|
||||
assert_eq!(&out[0..4], &STANDARD_MAGIC.to_le_bytes());
|
||||
assert_eq!(&out[4..8], &STANDARD_VERSION.to_le_bytes());
|
||||
// …and the body round-trips.
|
||||
for o in (8..total).step_by(4) {
|
||||
let v = (o as u32).wrapping_mul(0x9e37_79b1);
|
||||
assert_eq!(&out[o..o + 4], &v.to_le_bytes(), "word at {o:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_blob_in_plain_data() {
|
||||
let image = vec![0xAB_u8; 0x1000];
|
||||
assert!(extract_embedded_metadata(&image).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/// Per-word XOR keystream for the embedded-metadata packaging variant,
|
||||
/// recovered from a ciphertext/plaintext pair of one observed build.
|
||||
/// Key derivation for future builds is untraced; other builds simply do
|
||||
/// not match the header probe and are left untouched.
|
||||
pub(crate) const HEADER_KEYS: [u32; 64] = [
|
||||
0x39184c70, 0xd901afd4, 0x19b98815, 0x132906ed, 0x663e8ace, 0x299b1952, 0xe5404ab8, 0xd93b331c,
|
||||
0xb67d3761, 0x42da9259, 0xc29c7a59, 0x17cb841c, 0xd0bcb9c6, 0x21db779b, 0x43874deb, 0x89bf697b,
|
||||
0x0b7f97b4, 0xbe1c59f7, 0xc653ad92, 0x8cdf4336, 0x5e0b6b68, 0x1bd4d668, 0x7250ed61, 0x31a36491,
|
||||
0xaf144dcd, 0xc1e387d0, 0x9d6df5b7, 0x78514f32, 0xc2648cbf, 0x3b8272a5, 0xd2053679, 0x4b18af77,
|
||||
0x71b9ebdd, 0x0094daaa, 0xf3adfed8, 0xc0d082bc, 0xae5e523c, 0xa8dec0be, 0x090a7784, 0x2c0483d6,
|
||||
0x95f0e8f7, 0x234de6d4, 0xa7464527, 0x3b1c531d, 0xc2b31d82, 0xe1c60be0, 0x3d65a0c2, 0x2ea7d77a,
|
||||
0x4ababadb, 0xce484b16, 0x59ab3f99, 0x10a9a463, 0x70e2f78a, 0x0ed71c9c, 0xf8996b2e, 0xff637928,
|
||||
0xf413313d, 0x77c57bf9, 0xdab41dba, 0x0cd2ccbc, 0x3b2fbde3, 0x0b19b14d, 0xd2645dbc, 0x318113d4,
|
||||
];
|
||||
|
||||
/// (segment end offset, segment key) pairs; offsets relative to blob start.
|
||||
pub(crate) const SEGMENTS: [(u32, u32); 256] = [
|
||||
(0x3915c, 0xbb5dda1a),
|
||||
(0x736b4, 0x906dbe0f),
|
||||
(0xaedc4, 0x1e4ca8bd),
|
||||
(0xc506c, 0xe603cb21),
|
||||
(0xdfe34, 0x93bec702),
|
||||
(0xe8f10, 0x9a9f429f),
|
||||
(0x139088, 0x125a8b3f),
|
||||
(0x20bd30, 0x69a6395f),
|
||||
(0x20c548, 0xca807b9a),
|
||||
(0x20c774, 0x8b8880c4),
|
||||
(0x300518, 0x48087852),
|
||||
(0x36c07c, 0x32aa7b5b),
|
||||
(0x448b7c, 0x2e668589),
|
||||
(0x4592b0, 0x292e07d9),
|
||||
(0x45d374, 0x83b0a0ef),
|
||||
(0x474520, 0x8983245d),
|
||||
(0x47a1d4, 0xefb941b7),
|
||||
(0x4bdc74, 0x7c3b3458),
|
||||
(0x4c34c8, 0xee0a87b3),
|
||||
(0x4f1068, 0xf6a2069f),
|
||||
(0x51601c, 0x2e83612b),
|
||||
(0x549d40, 0xb413a58f),
|
||||
(0x56a714, 0x95596da3),
|
||||
(0x573c98, 0x68513e8d),
|
||||
(0x59d058, 0xc4ff5f9a),
|
||||
(0x5c4b34, 0x249ed022),
|
||||
(0x5f19bc, 0xc27272d3),
|
||||
(0x5f47c8, 0xd73aa37b),
|
||||
(0x627df4, 0x002334ba),
|
||||
(0x648f54, 0x868bb6c9),
|
||||
(0x6718a4, 0x17ff0ef4),
|
||||
(0x6a88b8, 0x22cfbc5f),
|
||||
(0x742dbc, 0x152072dd),
|
||||
(0x75603c, 0xbd31be45),
|
||||
(0x783238, 0x45911d6a),
|
||||
(0x7b94d8, 0x6f281add),
|
||||
(0x800060, 0xcf58c8d0),
|
||||
(0x819b50, 0xb40f0276),
|
||||
(0x82dea4, 0x1a1a8402),
|
||||
(0x880210, 0xf2c0824a),
|
||||
(0x8e4f08, 0x86c9ba90),
|
||||
(0x8ea544, 0x0e928544),
|
||||
(0x931454, 0xc3fa017b),
|
||||
(0x94eb70, 0x1dbe612a),
|
||||
(0x95993c, 0x902498fe),
|
||||
(0x98d2b0, 0xb7760451),
|
||||
(0x992034, 0x711cddfc),
|
||||
(0x9e14a0, 0x8bd95e64),
|
||||
(0xa1d2d0, 0xbfdce920),
|
||||
(0xa21ed8, 0x90cf0372),
|
||||
(0xa4d2b8, 0x91e88c9c),
|
||||
(0xa76f8c, 0x9c721e61),
|
||||
(0xac5ba4, 0xbda16e3e),
|
||||
(0xaf070c, 0xe02b6799),
|
||||
(0xaf3a78, 0x32953b4e),
|
||||
(0xb32510, 0x47ea48db),
|
||||
(0xb46550, 0x1443e512),
|
||||
(0xb54998, 0x9e123a75),
|
||||
(0xb5e24c, 0xe11a8efd),
|
||||
(0xb625cc, 0x3facfbf4),
|
||||
(0xb66ec8, 0x76c0c452),
|
||||
(0xb67ad0, 0x4de4ed6c),
|
||||
(0xb7ba5c, 0xe622d97a),
|
||||
(0xb85a90, 0x6f564f8b),
|
||||
(0xbff8b8, 0x3e25d671),
|
||||
(0xc03e50, 0x3563fc2b),
|
||||
(0xc6e958, 0xda8bc3b0),
|
||||
(0xc87f7c, 0x5a9d2269),
|
||||
(0xcb36a4, 0x0ab420cc),
|
||||
(0xcbe4d0, 0x9bbb091e),
|
||||
(0xccd7dc, 0x9e4fd577),
|
||||
(0xd078c0, 0x4b655ae1),
|
||||
(0xd275dc, 0x5ca2a2f4),
|
||||
(0xd2c840, 0xdb437f0d),
|
||||
(0xd3296c, 0x66487f75),
|
||||
(0xd7cbe8, 0xf5427945),
|
||||
(0xd8e0a0, 0x9a65bdb6),
|
||||
(0xda2ed4, 0x46dea4b3),
|
||||
(0xda6f1c, 0xb9916a02),
|
||||
(0xdee9ac, 0x18800a5c),
|
||||
(0xe3673c, 0x4afab3cd),
|
||||
(0xe65420, 0x52e80204),
|
||||
(0xe861f8, 0x639a02d7),
|
||||
(0xeb61c0, 0x21077eba),
|
||||
(0xed51dc, 0x17be91d8),
|
||||
(0xf048a4, 0xd30cc8cb),
|
||||
(0xf3b274, 0xdfb43f3f),
|
||||
(0xf76ac8, 0x63a8b363),
|
||||
(0xf84b64, 0x16508a16),
|
||||
(0xf8bb2c, 0x22ce110d),
|
||||
(0xfb3390, 0xf09a4eb2),
|
||||
(0xff07bc, 0xd2bb0e2c),
|
||||
(0x102d32c, 0xb424012c),
|
||||
(0x10795b0, 0x07338bb9),
|
||||
(0x108d65c, 0x5d68f86e),
|
||||
(0x10ce528, 0x1826c952),
|
||||
(0x10d3528, 0xa7473860),
|
||||
(0x10dca58, 0x92435967),
|
||||
(0x1115c78, 0x061200f4),
|
||||
(0x1171098, 0x94f538a1),
|
||||
(0x117ebf0, 0xd8731d88),
|
||||
(0x1186638, 0x4381b3f9),
|
||||
(0x118afdc, 0xf25ff376),
|
||||
(0x11ee3b4, 0x29605488),
|
||||
(0x11f182c, 0x04367932),
|
||||
(0x11f41dc, 0xaeaccadd),
|
||||
(0x11ff0f4, 0x7c4d358e),
|
||||
(0x120caac, 0xacbc8412),
|
||||
(0x12437cc, 0x3e0ac7e9),
|
||||
(0x124edf8, 0x06f523fd),
|
||||
(0x1263ba0, 0x0a1b9763),
|
||||
(0x12943f0, 0x24a86ba4),
|
||||
(0x12d9230, 0x0cd82e2e),
|
||||
(0x12fd9b4, 0xf3903fb9),
|
||||
(0x135a198, 0x2887f4a3),
|
||||
(0x1366180, 0x9f0d7ca5),
|
||||
(0x13680a4, 0x61e9a459),
|
||||
(0x13a1b44, 0xe61623a4),
|
||||
(0x13a860c, 0xdc44c798),
|
||||
(0x13c024c, 0xc90f7be6),
|
||||
(0x1475c00, 0xc2f338b3),
|
||||
(0x1480aa8, 0xb7b0609e),
|
||||
(0x14fb82c, 0x748e3939),
|
||||
(0x1511184, 0x98426fcf),
|
||||
(0x153c144, 0x1a452d5d),
|
||||
(0x1547838, 0x7dd360e9),
|
||||
(0x15565d4, 0x1d8f093b),
|
||||
(0x156e298, 0x102a1524),
|
||||
(0x159df70, 0xe42613f7),
|
||||
(0x15a13d0, 0xafc5fdc6),
|
||||
(0x15e7f24, 0x84fcd342),
|
||||
(0x15f0878, 0x55038958),
|
||||
(0x1614210, 0xe0602ae4),
|
||||
(0x1631b3c, 0xce2765f6),
|
||||
(0x164eb70, 0xf772dac5),
|
||||
(0x1688b68, 0x5f1a72c9),
|
||||
(0x16d5f8c, 0x7c77747d),
|
||||
(0x16e76dc, 0xac0e16fb),
|
||||
(0x1726374, 0x4a1e7fd7),
|
||||
(0x173455c, 0x870856b4),
|
||||
(0x17697d8, 0xbb2f0a5c),
|
||||
(0x176ed60, 0xc937b386),
|
||||
(0x1784fa8, 0x5e676ab2),
|
||||
(0x17ae3a0, 0xdbf662a1),
|
||||
(0x1866c7c, 0x4e3f1a7d),
|
||||
(0x186b844, 0xe30fce60),
|
||||
(0x18b507c, 0xdfc73c88),
|
||||
(0x18c2f64, 0xb7ee08e0),
|
||||
(0x18c8010, 0xd1471a25),
|
||||
(0x18de290, 0x292e6310),
|
||||
(0x19140d0, 0x9f346f05),
|
||||
(0x192c590, 0xf1eb61bf),
|
||||
(0x194fca0, 0x8888b1df),
|
||||
(0x1959d34, 0x92b89d15),
|
||||
(0x196c0c4, 0x5e152de5),
|
||||
(0x19a5710, 0x866e7bfa),
|
||||
(0x19abfd4, 0x3084ae26),
|
||||
(0x19b1550, 0x0581836f),
|
||||
(0x19b7214, 0xeefc34eb),
|
||||
(0x19c523c, 0xc980335d),
|
||||
(0x19dc4c0, 0x019084e6),
|
||||
(0x19dfb8c, 0xdb1a21a7),
|
||||
(0x19fbf3c, 0xec84cc17),
|
||||
(0x1a29b18, 0xcb31da7d),
|
||||
(0x1a4c670, 0xc5fe570e),
|
||||
(0x1a97024, 0xbbd80964),
|
||||
(0x1ac33bc, 0xe186586d),
|
||||
(0x1acd124, 0x1e413252),
|
||||
(0x1ad9bac, 0x48fc4c75),
|
||||
(0x1b1b728, 0x8071d7a5),
|
||||
(0x1b31d78, 0x9d958013),
|
||||
(0x1badb24, 0x2f236951),
|
||||
(0x1bccc00, 0x7023c620),
|
||||
(0x1bdab2c, 0x88b1e4b8),
|
||||
(0x1c000dc, 0x9e43291a),
|
||||
(0x1c9f0cc, 0x27a7d592),
|
||||
(0x1cd1328, 0x9c0bcc88),
|
||||
(0x1cd79a0, 0x63e0ed75),
|
||||
(0x1d0e484, 0xf51a0d3d),
|
||||
(0x1d17b10, 0xbfd2a7ac),
|
||||
(0x1d930c4, 0xf6b9e877),
|
||||
(0x1db115c, 0xf3eb7e37),
|
||||
(0x1df16b4, 0x682326ff),
|
||||
(0x1e389c0, 0xea11f566),
|
||||
(0x1eb7e48, 0x3dc5fa76),
|
||||
(0x1ec38fc, 0x296ffc1d),
|
||||
(0x1ee87a0, 0x1b9f7fd4),
|
||||
(0x1f19f88, 0x78972e8f),
|
||||
(0x1f33a0c, 0x390c2deb),
|
||||
(0x1f4e0fc, 0xe05e8c6b),
|
||||
(0x1f5a718, 0x367432ae),
|
||||
(0x1f61dcc, 0x7063e58a),
|
||||
(0x1f85878, 0x21c00cea),
|
||||
(0x1fc043c, 0x2676aaaa),
|
||||
(0x1ffdb94, 0xc270eb02),
|
||||
(0x202a618, 0x3a98aed2),
|
||||
(0x2037b34, 0x115d5afc),
|
||||
(0x203d92c, 0x11bced76),
|
||||
(0x203da14, 0xf2628105),
|
||||
(0x2066014, 0x97f32700),
|
||||
(0x208a908, 0xa68e2f71),
|
||||
(0x20ab8ac, 0x1daa2a78),
|
||||
(0x20ba504, 0x73919ef6),
|
||||
(0x20e71e0, 0x0b3fd1d3),
|
||||
(0x2102278, 0x6c123def),
|
||||
(0x21166dc, 0xee354161),
|
||||
(0x2126478, 0x299493f4),
|
||||
(0x2137090, 0x05ae2007),
|
||||
(0x2148270, 0x34b52663),
|
||||
(0x21482ac, 0xe381b5b6),
|
||||
(0x21813b8, 0x94244de1),
|
||||
(0x21a41e8, 0x02c38df5),
|
||||
(0x21a8c4c, 0xf72700dd),
|
||||
(0x21abbac, 0x34c2e7b5),
|
||||
(0x21bcb24, 0x442739ad),
|
||||
(0x21cbe84, 0x6e40d22c),
|
||||
(0x21e2798, 0xdbf774d0),
|
||||
(0x21f892c, 0xe90f1e0c),
|
||||
(0x222beec, 0xa27f27f3),
|
||||
(0x22394f4, 0x7f999a4f),
|
||||
(0x22437ec, 0xf12d28f8),
|
||||
(0x22480b0, 0xf58f3a7d),
|
||||
(0x2261a0c, 0x89b28301),
|
||||
(0x22a76c8, 0x1fe501e2),
|
||||
(0x22b2018, 0xf079db5f),
|
||||
(0x22cc610, 0xaf7d17b7),
|
||||
(0x22cd4f8, 0x71c010cb),
|
||||
(0x22d016c, 0x4a8daed0),
|
||||
(0x22e1c04, 0xe1201aca),
|
||||
(0x22f9994, 0xf3f0e4ee),
|
||||
(0x2384f5c, 0x6b8a5eb1),
|
||||
(0x23d5ecc, 0x5298a9c4),
|
||||
(0x23e15b0, 0xc7bf0afb),
|
||||
(0x23e248c, 0x2d67fecf),
|
||||
(0x2407898, 0x4eef422a),
|
||||
(0x241695c, 0x33ba9ce8),
|
||||
(0x243e8b0, 0x833c1d2c),
|
||||
(0x2460b64, 0x819c96ee),
|
||||
(0x247caec, 0x0ebccbd6),
|
||||
(0x24832b4, 0xf789d4b6),
|
||||
(0x24938b8, 0x9f63baeb),
|
||||
(0x24a7c64, 0x3384e552),
|
||||
(0x24bce94, 0x7bfec208),
|
||||
(0x24bd8f4, 0x9b5260cc),
|
||||
(0x24ce8ec, 0xaf854888),
|
||||
(0x24e741c, 0xda82f062),
|
||||
(0x254401c, 0xbb1a5d5a),
|
||||
(0x25a6a64, 0x24d202d3),
|
||||
(0x2617878, 0x6ac71e5f),
|
||||
(0x2617df0, 0x0e76bd90),
|
||||
(0x268e9b8, 0x874d931c),
|
||||
(0x26a8848, 0xefefd680),
|
||||
(0x26e4f10, 0xfc2799f7),
|
||||
(0x26e93d8, 0x1930ad55),
|
||||
(0x26eea84, 0xfa2f742f),
|
||||
(0x2701f30, 0xed9b92c4),
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Static IL2CPP metadata restoration interfaces.
|
||||
|
||||
mod embedded;
|
||||
mod keystream;
|
||||
mod method_tokens;
|
||||
|
||||
pub use embedded::{embedded_metadata_size, extract_embedded_metadata};
|
||||
pub use method_tokens::{
|
||||
DEFAULT_METHOD_TOKEN_SEED, Error, ImageKeyDiscovery, Report, SeedDiscoveryReport,
|
||||
discover_method_token_seeds, restore_method_tokens,
|
||||
};
|
||||
@@ -0,0 +1,659 @@
|
||||
//! Static restoration of protected IL2CPP v31 method tokens.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Seed embedded in the current `libil2cpp` module `0x0C`.
|
||||
pub const DEFAULT_METHOD_TOKEN_SEED: u32 = 0xa6fa_e968;
|
||||
|
||||
const MAGIC: u32 = 0xfab1_1baf;
|
||||
const SUPPORTED_VERSION: 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;
|
||||
const IMAGE_STRIDE: usize = 0x28;
|
||||
const IMAGE_TYPE_START_OFFSET: usize = 0x08;
|
||||
const IMAGE_TYPE_COUNT_OFFSET: usize = 0x0c;
|
||||
const METHOD_TOKEN_TABLE: u32 = 0x0600_0000;
|
||||
|
||||
/// Summary of one metadata restoration pass.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Report {
|
||||
pub version: u32,
|
||||
pub seed: String,
|
||||
pub encryption_status: String,
|
||||
pub images: usize,
|
||||
pub images_with_methods: usize,
|
||||
pub types: usize,
|
||||
pub methods: usize,
|
||||
pub visited_methods: usize,
|
||||
pub already_correct_before: usize,
|
||||
pub correct_after: usize,
|
||||
pub changed_tokens: usize,
|
||||
pub transformed_images: usize,
|
||||
}
|
||||
|
||||
/// Per-image constraints recovered from the encrypted MethodDef RID
|
||||
/// permutation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ImageKeyDiscovery {
|
||||
pub image: usize,
|
||||
pub method_count: u32,
|
||||
pub modulus: u32,
|
||||
pub clean: bool,
|
||||
pub seed_residues: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Result of statically testing the known five-round permutation against a
|
||||
/// metadata file without assuming a seed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct SeedDiscoveryReport {
|
||||
pub version: u32,
|
||||
pub images: Vec<ImageKeyDiscovery>,
|
||||
pub seed_candidates: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Metadata parsing or validation failure.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("not an IL2CPP global-metadata.dat")]
|
||||
NotMetadata,
|
||||
#[error("unsupported metadata version {0}")]
|
||||
UnsupportedVersion(u32),
|
||||
#[error("malformed metadata: {0}")]
|
||||
Malformed(String),
|
||||
#[error("method-token restoration failed: {0}")]
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
fn malformed<T>(message: impl Into<String>) -> Result<T> {
|
||||
Err(Error::Malformed(message.into()))
|
||||
}
|
||||
|
||||
fn validation<T>(message: impl Into<String>) -> Result<T> {
|
||||
Err(Error::Validation(message.into()))
|
||||
}
|
||||
|
||||
fn bytes(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
|
||||
let end = offset
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| Error::Malformed("byte range overflow".to_owned()))?;
|
||||
data.get(offset..end).ok_or_else(|| {
|
||||
Error::Malformed(format!(
|
||||
"byte range 0x{offset:x}..0x{end:x} is out of bounds"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn read_u16(data: &[u8], offset: usize) -> Result<u16> {
|
||||
let value: [u8; 2] = bytes(data, offset, 2)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::Malformed("invalid u16 range".to_owned()))?;
|
||||
Ok(u16::from_le_bytes(value))
|
||||
}
|
||||
|
||||
fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
|
||||
let value: [u8; 4] = bytes(data, offset, 4)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::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(|_| Error::Malformed("invalid i32 range".to_owned()))?;
|
||||
Ok(i32::from_le_bytes(value))
|
||||
}
|
||||
|
||||
fn table(data: &[u8], header_offset: usize) -> Result<(usize, usize)> {
|
||||
let offset = read_u32(data, header_offset)? as usize;
|
||||
let size = read_u32(data, header_offset + 4)? as usize;
|
||||
bytes(data, offset, size)?;
|
||||
Ok((offset, size))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn inverse_round(mut value: u32, count: u32, key: u32) -> u32 {
|
||||
let mirror = count.wrapping_mul(2).wrapping_sub(1);
|
||||
if value & 1 != 0 {
|
||||
value = mirror.wrapping_sub(value);
|
||||
}
|
||||
value >>= 1;
|
||||
if value >= count {
|
||||
value = mirror.wrapping_sub(value);
|
||||
}
|
||||
let value = value.wrapping_sub(key);
|
||||
if value > count {
|
||||
value.wrapping_add(count)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_rid(rid: u32, low: u32, high: u32, seed: u32) -> Result<u32> {
|
||||
let count = high
|
||||
.checked_add(1)
|
||||
.and_then(|value| value.checked_sub(low))
|
||||
.ok_or_else(|| Error::Validation("invalid image RID interval".to_owned()))?;
|
||||
if count < 2 {
|
||||
return validation("RID inverse permutation requires at least two entries");
|
||||
}
|
||||
let half = count / 2;
|
||||
if half == 0 {
|
||||
return validation("RID inverse permutation has a zero divisor");
|
||||
}
|
||||
let key = seed % half + count / 4;
|
||||
let mut value = rid
|
||||
.checked_sub(low)
|
||||
.ok_or_else(|| Error::Validation("encrypted RID lies below image minimum".to_owned()))?;
|
||||
for _ in 0..5 {
|
||||
value = inverse_round(value, count, key);
|
||||
}
|
||||
value
|
||||
.checked_add(low)
|
||||
.ok_or_else(|| Error::Validation("restored RID overflow".to_owned()))
|
||||
}
|
||||
|
||||
/// Restore MethodDef RID values exactly as module `0x0C` does.
|
||||
///
|
||||
/// The operation is idempotent for tooling purposes: an image whose tokens are
|
||||
/// already canonical is detected and left untouched instead of applying the
|
||||
/// native inverse permutation a second time.
|
||||
pub fn restore_method_tokens(data: &[u8], seed: u32) -> Result<(Vec<u8>, Report)> {
|
||||
if read_u32(data, 0).ok() != Some(MAGIC) {
|
||||
return Err(Error::NotMetadata);
|
||||
}
|
||||
let version = read_u32(data, 4)?;
|
||||
if version != SUPPORTED_VERSION {
|
||||
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
|
||||
|| type_size % TYPE_STRIDE != 0
|
||||
|| image_size % IMAGE_STRIDE != 0
|
||||
{
|
||||
return malformed("v31 table size is not divisible by its entry stride");
|
||||
}
|
||||
let method_count = method_size / METHOD_STRIDE;
|
||||
let type_count = type_size / TYPE_STRIDE;
|
||||
let image_count = image_size / IMAGE_STRIDE;
|
||||
let mut owners = vec![u32::MAX; method_count];
|
||||
let mut output = data.to_vec();
|
||||
let mut images_with_methods = 0_usize;
|
||||
let mut visited_methods = 0_usize;
|
||||
let mut already_correct_before = 0_usize;
|
||||
let mut correct_after = 0_usize;
|
||||
let mut changed_tokens = 0_usize;
|
||||
let mut transformed_images = 0_usize;
|
||||
|
||||
for image_index in 0..image_count {
|
||||
let image_base = image_offset + image_index * IMAGE_STRIDE;
|
||||
let type_start = read_i32(data, image_base + IMAGE_TYPE_START_OFFSET)?;
|
||||
let type_start = usize::try_from(type_start)
|
||||
.map_err(|_| Error::Malformed(format!("image {image_index} has negative typeStart")))?;
|
||||
let type_entries = read_u32(data, image_base + IMAGE_TYPE_COUNT_OFFSET)? as usize;
|
||||
let type_end = type_start
|
||||
.checked_add(type_entries)
|
||||
.ok_or_else(|| Error::Malformed("image type range overflow".to_owned()))?;
|
||||
if type_end > type_count {
|
||||
return malformed(format!("image {image_index} type range exceeds the table"));
|
||||
}
|
||||
|
||||
let mut methods = Vec::new();
|
||||
for type_index in type_start..type_end {
|
||||
let type_base = type_offset + type_index * TYPE_STRIDE;
|
||||
let method_entries = read_u16(data, type_base + TYPE_METHOD_COUNT_OFFSET)? as usize;
|
||||
if method_entries == 0 {
|
||||
continue;
|
||||
}
|
||||
let method_start = read_i32(data, type_base + TYPE_METHOD_START_OFFSET)?;
|
||||
let method_start = usize::try_from(method_start).map_err(|_| {
|
||||
Error::Malformed(format!(
|
||||
"type {type_index} has methods but negative methodStart"
|
||||
))
|
||||
})?;
|
||||
let method_end = method_start
|
||||
.checked_add(method_entries)
|
||||
.ok_or_else(|| Error::Malformed("type method range overflow".to_owned()))?;
|
||||
if method_end > method_count {
|
||||
return malformed(format!("type {type_index} method range exceeds the table"));
|
||||
}
|
||||
for (method_index, owner) in owners
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.take(method_end)
|
||||
.skip(method_start)
|
||||
{
|
||||
if *owner != u32::MAX {
|
||||
return malformed(format!("method {method_index} belongs to multiple images"));
|
||||
}
|
||||
*owner = u32::try_from(image_index)
|
||||
.map_err(|_| Error::Malformed("image index exceeds u32".to_owned()))?;
|
||||
methods.push(method_index);
|
||||
}
|
||||
}
|
||||
if methods.is_empty() {
|
||||
continue;
|
||||
}
|
||||
images_with_methods += 1;
|
||||
visited_methods += methods.len();
|
||||
let method_base = *methods
|
||||
.iter()
|
||||
.min()
|
||||
.ok_or_else(|| Error::Malformed("nonempty image lost its method minimum".to_owned()))?;
|
||||
let method_last = *methods
|
||||
.iter()
|
||||
.max()
|
||||
.ok_or_else(|| Error::Malformed("nonempty image lost its method maximum".to_owned()))?;
|
||||
if method_last - method_base + 1 != methods.len() {
|
||||
return malformed(format!(
|
||||
"image {image_index} method block is not contiguous"
|
||||
));
|
||||
}
|
||||
|
||||
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 = read_u32(data, token_offset)?;
|
||||
if token & 0xff00_0000 != METHOD_TOKEN_TABLE {
|
||||
return malformed(format!(
|
||||
"method {method_index} has non-MethodDef token 0x{token:08x}"
|
||||
));
|
||||
}
|
||||
let expected = u32::try_from(method_index - method_base + 1)
|
||||
.map_err(|_| Error::Validation("local method RID exceeds u32".to_owned()))?;
|
||||
let rid = token & 0x00ff_ffff;
|
||||
if rid == expected {
|
||||
already_correct_before += 1;
|
||||
} else {
|
||||
image_already_clean = false;
|
||||
}
|
||||
tokens.push((method_index, token_offset, token, expected));
|
||||
}
|
||||
|
||||
if image_already_clean {
|
||||
correct_after += tokens.len();
|
||||
continue;
|
||||
}
|
||||
transformed_images += 1;
|
||||
let low = tokens
|
||||
.iter()
|
||||
.map(|(_, _, token, _)| token & 0x00ff_ffff)
|
||||
.min()
|
||||
.ok_or_else(|| Error::Validation("image has no MethodDef RID".to_owned()))?;
|
||||
let high = tokens
|
||||
.iter()
|
||||
.map(|(_, _, token, _)| token & 0x00ff_ffff)
|
||||
.max()
|
||||
.ok_or_else(|| Error::Validation("image has no MethodDef RID".to_owned()))?;
|
||||
if high <= 1 {
|
||||
return validation(format!(
|
||||
"image {image_index} is noncanonical but native R > 1 gate would skip it"
|
||||
));
|
||||
}
|
||||
let interval = high - low + 1;
|
||||
if interval as usize != tokens.len() {
|
||||
return validation(format!(
|
||||
"image {image_index} RID interval {low}..={high} is not a permutation"
|
||||
));
|
||||
}
|
||||
for (method_index, token_offset, token, expected) in tokens {
|
||||
let restored_rid = decrypt_rid(token & 0x00ff_ffff, low, high, seed)?;
|
||||
if restored_rid != expected {
|
||||
return validation(format!(
|
||||
"method {method_index} restored RID {restored_rid} != expected {expected}"
|
||||
));
|
||||
}
|
||||
let restored_token = METHOD_TOKEN_TABLE | restored_rid;
|
||||
if restored_token != token {
|
||||
output[token_offset..token_offset + 4]
|
||||
.copy_from_slice(&restored_token.to_le_bytes());
|
||||
changed_tokens += 1;
|
||||
}
|
||||
correct_after += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if owners.contains(&u32::MAX) {
|
||||
return malformed("one or more method definitions are not owned by an image");
|
||||
}
|
||||
if visited_methods != method_count || correct_after != method_count {
|
||||
return validation(format!(
|
||||
"method coverage mismatch: visited={visited_methods}, correct={correct_after}, total={method_count}"
|
||||
));
|
||||
}
|
||||
|
||||
Ok((
|
||||
output,
|
||||
Report {
|
||||
version,
|
||||
seed: format!("0x{seed:08X}"),
|
||||
encryption_status: if changed_tokens == 0 {
|
||||
"clean".to_owned()
|
||||
} else {
|
||||
"encrypted".to_owned()
|
||||
},
|
||||
images: image_count,
|
||||
images_with_methods,
|
||||
types: type_count,
|
||||
methods: method_count,
|
||||
visited_methods,
|
||||
already_correct_before,
|
||||
correct_after,
|
||||
changed_tokens,
|
||||
transformed_images,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Discover seeds compatible with the known v31 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
|
||||
/// domain. An empty candidate list means that the sample changed the
|
||||
/// permutation itself rather than merely embedding a different seed.
|
||||
pub fn discover_method_token_seeds(data: &[u8]) -> Result<SeedDiscoveryReport> {
|
||||
if read_u32(data, 0).ok() != Some(MAGIC) {
|
||||
return Err(Error::NotMetadata);
|
||||
}
|
||||
let version = read_u32(data, 4)?;
|
||||
if version != SUPPORTED_VERSION {
|
||||
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
|
||||
|| type_size % TYPE_STRIDE != 0
|
||||
|| image_size % IMAGE_STRIDE != 0
|
||||
{
|
||||
return malformed("v31 table size is not divisible by its entry stride");
|
||||
}
|
||||
let method_count = method_size / METHOD_STRIDE;
|
||||
let type_count = type_size / TYPE_STRIDE;
|
||||
let image_count = image_size / IMAGE_STRIDE;
|
||||
let mut reports = Vec::with_capacity(image_count);
|
||||
for image_index in 0..image_count {
|
||||
let image_base = image_offset + image_index * IMAGE_STRIDE;
|
||||
let type_start = usize::try_from(read_i32(data, image_base + IMAGE_TYPE_START_OFFSET)?)
|
||||
.map_err(|_| Error::Malformed(format!("image {image_index} has negative typeStart")))?;
|
||||
let type_entries = read_u32(data, image_base + IMAGE_TYPE_COUNT_OFFSET)? as usize;
|
||||
let type_end = type_start
|
||||
.checked_add(type_entries)
|
||||
.ok_or_else(|| Error::Malformed("image type range overflow".to_owned()))?;
|
||||
if type_end > type_count {
|
||||
return malformed(format!("image {image_index} type range exceeds the table"));
|
||||
}
|
||||
let mut methods = Vec::new();
|
||||
for type_index in type_start..type_end {
|
||||
let type_base = type_offset + type_index * TYPE_STRIDE;
|
||||
let method_entries = read_u16(data, type_base + TYPE_METHOD_COUNT_OFFSET)? as usize;
|
||||
if method_entries == 0 {
|
||||
continue;
|
||||
}
|
||||
let method_start =
|
||||
usize::try_from(read_i32(data, type_base + TYPE_METHOD_START_OFFSET)?).map_err(
|
||||
|_| Error::Malformed(format!("type {type_index} has negative methodStart")),
|
||||
)?;
|
||||
let method_end = method_start
|
||||
.checked_add(method_entries)
|
||||
.ok_or_else(|| Error::Malformed("type method range overflow".to_owned()))?;
|
||||
if method_end > method_count {
|
||||
return malformed(format!("type {type_index} method range exceeds the table"));
|
||||
}
|
||||
methods.extend(method_start..method_end);
|
||||
}
|
||||
if methods.is_empty() {
|
||||
reports.push(ImageKeyDiscovery {
|
||||
image: image_index,
|
||||
method_count: 0,
|
||||
modulus: 0,
|
||||
clean: true,
|
||||
seed_residues: Vec::new(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let method_base = *methods
|
||||
.iter()
|
||||
.min()
|
||||
.ok_or_else(|| Error::Malformed("image method minimum is missing".to_owned()))?;
|
||||
let method_last = *methods
|
||||
.iter()
|
||||
.max()
|
||||
.ok_or_else(|| Error::Malformed("image method maximum is missing".to_owned()))?;
|
||||
if method_last - method_base + 1 != methods.len() {
|
||||
return validation(format!(
|
||||
"image {image_index} method block is not contiguous"
|
||||
));
|
||||
}
|
||||
let mut values = Vec::with_capacity(methods.len());
|
||||
let mut clean = true;
|
||||
for method_index in methods {
|
||||
let token = read_u32(
|
||||
data,
|
||||
method_offset + method_index * METHOD_STRIDE + METHOD_TOKEN_OFFSET,
|
||||
)?;
|
||||
if token & 0xff00_0000 != METHOD_TOKEN_TABLE {
|
||||
return validation(format!(
|
||||
"method {method_index} has non-MethodDef token 0x{token:08x}"
|
||||
));
|
||||
}
|
||||
let expected = u32::try_from(method_index - method_base + 1)
|
||||
.map_err(|_| Error::Validation("local method RID exceeds u32".to_owned()))?;
|
||||
let rid = token & 0x00ff_ffff;
|
||||
clean &= rid == expected;
|
||||
values.push((rid, expected));
|
||||
}
|
||||
let count = u32::try_from(values.len())
|
||||
.map_err(|_| Error::Validation("image method count exceeds u32".to_owned()))?;
|
||||
if clean {
|
||||
reports.push(ImageKeyDiscovery {
|
||||
image: image_index,
|
||||
method_count: count,
|
||||
modulus: count / 2,
|
||||
clean,
|
||||
seed_residues: Vec::new(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let low = values
|
||||
.iter()
|
||||
.map(|(rid, _)| *rid)
|
||||
.min()
|
||||
.ok_or_else(|| Error::Validation("image has no encrypted RID".to_owned()))?;
|
||||
let high = values
|
||||
.iter()
|
||||
.map(|(rid, _)| *rid)
|
||||
.max()
|
||||
.ok_or_else(|| Error::Validation("image has no encrypted RID".to_owned()))?;
|
||||
if high - low + 1 != count || count < 2 {
|
||||
return validation(format!(
|
||||
"image {image_index} RID interval is not a permutation"
|
||||
));
|
||||
}
|
||||
let half = count / 2;
|
||||
let quarter = count / 4;
|
||||
let mut residues = Vec::new();
|
||||
for key_delta in 0..half {
|
||||
let key = quarter + key_delta;
|
||||
let valid = values
|
||||
.iter()
|
||||
.all(|(rid, expected)| decrypt_rid_with_key(*rid, low, high, key) == *expected);
|
||||
if valid {
|
||||
residues.push(key_delta);
|
||||
}
|
||||
}
|
||||
reports.push(ImageKeyDiscovery {
|
||||
image: image_index,
|
||||
method_count: count,
|
||||
modulus: half,
|
||||
clean,
|
||||
seed_residues: residues,
|
||||
});
|
||||
}
|
||||
|
||||
let constraints = reports
|
||||
.iter()
|
||||
.filter(|report| !report.clean)
|
||||
.collect::<Vec<_>>();
|
||||
let mut seeds = Vec::new();
|
||||
if let Some(anchor) = constraints.iter().max_by_key(|report| report.modulus) {
|
||||
for &residue in &anchor.seed_residues {
|
||||
let mut candidate = u64::from(residue);
|
||||
let modulus = u64::from(anchor.modulus);
|
||||
while candidate <= u64::from(u32::MAX) {
|
||||
let valid = constraints.iter().all(|report| {
|
||||
report.modulus != 0
|
||||
&& !report.seed_residues.is_empty()
|
||||
&& report
|
||||
.seed_residues
|
||||
.iter()
|
||||
.any(|&value| candidate % u64::from(report.modulus) == u64::from(value))
|
||||
});
|
||||
if valid {
|
||||
seeds.push(candidate as u32);
|
||||
}
|
||||
candidate = candidate.saturating_add(modulus);
|
||||
}
|
||||
}
|
||||
}
|
||||
seeds.sort_unstable();
|
||||
seeds.dedup();
|
||||
Ok(SeedDiscoveryReport {
|
||||
version,
|
||||
images: reports,
|
||||
seed_candidates: seeds,
|
||||
})
|
||||
}
|
||||
|
||||
fn decrypt_rid_with_key(rid: u32, low: u32, high: u32, key: u32) -> u32 {
|
||||
let count = high - low + 1;
|
||||
let mut value = rid - low;
|
||||
for _ in 0..5 {
|
||||
value = inverse_round(value, count, key);
|
||||
}
|
||||
value + low
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn put_u16(data: &mut [u8], offset: usize, value: u16) {
|
||||
data[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
fn put_u32(data: &mut [u8], offset: usize, value: u32) {
|
||||
data[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
fn encrypted_rid(expected: u32, count: u32, seed: u32) -> u32 {
|
||||
(1..=count)
|
||||
.find(|&candidate| decrypt_rid(candidate, 1, count, seed) == Ok(expected))
|
||||
.expect("inverse permutation must be bijective")
|
||||
}
|
||||
|
||||
fn build(tokens: &[u32]) -> (Vec<u8>, usize) {
|
||||
let header_size = 0x100;
|
||||
let images = header_size;
|
||||
let types = images + IMAGE_STRIDE;
|
||||
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, HDR_METHODS, methods as u32);
|
||||
put_u32(
|
||||
&mut data,
|
||||
HDR_METHODS + 4,
|
||||
(tokens.len() * 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);
|
||||
// Deliberately traverse the high method indices first.
|
||||
put_u32(&mut data, types + TYPE_METHOD_START_OFFSET, 4);
|
||||
put_u16(&mut data, types + TYPE_METHOD_COUNT_OFFSET, 3);
|
||||
put_u32(&mut data, types + TYPE_STRIDE + TYPE_METHOD_START_OFFSET, 0);
|
||||
put_u16(&mut data, types + TYPE_STRIDE + TYPE_METHOD_COUNT_OFFSET, 4);
|
||||
for (index, &token) in tokens.iter().enumerate() {
|
||||
put_u32(
|
||||
&mut data,
|
||||
methods + index * METHOD_STRIDE + METHOD_TOKEN_OFFSET,
|
||||
token,
|
||||
);
|
||||
}
|
||||
(data, methods)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restores_five_round_permutation_by_physical_method_index() {
|
||||
let tokens = (1..=7)
|
||||
.map(|expected| {
|
||||
METHOD_TOKEN_TABLE | encrypted_rid(expected, 7, DEFAULT_METHOD_TOKEN_SEED)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let (data, methods) = build(&tokens);
|
||||
let (restored, report) =
|
||||
restore_method_tokens(&data, DEFAULT_METHOD_TOKEN_SEED).expect("restore");
|
||||
assert_eq!(report.encryption_status, "encrypted");
|
||||
assert!(report.changed_tokens > 0);
|
||||
assert_eq!(report.correct_after, 7);
|
||||
for index in 0..7 {
|
||||
assert_eq!(
|
||||
read_u32(
|
||||
&restored,
|
||||
methods + index * METHOD_STRIDE + METHOD_TOKEN_OFFSET
|
||||
)
|
||||
.expect("token"),
|
||||
METHOD_TOKEN_TABLE | (index as u32 + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_metadata_is_idempotent() {
|
||||
let tokens = (1..=7)
|
||||
.map(|rid| METHOD_TOKEN_TABLE | rid)
|
||||
.collect::<Vec<_>>();
|
||||
let (data, _) = build(&tokens);
|
||||
let (restored, report) =
|
||||
restore_method_tokens(&data, DEFAULT_METHOD_TOKEN_SEED).expect("restore");
|
||||
assert_eq!(report.encryption_status, "clean");
|
||||
assert_eq!(report.changed_tokens, 0);
|
||||
assert_eq!(restored, data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_metadata_rejects_the_wrong_seed() {
|
||||
let tokens = (1..=7)
|
||||
.map(|expected| {
|
||||
METHOD_TOKEN_TABLE | encrypted_rid(expected, 7, DEFAULT_METHOD_TOKEN_SEED)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let (data, _) = build(&tokens);
|
||||
let wrong_seed = DEFAULT_METHOD_TOKEN_SEED.wrapping_add(1);
|
||||
|
||||
assert!(matches!(
|
||||
restore_method_tokens(&data, wrong_seed),
|
||||
Err(Error::Validation(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user