mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
feat: add static Android il2cpp restoration
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+390
@@ -0,0 +1,390 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.189"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap2"
|
||||||
|
version = "0.9.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "senbei-android-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"senbei-android-io",
|
||||||
|
"senbei-android-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "senbei-android-crypto"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "senbei-android-elf"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"memmap2",
|
||||||
|
"senbei-android-crypto",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "senbei-android-io"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"senbei-android-elf",
|
||||||
|
"senbei-android-metadata",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "senbei-android-metadata"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"senbei-android-cli",
|
||||||
|
"senbei-android-crypto",
|
||||||
|
"senbei-android-elf",
|
||||||
|
"senbei-android-io",
|
||||||
|
"senbei-android-metadata",
|
||||||
|
]
|
||||||
|
default-members = ["senbei-android-cli"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.85"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
aes = "0.8"
|
||||||
|
anyhow = "1"
|
||||||
|
memmap2 = "0.9"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
sha2 = "0.10"
|
||||||
|
tempfile = "3"
|
||||||
|
thiserror = "2"
|
||||||
|
|
||||||
|
senbei-android-crypto = { path = "senbei-android-crypto" }
|
||||||
|
senbei-android-elf = { path = "senbei-android-elf" }
|
||||||
|
senbei-android-io = { path = "senbei-android-io" }
|
||||||
|
senbei-android-metadata = { path = "senbei-android-metadata" }
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
unsafe_op_in_unsafe_fn = "deny"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
correctness = { level = "deny", priority = -1 }
|
||||||
|
suspicious = { level = "warn", priority = -1 }
|
||||||
|
complexity = { level = "warn", priority = -1 }
|
||||||
|
perf = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Senbei Android
|
||||||
|
|
||||||
|
用于静态还原 Android 版受保护的 `libil2cpp.so` 和 IL2CPP
|
||||||
|
`global-metadata.dat`。生产路径已经完全 Rust 化,不执行保护器代码,也不依赖
|
||||||
|
Unicorn、IDA 或 Python。
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
| 游戏 | 平台 | 版本 | 架构 | libil2cpp.so | global-metadata.dat |
|
||||||
|
|------|------|------|------|--------------|---------------------|
|
||||||
|
| リバースブルー×リバースエンド | Android | 1.28.2 | AArch64 | 支持 | v31 MethodDef token |
|
||||||
|
|
||||||
|
当前 SO 实现针对该版本的 Stage 2 模块格式,运行时会从模块产物中发现
|
||||||
|
`0x9B` 的种子、AES-256 key schedule 和相关配置,不硬编码样本 offset。
|
||||||
|
metadata 默认使用模块 `0x0C` 中确认的 seed `0xA6FAE968`。
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的程序为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target\release\senbei-android.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 还原 libil2cpp.so
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
senbei-android restore-so INPUT OUTPUT --index INDEX_JSON --report REPORT_JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
senbei-android restore-so `
|
||||||
|
Native\libil2cpp.so `
|
||||||
|
Native\libil2cpp_restored.so `
|
||||||
|
--index Native\libil2cpp_stage2_modules\index.json `
|
||||||
|
--report Native\libil2cpp_restore_report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
省略 `--index` 时,默认读取输入文件同目录下的:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libil2cpp_stage2_modules\index.json
|
||||||
|
```
|
||||||
|
|
||||||
|
可选参数:
|
||||||
|
|
||||||
|
- `--dump-aux FILE`:保存解码后的辅助 ELF 数据。
|
||||||
|
- `--outer-only`:只还原主容器,不物化辅助动态链接表。
|
||||||
|
- `--preserve-entrypoint`:保留保护器入口点;正常干净输出不应使用此项。
|
||||||
|
|
||||||
|
完整还原会静态处理 `0x9B/0x9D/0x9E` 数据,恢复 ELF load image、隐藏动态
|
||||||
|
符号、字符串、SysV/GNU hash、version、`.rela.dyn` 和 `.rela.plt`,移除
|
||||||
|
`SHT_LOUSER` 私有区并将入口点归零。
|
||||||
|
|
||||||
|
## 还原 metadata
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
senbei-android restore-metadata INPUT OUTPUT --report REPORT_JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
senbei-android restore-metadata `
|
||||||
|
Package\base\assets\bin\Data\Managed\Metadata\global-metadata.dat `
|
||||||
|
Package\base\assets\bin\Data\Managed\Metadata\global-metadata_restored.dat `
|
||||||
|
--report metadata_restore_report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
可用 `--seed 0xA6FAE968` 显式指定十六进制 seed,也支持十进制。还原操作是
|
||||||
|
幂等的:已规范化的 image 会保持不变。
|
||||||
|
|
||||||
|
## Workspace
|
||||||
|
|
||||||
|
| Crate | 职责 |
|
||||||
|
|-------|------|
|
||||||
|
| `senbei-android-cli` | 命令行参数解析与结果输出 |
|
||||||
|
| `senbei-android-io` | 路径推导、原地覆盖保护、原子写入与 JSON 报告 |
|
||||||
|
| `senbei-android-elf` | AArch64 ELF 还原与结构验证 |
|
||||||
|
| `senbei-android-crypto` | `0x9B/0x9D` 容器、AES、Huffman/LZ 和字变换 |
|
||||||
|
| `senbei-android-metadata` | v31 MethodDef token 静态逆变换与覆盖验证 |
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "senbei-android-cli"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "Command-line entry point for Senbei Android"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "senbei-android"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
senbei-android-io.workspace = true
|
||||||
|
senbei-android-metadata.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use senbei_android_io::{RestoreMetadataJob, RestoreSoJob, run_restore_metadata, run_restore_so};
|
||||||
|
|
||||||
|
fn main() -> std::process::ExitCode {
|
||||||
|
match run(std::env::args_os().skip(1)) {
|
||||||
|
Ok(()) => std::process::ExitCode::SUCCESS,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("error: {error:#}");
|
||||||
|
std::process::ExitCode::from(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(args: impl Iterator<Item = OsString>) -> Result<()> {
|
||||||
|
let mut args = args.peekable();
|
||||||
|
let Some(command) = args.next() else {
|
||||||
|
print_help();
|
||||||
|
bail!("missing command");
|
||||||
|
};
|
||||||
|
let command = command.to_string_lossy();
|
||||||
|
match command.as_ref() {
|
||||||
|
"restore-so" => restore_so(args.collect()),
|
||||||
|
"restore-metadata" => restore_metadata(args.collect()),
|
||||||
|
"-h" | "--help" => {
|
||||||
|
print_help();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
"-V" | "--version" => {
|
||||||
|
println!("senbei-android {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => bail!("unknown command `{command}`; use --help for usage"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_so(args: Vec<OsString>) -> Result<()> {
|
||||||
|
let mut positional = Vec::new();
|
||||||
|
let mut index = None;
|
||||||
|
let mut report = None;
|
||||||
|
let mut dump_auxiliary = None;
|
||||||
|
let mut outer_only = false;
|
||||||
|
let mut preserve_entrypoint = false;
|
||||||
|
let mut cursor = 0;
|
||||||
|
while cursor < args.len() {
|
||||||
|
match args[cursor].to_string_lossy().as_ref() {
|
||||||
|
"--index" => index = Some(option_path(&args, &mut cursor, "--index")?),
|
||||||
|
"--report" => report = Some(option_path(&args, &mut cursor, "--report")?),
|
||||||
|
"--dump-aux" => {
|
||||||
|
dump_auxiliary = Some(option_path(&args, &mut cursor, "--dump-aux")?);
|
||||||
|
}
|
||||||
|
"--outer-only" => outer_only = true,
|
||||||
|
"--preserve-entrypoint" => preserve_entrypoint = true,
|
||||||
|
"-h" | "--help" => {
|
||||||
|
print_so_help();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
option if option.starts_with('-') => bail!("unknown restore-so option `{option}`"),
|
||||||
|
_ => positional.push(PathBuf::from(&args[cursor])),
|
||||||
|
}
|
||||||
|
cursor += 1;
|
||||||
|
}
|
||||||
|
let [input, output] = positional.as_slice() else {
|
||||||
|
bail!("restore-so requires INPUT and OUTPUT; use --help for usage");
|
||||||
|
};
|
||||||
|
let result = run_restore_so(&RestoreSoJob {
|
||||||
|
input: input.clone(),
|
||||||
|
output: output.clone(),
|
||||||
|
index,
|
||||||
|
report,
|
||||||
|
dump_auxiliary,
|
||||||
|
outer_only,
|
||||||
|
preserve_entrypoint,
|
||||||
|
})?;
|
||||||
|
println!("Restored {} bytes to {}", result.output_size, result.output);
|
||||||
|
println!("SHA-256 {}", result.output_sha256);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_metadata(args: Vec<OsString>) -> Result<()> {
|
||||||
|
let mut positional = Vec::new();
|
||||||
|
let mut report = None;
|
||||||
|
let mut seed = senbei_android_metadata::DEFAULT_METHOD_TOKEN_SEED;
|
||||||
|
let mut cursor = 0;
|
||||||
|
while cursor < args.len() {
|
||||||
|
match args[cursor].to_string_lossy().as_ref() {
|
||||||
|
"--seed" => {
|
||||||
|
let value = option_string(&args, &mut cursor, "--seed")?;
|
||||||
|
seed = parse_u32(&value).with_context(|| format!("invalid --seed `{value}`"))?;
|
||||||
|
}
|
||||||
|
"--report" => report = Some(option_path(&args, &mut cursor, "--report")?),
|
||||||
|
"-h" | "--help" => {
|
||||||
|
print_metadata_help();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
option if option.starts_with('-') => {
|
||||||
|
bail!("unknown restore-metadata option `{option}`");
|
||||||
|
}
|
||||||
|
_ => positional.push(PathBuf::from(&args[cursor])),
|
||||||
|
}
|
||||||
|
cursor += 1;
|
||||||
|
}
|
||||||
|
let [input, output] = positional.as_slice() else {
|
||||||
|
bail!("restore-metadata requires INPUT and OUTPUT; use --help for usage");
|
||||||
|
};
|
||||||
|
let result = run_restore_metadata(&RestoreMetadataJob {
|
||||||
|
input: input.clone(),
|
||||||
|
output: output.clone(),
|
||||||
|
seed,
|
||||||
|
report,
|
||||||
|
})?;
|
||||||
|
println!(
|
||||||
|
"Restored {}/{} MethodDef tokens ({} already canonical)",
|
||||||
|
result.changed_tokens, result.methods, result.already_correct_before
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn option_path(args: &[OsString], cursor: &mut usize, name: &str) -> Result<PathBuf> {
|
||||||
|
*cursor += 1;
|
||||||
|
args.get(*cursor)
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.with_context(|| format!("{name} requires a path"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn option_string(args: &[OsString], cursor: &mut usize, name: &str) -> Result<String> {
|
||||||
|
*cursor += 1;
|
||||||
|
args.get(*cursor)
|
||||||
|
.map(|value| value.to_string_lossy().into_owned())
|
||||||
|
.with_context(|| format!("{name} requires a value"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u32(value: &str) -> Result<u32> {
|
||||||
|
if let Some(hex) = value
|
||||||
|
.strip_prefix("0x")
|
||||||
|
.or_else(|| value.strip_prefix("0X"))
|
||||||
|
{
|
||||||
|
Ok(u32::from_str_radix(hex, 16)?)
|
||||||
|
} else {
|
||||||
|
Ok(value.parse()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!("senbei-android {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
println!("Usage:");
|
||||||
|
println!(" senbei-android restore-so INPUT OUTPUT [OPTIONS]");
|
||||||
|
println!(" senbei-android restore-metadata INPUT OUTPUT [OPTIONS]");
|
||||||
|
println!(" senbei-android --version");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_so_help() {
|
||||||
|
println!("senbei-android restore-so INPUT OUTPUT [OPTIONS]");
|
||||||
|
println!(" --index FILE Stage 2 module index.json");
|
||||||
|
println!(" --report FILE Write a JSON restoration report");
|
||||||
|
println!(" --dump-aux FILE Dump decoded auxiliary ELF data");
|
||||||
|
println!(" --outer-only Skip auxiliary ELF table materialization");
|
||||||
|
println!(" --preserve-entrypoint Keep the protector entrypoint");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_metadata_help() {
|
||||||
|
println!("senbei-android restore-metadata INPUT OUTPUT [OPTIONS]");
|
||||||
|
println!(" --seed VALUE Module 0x0C seed (decimal or 0x-prefixed hex)");
|
||||||
|
println!(" --report FILE Write a JSON restoration report");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_decimal_and_hex_seeds() {
|
||||||
|
assert_eq!(parse_u32("42").unwrap(), 42);
|
||||||
|
assert_eq!(parse_u32("0xA6FAE968").unwrap(), 0xa6fa_e968);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "senbei-android-crypto"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "Protector container primitives for Senbei Android"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aes.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
//! Cryptographic and compression primitives used by the Android protector.
|
||||||
|
|
||||||
|
use aes::Aes256;
|
||||||
|
use aes::cipher::{Block, BlockDecrypt, KeyInit};
|
||||||
|
|
||||||
|
const RECORD_SIZE: usize = 0x5c;
|
||||||
|
|
||||||
|
/// Errors raised while parsing or decoding protector containers.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("{0}")]
|
||||||
|
Invalid(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
fn invalid<T>(message: impl Into<String>) -> Result<T> {
|
||||||
|
Err(Error::Invalid(message.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
|
||||||
|
let end = offset
|
||||||
|
.checked_add(size)
|
||||||
|
.ok_or_else(|| Error::Invalid("byte range overflow".to_owned()))?;
|
||||||
|
data.get(offset..end).ok_or_else(|| {
|
||||||
|
Error::Invalid(format!(
|
||||||
|
"byte range 0x{offset:x}..0x{end:x} is out of bounds"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u16(data: &[u8], offset: usize) -> Result<u16> {
|
||||||
|
let bytes: [u8; 2] = range(data, offset, 2)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Invalid("invalid u16 range".to_owned()))?;
|
||||||
|
Ok(u16::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
|
||||||
|
let bytes: [u8; 4] = range(data, offset, 4)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Invalid("invalid u32 range".to_owned()))?;
|
||||||
|
Ok(u32::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_up(value: usize, alignment: usize) -> Result<usize> {
|
||||||
|
let mask = alignment
|
||||||
|
.checked_sub(1)
|
||||||
|
.ok_or_else(|| Error::Invalid("zero alignment".to_owned()))?;
|
||||||
|
value
|
||||||
|
.checked_add(mask)
|
||||||
|
.map(|v| v & !mask)
|
||||||
|
.ok_or_else(|| Error::Invalid("alignment overflow".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multiply by the fixed element used by the native GF(2^32) transform.
|
||||||
|
#[must_use]
|
||||||
|
pub fn gf32_mul_fixed(mut value: u32) -> u32 {
|
||||||
|
let mut multiplier = 0x9451_1dd2_u32;
|
||||||
|
let mut result = 0_u32;
|
||||||
|
while multiplier != 0 {
|
||||||
|
if multiplier & 1 != 0 {
|
||||||
|
result ^= value;
|
||||||
|
}
|
||||||
|
let carry = value >> 31;
|
||||||
|
value = value.wrapping_shl(1);
|
||||||
|
if carry != 0 {
|
||||||
|
value ^= 0x5793_57eb;
|
||||||
|
}
|
||||||
|
multiplier >>= 1;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mix_columns(block: [u8; 16]) -> [u8; 16] {
|
||||||
|
const fn xtime(value: u8) -> u8 {
|
||||||
|
(value << 1) ^ if value & 0x80 != 0 { 0x1b } else { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut output = [0_u8; 16];
|
||||||
|
for offset in (0..16).step_by(4) {
|
||||||
|
let [a, b, c, d] = block[offset..offset + 4] else {
|
||||||
|
unreachable!("fixed four-byte AES column")
|
||||||
|
};
|
||||||
|
output[offset] = xtime(a) ^ (xtime(b) ^ b) ^ c ^ d;
|
||||||
|
output[offset + 1] = a ^ xtime(b) ^ (xtime(c) ^ c) ^ d;
|
||||||
|
output[offset + 2] = a ^ b ^ xtime(c) ^ (xtime(d) ^ d);
|
||||||
|
output[offset + 3] = (xtime(a) ^ a) ^ b ^ c ^ xtime(d);
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Static configuration recovered from module `0x9B`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Module9bConfig {
|
||||||
|
pub header_seed: u32,
|
||||||
|
pub container_seed: u32,
|
||||||
|
pub aes_key: [u8; 32],
|
||||||
|
pub skip_aes: bool,
|
||||||
|
pub schedule_offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module9bConfig {
|
||||||
|
/// Parse the unique AES-256 decryption schedule and adjacent configuration.
|
||||||
|
pub fn parse(image: &[u8]) -> Result<Self> {
|
||||||
|
const MARKER: [u8; 4] = [0x00, 0x01, 0x0e, 0x00];
|
||||||
|
let mut matches = image
|
||||||
|
.windows(MARKER.len())
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(offset, bytes)| (bytes == MARKER).then_some(offset));
|
||||||
|
let schedule_offset = matches
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::Invalid("cannot locate the 0x9B AES-256 schedule".to_owned()))?;
|
||||||
|
if schedule_offset < 8 || matches.next().is_some() {
|
||||||
|
return invalid("cannot uniquely locate the 0x9B AES-256 schedule");
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_seed = read_u32(image, schedule_offset - 8)?;
|
||||||
|
let schedule_size = read_u32(image, schedule_offset - 4)?;
|
||||||
|
if schedule_size != 0xf4 {
|
||||||
|
return invalid(format!(
|
||||||
|
"unexpected 0x9B AES schedule size 0x{schedule_size:x}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let bits = read_u16(image, schedule_offset)?;
|
||||||
|
let rounds = read_u16(image, schedule_offset + 2)?;
|
||||||
|
if (bits, rounds) != (0x100, 14) {
|
||||||
|
return invalid(format!(
|
||||||
|
"unexpected AES schedule header 0x{bits:x}/{rounds}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let schedule = range(image, schedule_offset + 4, 15 * 16)?;
|
||||||
|
let mut round_keys = [[0_u8; 16]; 15];
|
||||||
|
for (round, output) in round_keys.iter_mut().enumerate() {
|
||||||
|
let source = &schedule[round * 16..round * 16 + 16];
|
||||||
|
for word in 0..4 {
|
||||||
|
let start = word * 4;
|
||||||
|
for byte in 0..4 {
|
||||||
|
output[start + byte] = source[start + 3 - byte];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut aes_key = [0_u8; 32];
|
||||||
|
aes_key[..16].copy_from_slice(&round_keys[14]);
|
||||||
|
aes_key[16..].copy_from_slice(&mix_columns(round_keys[13]));
|
||||||
|
|
||||||
|
let container_seed_offset = schedule_offset
|
||||||
|
.checked_add(0x100)
|
||||||
|
.ok_or_else(|| Error::Invalid("container seed offset overflow".to_owned()))?;
|
||||||
|
let skip_aes_offset = schedule_offset
|
||||||
|
.checked_add(0x240)
|
||||||
|
.ok_or_else(|| Error::Invalid("skip-AES offset overflow".to_owned()))?;
|
||||||
|
let skip_aes = *image.get(skip_aes_offset).ok_or_else(|| {
|
||||||
|
Error::Invalid("0x9B static configuration exceeds its image".to_owned())
|
||||||
|
})? != 0;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
header_seed,
|
||||||
|
container_seed: read_u32(image, container_seed_offset)?,
|
||||||
|
aes_key,
|
||||||
|
skip_aes,
|
||||||
|
schedule_offset,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypted header at the start of direct-data object `0x9D`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ProtectedDescriptor {
|
||||||
|
pub command_id: u32,
|
||||||
|
pub flags: u32,
|
||||||
|
pub outer_offset: u32,
|
||||||
|
pub outer_expected_size: u32,
|
||||||
|
pub auxiliary_offset: u32,
|
||||||
|
pub auxiliary_expected_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtectedDescriptor {
|
||||||
|
/// Decrypt the `0x5c`-byte descriptor with the module header seed.
|
||||||
|
pub fn decrypt(data: &[u8], seed: u32) -> Result<Self> {
|
||||||
|
if data.len() < RECORD_SIZE {
|
||||||
|
return invalid("0x9D descriptor is truncated");
|
||||||
|
}
|
||||||
|
let base0 = seed.wrapping_add(0xd3e8_7144).wrapping_mul(seed);
|
||||||
|
let base1 = base0.wrapping_add(seed.wrapping_mul(0x0bd9_418d));
|
||||||
|
let mut words = [0_u32; RECORD_SIZE / 4];
|
||||||
|
for (index, word) in words.iter_mut().enumerate() {
|
||||||
|
let cipher = read_u32(data, index * 4)?;
|
||||||
|
let subtractor = base0.wrapping_shl(if index & 1 != 0 { 4 } else { 0 });
|
||||||
|
*word = cipher.wrapping_sub(subtractor)
|
||||||
|
^ base1.wrapping_shr((seed.wrapping_add((index as u32).wrapping_mul(4))) & 7);
|
||||||
|
}
|
||||||
|
if words[6..].iter().any(|&word| word != 0) {
|
||||||
|
return invalid("unexpected nonzero reserved words in the 0x9D descriptor");
|
||||||
|
}
|
||||||
|
let descriptor = Self {
|
||||||
|
command_id: words[0],
|
||||||
|
flags: words[1],
|
||||||
|
outer_offset: words[2],
|
||||||
|
outer_expected_size: words[3],
|
||||||
|
auxiliary_offset: words[4],
|
||||||
|
auxiliary_expected_size: words[5],
|
||||||
|
};
|
||||||
|
if descriptor.command_id != 0x9d || descriptor.outer_offset as usize != RECORD_SIZE {
|
||||||
|
return invalid("unexpected decrypted 0x9D descriptor");
|
||||||
|
}
|
||||||
|
Ok(descriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One encrypted segment in a decoded `0x9D` container header.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct EncodedSegment {
|
||||||
|
pub offset: u32,
|
||||||
|
pub size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed primary or auxiliary `0x9D` container.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ContainerHeader {
|
||||||
|
pub start: usize,
|
||||||
|
pub output_size: u32,
|
||||||
|
pub skip_aes: bool,
|
||||||
|
pub tree: Vec<u8>,
|
||||||
|
pub segments: Vec<EncodedSegment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerHeader {
|
||||||
|
/// Parse and decrypt a container header, Huffman tree, and segment table.
|
||||||
|
pub fn parse(data: &[u8], start: usize, seed: u32) -> Result<Self> {
|
||||||
|
range(data, start, 12)?;
|
||||||
|
let seed_square = seed.wrapping_mul(seed);
|
||||||
|
let state = seed_square.wrapping_shr(17) ^ seed_square.wrapping_shl(11);
|
||||||
|
let raw0 = read_u32(data, start)?;
|
||||||
|
let raw1 = read_u32(data, start + 4)?;
|
||||||
|
let raw2 = read_u32(data, start + 8)?;
|
||||||
|
let output_size = 0xa21d_fb3a_u32
|
||||||
|
.wrapping_shl(state & 7)
|
||||||
|
.wrapping_add(state.wrapping_mul(0xf87b_337c))
|
||||||
|
.wrapping_add(gf32_mul_fixed(raw0));
|
||||||
|
let flag_word = gf32_mul_fixed(raw1)
|
||||||
|
^ state
|
||||||
|
.wrapping_add(0xbd19_c63c)
|
||||||
|
.wrapping_add(0x416e_2af2_u32.wrapping_shr(state & 0x0d));
|
||||||
|
let segment_count = (flag_word & 0xff) as usize;
|
||||||
|
let skip_aes = (flag_word >> 8) & 0xff == 1;
|
||||||
|
let tree_size = 0x643a_3a3b_u32
|
||||||
|
.wrapping_shl(state & 0x0b)
|
||||||
|
.wrapping_sub(state ^ 0x3b2b_f538)
|
||||||
|
.wrapping_add(gf32_mul_fixed(raw2)) as usize;
|
||||||
|
if segment_count == 0 || tree_size > 0x1b00 {
|
||||||
|
return invalid(format!(
|
||||||
|
"invalid container fields: segments={segment_count}, tree=0x{tree_size:x}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tree_start = start
|
||||||
|
.checked_add(12)
|
||||||
|
.ok_or_else(|| Error::Invalid("tree offset overflow".to_owned()))?;
|
||||||
|
let mut tree = range(data, tree_start, tree_size)?.to_vec();
|
||||||
|
for offset in (0..tree_size & !3).step_by(4) {
|
||||||
|
let value = read_u32(&tree, offset)?;
|
||||||
|
tree[offset..offset + 4].copy_from_slice(&gf32_mul_fixed(value).to_le_bytes());
|
||||||
|
}
|
||||||
|
let tree_state = state.wrapping_add(0xf1cb_5b81).wrapping_mul(state);
|
||||||
|
let tree_delta = tree_state.wrapping_sub(0x23b3_2203_u32.wrapping_mul(state));
|
||||||
|
for (index, byte) in tree.iter_mut().enumerate() {
|
||||||
|
let shift = u32::try_from(index & 0x1b)
|
||||||
|
.map_err(|_| Error::Invalid("tree shift conversion failed".to_owned()))?;
|
||||||
|
let left = gf32_mul_fixed(tree_state.wrapping_shl(shift));
|
||||||
|
let right = tree_delta.wrapping_shr((index & 0x17) as u32);
|
||||||
|
let adjustment = left.wrapping_sub(right).wrapping_shr((index & 0x1f) as u32);
|
||||||
|
*byte = byte.wrapping_add(adjustment as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
let table_start = start
|
||||||
|
.checked_add(align_up(12 + tree_size, 4)?)
|
||||||
|
.ok_or_else(|| Error::Invalid("segment table offset overflow".to_owned()))?;
|
||||||
|
let table_size = segment_count
|
||||||
|
.checked_mul(8)
|
||||||
|
.ok_or_else(|| Error::Invalid("segment table size overflow".to_owned()))?;
|
||||||
|
let mut table = range(data, table_start, table_size)?.to_vec();
|
||||||
|
let table_state = state.wrapping_add(0xb31f_451c).wrapping_mul(state);
|
||||||
|
let table_xor = table_state.wrapping_shl(3);
|
||||||
|
let table_add = table_state.wrapping_sub(0x822f_e82d_u32.wrapping_mul(state));
|
||||||
|
for offset in (0..table_size).step_by(4) {
|
||||||
|
let value = read_u32(&table, offset)?;
|
||||||
|
let decoded = gf32_mul_fixed(value ^ table_xor)
|
||||||
|
.wrapping_add(table_add.wrapping_shr(((offset & 7) + 5) as u32));
|
||||||
|
table[offset..offset + 4].copy_from_slice(&decoded.to_le_bytes());
|
||||||
|
}
|
||||||
|
let mut segments = Vec::with_capacity(segment_count);
|
||||||
|
for index in 0..segment_count {
|
||||||
|
let offset = read_u32(&table, index * 8)?;
|
||||||
|
let size = read_u32(&table, index * 8 + 4)?;
|
||||||
|
let absolute = start
|
||||||
|
.checked_add(offset as usize)
|
||||||
|
.and_then(|value| value.checked_add(size as usize));
|
||||||
|
if size == 0 || absolute.is_none_or(|end| end > data.len()) {
|
||||||
|
return invalid(format!("container segment {index} lies outside 0x9D"));
|
||||||
|
}
|
||||||
|
segments.push(EncodedSegment { offset, size });
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
start,
|
||||||
|
output_size,
|
||||||
|
skip_aes,
|
||||||
|
tree,
|
||||||
|
segments,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End offset of the furthest encrypted segment.
|
||||||
|
pub fn encoded_end(&self) -> Result<usize> {
|
||||||
|
self.segments
|
||||||
|
.iter()
|
||||||
|
.map(|segment| {
|
||||||
|
self.start
|
||||||
|
.checked_add(segment.offset as usize)
|
||||||
|
.and_then(|value| value.checked_add(segment.size as usize))
|
||||||
|
.ok_or_else(|| Error::Invalid("encoded segment end overflow".to_owned()))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?
|
||||||
|
.into_iter()
|
||||||
|
.max()
|
||||||
|
.ok_or_else(|| Error::Invalid("container has no encoded segments".to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decoder for the protector's Huffman/LZ writer streams.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HuffmanLzDecoder {
|
||||||
|
tree: Vec<u8>,
|
||||||
|
lookup_symbols: Vec<u16>,
|
||||||
|
lookup_bits: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HuffmanLzDecoder {
|
||||||
|
/// Build the full 16-bit prefix lookup used by the static decoder.
|
||||||
|
pub fn new(tree: &[u8]) -> Result<Self> {
|
||||||
|
if tree.len() < 256 * 3 || tree.len() % 3 != 0 {
|
||||||
|
return invalid(format!("invalid Huffman tree size 0x{:x}", tree.len()));
|
||||||
|
}
|
||||||
|
let mut result = Self {
|
||||||
|
tree: tree.to_vec(),
|
||||||
|
lookup_symbols: vec![0; 0x1_0000],
|
||||||
|
lookup_bits: vec![0; 0x1_0000],
|
||||||
|
};
|
||||||
|
for word in 0..0x1_0000_u32 {
|
||||||
|
let (symbol, bits) = result.decode_symbol(word)?;
|
||||||
|
if bits <= 16 {
|
||||||
|
result.lookup_symbols[word as usize] = symbol;
|
||||||
|
result.lookup_bits[word as usize] = bits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry(&self, index: usize) -> Result<(u16, bool, u8)> {
|
||||||
|
let offset = index
|
||||||
|
.checked_mul(3)
|
||||||
|
.ok_or_else(|| Error::Invalid("Huffman node offset overflow".to_owned()))?;
|
||||||
|
let bytes = range(&self.tree, offset, 3)?;
|
||||||
|
let raw = u16::from(bytes[0]) | (u16::from(bytes[1]) << 8);
|
||||||
|
Ok((raw & 0x7fff, raw & 0x8000 != 0, bytes[2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_symbol(&self, word: u32) -> Result<(u16, u8)> {
|
||||||
|
let (mut value, leaf, extra) = self.entry((word & 0xff) as usize)?;
|
||||||
|
if leaf {
|
||||||
|
if extra == 0 {
|
||||||
|
return invalid("zero-width Huffman leaf");
|
||||||
|
}
|
||||||
|
return Ok((value, extra));
|
||||||
|
}
|
||||||
|
let mut bits = extra
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or_else(|| Error::Invalid("Huffman bit count overflow".to_owned()))?;
|
||||||
|
let mut mask = 1_u32.wrapping_shl(u32::from(extra));
|
||||||
|
loop {
|
||||||
|
let branch = usize::from(word & mask != 0);
|
||||||
|
let (next, is_leaf, _) = self.entry(usize::from(value) + branch)?;
|
||||||
|
value = next;
|
||||||
|
if is_leaf {
|
||||||
|
return Ok((value, bits));
|
||||||
|
}
|
||||||
|
mask = mask.wrapping_shl(1);
|
||||||
|
bits = bits
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or_else(|| Error::Invalid("Huffman bit count overflow".to_owned()))?;
|
||||||
|
if bits > 31 {
|
||||||
|
return invalid("Huffman code exceeds the native 32-bit window");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode one compressed writer payload to its exact expected size.
|
||||||
|
pub fn decode(&self, source: &[u8], output_size: usize) -> Result<Vec<u8>> {
|
||||||
|
let mut output = vec![0_u8; output_size];
|
||||||
|
let mut source_pos = 0_usize;
|
||||||
|
let mut bit_buffer = 0_u64;
|
||||||
|
let mut available = 0_u8;
|
||||||
|
let mut consumed_bits = 0_usize;
|
||||||
|
let mut output_pos = 0_usize;
|
||||||
|
let mut prefix = 0_usize;
|
||||||
|
|
||||||
|
while output_pos < output_size {
|
||||||
|
while available < 24 && source_pos < source.len() {
|
||||||
|
bit_buffer |= u64::from(source[source_pos]) << available;
|
||||||
|
source_pos += 1;
|
||||||
|
available += 8;
|
||||||
|
}
|
||||||
|
let key = (bit_buffer & 0xffff) as usize;
|
||||||
|
let mut bits = self.lookup_bits[key];
|
||||||
|
let symbol = if bits != 0 {
|
||||||
|
self.lookup_symbols[key]
|
||||||
|
} else {
|
||||||
|
let mut value_offset = ((bit_buffer & 0xff) as usize) * 3;
|
||||||
|
let mut node = range(&self.tree, value_offset, 3)?;
|
||||||
|
let mut raw = u16::from(node[0]) | (u16::from(node[1]) << 8);
|
||||||
|
if raw & 0x8000 != 0 {
|
||||||
|
bits = node[2];
|
||||||
|
raw & 0x7fff
|
||||||
|
} else {
|
||||||
|
let extra = node[2];
|
||||||
|
bits = extra + 1;
|
||||||
|
let mut mask = 1_u64 << extra;
|
||||||
|
loop {
|
||||||
|
let branch = usize::from(bit_buffer & mask != 0);
|
||||||
|
let index = usize::from(raw & 0x7fff) + branch;
|
||||||
|
value_offset = index
|
||||||
|
.checked_mul(3)
|
||||||
|
.ok_or_else(|| Error::Invalid("Huffman node overflow".to_owned()))?;
|
||||||
|
node = range(&self.tree, value_offset, 3)?;
|
||||||
|
raw = u16::from(node[0]) | (u16::from(node[1]) << 8);
|
||||||
|
if raw & 0x8000 != 0 {
|
||||||
|
break raw & 0x7fff;
|
||||||
|
}
|
||||||
|
mask <<= 1;
|
||||||
|
bits += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if bits == 0 || bits > available {
|
||||||
|
return invalid("compressed stream ends inside a Huffman code");
|
||||||
|
}
|
||||||
|
bit_buffer >>= bits;
|
||||||
|
available -= bits;
|
||||||
|
consumed_bits = consumed_bits
|
||||||
|
.checked_add(usize::from(bits))
|
||||||
|
.ok_or_else(|| Error::Invalid("consumed bit count overflow".to_owned()))?;
|
||||||
|
|
||||||
|
let kind = symbol & 0x300;
|
||||||
|
let value = usize::from(symbol & 0xff);
|
||||||
|
match kind {
|
||||||
|
0 => {
|
||||||
|
output[output_pos] = value as u8;
|
||||||
|
output_pos += 1;
|
||||||
|
}
|
||||||
|
0x100 => {
|
||||||
|
if prefix > 0xff {
|
||||||
|
return invalid("compressed prefix exceeds 16 bits");
|
||||||
|
}
|
||||||
|
prefix = if prefix == 0 {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
value | (prefix << 8)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
0x200 => {
|
||||||
|
if prefix == 0 {
|
||||||
|
prefix = 1;
|
||||||
|
}
|
||||||
|
let count = value
|
||||||
|
.checked_mul(prefix)
|
||||||
|
.ok_or_else(|| Error::Invalid("repeat count overflow".to_owned()))?;
|
||||||
|
if !matches!(value, 1 | 2 | 4)
|
||||||
|
|| value > output_pos
|
||||||
|
|| output_pos
|
||||||
|
.checked_add(count)
|
||||||
|
.is_none_or(|end| end > output_size)
|
||||||
|
{
|
||||||
|
return invalid("invalid compressed repeated-pattern command");
|
||||||
|
}
|
||||||
|
let pattern = output[output_pos - value..output_pos].to_vec();
|
||||||
|
for chunk in output[output_pos..output_pos + count].chunks_exact_mut(value) {
|
||||||
|
chunk.copy_from_slice(&pattern);
|
||||||
|
}
|
||||||
|
output_pos += count;
|
||||||
|
prefix = 0;
|
||||||
|
}
|
||||||
|
0x300 => {
|
||||||
|
let length = value;
|
||||||
|
let distance = prefix.checked_add(length).ok_or_else(|| {
|
||||||
|
Error::Invalid("back-reference distance overflow".to_owned())
|
||||||
|
})?;
|
||||||
|
if distance > output_pos
|
||||||
|
|| output_pos
|
||||||
|
.checked_add(length)
|
||||||
|
.is_none_or(|end| end > output_size)
|
||||||
|
{
|
||||||
|
return invalid("invalid compressed back-reference");
|
||||||
|
}
|
||||||
|
let source_start = output_pos - distance;
|
||||||
|
output.copy_within(source_start..source_start + length, output_pos);
|
||||||
|
output_pos += length;
|
||||||
|
prefix = 0;
|
||||||
|
}
|
||||||
|
_ => unreachable!("masked Huffman symbol kind"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if consumed_bits.div_ceil(8) != source.len() {
|
||||||
|
return invalid(format!(
|
||||||
|
"compressed input consumption mismatch: used=0x{:x}, size=0x{:x}",
|
||||||
|
consumed_bits.div_ceil(8),
|
||||||
|
source.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the native word transform and optional AES-256-CBC decryption.
|
||||||
|
pub fn transform_segment(
|
||||||
|
data: &[u8],
|
||||||
|
seed: u32,
|
||||||
|
aes_key: &[u8; 32],
|
||||||
|
decrypt_aes: bool,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let mut transformed = data.to_vec();
|
||||||
|
let mut state = seed;
|
||||||
|
let mut left = 0xe34e_ac63_u32;
|
||||||
|
let mut right = 0x07b4_8238_u32;
|
||||||
|
for (index, chunk) in transformed.chunks_exact_mut(4).enumerate() {
|
||||||
|
let index32 = u32::try_from(index)
|
||||||
|
.map_err(|_| Error::Invalid("segment word index exceeds u32".to_owned()))?;
|
||||||
|
left = state
|
||||||
|
.wrapping_add(0x72f6_fcbe)
|
||||||
|
.wrapping_add(left.wrapping_add(0x4f8b_1bca).wrapping_mul(left))
|
||||||
|
.wrapping_shr(index32.wrapping_mul(index32) & 0x0f);
|
||||||
|
right = state
|
||||||
|
.wrapping_sub(0x71b6_a98d)
|
||||||
|
.wrapping_add(right.wrapping_sub(0x1605_a81c).wrapping_mul(right))
|
||||||
|
.wrapping_shl(index32 & 7);
|
||||||
|
state = left ^ right;
|
||||||
|
let bytes: [u8; 4] = chunk
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Invalid("invalid transformed word".to_owned()))?;
|
||||||
|
let mut value = u32::from_le_bytes(bytes);
|
||||||
|
value = value.wrapping_add(0xb43b_9baf_u32.wrapping_mul(index32 & 0x0d));
|
||||||
|
value ^= 0xaf57_f7fb_u32.wrapping_mul(index32 & 3);
|
||||||
|
value = value.wrapping_sub(state) ^ state;
|
||||||
|
chunk.copy_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
if decrypt_aes {
|
||||||
|
let cipher = Aes256::new_from_slice(aes_key)
|
||||||
|
.map_err(|_| Error::Invalid("invalid AES-256 key length".to_owned()))?;
|
||||||
|
let aligned_size = transformed.len() & !0x0f;
|
||||||
|
let mut previous = [0_u8; 16];
|
||||||
|
for chunk in transformed[..aligned_size].chunks_exact_mut(16) {
|
||||||
|
let mut ciphertext = [0_u8; 16];
|
||||||
|
ciphertext.copy_from_slice(chunk);
|
||||||
|
cipher.decrypt_block(Block::<Aes256>::from_mut_slice(chunk));
|
||||||
|
for (byte, prior) in chunk.iter_mut().zip(previous) {
|
||||||
|
*byte ^= prior;
|
||||||
|
}
|
||||||
|
previous = ciphertext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(transformed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aes_mix_columns_matches_fips_example() {
|
||||||
|
let input = [
|
||||||
|
0xdb, 0x13, 0x53, 0x45, 0xf2, 0x0a, 0x22, 0x5c, 0x01, 0x01, 0x01, 0x01, 0xc6, 0xc6,
|
||||||
|
0xc6, 0xc6,
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
mix_columns(input),
|
||||||
|
[
|
||||||
|
0x8e, 0x4d, 0xa1, 0xbc, 0x9f, 0xdc, 0x58, 0x9d, 0x01, 0x01, 0x01, 0x01, 0xc6, 0xc6,
|
||||||
|
0xc6, 0xc6,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn descriptor_rejects_truncated_input() {
|
||||||
|
assert!(ProtectedDescriptor::decrypt(&[0_u8; 16], 1).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "senbei-android-elf"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "AArch64 ELF restoration for Senbei Android"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
memmap2.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
tempfile.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
senbei-android-crypto.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result, invalid};
|
||||||
|
|
||||||
|
const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) struct Artifact {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact>> {
|
||||||
|
let text = std::fs::read_to_string(index_path)
|
||||||
|
.map_err(|error| Error::io("read module index", index_path, error))?;
|
||||||
|
let document: Value = serde_json::from_str(&text)?;
|
||||||
|
let root = index_path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
|
let mut result = BTreeMap::new();
|
||||||
|
|
||||||
|
if let Some(items) = document.get("module_registry").and_then(Value::as_array) {
|
||||||
|
for item in items {
|
||||||
|
let Some(command_id) = item.get("command_id").and_then(Value::as_u64) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let command_id = u32::try_from(command_id)
|
||||||
|
.map_err(|_| Error::Invalid("module command ID exceeds u32".to_owned()))?;
|
||||||
|
if !REQUIRED_IDS.contains(&command_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(path) = item.get("image_path").and_then(Value::as_str) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let size = item
|
||||||
|
.get("size")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.ok_or_else(|| Error::Invalid(format!("module 0x{command_id:02X} lacks size")))?;
|
||||||
|
result.insert(
|
||||||
|
command_id,
|
||||||
|
Artifact {
|
||||||
|
path: root.join(path),
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(streams) = document.get("streams").and_then(Value::as_array) {
|
||||||
|
for stream in streams {
|
||||||
|
let Some(records) = stream.get("records").and_then(Value::as_array) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for record in records {
|
||||||
|
let Some(command_id) = record.get("command_id").and_then(Value::as_u64) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let command_id = u32::try_from(command_id)
|
||||||
|
.map_err(|_| Error::Invalid("record command ID exceeds u32".to_owned()))?;
|
||||||
|
if !REQUIRED_IDS.contains(&command_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(image) = record.get("image") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(path) = image.get("path").and_then(Value::as_str) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let size = image.get("size").and_then(Value::as_u64).ok_or_else(|| {
|
||||||
|
Error::Invalid(format!("record 0x{command_id:02X} lacks image size"))
|
||||||
|
})?;
|
||||||
|
result.insert(
|
||||||
|
command_id,
|
||||||
|
Artifact {
|
||||||
|
path: root.join(path),
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let missing = REQUIRED_IDS
|
||||||
|
.iter()
|
||||||
|
.filter(|id| !result.contains_key(id))
|
||||||
|
.map(|id| format!("0x{id:02X}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !missing.is_empty() {
|
||||||
|
return invalid(format!(
|
||||||
|
"module index lacks required IDs: {}",
|
||||||
|
missing.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for (&command_id, artifact) in &result {
|
||||||
|
let metadata = std::fs::metadata(&artifact.path)
|
||||||
|
.map_err(|error| Error::io("inspect artifact", &artifact.path, error))?;
|
||||||
|
if !metadata.is_file() || metadata.len() != artifact.size {
|
||||||
|
return invalid(format!(
|
||||||
|
"invalid artifact for module 0x{command_id:02X}: {}",
|
||||||
|
artifact.path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// ELF restoration failure.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("{action} `{path}`: {source}")]
|
||||||
|
Io {
|
||||||
|
action: &'static str,
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
#[error("cannot parse module index: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Crypto(#[from] senbei_android_crypto::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Invalid(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub(crate) fn io(action: &'static str, path: &Path, source: std::io::Error) -> Self {
|
||||||
|
Self::Io {
|
||||||
|
action,
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
pub(crate) fn invalid<T>(message: impl Into<String>) -> Result<T> {
|
||||||
|
Err(Error::Invalid(message.into()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
use crate::error::{Error, Result, invalid};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn elf_hash(name: &[u8]) -> u32 {
|
||||||
|
let mut value = 0_u32;
|
||||||
|
for &byte in name {
|
||||||
|
value = value.wrapping_shl(4).wrapping_add(u32::from(byte));
|
||||||
|
let high = value & 0xf000_0000;
|
||||||
|
if high != 0 {
|
||||||
|
value ^= high >> 24;
|
||||||
|
value &= !high;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn gnu_hash(name: &[u8]) -> u32 {
|
||||||
|
name.iter().fold(5381_u32, |value, &byte| {
|
||||||
|
value.wrapping_mul(33).wrapping_add(u32::from(byte))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_sysv_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
|
||||||
|
if names.len() < 2 {
|
||||||
|
return invalid("dynamic symbol table is unexpectedly empty");
|
||||||
|
}
|
||||||
|
let bucket_count = names.len();
|
||||||
|
let symbol_count = names.len();
|
||||||
|
let mut buckets = vec![0_u32; bucket_count];
|
||||||
|
let mut chains = vec![0_u32; symbol_count];
|
||||||
|
for (symbol_index, name) in names.iter().enumerate().skip(1) {
|
||||||
|
let bucket_index = elf_hash(name) as usize % bucket_count;
|
||||||
|
let symbol_index32 = u32::try_from(symbol_index)
|
||||||
|
.map_err(|_| Error::Invalid("dynamic symbol index exceeds u32".to_owned()))?;
|
||||||
|
if buckets[bucket_index] == 0 {
|
||||||
|
buckets[bucket_index] = symbol_index32;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut chain_index = buckets[bucket_index] as usize;
|
||||||
|
while chains[chain_index] != 0 {
|
||||||
|
chain_index = chains[chain_index] as usize;
|
||||||
|
}
|
||||||
|
chains[chain_index] = symbol_index32;
|
||||||
|
}
|
||||||
|
let mut output = Vec::with_capacity((2 + bucket_count + symbol_count) * 4);
|
||||||
|
output.extend_from_slice(
|
||||||
|
&u32::try_from(bucket_count)
|
||||||
|
.map_err(|_| Error::Invalid("SysV bucket count exceeds u32".to_owned()))?
|
||||||
|
.to_le_bytes(),
|
||||||
|
);
|
||||||
|
output.extend_from_slice(
|
||||||
|
&u32::try_from(symbol_count)
|
||||||
|
.map_err(|_| Error::Invalid("SysV symbol count exceeds u32".to_owned()))?
|
||||||
|
.to_le_bytes(),
|
||||||
|
);
|
||||||
|
for value in buckets.into_iter().chain(chains) {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_gnu_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
|
||||||
|
let hashes = names
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.map(|name| gnu_hash(name))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if hashes.is_empty() {
|
||||||
|
return invalid("GNU hash requires at least one dynamic symbol");
|
||||||
|
}
|
||||||
|
let bloom_shift = 5_u32;
|
||||||
|
let mut bloom_word = 0_u64;
|
||||||
|
for &value in &hashes {
|
||||||
|
bloom_word |= 1_u64 << (value & 63);
|
||||||
|
bloom_word |= 1_u64 << ((value >> bloom_shift) & 63);
|
||||||
|
}
|
||||||
|
let mut chains = hashes
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| value & !1)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let last = chains
|
||||||
|
.last_mut()
|
||||||
|
.ok_or_else(|| Error::Invalid("GNU hash chain is empty".to_owned()))?;
|
||||||
|
*last |= 1;
|
||||||
|
let mut output = Vec::with_capacity(28 + chains.len() * 4);
|
||||||
|
for value in [1_u32, 1, 1, bloom_shift] {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
output.extend_from_slice(&bloom_word.to_le_bytes());
|
||||||
|
output.extend_from_slice(&1_u32.to_le_bytes());
|
||||||
|
for value in chains {
|
||||||
|
output.extend_from_slice(&value.to_le_bytes());
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standard_elf_hash_is_stable() {
|
||||||
|
assert_eq!(elf_hash(b"printf"), 0x0779_05a6);
|
||||||
|
assert_eq!(gnu_hash(b"printf"), 0x156b_2bb8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
use crate::error::{Error, Result, invalid};
|
||||||
|
|
||||||
|
pub(crate) const SHT_NOBITS: u32 = 8;
|
||||||
|
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000;
|
||||||
|
pub(crate) const SHF_ALLOC: u64 = 2;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) struct LoadSegment {
|
||||||
|
pub offset: u64,
|
||||||
|
pub virtual_address: u64,
|
||||||
|
pub file_size: u64,
|
||||||
|
pub memory_size: u64,
|
||||||
|
pub flags: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) struct SectionHeader {
|
||||||
|
pub name: u32,
|
||||||
|
pub section_type: u32,
|
||||||
|
pub flags: u64,
|
||||||
|
pub address: u64,
|
||||||
|
pub offset: u64,
|
||||||
|
pub size: u64,
|
||||||
|
pub link: u32,
|
||||||
|
pub info: u32,
|
||||||
|
pub alignment: u64,
|
||||||
|
pub entry_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SectionHeader {
|
||||||
|
pub const SIZE: usize = 0x40;
|
||||||
|
|
||||||
|
fn parse(data: &[u8], offset: usize) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
name: read_u32(data, offset)?,
|
||||||
|
section_type: read_u32(data, offset + 4)?,
|
||||||
|
flags: read_u64(data, offset + 8)?,
|
||||||
|
address: read_u64(data, offset + 0x10)?,
|
||||||
|
offset: read_u64(data, offset + 0x18)?,
|
||||||
|
size: read_u64(data, offset + 0x20)?,
|
||||||
|
link: read_u32(data, offset + 0x28)?,
|
||||||
|
info: read_u32(data, offset + 0x2c)?,
|
||||||
|
alignment: read_u64(data, offset + 0x30)?,
|
||||||
|
entry_size: read_u64(data, offset + 0x38)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(self) -> [u8; Self::SIZE] {
|
||||||
|
let mut output = [0_u8; Self::SIZE];
|
||||||
|
output[0..4].copy_from_slice(&self.name.to_le_bytes());
|
||||||
|
output[4..8].copy_from_slice(&self.section_type.to_le_bytes());
|
||||||
|
output[8..0x10].copy_from_slice(&self.flags.to_le_bytes());
|
||||||
|
output[0x10..0x18].copy_from_slice(&self.address.to_le_bytes());
|
||||||
|
output[0x18..0x20].copy_from_slice(&self.offset.to_le_bytes());
|
||||||
|
output[0x20..0x28].copy_from_slice(&self.size.to_le_bytes());
|
||||||
|
output[0x28..0x2c].copy_from_slice(&self.link.to_le_bytes());
|
||||||
|
output[0x2c..0x30].copy_from_slice(&self.info.to_le_bytes());
|
||||||
|
output[0x30..0x38].copy_from_slice(&self.alignment.to_le_bytes());
|
||||||
|
output[0x38..0x40].copy_from_slice(&self.entry_size.to_le_bytes());
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) struct ElfLayout {
|
||||||
|
pub entrypoint: u64,
|
||||||
|
pub program_headers: Vec<LoadSegment>,
|
||||||
|
pub section_headers: Vec<SectionHeader>,
|
||||||
|
pub section_name_index: usize,
|
||||||
|
pub private_section_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ElfLayout {
|
||||||
|
pub fn parse(data: &[u8], require_private: bool) -> Result<Self> {
|
||||||
|
let ident = slice(data, 0, 6)?;
|
||||||
|
if ident[..4] != *b"\x7fELF" || ident[4] != 2 || ident[5] != 1 {
|
||||||
|
return invalid("input is not a little-endian ELF64 file");
|
||||||
|
}
|
||||||
|
if read_u16(data, 0x12)? != 0xb7 {
|
||||||
|
return invalid("input is not an AArch64 ELF");
|
||||||
|
}
|
||||||
|
let entrypoint = read_u64(data, 0x18)?;
|
||||||
|
let program_header_offset = usize_from_u64(read_u64(data, 0x20)?, "program header offset")?;
|
||||||
|
let section_header_offset = usize_from_u64(read_u64(data, 0x28)?, "section header offset")?;
|
||||||
|
let program_header_size = usize::from(read_u16(data, 0x36)?);
|
||||||
|
let program_header_count = usize::from(read_u16(data, 0x38)?);
|
||||||
|
let section_header_size = usize::from(read_u16(data, 0x3a)?);
|
||||||
|
let section_header_count = usize::from(read_u16(data, 0x3c)?);
|
||||||
|
let section_name_index = usize::from(read_u16(data, 0x3e)?);
|
||||||
|
if program_header_size != 0x38 || section_header_size != SectionHeader::SIZE {
|
||||||
|
return invalid("unexpected ELF program/section header size");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut program_headers = Vec::new();
|
||||||
|
for index in 0..program_header_count {
|
||||||
|
let offset = checked_index(program_header_offset, index, program_header_size)?;
|
||||||
|
if read_u32(data, offset)? != 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let segment = LoadSegment {
|
||||||
|
flags: read_u32(data, offset + 4)?,
|
||||||
|
offset: read_u64(data, offset + 8)?,
|
||||||
|
virtual_address: read_u64(data, offset + 0x10)?,
|
||||||
|
file_size: read_u64(data, offset + 0x20)?,
|
||||||
|
memory_size: read_u64(data, offset + 0x28)?,
|
||||||
|
};
|
||||||
|
let file_end = segment
|
||||||
|
.offset
|
||||||
|
.checked_add(segment.file_size)
|
||||||
|
.ok_or_else(|| Error::Invalid(format!("PT_LOAD {index} file range overflow")))?;
|
||||||
|
if file_end > data.len() as u64 {
|
||||||
|
return invalid(format!("PT_LOAD {index} exceeds input file"));
|
||||||
|
}
|
||||||
|
program_headers.push(segment);
|
||||||
|
}
|
||||||
|
if program_headers.is_empty() {
|
||||||
|
return invalid("input ELF contains no PT_LOAD segments");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut section_headers = Vec::with_capacity(section_header_count);
|
||||||
|
for index in 0..section_header_count {
|
||||||
|
let offset = checked_index(section_header_offset, index, section_header_size)?;
|
||||||
|
section_headers.push(SectionHeader::parse(data, offset)?);
|
||||||
|
}
|
||||||
|
if section_name_index >= section_headers.len() {
|
||||||
|
return invalid("ELF section-name index is out of range");
|
||||||
|
}
|
||||||
|
let private = section_headers
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, section)| (section.section_type == SHT_LOUSER).then_some(index))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let private_section_index = match private.as_slice() {
|
||||||
|
[index] => *index,
|
||||||
|
[] if !require_private => usize::MAX,
|
||||||
|
_ => {
|
||||||
|
return invalid(format!(
|
||||||
|
"expected {} SHT_LOUSER section, found {}",
|
||||||
|
if require_private {
|
||||||
|
"one"
|
||||||
|
} else {
|
||||||
|
"at most one"
|
||||||
|
},
|
||||||
|
private.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
entrypoint,
|
||||||
|
program_headers,
|
||||||
|
section_headers,
|
||||||
|
section_name_index,
|
||||||
|
private_section_index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn private_section(&self) -> Result<SectionHeader> {
|
||||||
|
self.section_headers
|
||||||
|
.get(self.private_section_index)
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| Error::Invalid("ELF has no private section".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_end(&self) -> Result<u64> {
|
||||||
|
self.program_headers
|
||||||
|
.iter()
|
||||||
|
.map(|segment| {
|
||||||
|
segment
|
||||||
|
.virtual_address
|
||||||
|
.checked_add(segment.memory_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("PT_LOAD memory end overflow".to_owned()))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?
|
||||||
|
.into_iter()
|
||||||
|
.max()
|
||||||
|
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD memory range".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_load_end(&self) -> Result<u64> {
|
||||||
|
self.program_headers
|
||||||
|
.iter()
|
||||||
|
.map(|segment| {
|
||||||
|
segment
|
||||||
|
.offset
|
||||||
|
.checked_add(segment.file_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("PT_LOAD file end overflow".to_owned()))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?
|
||||||
|
.into_iter()
|
||||||
|
.max()
|
||||||
|
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD file range".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_names(&self, data: &[u8]) -> Result<Vec<String>> {
|
||||||
|
let table = self.section_headers[self.section_name_index];
|
||||||
|
let strings = slice_u64(data, table.offset, table.size)?;
|
||||||
|
self.section_headers
|
||||||
|
.iter()
|
||||||
|
.map(|section| {
|
||||||
|
let offset = section.name as usize;
|
||||||
|
if offset >= strings.len() {
|
||||||
|
return Ok(String::new());
|
||||||
|
}
|
||||||
|
let end = strings[offset..]
|
||||||
|
.iter()
|
||||||
|
.position(|&byte| byte == 0)
|
||||||
|
.map_or(strings.len(), |length| offset + length);
|
||||||
|
Ok(String::from_utf8_lossy(&strings[offset..end]).into_owned())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_offset_to_virtual_address(&self, offset: u64, size: u64) -> Result<u64> {
|
||||||
|
let end = offset
|
||||||
|
.checked_add(size)
|
||||||
|
.ok_or_else(|| Error::Invalid("file range overflow".to_owned()))?;
|
||||||
|
for segment in &self.program_headers {
|
||||||
|
let segment_end = segment
|
||||||
|
.offset
|
||||||
|
.checked_add(segment.file_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("PT_LOAD file range overflow".to_owned()))?;
|
||||||
|
if segment.offset <= offset && end <= segment_end {
|
||||||
|
return segment
|
||||||
|
.virtual_address
|
||||||
|
.checked_add(offset - segment.offset)
|
||||||
|
.ok_or_else(|| Error::Invalid("virtual address overflow".to_owned()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalid(format!(
|
||||||
|
"file range 0x{offset:x}..0x{end:x} is not in PT_LOAD"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
|
||||||
|
let end = offset
|
||||||
|
.checked_add(size)
|
||||||
|
.ok_or_else(|| Error::Invalid("byte range overflow".to_owned()))?;
|
||||||
|
data.get(offset..end).ok_or_else(|| {
|
||||||
|
Error::Invalid(format!(
|
||||||
|
"byte range 0x{offset:x}..0x{end:x} is out of bounds"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn slice_u64(data: &[u8], offset: u64, size: u64) -> Result<&[u8]> {
|
||||||
|
slice(
|
||||||
|
data,
|
||||||
|
usize_from_u64(offset, "file offset")?,
|
||||||
|
usize_from_u64(size, "file size")?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_u16(data: &[u8], offset: usize) -> Result<u16> {
|
||||||
|
let bytes: [u8; 2] = slice(data, offset, 2)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Invalid("invalid u16 range".to_owned()))?;
|
||||||
|
Ok(u16::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
|
||||||
|
let bytes: [u8; 4] = slice(data, offset, 4)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Invalid("invalid u32 range".to_owned()))?;
|
||||||
|
Ok(u32::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_u64(data: &[u8], offset: usize) -> Result<u64> {
|
||||||
|
let bytes: [u8; 8] = slice(data, offset, 8)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Invalid("invalid u64 range".to_owned()))?;
|
||||||
|
Ok(u64::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_i64(data: &[u8], offset: usize) -> Result<i64> {
|
||||||
|
let bytes: [u8; 8] = slice(data, offset, 8)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Invalid("invalid i64 range".to_owned()))?;
|
||||||
|
Ok(i64::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn usize_from_u64(value: u64, field: &str) -> Result<usize> {
|
||||||
|
usize::try_from(value).map_err(|_| Error::Invalid(format!("{field} 0x{value:x} exceeds usize")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn checked_index(base: usize, index: usize, stride: usize) -> Result<usize> {
|
||||||
|
index
|
||||||
|
.checked_mul(stride)
|
||||||
|
.and_then(|value| base.checked_add(value))
|
||||||
|
.ok_or_else(|| Error::Invalid("table index overflow".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn align_up(value: u64, alignment: u64) -> Result<u64> {
|
||||||
|
if alignment == 0 || !alignment.is_power_of_two() {
|
||||||
|
return invalid(format!("invalid alignment {alignment}"));
|
||||||
|
}
|
||||||
|
value
|
||||||
|
.checked_add(alignment - 1)
|
||||||
|
.map(|aligned| aligned & !(alignment - 1))
|
||||||
|
.ok_or_else(|| Error::Invalid("alignment overflow".to_owned()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Static restoration of the current protected AArch64 `libil2cpp.so`.
|
||||||
|
|
||||||
|
mod artifact;
|
||||||
|
mod error;
|
||||||
|
mod hash;
|
||||||
|
mod layout;
|
||||||
|
mod restore;
|
||||||
|
|
||||||
|
pub use error::Error;
|
||||||
|
pub use restore::{RestoreOptions, RestoreReport, restore_libil2cpp};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "senbei-android-io"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "Filesystem and CLI orchestration for Senbei Android"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
sha2.workspace = true
|
||||||
|
tempfile.workspace = true
|
||||||
|
senbei-android-elf.workspace = true
|
||||||
|
senbei-android-metadata.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
//! Filesystem orchestration for the Android restoration commands.
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use senbei_android_elf::{RestoreOptions, RestoreReport, restore_libil2cpp};
|
||||||
|
use senbei_android_metadata::{DEFAULT_METHOD_TOKEN_SEED, Report as MetadataReport};
|
||||||
|
use serde::Serialize;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
/// Filesystem arguments for restoring one protected `libil2cpp.so`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RestoreSoJob {
|
||||||
|
pub input: PathBuf,
|
||||||
|
pub output: PathBuf,
|
||||||
|
pub index: Option<PathBuf>,
|
||||||
|
pub report: Option<PathBuf>,
|
||||||
|
pub dump_auxiliary: Option<PathBuf>,
|
||||||
|
pub outer_only: bool,
|
||||||
|
pub preserve_entrypoint: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filesystem arguments for restoring one `global-metadata.dat`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RestoreMetadataJob {
|
||||||
|
pub input: PathBuf,
|
||||||
|
pub output: PathBuf,
|
||||||
|
pub seed: u32,
|
||||||
|
pub report: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RestoreMetadataJob {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(input: PathBuf, output: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
seed: DEFAULT_METHOD_TOKEN_SEED,
|
||||||
|
report: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Infer the Stage 2 module index produced for `libil2cpp.so`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn default_module_index(input: &Path) -> PathBuf {
|
||||||
|
input
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new("."))
|
||||||
|
.join("libil2cpp_stage2_modules")
|
||||||
|
.join("index.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run static SO restoration and optionally emit its JSON report.
|
||||||
|
pub fn run_restore_so(job: &RestoreSoJob) -> Result<RestoreReport> {
|
||||||
|
refuse_in_place(&job.input, &job.output)?;
|
||||||
|
let options = RestoreOptions {
|
||||||
|
input: job.input.clone(),
|
||||||
|
output: job.output.clone(),
|
||||||
|
index: job
|
||||||
|
.index
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| default_module_index(&job.input)),
|
||||||
|
dump_auxiliary: job.dump_auxiliary.clone(),
|
||||||
|
outer_only: job.outer_only,
|
||||||
|
preserve_entrypoint: job.preserve_entrypoint,
|
||||||
|
};
|
||||||
|
let result = restore_libil2cpp(&options).context("restore protected libil2cpp.so")?;
|
||||||
|
if let Some(path) = &job.report {
|
||||||
|
write_json_atomic(path, &result)?;
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore MethodDef tokens and atomically write the cleaned metadata.
|
||||||
|
pub fn run_restore_metadata(job: &RestoreMetadataJob) -> Result<MetadataReport> {
|
||||||
|
refuse_in_place(&job.input, &job.output)?;
|
||||||
|
let input =
|
||||||
|
std::fs::read(&job.input).with_context(|| format!("read `{}`", job.input.display()))?;
|
||||||
|
let (output, result) = senbei_android_metadata::restore_method_tokens(&input, job.seed)
|
||||||
|
.with_context(|| format!("restore `{}`", job.input.display()))?;
|
||||||
|
write_atomic(&job.output, &output)?;
|
||||||
|
if let Some(path) = &job.report {
|
||||||
|
write_json_atomic(path, &result)?;
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refuse_in_place(input: &Path, output: &Path) -> Result<()> {
|
||||||
|
let input_absolute = absolute(input)?;
|
||||||
|
let output_absolute = absolute(output)?;
|
||||||
|
let same_existing_file =
|
||||||
|
output.exists() && std::fs::canonicalize(input).ok() == std::fs::canonicalize(output).ok();
|
||||||
|
if input_absolute == output_absolute || same_existing_file {
|
||||||
|
bail!(
|
||||||
|
"refusing to overwrite input in place: `{}`",
|
||||||
|
input.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn absolute(path: &Path) -> Result<PathBuf> {
|
||||||
|
if path.is_absolute() {
|
||||||
|
Ok(path.to_path_buf())
|
||||||
|
} else {
|
||||||
|
Ok(std::env::current_dir()
|
||||||
|
.context("query current directory")?
|
||||||
|
.join(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_json_atomic(path: &Path, value: &impl Serialize) -> Result<()> {
|
||||||
|
let mut data = serde_json::to_vec_pretty(value).context("serialize JSON report")?;
|
||||||
|
data.push(b'\n');
|
||||||
|
write_atomic(path, &data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
|
||||||
|
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("create output directory `{}`", parent.display()))?;
|
||||||
|
let mut temporary = NamedTempFile::new_in(parent)
|
||||||
|
.with_context(|| format!("create temporary file in `{}`", parent.display()))?;
|
||||||
|
temporary
|
||||||
|
.write_all(data)
|
||||||
|
.and_then(|()| temporary.as_file().sync_all())
|
||||||
|
.with_context(|| format!("write temporary output for `{}`", path.display()))?;
|
||||||
|
temporary
|
||||||
|
.persist(path)
|
||||||
|
.map_err(|error| error.error)
|
||||||
|
.with_context(|| format!("replace output `{}`", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derives_index_next_to_input() {
|
||||||
|
assert_eq!(
|
||||||
|
default_module_index(Path::new(r"C:\game\Native\libil2cpp.so")),
|
||||||
|
PathBuf::from(r"C:\game\Native\libil2cpp_stage2_modules\index.json")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn metadata_job_uses_current_seed() {
|
||||||
|
let job = RestoreMetadataJob::new(PathBuf::from("in"), PathBuf::from("out"));
|
||||||
|
assert_eq!(job.seed, DEFAULT_METHOD_TOKEN_SEED);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,421 @@
|
|||||||
|
//! 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 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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}"),
|
||||||
|
images: image_count,
|
||||||
|
images_with_methods,
|
||||||
|
types: type_count,
|
||||||
|
methods: method_count,
|
||||||
|
visited_methods,
|
||||||
|
already_correct_before,
|
||||||
|
correct_after,
|
||||||
|
changed_tokens,
|
||||||
|
transformed_images,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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.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.changed_tokens, 0);
|
||||||
|
assert_eq!(restored, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user