mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
feat: add static stage extraction and metadata detection
This commit is contained in:
Generated
+73
-2
@@ -116,6 +116,17 @@ dependencies = [
|
|||||||
"r-efi",
|
"r-efi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "goblin"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17582616a7718cca54cec18e534a76c7c4aec11a8b9a85695712f262fd15a4c8"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"plain",
|
||||||
|
"scroll",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -143,6 +154,12 @@ version = "0.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
@@ -164,6 +181,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plain"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
@@ -201,6 +224,26 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scroll"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add"
|
||||||
|
dependencies = [
|
||||||
|
"scroll_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scroll_derive"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "senbei-android-cli"
|
name = "senbei-android-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -208,6 +251,8 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"senbei-android-io",
|
"senbei-android-io",
|
||||||
"senbei-android-metadata",
|
"senbei-android-metadata",
|
||||||
|
"senbei-android-stage2",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -238,6 +283,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"senbei-android-elf",
|
"senbei-android-elf",
|
||||||
"senbei-android-metadata",
|
"senbei-android-metadata",
|
||||||
|
"senbei-android-stage2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -252,6 +298,20 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "senbei-android-stage2"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"goblin",
|
||||||
|
"memmap2",
|
||||||
|
"senbei-android-crypto",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -279,7 +339,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -306,6 +366,17 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.3"
|
version = "3.0.3"
|
||||||
@@ -347,7 +418,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ members = [
|
|||||||
"senbei-android-elf",
|
"senbei-android-elf",
|
||||||
"senbei-android-io",
|
"senbei-android-io",
|
||||||
"senbei-android-metadata",
|
"senbei-android-metadata",
|
||||||
|
"senbei-android-stage2",
|
||||||
]
|
]
|
||||||
default-members = ["senbei-android-cli"]
|
default-members = ["senbei-android-cli"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
@@ -18,6 +19,7 @@ license = "AGPL-3.0-only"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
aes = "0.8"
|
aes = "0.8"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
goblin = "0.10"
|
||||||
memmap2 = "0.9"
|
memmap2 = "0.9"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -29,6 +31,7 @@ senbei-android-crypto = { path = "senbei-android-crypto" }
|
|||||||
senbei-android-elf = { path = "senbei-android-elf" }
|
senbei-android-elf = { path = "senbei-android-elf" }
|
||||||
senbei-android-io = { path = "senbei-android-io" }
|
senbei-android-io = { path = "senbei-android-io" }
|
||||||
senbei-android-metadata = { path = "senbei-android-metadata" }
|
senbei-android-metadata = { path = "senbei-android-metadata" }
|
||||||
|
senbei-android-stage2 = { path = "senbei-android-stage2" }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_op_in_unsafe_fn = "deny"
|
unsafe_op_in_unsafe_fn = "deny"
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ target\release\senbei-android.exe
|
|||||||
|
|
||||||
## 还原 libil2cpp.so
|
## 还原 libil2cpp.so
|
||||||
|
|
||||||
|
先直接从受保护 SO 静态提取 Stage 1/Stage 2 和模块索引:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
senbei-android extract-stage2 INPUT OUTPUT_DIR
|
||||||
|
```
|
||||||
|
|
||||||
|
默认在 `OUTPUT_DIR` 写入紧凑的 `index.json` 及后续还原实际需要的模块产物;
|
||||||
|
需要保留完整 Stage 2 镜像用于分析时,额外传入 `--stage2-out FILE`。
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
senbei-android restore-so INPUT OUTPUT --index INDEX_JSON --report REPORT_JSON
|
senbei-android restore-so INPUT OUTPUT --index INDEX_JSON --report REPORT_JSON
|
||||||
```
|
```
|
||||||
@@ -74,7 +83,23 @@ senbei-android restore-metadata `
|
|||||||
```
|
```
|
||||||
|
|
||||||
可用 `--seed 0xA6FAE968` 显式指定十六进制 seed,也支持十进制。还原操作是
|
可用 `--seed 0xA6FAE968` 显式指定十六进制 seed,也支持十进制。还原操作是
|
||||||
幂等的:已规范化的 image 会保持不变。
|
幂等的,并严格先检测状态、再决定是否解密:
|
||||||
|
|
||||||
|
- 先验证 metadata magic、版本、表边界、MethodDef token 类型与完整归属关系。
|
||||||
|
- 所有 image 的 RID 已规范时报告 `encryption_status: "clean"`,不执行逆置换,
|
||||||
|
输出与输入逐字节一致。
|
||||||
|
- 存在非规范 RID 时,必须先确认它们构成合法置换,并让指定 seed 对所有 image
|
||||||
|
完整通过五轮逆置换校验;只有此时才报告 `encryption_status: "encrypted"` 并写出结果。
|
||||||
|
- seed 错误、算法变化或数据损坏会直接报错,不生成输出文件和报告。
|
||||||
|
|
||||||
|
不确定样本 seed 时可先执行只读诊断:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
senbei-android discover-metadata INPUT
|
||||||
|
```
|
||||||
|
|
||||||
|
该命令不会修改文件,会列出每个 image 的状态、seed residue 以及满足当前 v31
|
||||||
|
算法的 32 位 seed 候选。
|
||||||
|
|
||||||
## Workspace
|
## Workspace
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
senbei-android-io.workspace = true
|
senbei-android-io.workspace = true
|
||||||
senbei-android-metadata.workspace = true
|
senbei-android-metadata.workspace = true
|
||||||
|
senbei-android-stage2.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ use std::ffi::OsString;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use senbei_android_io::{RestoreMetadataJob, RestoreSoJob, run_restore_metadata, run_restore_so};
|
use senbei_android_io::{
|
||||||
|
ExtractStage2Job, RestoreMetadataJob, RestoreSoJob, run_extract_stage2, run_restore_metadata,
|
||||||
|
run_restore_so,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() -> std::process::ExitCode {
|
fn main() -> std::process::ExitCode {
|
||||||
match run(std::env::args_os().skip(1)) {
|
match run(std::env::args_os().skip(1)) {
|
||||||
@@ -24,6 +27,8 @@ fn run(args: impl Iterator<Item = OsString>) -> Result<()> {
|
|||||||
match command.as_ref() {
|
match command.as_ref() {
|
||||||
"restore-so" => restore_so(args.collect()),
|
"restore-so" => restore_so(args.collect()),
|
||||||
"restore-metadata" => restore_metadata(args.collect()),
|
"restore-metadata" => restore_metadata(args.collect()),
|
||||||
|
"discover-metadata" => discover_metadata(args.collect()),
|
||||||
|
"extract-stage2" => extract_stage2(args.collect()),
|
||||||
"-h" | "--help" => {
|
"-h" | "--help" => {
|
||||||
print_help();
|
print_help();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -36,6 +41,87 @@ fn run(args: impl Iterator<Item = OsString>) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn discover_metadata(args: Vec<OsString>) -> Result<()> {
|
||||||
|
let mut positional = Vec::new();
|
||||||
|
for value in args {
|
||||||
|
if value == "-h" || value == "--help" {
|
||||||
|
println!("senbei-android discover-metadata INPUT");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if value.to_string_lossy().starts_with('-') {
|
||||||
|
bail!(
|
||||||
|
"unknown discover-metadata option `{}`",
|
||||||
|
value.to_string_lossy()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
positional.push(PathBuf::from(value));
|
||||||
|
}
|
||||||
|
let [input] = positional.as_slice() else {
|
||||||
|
bail!("discover-metadata requires INPUT; use --help for usage");
|
||||||
|
};
|
||||||
|
let data =
|
||||||
|
std::fs::read(input).with_context(|| format!("read metadata `{}`", input.display()))?;
|
||||||
|
let report = senbei_android_metadata::discover_method_token_seeds(&data)
|
||||||
|
.with_context(|| format!("discover metadata seed `{}`", input.display()))?;
|
||||||
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_stage2(args: Vec<OsString>) -> Result<()> {
|
||||||
|
let mut positional = Vec::new();
|
||||||
|
let mut stage2_output = None;
|
||||||
|
let mut outer_size = senbei_android_stage2::DEFAULT_OUTER_SIZE;
|
||||||
|
let mut cipher_constant = senbei_android_stage2::DEFAULT_CIPHER_CONSTANT;
|
||||||
|
let mut cursor = 0;
|
||||||
|
while cursor < args.len() {
|
||||||
|
match args[cursor].to_string_lossy().as_ref() {
|
||||||
|
"--stage2-out" => {
|
||||||
|
stage2_output = Some(option_path(&args, &mut cursor, "--stage2-out")?);
|
||||||
|
}
|
||||||
|
"--outer-size" => {
|
||||||
|
let value = option_string(&args, &mut cursor, "--outer-size")?;
|
||||||
|
outer_size = usize::try_from(parse_u64(&value)?)
|
||||||
|
.with_context(|| format!("invalid --outer-size `{value}`"))?;
|
||||||
|
}
|
||||||
|
"--cipher-constant" => {
|
||||||
|
let value = option_string(&args, &mut cursor, "--cipher-constant")?;
|
||||||
|
cipher_constant = parse_u32(&value)
|
||||||
|
.with_context(|| format!("invalid --cipher-constant `{value}`"))?;
|
||||||
|
}
|
||||||
|
"-h" | "--help" => {
|
||||||
|
print_extract_help();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
option if option.starts_with('-') => {
|
||||||
|
bail!("unknown extract-stage2 option `{option}");
|
||||||
|
}
|
||||||
|
_ => positional.push(PathBuf::from(&args[cursor])),
|
||||||
|
}
|
||||||
|
cursor += 1;
|
||||||
|
}
|
||||||
|
let [input, output_dir] = positional.as_slice() else {
|
||||||
|
bail!("extract-stage2 requires INPUT and OUTPUT_DIR; use --help for usage");
|
||||||
|
};
|
||||||
|
let mut job = ExtractStage2Job::new(input.clone(), output_dir.clone());
|
||||||
|
job.stage2_output = stage2_output;
|
||||||
|
job.outer_size = outer_size;
|
||||||
|
job.cipher_constant = cipher_constant;
|
||||||
|
let result = run_extract_stage2(&job)?;
|
||||||
|
let module_images = result
|
||||||
|
.module_registry
|
||||||
|
.iter()
|
||||||
|
.filter(|module| module.classification == "module_image")
|
||||||
|
.count();
|
||||||
|
println!(
|
||||||
|
"Extracted {} streams, {} modules and {} compact artifacts",
|
||||||
|
result.streams.len(),
|
||||||
|
module_images,
|
||||||
|
result.artifacts.len()
|
||||||
|
);
|
||||||
|
println!("Index {}", output_dir.join("index.json").display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn restore_so(args: Vec<OsString>) -> Result<()> {
|
fn restore_so(args: Vec<OsString>) -> Result<()> {
|
||||||
let mut positional = Vec::new();
|
let mut positional = Vec::new();
|
||||||
let mut index = None;
|
let mut index = None;
|
||||||
@@ -112,8 +198,11 @@ fn restore_metadata(args: Vec<OsString>) -> Result<()> {
|
|||||||
report,
|
report,
|
||||||
})?;
|
})?;
|
||||||
println!(
|
println!(
|
||||||
"Restored {}/{} MethodDef tokens ({} already canonical)",
|
"Metadata status={} restored {}/{} MethodDef tokens ({} already canonical)",
|
||||||
result.changed_tokens, result.methods, result.already_correct_before
|
result.encryption_status,
|
||||||
|
result.changed_tokens,
|
||||||
|
result.methods,
|
||||||
|
result.already_correct_before
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -133,11 +222,15 @@ fn option_string(args: &[OsString], cursor: &mut usize, name: &str) -> Result<St
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_u32(value: &str) -> Result<u32> {
|
fn parse_u32(value: &str) -> Result<u32> {
|
||||||
|
Ok(u32::try_from(parse_u64(value)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u64(value: &str) -> Result<u64> {
|
||||||
if let Some(hex) = value
|
if let Some(hex) = value
|
||||||
.strip_prefix("0x")
|
.strip_prefix("0x")
|
||||||
.or_else(|| value.strip_prefix("0X"))
|
.or_else(|| value.strip_prefix("0X"))
|
||||||
{
|
{
|
||||||
Ok(u32::from_str_radix(hex, 16)?)
|
Ok(u64::from_str_radix(hex, 16)?)
|
||||||
} else {
|
} else {
|
||||||
Ok(value.parse()?)
|
Ok(value.parse()?)
|
||||||
}
|
}
|
||||||
@@ -148,9 +241,18 @@ fn print_help() {
|
|||||||
println!("Usage:");
|
println!("Usage:");
|
||||||
println!(" senbei-android restore-so INPUT OUTPUT [OPTIONS]");
|
println!(" senbei-android restore-so INPUT OUTPUT [OPTIONS]");
|
||||||
println!(" senbei-android restore-metadata INPUT OUTPUT [OPTIONS]");
|
println!(" senbei-android restore-metadata INPUT OUTPUT [OPTIONS]");
|
||||||
|
println!(" senbei-android discover-metadata INPUT");
|
||||||
|
println!(" senbei-android extract-stage2 INPUT OUTPUT_DIR [OPTIONS]");
|
||||||
println!(" senbei-android --version");
|
println!(" senbei-android --version");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_extract_help() {
|
||||||
|
println!("senbei-android extract-stage2 INPUT OUTPUT_DIR [OPTIONS]");
|
||||||
|
println!(" --stage2-out FILE Write the raw decrypted Stage 2 image");
|
||||||
|
println!(" --outer-size VALUE Stage 1 outer wrapper size (default 0x23C)");
|
||||||
|
println!(" --cipher-constant VALUE Stage 1 cipher constant (default 0xBF20165D)");
|
||||||
|
}
|
||||||
|
|
||||||
fn print_so_help() {
|
fn print_so_help() {
|
||||||
println!("senbei-android restore-so INPUT OUTPUT [OPTIONS]");
|
println!("senbei-android restore-so INPUT OUTPUT [OPTIONS]");
|
||||||
println!(" --index FILE Stage 2 module index.json");
|
println!(" --index FILE Stage 2 module index.json");
|
||||||
|
|||||||
@@ -103,6 +103,18 @@ pub struct Module9bConfig {
|
|||||||
impl Module9bConfig {
|
impl Module9bConfig {
|
||||||
/// Parse the unique AES-256 decryption schedule and adjacent configuration.
|
/// Parse the unique AES-256 decryption schedule and adjacent configuration.
|
||||||
pub fn parse(image: &[u8]) -> Result<Self> {
|
pub fn parse(image: &[u8]) -> Result<Self> {
|
||||||
|
Self::parse_inner(image, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the decoder configuration embedded in the raw Stage 2 image.
|
||||||
|
///
|
||||||
|
/// The embedded decoder ends before the interpreter-only `skip_aes`
|
||||||
|
/// field, so that flag is definitionally false for this layout.
|
||||||
|
pub fn parse_embedded(image: &[u8]) -> Result<Self> {
|
||||||
|
Self::parse_inner(image, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_inner(image: &[u8], has_skip_aes: bool) -> Result<Self> {
|
||||||
const MARKER: [u8; 4] = [0x00, 0x01, 0x0e, 0x00];
|
const MARKER: [u8; 4] = [0x00, 0x01, 0x0e, 0x00];
|
||||||
let mut matches = image
|
let mut matches = image
|
||||||
.windows(MARKER.len())
|
.windows(MARKER.len())
|
||||||
@@ -117,7 +129,7 @@ impl Module9bConfig {
|
|||||||
|
|
||||||
let header_seed = read_u32(image, schedule_offset - 8)?;
|
let header_seed = read_u32(image, schedule_offset - 8)?;
|
||||||
let schedule_size = read_u32(image, schedule_offset - 4)?;
|
let schedule_size = read_u32(image, schedule_offset - 4)?;
|
||||||
if schedule_size != 0xf4 {
|
if !matches!(schedule_size, 0 | 0xf4) {
|
||||||
return invalid(format!(
|
return invalid(format!(
|
||||||
"unexpected 0x9B AES schedule size 0x{schedule_size:x}"
|
"unexpected 0x9B AES schedule size 0x{schedule_size:x}"
|
||||||
));
|
));
|
||||||
@@ -148,16 +160,24 @@ impl Module9bConfig {
|
|||||||
let container_seed_offset = schedule_offset
|
let container_seed_offset = schedule_offset
|
||||||
.checked_add(0x100)
|
.checked_add(0x100)
|
||||||
.ok_or_else(|| Error::Invalid("container seed offset overflow".to_owned()))?;
|
.ok_or_else(|| Error::Invalid("container seed offset overflow".to_owned()))?;
|
||||||
let skip_aes_offset = schedule_offset
|
let skip_aes = if has_skip_aes {
|
||||||
.checked_add(0x240)
|
let skip_aes_offset = schedule_offset
|
||||||
.ok_or_else(|| Error::Invalid("skip-AES offset overflow".to_owned()))?;
|
.checked_add(0x240)
|
||||||
let skip_aes = *image.get(skip_aes_offset).ok_or_else(|| {
|
.ok_or_else(|| Error::Invalid("skip-AES offset overflow".to_owned()))?;
|
||||||
Error::Invalid("0x9B static configuration exceeds its image".to_owned())
|
*image.get(skip_aes_offset).ok_or_else(|| {
|
||||||
})? != 0;
|
Error::Invalid("module static configuration exceeds its image".to_owned())
|
||||||
|
})? != 0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
header_seed,
|
header_seed,
|
||||||
container_seed: read_u32(image, container_seed_offset)?,
|
container_seed: if has_skip_aes {
|
||||||
|
read_u32(image, container_seed_offset)?
|
||||||
|
} else {
|
||||||
|
header_seed
|
||||||
|
},
|
||||||
aes_key,
|
aes_key,
|
||||||
skip_aes,
|
skip_aes,
|
||||||
schedule_offset,
|
schedule_offset,
|
||||||
@@ -572,6 +592,105 @@ pub fn transform_segment(
|
|||||||
Ok(transformed)
|
Ok(transformed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode one complete protector container into its flat output buffer.
|
||||||
|
///
|
||||||
|
/// This is the static equivalent of the decoder entrypoint embedded in Stage
|
||||||
|
/// 2 and in each nested interpreter module.
|
||||||
|
pub fn decode_container(
|
||||||
|
data: &[u8],
|
||||||
|
config: &Module9bConfig,
|
||||||
|
expected_size: usize,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let header = ContainerHeader::parse(data, 0, config.container_seed)?;
|
||||||
|
let header_size = usize::try_from(header.output_size)
|
||||||
|
.map_err(|_| Error::Invalid("container output size exceeds usize".to_owned()))?;
|
||||||
|
if header_size != expected_size {
|
||||||
|
return invalid(format!(
|
||||||
|
"container output size 0x{header_size:x} != expected 0x{expected_size:x}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let decoder = HuffmanLzDecoder::new(&header.tree)?;
|
||||||
|
let decrypt_aes = !(config.skip_aes || header.skip_aes);
|
||||||
|
let mut output = vec![0_u8; expected_size];
|
||||||
|
|
||||||
|
for (segment_index, encoded) in header.segments.iter().enumerate() {
|
||||||
|
let start = header
|
||||||
|
.start
|
||||||
|
.checked_add(encoded.offset as usize)
|
||||||
|
.ok_or_else(|| Error::Invalid("encoded segment start overflow".to_owned()))?;
|
||||||
|
let encoded_data = range(data, start, encoded.size as usize)?;
|
||||||
|
let transformed = transform_segment(
|
||||||
|
encoded_data,
|
||||||
|
config.container_seed,
|
||||||
|
&config.aes_key,
|
||||||
|
decrypt_aes,
|
||||||
|
)?;
|
||||||
|
if transformed.len() < 16 {
|
||||||
|
return invalid(format!(
|
||||||
|
"decoded segment {segment_index} is shorter than its header"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let base_offset = read_u32(&transformed, 0)? as usize;
|
||||||
|
let writer_count = read_u32(&transformed, 4)? as usize;
|
||||||
|
let table_offset = read_u32(&transformed, 8)? as usize;
|
||||||
|
let data_offset = read_u32(&transformed, 12)? as usize;
|
||||||
|
let table_size = writer_count
|
||||||
|
.checked_mul(16)
|
||||||
|
.ok_or_else(|| Error::Invalid("writer table size overflow".to_owned()))?;
|
||||||
|
let table_end = table_offset
|
||||||
|
.checked_add(table_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("writer table end overflow".to_owned()))?;
|
||||||
|
if table_end > transformed.len() || data_offset > transformed.len() {
|
||||||
|
return invalid(format!(
|
||||||
|
"decoded segment {segment_index} has invalid writer offsets"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data_cursor = data_offset;
|
||||||
|
for writer_index in 0..writer_count {
|
||||||
|
let record =
|
||||||
|
table_offset
|
||||||
|
.checked_add(writer_index.checked_mul(16).ok_or_else(|| {
|
||||||
|
Error::Invalid("writer record offset overflow".to_owned())
|
||||||
|
})?)
|
||||||
|
.ok_or_else(|| Error::Invalid("writer record offset overflow".to_owned()))?;
|
||||||
|
let output_offset = read_u32(&transformed, record)? as usize;
|
||||||
|
let output_size = read_u32(&transformed, record + 4)? as usize;
|
||||||
|
let encoded_size = read_u32(&transformed, record + 8)? as usize;
|
||||||
|
let reserved = read_u32(&transformed, record + 12)?;
|
||||||
|
let encoded_end = data_cursor
|
||||||
|
.checked_add(encoded_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("writer data end overflow".to_owned()))?;
|
||||||
|
if reserved != 0 || encoded_end > transformed.len() {
|
||||||
|
return invalid(format!(
|
||||||
|
"segment {segment_index} writer {writer_index} has invalid bounds"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let source = &transformed[data_cursor..encoded_end];
|
||||||
|
let decoded = if encoded_size == output_size {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(decoder.decode(source, output_size)?)
|
||||||
|
};
|
||||||
|
let decoded = decoded.as_deref().unwrap_or(source);
|
||||||
|
let target = base_offset
|
||||||
|
.checked_add(output_offset)
|
||||||
|
.ok_or_else(|| Error::Invalid("writer target offset overflow".to_owned()))?;
|
||||||
|
let target_end = target
|
||||||
|
.checked_add(decoded.len())
|
||||||
|
.ok_or_else(|| Error::Invalid("writer target end overflow".to_owned()))?;
|
||||||
|
let destination = output.get_mut(target..target_end).ok_or_else(|| {
|
||||||
|
Error::Invalid(format!(
|
||||||
|
"segment {segment_index} writer {writer_index} target is out of range"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
destination.copy_from_slice(decoded);
|
||||||
|
data_cursor = encoded_end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -437,12 +437,33 @@ impl AuxiliaryElfImage {
|
|||||||
let dynsym_end = u64::from(result.dynsym_offset)
|
let dynsym_end = u64::from(result.dynsym_offset)
|
||||||
+ u64::from(result.dynsym_count) * ELF64_SYMBOL_SIZE as u64;
|
+ u64::from(result.dynsym_count) * ELF64_SYMBOL_SIZE as u64;
|
||||||
let dynstr_end = u64::from(result.dynstr_offset) + u64::from(result.dynstr_size);
|
let dynstr_end = u64::from(result.dynstr_offset) + u64::from(result.dynstr_size);
|
||||||
if relocation1_end != u64::from(result.relocation2_offset)
|
let expected_relocation2 = align_up(relocation1_end, 0x10)?;
|
||||||
|| relocation2_end != u64::from(result.dynsym_offset)
|
let expected_dynsym = align_up(relocation2_end, 0x10)?;
|
||||||
|
if expected_relocation2 != u64::from(result.relocation2_offset)
|
||||||
|
|| expected_dynsym != u64::from(result.dynsym_offset)
|
||||||
|| dynsym_end != u64::from(result.dynstr_offset)
|
|| dynsym_end != u64::from(result.dynstr_offset)
|
||||||
|| dynstr_end != data.len() as u64
|
|| dynstr_end != data.len() as u64
|
||||||
{
|
{
|
||||||
return invalid("auxiliary ELF tables are not contiguous");
|
return invalid(format!(
|
||||||
|
"auxiliary ELF layout mismatch: rela1_end=0x{relocation1_end:x}/rela2=0x{:x}, rela2_end=0x{relocation2_end:x}/dynsym=0x{:x}, dynsym_end=0x{dynsym_end:x}/dynstr=0x{:x}, dynstr_end=0x{dynstr_end:x}/size=0x{:x}",
|
||||||
|
result.relocation2_offset,
|
||||||
|
result.dynsym_offset,
|
||||||
|
result.dynstr_offset,
|
||||||
|
data.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for (start, end) in [
|
||||||
|
(relocation1_end, expected_relocation2),
|
||||||
|
(relocation2_end, expected_dynsym),
|
||||||
|
] {
|
||||||
|
if slice_u64(data, start, end - start)?
|
||||||
|
.iter()
|
||||||
|
.any(|&byte| byte != 0)
|
||||||
|
{
|
||||||
|
return invalid(format!(
|
||||||
|
"auxiliary ELF alignment padding 0x{start:x}..0x{end:x} is nonzero"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if result.dynsym_count < 2 {
|
if result.dynsym_count < 2 {
|
||||||
return invalid("auxiliary dynamic symbol table is empty");
|
return invalid("auxiliary dynamic symbol table is empty");
|
||||||
@@ -537,9 +558,12 @@ fn restore_hidden_symbols(
|
|||||||
let target_offset = target_index as usize * ELF64_SYMBOL_SIZE;
|
let target_offset = target_index as usize * ELF64_SYMBOL_SIZE;
|
||||||
symbols[target_offset..target_offset + ELF64_SYMBOL_SIZE].copy_from_slice(source_symbol);
|
symbols[target_offset..target_offset + ELF64_SYMBOL_SIZE].copy_from_slice(source_symbol);
|
||||||
}
|
}
|
||||||
if cursor != table_end {
|
let string_padding = patch_data.get(cursor..table_end).ok_or_else(|| {
|
||||||
|
Error::Invalid("0x9E symbol strings exceed the primary patch blob".to_owned())
|
||||||
|
})?;
|
||||||
|
if string_padding.len() > 3 || string_padding.iter().any(|&byte| byte != 0) {
|
||||||
return invalid(format!(
|
return invalid(format!(
|
||||||
"0x9E symbol strings end at 0x{cursor:x}, expected 0x{table_end:x}"
|
"0x9E symbol strings have invalid padding at 0x{cursor:x}..0x{table_end:x}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let first_target_index = patched_indices
|
let first_target_index = patched_indices
|
||||||
@@ -715,12 +739,11 @@ fn patch_dynamic_tags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, usize>> {
|
fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, usize>> {
|
||||||
const REQUIRED: [&str; 10] = [
|
const REQUIRED: [&str; 9] = [
|
||||||
".dynsym",
|
".dynsym",
|
||||||
".gnu.version",
|
".gnu.version",
|
||||||
".gnu.version_r",
|
".gnu.version_r",
|
||||||
".gnu.hash",
|
".gnu.hash",
|
||||||
".hash",
|
|
||||||
".dynstr",
|
".dynstr",
|
||||||
".rela.dyn",
|
".rela.dyn",
|
||||||
".rela.plt",
|
".rela.plt",
|
||||||
@@ -742,6 +765,18 @@ fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, us
|
|||||||
_ => return invalid(format!("ELF contains duplicate section {required}")),
|
_ => return invalid(format!("ELF contains duplicate section {required}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let sysv_hash = names
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, name)| (name == ".hash").then_some(index))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
match sysv_hash.as_slice() {
|
||||||
|
[index] => {
|
||||||
|
result.insert(".hash", *index);
|
||||||
|
}
|
||||||
|
[] => {}
|
||||||
|
_ => return invalid("ELF contains duplicate section .hash"),
|
||||||
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,9 +846,13 @@ fn materialize_static_elf_tables(
|
|||||||
merged_versions.extend_from_slice(&VER_NDX_GLOBAL.to_le_bytes());
|
merged_versions.extend_from_slice(&VER_NDX_GLOBAL.to_le_bytes());
|
||||||
}
|
}
|
||||||
let merged_names = dynamic_symbol_names(&merged_symbols, &merged_strings)?;
|
let merged_names = dynamic_symbol_names(&merged_symbols, &merged_strings)?;
|
||||||
let sysv_hash = build_sysv_hash(&merged_names)?;
|
let sysv_hash = indices
|
||||||
|
.contains_key(".hash")
|
||||||
|
.then(|| build_sysv_hash(&merged_names))
|
||||||
|
.transpose()?;
|
||||||
let gnu_hash_table = build_gnu_hash(&merged_names)?;
|
let gnu_hash_table = build_gnu_hash(&merged_names)?;
|
||||||
let new_symbol_count = merged_names.len();
|
let new_symbol_count = merged_names.len();
|
||||||
|
let new_dynstr_size = merged_strings.len();
|
||||||
|
|
||||||
if rela_dyn.entry_size != ELF64_RELA_SIZE as u64
|
if rela_dyn.entry_size != ELF64_RELA_SIZE as u64
|
||||||
|| rela_plt.entry_size != ELF64_RELA_SIZE as u64
|
|| rela_plt.entry_size != ELF64_RELA_SIZE as u64
|
||||||
@@ -887,7 +926,7 @@ fn materialize_static_elf_tables(
|
|||||||
alignment: u64,
|
alignment: u64,
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
}
|
}
|
||||||
let tables = vec![
|
let mut tables = vec![
|
||||||
TablePayload {
|
TablePayload {
|
||||||
name: ".dynsym",
|
name: ".dynsym",
|
||||||
alignment: 8,
|
alignment: 8,
|
||||||
@@ -908,11 +947,15 @@ fn materialize_static_elf_tables(
|
|||||||
alignment: 8,
|
alignment: 8,
|
||||||
data: gnu_hash_table,
|
data: gnu_hash_table,
|
||||||
},
|
},
|
||||||
TablePayload {
|
];
|
||||||
|
if let Some(data) = sysv_hash {
|
||||||
|
tables.push(TablePayload {
|
||||||
name: ".hash",
|
name: ".hash",
|
||||||
alignment: 4,
|
alignment: 4,
|
||||||
data: sysv_hash,
|
data,
|
||||||
},
|
});
|
||||||
|
}
|
||||||
|
tables.extend([
|
||||||
TablePayload {
|
TablePayload {
|
||||||
name: ".dynstr",
|
name: ".dynstr",
|
||||||
alignment: 1,
|
alignment: 1,
|
||||||
@@ -928,7 +971,7 @@ fn materialize_static_elf_tables(
|
|||||||
alignment: 8,
|
alignment: 8,
|
||||||
data: merged_rela_plt,
|
data: merged_rela_plt,
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
let metadata_start = dynsym.offset;
|
let metadata_start = dynsym.offset;
|
||||||
let mut cursor = metadata_start;
|
let mut cursor = metadata_start;
|
||||||
let mut placements = BTreeMap::new();
|
let mut placements = BTreeMap::new();
|
||||||
@@ -981,24 +1024,23 @@ fn materialize_static_elf_tables(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let section_address = |name: &'static str| -> u64 { updated_sections[indices[name]].address };
|
let section_address = |name: &'static str| -> u64 { updated_sections[indices[name]].address };
|
||||||
patch_dynamic_tags(
|
let mut dynamic_values = BTreeMap::from([
|
||||||
output,
|
(DT_PLTRELSZ, (rela_plt_count * ELF64_RELA_SIZE) as u64),
|
||||||
dynamic,
|
(DT_STRTAB, section_address(".dynstr")),
|
||||||
&BTreeMap::from([
|
(DT_SYMTAB, section_address(".dynsym")),
|
||||||
(DT_PLTRELSZ, (rela_plt_count * ELF64_RELA_SIZE) as u64),
|
(DT_RELA, section_address(".rela.dyn")),
|
||||||
(DT_HASH, section_address(".hash")),
|
(DT_RELASZ, (rela_dyn_count * ELF64_RELA_SIZE) as u64),
|
||||||
(DT_STRTAB, section_address(".dynstr")),
|
(DT_STRSZ, new_dynstr_size as u64),
|
||||||
(DT_SYMTAB, section_address(".dynsym")),
|
(DT_JMPREL, section_address(".rela.plt")),
|
||||||
(DT_RELA, section_address(".rela.dyn")),
|
(DT_GNU_HASH, section_address(".gnu.hash")),
|
||||||
(DT_RELASZ, (rela_dyn_count * ELF64_RELA_SIZE) as u64),
|
(DT_VERSYM, section_address(".gnu.version")),
|
||||||
(DT_STRSZ, tables[5].data.len() as u64),
|
(DT_RELACOUNT, relative_count as u64),
|
||||||
(DT_JMPREL, section_address(".rela.plt")),
|
(DT_VERNEED, section_address(".gnu.version_r")),
|
||||||
(DT_GNU_HASH, section_address(".gnu.hash")),
|
]);
|
||||||
(DT_VERSYM, section_address(".gnu.version")),
|
if indices.contains_key(".hash") {
|
||||||
(DT_RELACOUNT, relative_count as u64),
|
dynamic_values.insert(DT_HASH, section_address(".hash"));
|
||||||
(DT_VERNEED, section_address(".gnu.version_r")),
|
}
|
||||||
]),
|
patch_dynamic_tags(output, dynamic, &dynamic_values)?;
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut restored_layout = layout.clone();
|
let mut restored_layout = layout.clone();
|
||||||
restored_layout.section_headers = updated_sections;
|
restored_layout.section_headers = updated_sections;
|
||||||
@@ -1012,7 +1054,7 @@ fn materialize_static_elf_tables(
|
|||||||
new_symbol_count,
|
new_symbol_count,
|
||||||
old_dynstr_size: old_strings.len(),
|
old_dynstr_size: old_strings.len(),
|
||||||
auxiliary_dynstr_size: auxiliary.dynstr_size,
|
auxiliary_dynstr_size: auxiliary.dynstr_size,
|
||||||
new_dynstr_size: tables[5].data.len(),
|
new_dynstr_size,
|
||||||
rela_dyn_count,
|
rela_dyn_count,
|
||||||
rela_plt_count,
|
rela_plt_count,
|
||||||
relative_prefix_count: relative_count,
|
relative_prefix_count: relative_count,
|
||||||
@@ -1247,8 +1289,20 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
|||||||
let payload = map_read_only(&payload_file, payload_path)?;
|
let payload = map_read_only(&payload_file, payload_path)?;
|
||||||
let layout = ElfLayout::parse(&source, true)?;
|
let layout = ElfLayout::parse(&source, true)?;
|
||||||
let private = layout.private_section()?;
|
let private = layout.private_section()?;
|
||||||
if private.offset != layout.file_load_end()? {
|
let file_load_end = layout.file_load_end()?;
|
||||||
return invalid("SHT_LOUSER does not begin at the file-backed PT_LOAD end");
|
let aligned_load_end = align_up(file_load_end, 0x10)?;
|
||||||
|
if private.offset != aligned_load_end {
|
||||||
|
return invalid(format!(
|
||||||
|
"SHT_LOUSER offset 0x{:x} != aligned file-backed PT_LOAD end 0x{aligned_load_end:x} (raw 0x{file_load_end:x})",
|
||||||
|
private.offset
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let load_padding = slice_u64(&source, file_load_end, private.offset - file_load_end)?;
|
||||||
|
if load_padding.iter().any(|&byte| byte != 0) {
|
||||||
|
return invalid(format!(
|
||||||
|
"nonzero padding between PT_LOAD end 0x{file_load_end:x} and SHT_LOUSER 0x{:x}",
|
||||||
|
private.offset
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let descriptor = ProtectedDescriptor::decrypt(&payload, config.header_seed)?;
|
let descriptor = ProtectedDescriptor::decrypt(&payload, config.header_seed)?;
|
||||||
let load_end = layout.load_end()?;
|
let load_end = layout.load_end()?;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ sha2.workspace = true
|
|||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
senbei-android-elf.workspace = true
|
senbei-android-elf.workspace = true
|
||||||
senbei-android-metadata.workspace = true
|
senbei-android-metadata.workspace = true
|
||||||
|
senbei-android-stage2.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use senbei_android_elf::{RestoreOptions, RestoreReport, restore_libil2cpp};
|
use senbei_android_elf::{RestoreOptions, RestoreReport, restore_libil2cpp};
|
||||||
use senbei_android_metadata::{DEFAULT_METHOD_TOKEN_SEED, Report as MetadataReport};
|
use senbei_android_metadata::{DEFAULT_METHOD_TOKEN_SEED, Report as MetadataReport};
|
||||||
|
use senbei_android_stage2::{
|
||||||
|
DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE, ExtractOptions, ExtractionReport, extract_stage2,
|
||||||
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
@@ -30,6 +33,29 @@ pub struct RestoreMetadataJob {
|
|||||||
pub report: Option<PathBuf>,
|
pub report: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filesystem arguments for pure-static Stage 1 and Stage 2 extraction.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ExtractStage2Job {
|
||||||
|
pub input: PathBuf,
|
||||||
|
pub output_dir: PathBuf,
|
||||||
|
pub stage2_output: Option<PathBuf>,
|
||||||
|
pub outer_size: usize,
|
||||||
|
pub cipher_constant: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtractStage2Job {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(input: PathBuf, output_dir: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
output_dir,
|
||||||
|
stage2_output: None,
|
||||||
|
outer_size: DEFAULT_OUTER_SIZE,
|
||||||
|
cipher_constant: DEFAULT_CIPHER_CONSTANT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RestoreMetadataJob {
|
impl RestoreMetadataJob {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(input: PathBuf, output: PathBuf) -> Self {
|
pub fn new(input: PathBuf, output: PathBuf) -> Self {
|
||||||
@@ -87,6 +113,18 @@ pub fn run_restore_metadata(job: &RestoreMetadataJob) -> Result<MetadataReport>
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract Stage 2 modules directly from one protected ELF.
|
||||||
|
pub fn run_extract_stage2(job: &ExtractStage2Job) -> Result<ExtractionReport> {
|
||||||
|
extract_stage2(&ExtractOptions {
|
||||||
|
input: job.input.clone(),
|
||||||
|
output_dir: job.output_dir.clone(),
|
||||||
|
stage2_output: job.stage2_output.clone(),
|
||||||
|
outer_size: job.outer_size,
|
||||||
|
cipher_constant: job.cipher_constant,
|
||||||
|
})
|
||||||
|
.context("extract protected Stage 1/Stage 2 payload")
|
||||||
|
}
|
||||||
|
|
||||||
fn refuse_in_place(input: &Path, output: &Path) -> Result<()> {
|
fn refuse_in_place(input: &Path, output: &Path) -> Result<()> {
|
||||||
let input_absolute = absolute(input)?;
|
let input_absolute = absolute(input)?;
|
||||||
let output_absolute = absolute(output)?;
|
let output_absolute = absolute(output)?;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const METHOD_TOKEN_TABLE: u32 = 0x0600_0000;
|
|||||||
pub struct Report {
|
pub struct Report {
|
||||||
pub version: u32,
|
pub version: u32,
|
||||||
pub seed: String,
|
pub seed: String,
|
||||||
|
pub encryption_status: String,
|
||||||
pub images: usize,
|
pub images: usize,
|
||||||
pub images_with_methods: usize,
|
pub images_with_methods: usize,
|
||||||
pub types: usize,
|
pub types: usize,
|
||||||
@@ -36,6 +37,26 @@ pub struct Report {
|
|||||||
pub transformed_images: usize,
|
pub transformed_images: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-image constraints recovered from the encrypted MethodDef RID
|
||||||
|
/// permutation.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct ImageKeyDiscovery {
|
||||||
|
pub image: usize,
|
||||||
|
pub method_count: u32,
|
||||||
|
pub modulus: u32,
|
||||||
|
pub clean: bool,
|
||||||
|
pub seed_residues: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of statically testing the known five-round permutation against a
|
||||||
|
/// metadata file without assuming a seed.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct SeedDiscoveryReport {
|
||||||
|
pub version: u32,
|
||||||
|
pub images: Vec<ImageKeyDiscovery>,
|
||||||
|
pub seed_candidates: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Metadata parsing or validation failure.
|
/// Metadata parsing or validation failure.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@@ -318,6 +339,11 @@ pub fn restore_method_tokens(data: &[u8], seed: u32) -> Result<(Vec<u8>, Report)
|
|||||||
Report {
|
Report {
|
||||||
version,
|
version,
|
||||||
seed: format!("0x{seed:08X}"),
|
seed: format!("0x{seed:08X}"),
|
||||||
|
encryption_status: if changed_tokens == 0 {
|
||||||
|
"clean".to_owned()
|
||||||
|
} else {
|
||||||
|
"encrypted".to_owned()
|
||||||
|
},
|
||||||
images: image_count,
|
images: image_count,
|
||||||
images_with_methods,
|
images_with_methods,
|
||||||
types: type_count,
|
types: type_count,
|
||||||
@@ -331,6 +357,199 @@ pub fn restore_method_tokens(data: &[u8], seed: u32) -> Result<(Vec<u8>, Report)
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Discover seeds compatible with the known v31 five-round RID permutation.
|
||||||
|
///
|
||||||
|
/// This is diagnostic and does not modify metadata. It enumerates the only
|
||||||
|
/// possible per-image key residues and intersects them over the 32-bit seed
|
||||||
|
/// domain. An empty candidate list means that the sample changed the
|
||||||
|
/// permutation itself rather than merely embedding a different seed.
|
||||||
|
pub fn discover_method_token_seeds(data: &[u8]) -> Result<SeedDiscoveryReport> {
|
||||||
|
if read_u32(data, 0).ok() != Some(MAGIC) {
|
||||||
|
return Err(Error::NotMetadata);
|
||||||
|
}
|
||||||
|
let version = read_u32(data, 4)?;
|
||||||
|
if version != SUPPORTED_VERSION {
|
||||||
|
return Ok(SeedDiscoveryReport {
|
||||||
|
version,
|
||||||
|
images: Vec::new(),
|
||||||
|
seed_candidates: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let (method_offset, method_size) = table(data, HDR_METHODS)?;
|
||||||
|
let (type_offset, type_size) = table(data, HDR_TYPES)?;
|
||||||
|
let (image_offset, image_size) = table(data, HDR_IMAGES)?;
|
||||||
|
if method_size % METHOD_STRIDE != 0
|
||||||
|
|| type_size % TYPE_STRIDE != 0
|
||||||
|
|| image_size % IMAGE_STRIDE != 0
|
||||||
|
{
|
||||||
|
return malformed("v31 table size is not divisible by its entry stride");
|
||||||
|
}
|
||||||
|
let method_count = method_size / METHOD_STRIDE;
|
||||||
|
let type_count = type_size / TYPE_STRIDE;
|
||||||
|
let image_count = image_size / IMAGE_STRIDE;
|
||||||
|
let mut reports = Vec::with_capacity(image_count);
|
||||||
|
for image_index in 0..image_count {
|
||||||
|
let image_base = image_offset + image_index * IMAGE_STRIDE;
|
||||||
|
let type_start = usize::try_from(read_i32(data, image_base + IMAGE_TYPE_START_OFFSET)?)
|
||||||
|
.map_err(|_| Error::Malformed(format!("image {image_index} has negative typeStart")))?;
|
||||||
|
let type_entries = read_u32(data, image_base + IMAGE_TYPE_COUNT_OFFSET)? as usize;
|
||||||
|
let type_end = type_start
|
||||||
|
.checked_add(type_entries)
|
||||||
|
.ok_or_else(|| Error::Malformed("image type range overflow".to_owned()))?;
|
||||||
|
if type_end > type_count {
|
||||||
|
return malformed(format!("image {image_index} type range exceeds the table"));
|
||||||
|
}
|
||||||
|
let mut methods = Vec::new();
|
||||||
|
for type_index in type_start..type_end {
|
||||||
|
let type_base = type_offset + type_index * TYPE_STRIDE;
|
||||||
|
let method_entries = read_u16(data, type_base + TYPE_METHOD_COUNT_OFFSET)? as usize;
|
||||||
|
if method_entries == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let method_start =
|
||||||
|
usize::try_from(read_i32(data, type_base + TYPE_METHOD_START_OFFSET)?).map_err(
|
||||||
|
|_| Error::Malformed(format!("type {type_index} has negative methodStart")),
|
||||||
|
)?;
|
||||||
|
let method_end = method_start
|
||||||
|
.checked_add(method_entries)
|
||||||
|
.ok_or_else(|| Error::Malformed("type method range overflow".to_owned()))?;
|
||||||
|
if method_end > method_count {
|
||||||
|
return malformed(format!("type {type_index} method range exceeds the table"));
|
||||||
|
}
|
||||||
|
methods.extend(method_start..method_end);
|
||||||
|
}
|
||||||
|
if methods.is_empty() {
|
||||||
|
reports.push(ImageKeyDiscovery {
|
||||||
|
image: image_index,
|
||||||
|
method_count: 0,
|
||||||
|
modulus: 0,
|
||||||
|
clean: true,
|
||||||
|
seed_residues: Vec::new(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let method_base = *methods
|
||||||
|
.iter()
|
||||||
|
.min()
|
||||||
|
.ok_or_else(|| Error::Malformed("image method minimum is missing".to_owned()))?;
|
||||||
|
let method_last = *methods
|
||||||
|
.iter()
|
||||||
|
.max()
|
||||||
|
.ok_or_else(|| Error::Malformed("image method maximum is missing".to_owned()))?;
|
||||||
|
if method_last - method_base + 1 != methods.len() {
|
||||||
|
return validation(format!(
|
||||||
|
"image {image_index} method block is not contiguous"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut values = Vec::with_capacity(methods.len());
|
||||||
|
let mut clean = true;
|
||||||
|
for method_index in methods {
|
||||||
|
let token = read_u32(
|
||||||
|
data,
|
||||||
|
method_offset + method_index * METHOD_STRIDE + METHOD_TOKEN_OFFSET,
|
||||||
|
)?;
|
||||||
|
if token & 0xff00_0000 != METHOD_TOKEN_TABLE {
|
||||||
|
return validation(format!(
|
||||||
|
"method {method_index} has non-MethodDef token 0x{token:08x}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let expected = u32::try_from(method_index - method_base + 1)
|
||||||
|
.map_err(|_| Error::Validation("local method RID exceeds u32".to_owned()))?;
|
||||||
|
let rid = token & 0x00ff_ffff;
|
||||||
|
clean &= rid == expected;
|
||||||
|
values.push((rid, expected));
|
||||||
|
}
|
||||||
|
let count = u32::try_from(values.len())
|
||||||
|
.map_err(|_| Error::Validation("image method count exceeds u32".to_owned()))?;
|
||||||
|
if clean {
|
||||||
|
reports.push(ImageKeyDiscovery {
|
||||||
|
image: image_index,
|
||||||
|
method_count: count,
|
||||||
|
modulus: count / 2,
|
||||||
|
clean,
|
||||||
|
seed_residues: Vec::new(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let low = values
|
||||||
|
.iter()
|
||||||
|
.map(|(rid, _)| *rid)
|
||||||
|
.min()
|
||||||
|
.ok_or_else(|| Error::Validation("image has no encrypted RID".to_owned()))?;
|
||||||
|
let high = values
|
||||||
|
.iter()
|
||||||
|
.map(|(rid, _)| *rid)
|
||||||
|
.max()
|
||||||
|
.ok_or_else(|| Error::Validation("image has no encrypted RID".to_owned()))?;
|
||||||
|
if high - low + 1 != count || count < 2 {
|
||||||
|
return validation(format!(
|
||||||
|
"image {image_index} RID interval is not a permutation"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let half = count / 2;
|
||||||
|
let quarter = count / 4;
|
||||||
|
let mut residues = Vec::new();
|
||||||
|
for key_delta in 0..half {
|
||||||
|
let key = quarter + key_delta;
|
||||||
|
let valid = values
|
||||||
|
.iter()
|
||||||
|
.all(|(rid, expected)| decrypt_rid_with_key(*rid, low, high, key) == *expected);
|
||||||
|
if valid {
|
||||||
|
residues.push(key_delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reports.push(ImageKeyDiscovery {
|
||||||
|
image: image_index,
|
||||||
|
method_count: count,
|
||||||
|
modulus: half,
|
||||||
|
clean,
|
||||||
|
seed_residues: residues,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let constraints = reports
|
||||||
|
.iter()
|
||||||
|
.filter(|report| !report.clean)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut seeds = Vec::new();
|
||||||
|
if let Some(anchor) = constraints.iter().max_by_key(|report| report.modulus) {
|
||||||
|
for &residue in &anchor.seed_residues {
|
||||||
|
let mut candidate = u64::from(residue);
|
||||||
|
let modulus = u64::from(anchor.modulus);
|
||||||
|
while candidate <= u64::from(u32::MAX) {
|
||||||
|
let valid = constraints.iter().all(|report| {
|
||||||
|
report.modulus != 0
|
||||||
|
&& !report.seed_residues.is_empty()
|
||||||
|
&& report
|
||||||
|
.seed_residues
|
||||||
|
.iter()
|
||||||
|
.any(|&value| candidate % u64::from(report.modulus) == u64::from(value))
|
||||||
|
});
|
||||||
|
if valid {
|
||||||
|
seeds.push(candidate as u32);
|
||||||
|
}
|
||||||
|
candidate = candidate.saturating_add(modulus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seeds.sort_unstable();
|
||||||
|
seeds.dedup();
|
||||||
|
Ok(SeedDiscoveryReport {
|
||||||
|
version,
|
||||||
|
images: reports,
|
||||||
|
seed_candidates: seeds,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_rid_with_key(rid: u32, low: u32, high: u32, key: u32) -> u32 {
|
||||||
|
let count = high - low + 1;
|
||||||
|
let mut value = rid - low;
|
||||||
|
for _ in 0..5 {
|
||||||
|
value = inverse_round(value, count, key);
|
||||||
|
}
|
||||||
|
value + low
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -394,6 +613,8 @@ mod tests {
|
|||||||
let (data, methods) = build(&tokens);
|
let (data, methods) = build(&tokens);
|
||||||
let (restored, report) =
|
let (restored, report) =
|
||||||
restore_method_tokens(&data, DEFAULT_METHOD_TOKEN_SEED).expect("restore");
|
restore_method_tokens(&data, DEFAULT_METHOD_TOKEN_SEED).expect("restore");
|
||||||
|
assert_eq!(report.encryption_status, "encrypted");
|
||||||
|
assert!(report.changed_tokens > 0);
|
||||||
assert_eq!(report.correct_after, 7);
|
assert_eq!(report.correct_after, 7);
|
||||||
for index in 0..7 {
|
for index in 0..7 {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -415,7 +636,24 @@ mod tests {
|
|||||||
let (data, _) = build(&tokens);
|
let (data, _) = build(&tokens);
|
||||||
let (restored, report) =
|
let (restored, report) =
|
||||||
restore_method_tokens(&data, DEFAULT_METHOD_TOKEN_SEED).expect("restore");
|
restore_method_tokens(&data, DEFAULT_METHOD_TOKEN_SEED).expect("restore");
|
||||||
|
assert_eq!(report.encryption_status, "clean");
|
||||||
assert_eq!(report.changed_tokens, 0);
|
assert_eq!(report.changed_tokens, 0);
|
||||||
assert_eq!(restored, data);
|
assert_eq!(restored, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypted_metadata_rejects_the_wrong_seed() {
|
||||||
|
let tokens = (1..=7)
|
||||||
|
.map(|expected| {
|
||||||
|
METHOD_TOKEN_TABLE | encrypted_rid(expected, 7, DEFAULT_METHOD_TOKEN_SEED)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let (data, _) = build(&tokens);
|
||||||
|
let wrong_seed = DEFAULT_METHOD_TOKEN_SEED.wrapping_add(1);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
restore_method_tokens(&data, wrong_seed),
|
||||||
|
Err(Error::Validation(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "senbei-android-stage2"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
description = "Static Stage 1 and Stage 2 extraction for Senbei Android"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
goblin.workspace = true
|
||||||
|
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,63 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Stage 1 or Stage 2 extraction failure.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("{action} `{path}`: {source}")]
|
||||||
|
Io {
|
||||||
|
action: &'static str,
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
#[error("parse ELF `{path}`: {source}")]
|
||||||
|
Elf {
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: goblin::error::Error,
|
||||||
|
},
|
||||||
|
#[error("serialize extraction index: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
#[error("embedded Stage 2 decoder configuration: {0}")]
|
||||||
|
EmbeddedConfig(#[source] senbei_android_crypto::Error),
|
||||||
|
#[error(
|
||||||
|
"depth {depth} stream 0x{stream_id:02X} interpreter 0x{interpreter_id:02X} configuration: {source}"
|
||||||
|
)]
|
||||||
|
InterpreterConfig {
|
||||||
|
depth: usize,
|
||||||
|
stream_id: u32,
|
||||||
|
interpreter_id: u32,
|
||||||
|
#[source]
|
||||||
|
source: senbei_android_crypto::Error,
|
||||||
|
},
|
||||||
|
#[error(
|
||||||
|
"depth {depth} stream 0x{stream_id:02X} record {record_index} command 0x{command_id:02X} {part}: {source}"
|
||||||
|
)]
|
||||||
|
RecordDecode {
|
||||||
|
depth: usize,
|
||||||
|
stream_id: u32,
|
||||||
|
record_index: usize,
|
||||||
|
command_id: u32,
|
||||||
|
part: &'static str,
|
||||||
|
#[source]
|
||||||
|
source: 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,529 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
|
use std::fs::{File, create_dir_all};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use memmap2::MmapOptions;
|
||||||
|
use senbei_android_crypto::{Module9bConfig, decode_container};
|
||||||
|
use serde_json::to_vec_pretty;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result, invalid};
|
||||||
|
use crate::report::{
|
||||||
|
ArtifactReport, DecoderReport, ExtractionReport, ModuleRegistryEntry, RecordReport,
|
||||||
|
Stage1Report, StreamParent, StreamReport,
|
||||||
|
};
|
||||||
|
use crate::stage1::{
|
||||||
|
DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE, SHT_LOUSER, Stage1Result, inspect,
|
||||||
|
};
|
||||||
|
use crate::stream::{DIRECT_FLAG, Record, parse_record_stream};
|
||||||
|
|
||||||
|
/// Inputs and output locations for one complete static Stage 2 extraction.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ExtractOptions {
|
||||||
|
pub input: PathBuf,
|
||||||
|
pub output_dir: PathBuf,
|
||||||
|
pub stage2_output: Option<PathBuf>,
|
||||||
|
pub outer_size: usize,
|
||||||
|
pub cipher_constant: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtractOptions {
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_defaults(input: PathBuf, output_dir: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
output_dir,
|
||||||
|
stage2_output: None,
|
||||||
|
outer_size: DEFAULT_OUTER_SIZE,
|
||||||
|
cipher_constant: DEFAULT_CIPHER_CONSTANT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct LoadedModule {
|
||||||
|
image: Vec<u8>,
|
||||||
|
metadata: Option<Vec<u8>>,
|
||||||
|
image_path: String,
|
||||||
|
metadata_path: Option<String>,
|
||||||
|
sha256: String,
|
||||||
|
depth: usize,
|
||||||
|
record_index: usize,
|
||||||
|
command_id: u32,
|
||||||
|
init_offset: u32,
|
||||||
|
entry_offset: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct ArtifactSpec<'a> {
|
||||||
|
suffix: &'a str,
|
||||||
|
kind: &'a str,
|
||||||
|
classification: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Extractor {
|
||||||
|
output_dir: PathBuf,
|
||||||
|
streams: Vec<StreamReport>,
|
||||||
|
artifacts: Vec<ArtifactReport>,
|
||||||
|
registry: BTreeMap<u32, LoadedModule>,
|
||||||
|
seen_streams: HashSet<(u32, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_stage2(options: &ExtractOptions) -> Result<ExtractionReport> {
|
||||||
|
let input_path = absolute(&options.input)?;
|
||||||
|
let output_dir = absolute(&options.output_dir)?;
|
||||||
|
if !input_path.is_file() {
|
||||||
|
return invalid(format!(
|
||||||
|
"protected ELF does not exist: {}",
|
||||||
|
input_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(stage2_output) = &options.stage2_output {
|
||||||
|
let stage2_output = absolute(stage2_output)?;
|
||||||
|
if stage2_output == input_path {
|
||||||
|
return invalid("refusing to overwrite the protected ELF with Stage 2 output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
create_dir_all(&output_dir)
|
||||||
|
.map_err(|source| Error::io("create Stage 2 output directory", &output_dir, source))?;
|
||||||
|
|
||||||
|
let file = File::open(&input_path)
|
||||||
|
.map_err(|source| Error::io("open protected ELF", &input_path, source))?;
|
||||||
|
// SAFETY: the mapping is read-only, the file remains open for the mapping
|
||||||
|
// lifetime, and extraction never mutates or truncates the source.
|
||||||
|
let source = unsafe { MmapOptions::new().map(&file) }
|
||||||
|
.map_err(|source| Error::io("map protected ELF", &input_path, source))?;
|
||||||
|
let stage1 = inspect(
|
||||||
|
&source,
|
||||||
|
&input_path,
|
||||||
|
options.outer_size,
|
||||||
|
options.cipher_constant,
|
||||||
|
)?;
|
||||||
|
if let Some(stage2_output) = &options.stage2_output {
|
||||||
|
write_atomic(&absolute(stage2_output)?, &stage1.plaintext)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let core_config =
|
||||||
|
Module9bConfig::parse_embedded(&stage1.plaintext).map_err(Error::EmbeddedConfig)?;
|
||||||
|
let bootstrap_end = stage1
|
||||||
|
.remaining_file_offset
|
||||||
|
.checked_add(stage1.remaining_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("Stage 2 bootstrap range overflow".to_owned()))?;
|
||||||
|
let bootstrap = source
|
||||||
|
.get(stage1.remaining_file_offset..bootstrap_end)
|
||||||
|
.ok_or_else(|| Error::Invalid("Stage 2 bootstrap range is outside the ELF".to_owned()))?;
|
||||||
|
let mut extractor = Extractor {
|
||||||
|
output_dir: output_dir.clone(),
|
||||||
|
streams: Vec::new(),
|
||||||
|
artifacts: Vec::new(),
|
||||||
|
registry: BTreeMap::new(),
|
||||||
|
seen_streams: HashSet::new(),
|
||||||
|
};
|
||||||
|
extractor.extract_stream(
|
||||||
|
bootstrap,
|
||||||
|
0xe2,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
Some(stage1.remaining_file_offset),
|
||||||
|
core_config,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let module_registry = extractor
|
||||||
|
.registry
|
||||||
|
.values()
|
||||||
|
.map(|module| ModuleRegistryEntry {
|
||||||
|
command_id: module.command_id,
|
||||||
|
size: module.image.len(),
|
||||||
|
sha256: module.sha256.clone(),
|
||||||
|
depth: module.depth,
|
||||||
|
record_index: module.record_index,
|
||||||
|
image_path: module.image_path.clone(),
|
||||||
|
metadata_path: module.metadata_path.clone(),
|
||||||
|
init_offset: module.init_offset,
|
||||||
|
entry_offset: module.entry_offset,
|
||||||
|
classification: if module.metadata.is_some() {
|
||||||
|
"module_image".to_owned()
|
||||||
|
} else {
|
||||||
|
"decoded_data".to_owned()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let report = ExtractionReport {
|
||||||
|
format_version: 4,
|
||||||
|
protected_elf: input_path.display().to_string(),
|
||||||
|
output_dir: output_dir.display().to_string(),
|
||||||
|
stage1: stage1_report(&stage1, options.outer_size),
|
||||||
|
streams: extractor.streams,
|
||||||
|
artifacts: extractor.artifacts,
|
||||||
|
errors: Vec::new(),
|
||||||
|
module_registry,
|
||||||
|
};
|
||||||
|
write_json_atomic(&output_dir.join("index.json"), &report)?;
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Extractor {
|
||||||
|
fn extract_stream(
|
||||||
|
&mut self,
|
||||||
|
stream: &[u8],
|
||||||
|
stream_id: u32,
|
||||||
|
depth: usize,
|
||||||
|
parent: Option<StreamParent>,
|
||||||
|
source_file_offset: Option<usize>,
|
||||||
|
config: Module9bConfig,
|
||||||
|
) -> Result<()> {
|
||||||
|
let digest = sha256(stream);
|
||||||
|
if !self.seen_streams.insert((stream_id, digest.clone())) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let (header, records, table_size) =
|
||||||
|
parse_record_stream(stream, stream_id).map_err(|source| {
|
||||||
|
Error::Invalid(format!(
|
||||||
|
"depth {depth} stream 0x{stream_id:02X} record table: {source}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let mut stream_report = StreamReport {
|
||||||
|
depth,
|
||||||
|
stream_id,
|
||||||
|
parent,
|
||||||
|
source_file_offset,
|
||||||
|
available_size: stream.len(),
|
||||||
|
descriptor_table_size: table_size,
|
||||||
|
encrypted_header_words: header.encrypted_words,
|
||||||
|
decrypted_header_words: header.decrypted_words,
|
||||||
|
record_state: header.record_state,
|
||||||
|
sha256: digest,
|
||||||
|
decoder: decoder_report(
|
||||||
|
if depth == 0 {
|
||||||
|
"embedded_stage2"
|
||||||
|
} else {
|
||||||
|
"decoded_interpreter"
|
||||||
|
},
|
||||||
|
(depth != 0).then_some(stream_id),
|
||||||
|
&config,
|
||||||
|
),
|
||||||
|
records: Vec::with_capacity(records.len()),
|
||||||
|
};
|
||||||
|
let mut direct_records = Vec::new();
|
||||||
|
let mut modules_at_level = BTreeSet::new();
|
||||||
|
|
||||||
|
for record in records {
|
||||||
|
let mut result = record_report(record);
|
||||||
|
let mut image_data = None;
|
||||||
|
let mut metadata_data = None;
|
||||||
|
|
||||||
|
if !record.direct() && record.image_size != 0 {
|
||||||
|
let image_source = record_tail(stream, record.image_offset)?;
|
||||||
|
let image = decode_container(image_source, &config, record.image_size as usize)
|
||||||
|
.map_err(|source| Error::RecordDecode {
|
||||||
|
depth,
|
||||||
|
stream_id,
|
||||||
|
record_index: record.index,
|
||||||
|
command_id: record.command_id,
|
||||||
|
part: "image decode",
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
let classification = if record.metadata_size != 0 {
|
||||||
|
"module_image"
|
||||||
|
} else {
|
||||||
|
"decoded_data"
|
||||||
|
};
|
||||||
|
let artifact = self.write_artifact(
|
||||||
|
&record,
|
||||||
|
depth,
|
||||||
|
stream_id,
|
||||||
|
ArtifactSpec {
|
||||||
|
suffix: "module.bin",
|
||||||
|
kind: "decoded_container",
|
||||||
|
classification,
|
||||||
|
},
|
||||||
|
&image,
|
||||||
|
)?;
|
||||||
|
result.image = Some(artifact.clone());
|
||||||
|
image_data = Some((image, artifact));
|
||||||
|
}
|
||||||
|
if record.metadata_size != 0 {
|
||||||
|
let metadata_source = record_tail(stream, record.metadata_offset)?;
|
||||||
|
let metadata =
|
||||||
|
decode_container(metadata_source, &config, record.metadata_size as usize)
|
||||||
|
.map_err(|source| Error::RecordDecode {
|
||||||
|
depth,
|
||||||
|
stream_id,
|
||||||
|
record_index: record.index,
|
||||||
|
command_id: record.command_id,
|
||||||
|
part: "metadata decode",
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
let artifact = self.write_artifact(
|
||||||
|
&record,
|
||||||
|
depth,
|
||||||
|
stream_id,
|
||||||
|
ArtifactSpec {
|
||||||
|
suffix: "metadata.bin",
|
||||||
|
kind: "decoded_metadata",
|
||||||
|
classification: "decoded_metadata",
|
||||||
|
},
|
||||||
|
&metadata,
|
||||||
|
)?;
|
||||||
|
result.metadata = Some(artifact.clone());
|
||||||
|
metadata_data = Some((metadata, artifact));
|
||||||
|
}
|
||||||
|
if let Some((image, image_artifact)) = image_data {
|
||||||
|
let (metadata, metadata_path) = if let Some((data, artifact)) = metadata_data {
|
||||||
|
(Some(data), Some(artifact.path))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
self.register_module(LoadedModule {
|
||||||
|
sha256: image_artifact.sha256.clone(),
|
||||||
|
image_path: image_artifact.path.clone(),
|
||||||
|
metadata_path,
|
||||||
|
image,
|
||||||
|
metadata,
|
||||||
|
depth,
|
||||||
|
record_index: record.index,
|
||||||
|
command_id: record.command_id,
|
||||||
|
init_offset: record.init_offset,
|
||||||
|
entry_offset: record.entry_offset,
|
||||||
|
})?;
|
||||||
|
modules_at_level.insert(record.command_id);
|
||||||
|
}
|
||||||
|
if record.direct() && record.image_size != 0 {
|
||||||
|
direct_records.push((record, stream_report.records.len()));
|
||||||
|
}
|
||||||
|
stream_report.records.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut children = Vec::new();
|
||||||
|
for (record, report_index) in direct_records {
|
||||||
|
let next_stream_id = record.command_id.wrapping_sub(0x10);
|
||||||
|
if modules_at_level.contains(&next_stream_id) {
|
||||||
|
stream_report.records[report_index].nested_stream_id = Some(next_stream_id);
|
||||||
|
children.push((record, next_stream_id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let direct_data = record_slice(stream, record.image_offset, record.image_size)?;
|
||||||
|
let artifact = self.write_artifact(
|
||||||
|
&record,
|
||||||
|
depth,
|
||||||
|
stream_id,
|
||||||
|
ArtifactSpec {
|
||||||
|
suffix: "direct.bin",
|
||||||
|
kind: "direct",
|
||||||
|
classification: "direct_data",
|
||||||
|
},
|
||||||
|
direct_data,
|
||||||
|
)?;
|
||||||
|
stream_report.records[report_index].image = Some(artifact);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.streams.push(stream_report);
|
||||||
|
for (record, next_stream_id) in children {
|
||||||
|
let child_data = record_slice(stream, record.image_offset, record.image_size)?;
|
||||||
|
let parent = StreamParent {
|
||||||
|
stream_id,
|
||||||
|
record_index: record.index,
|
||||||
|
command_id: record.command_id,
|
||||||
|
};
|
||||||
|
let interpreter = self.registry.get(&next_stream_id).ok_or_else(|| {
|
||||||
|
Error::Invalid(format!(
|
||||||
|
"depth {depth} stream 0x{stream_id:02X} child 0x{next_stream_id:02X} has no interpreter module"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let interpreter_config =
|
||||||
|
Module9bConfig::parse(&interpreter.image).map_err(|source| {
|
||||||
|
Error::InterpreterConfig {
|
||||||
|
depth: depth + 1,
|
||||||
|
stream_id: next_stream_id,
|
||||||
|
interpreter_id: next_stream_id,
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
self.extract_stream(
|
||||||
|
child_data,
|
||||||
|
next_stream_id,
|
||||||
|
depth + 1,
|
||||||
|
Some(parent),
|
||||||
|
None,
|
||||||
|
interpreter_config,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_module(&mut self, module: LoadedModule) -> Result<()> {
|
||||||
|
if let Some(previous) = self.registry.get(&module.command_id) {
|
||||||
|
if previous.sha256 != module.sha256 {
|
||||||
|
return invalid(format!(
|
||||||
|
"module 0x{:02X} produced conflicting images: {} and {}",
|
||||||
|
module.command_id, previous.sha256, module.sha256
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.registry.insert(module.command_id, module);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_artifact(
|
||||||
|
&mut self,
|
||||||
|
record: &Record,
|
||||||
|
depth: usize,
|
||||||
|
stream_id: u32,
|
||||||
|
spec: ArtifactSpec<'_>,
|
||||||
|
data: &[u8],
|
||||||
|
) -> Result<ArtifactReport> {
|
||||||
|
let digest = sha256(data);
|
||||||
|
let filename = format!(
|
||||||
|
"d{depth:02}_s{stream_id:02X}_r{:03}_id{:08X}_{}.{}",
|
||||||
|
record.index,
|
||||||
|
record.command_id,
|
||||||
|
&digest[..12],
|
||||||
|
spec.suffix
|
||||||
|
);
|
||||||
|
let path = self.output_dir.join(filename);
|
||||||
|
write_atomic(&path, data)?;
|
||||||
|
let artifact = ArtifactReport {
|
||||||
|
kind: spec.kind.to_owned(),
|
||||||
|
path: path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| Error::Invalid("artifact path has no file name".to_owned()))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned(),
|
||||||
|
size: data.len(),
|
||||||
|
sha256: digest,
|
||||||
|
depth,
|
||||||
|
stream_id,
|
||||||
|
record_index: Some(record.index),
|
||||||
|
command_id: Some(record.command_id),
|
||||||
|
classification: spec.classification.to_owned(),
|
||||||
|
};
|
||||||
|
self.artifacts.push(artifact.clone());
|
||||||
|
Ok(artifact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_report(record: Record) -> RecordReport {
|
||||||
|
RecordReport {
|
||||||
|
index: record.index,
|
||||||
|
command_id: record.command_id,
|
||||||
|
flags: record.flags,
|
||||||
|
image_offset: record.image_offset,
|
||||||
|
image_size: record.image_size,
|
||||||
|
metadata_offset: record.metadata_offset,
|
||||||
|
metadata_size: record.metadata_size,
|
||||||
|
id_copy: record.id_copy,
|
||||||
|
entry_offset: record.entry_offset,
|
||||||
|
init_offset: record.init_offset,
|
||||||
|
direct: record.flags & DIRECT_FLAG != 0,
|
||||||
|
extraction_status: "complete".to_owned(),
|
||||||
|
image: None,
|
||||||
|
metadata: None,
|
||||||
|
nested_stream_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decoder_report(
|
||||||
|
kind: &str,
|
||||||
|
interpreter_id: Option<u32>,
|
||||||
|
config: &Module9bConfig,
|
||||||
|
) -> DecoderReport {
|
||||||
|
DecoderReport {
|
||||||
|
kind: kind.to_owned(),
|
||||||
|
interpreter_id,
|
||||||
|
header_seed: config.header_seed,
|
||||||
|
container_seed: config.container_seed,
|
||||||
|
schedule_offset: config.schedule_offset,
|
||||||
|
aes_key_sha256: sha256(&config.aes_key),
|
||||||
|
skip_aes: config.skip_aes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_slice(stream: &[u8], offset: u32, size: u32) -> Result<&[u8]> {
|
||||||
|
let offset = usize::try_from(offset)
|
||||||
|
.map_err(|_| Error::Invalid("record payload offset exceeds usize".to_owned()))?;
|
||||||
|
let size = usize::try_from(size)
|
||||||
|
.map_err(|_| Error::Invalid("record payload size exceeds usize".to_owned()))?;
|
||||||
|
let end = offset
|
||||||
|
.checked_add(size)
|
||||||
|
.ok_or_else(|| Error::Invalid("record payload range overflows usize".to_owned()))?;
|
||||||
|
stream.get(offset..end).ok_or_else(|| {
|
||||||
|
Error::Invalid(format!(
|
||||||
|
"record payload range 0x{offset:x}..0x{end:x} exceeds stream 0x{:x}",
|
||||||
|
stream.len()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_tail(stream: &[u8], offset: u32) -> Result<&[u8]> {
|
||||||
|
let offset = usize::try_from(offset)
|
||||||
|
.map_err(|_| Error::Invalid("record container offset exceeds usize".to_owned()))?;
|
||||||
|
stream.get(offset..).ok_or_else(|| {
|
||||||
|
Error::Invalid(format!(
|
||||||
|
"record container offset 0x{offset:x} exceeds stream 0x{:x}",
|
||||||
|
stream.len()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stage1_report(stage1: &Stage1Result, outer_size: usize) -> Stage1Report {
|
||||||
|
Stage1Report {
|
||||||
|
section_index: stage1.section_index,
|
||||||
|
section_type: SHT_LOUSER,
|
||||||
|
section_offset: stage1.section_offset,
|
||||||
|
section_size: stage1.section_size,
|
||||||
|
outer_size,
|
||||||
|
header_offset: stage1.header_offset,
|
||||||
|
header_key: stage1.header.key,
|
||||||
|
payload_offset: stage1.header.payload_offset,
|
||||||
|
payload_size: stage1.header.payload_size,
|
||||||
|
payload_key: stage1.header.payload_key,
|
||||||
|
entry_offset: stage1.header.entry_offset,
|
||||||
|
protect_size: stage1.header.protect_size,
|
||||||
|
stage2_file_offset: stage1.payload_file_offset,
|
||||||
|
stage2_size: stage1.plaintext.len(),
|
||||||
|
stage2_sha256: sha256(&stage1.plaintext),
|
||||||
|
remaining_file_offset: stage1.remaining_file_offset,
|
||||||
|
remaining_size: stage1.remaining_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_json_atomic(path: &Path, value: &impl serde::Serialize) -> Result<()> {
|
||||||
|
let mut bytes = to_vec_pretty(value)?;
|
||||||
|
bytes.push(b'\n');
|
||||||
|
write_atomic(path, &bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
|
||||||
|
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
|
create_dir_all(parent)
|
||||||
|
.map_err(|source| Error::io("create output directory", parent, source))?;
|
||||||
|
let mut temporary = NamedTempFile::new_in(parent)
|
||||||
|
.map_err(|source| Error::io("create temporary output", parent, source))?;
|
||||||
|
temporary
|
||||||
|
.write_all(data)
|
||||||
|
.and_then(|()| temporary.as_file().sync_all())
|
||||||
|
.map_err(|source| Error::io("write temporary output", temporary.path(), source))?;
|
||||||
|
temporary
|
||||||
|
.persist(path)
|
||||||
|
.map_err(|error| Error::io("replace output", path, error.error))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn absolute(path: &Path) -> Result<PathBuf> {
|
||||||
|
if path.is_absolute() {
|
||||||
|
Ok(path.to_path_buf())
|
||||||
|
} else {
|
||||||
|
std::env::current_dir()
|
||||||
|
.map(|current| current.join(path))
|
||||||
|
.map_err(|source| Error::io("query current directory", path, source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sha256(data: &[u8]) -> String {
|
||||||
|
let mut digest = Sha256::new();
|
||||||
|
digest.update(data);
|
||||||
|
format!("{:x}", digest.finalize())
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//! Pure-static Stage 1 decryption and recursive Stage 2 module extraction.
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod extract;
|
||||||
|
mod report;
|
||||||
|
mod stage1;
|
||||||
|
mod stream;
|
||||||
|
|
||||||
|
pub use error::Error;
|
||||||
|
pub use extract::{ExtractOptions, extract_stage2};
|
||||||
|
pub use report::ExtractionReport;
|
||||||
|
pub use stage1::{DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE};
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct Stage1Report {
|
||||||
|
pub section_index: usize,
|
||||||
|
pub section_type: u32,
|
||||||
|
pub section_offset: usize,
|
||||||
|
pub section_size: usize,
|
||||||
|
pub outer_size: usize,
|
||||||
|
pub header_offset: usize,
|
||||||
|
pub header_key: u32,
|
||||||
|
pub payload_offset: u32,
|
||||||
|
pub payload_size: u32,
|
||||||
|
pub payload_key: u32,
|
||||||
|
pub entry_offset: u32,
|
||||||
|
pub protect_size: u32,
|
||||||
|
pub stage2_file_offset: usize,
|
||||||
|
pub stage2_size: usize,
|
||||||
|
pub stage2_sha256: String,
|
||||||
|
pub remaining_file_offset: usize,
|
||||||
|
pub remaining_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DecoderReport {
|
||||||
|
pub kind: String,
|
||||||
|
pub interpreter_id: Option<u32>,
|
||||||
|
pub header_seed: u32,
|
||||||
|
pub container_seed: u32,
|
||||||
|
pub schedule_offset: usize,
|
||||||
|
pub aes_key_sha256: String,
|
||||||
|
pub skip_aes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct ArtifactReport {
|
||||||
|
pub kind: String,
|
||||||
|
pub path: String,
|
||||||
|
pub size: usize,
|
||||||
|
pub sha256: String,
|
||||||
|
pub depth: usize,
|
||||||
|
pub stream_id: u32,
|
||||||
|
pub record_index: Option<usize>,
|
||||||
|
pub command_id: Option<u32>,
|
||||||
|
pub classification: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct RecordReport {
|
||||||
|
pub index: usize,
|
||||||
|
pub command_id: u32,
|
||||||
|
pub flags: u32,
|
||||||
|
pub image_offset: u32,
|
||||||
|
pub image_size: u32,
|
||||||
|
pub metadata_offset: u32,
|
||||||
|
pub metadata_size: u32,
|
||||||
|
pub id_copy: u32,
|
||||||
|
pub entry_offset: u32,
|
||||||
|
pub init_offset: u32,
|
||||||
|
pub direct: bool,
|
||||||
|
pub extraction_status: String,
|
||||||
|
pub image: Option<ArtifactReport>,
|
||||||
|
pub metadata: Option<ArtifactReport>,
|
||||||
|
pub nested_stream_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct StreamParent {
|
||||||
|
pub stream_id: u32,
|
||||||
|
pub record_index: usize,
|
||||||
|
pub command_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct StreamReport {
|
||||||
|
pub depth: usize,
|
||||||
|
pub stream_id: u32,
|
||||||
|
pub parent: Option<StreamParent>,
|
||||||
|
pub source_file_offset: Option<usize>,
|
||||||
|
pub available_size: usize,
|
||||||
|
pub descriptor_table_size: usize,
|
||||||
|
pub encrypted_header_words: [u32; 2],
|
||||||
|
pub decrypted_header_words: [u32; 2],
|
||||||
|
pub record_state: u32,
|
||||||
|
pub sha256: String,
|
||||||
|
pub decoder: DecoderReport,
|
||||||
|
pub records: Vec<RecordReport>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct ModuleRegistryEntry {
|
||||||
|
pub command_id: u32,
|
||||||
|
pub size: usize,
|
||||||
|
pub sha256: String,
|
||||||
|
pub depth: usize,
|
||||||
|
pub record_index: usize,
|
||||||
|
pub image_path: String,
|
||||||
|
pub metadata_path: Option<String>,
|
||||||
|
pub init_offset: u32,
|
||||||
|
pub entry_offset: u32,
|
||||||
|
pub classification: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Machine-readable output of one complete static Stage 2 extraction.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct ExtractionReport {
|
||||||
|
pub format_version: u32,
|
||||||
|
pub protected_elf: String,
|
||||||
|
pub output_dir: String,
|
||||||
|
pub stage1: Stage1Report,
|
||||||
|
pub streams: Vec<StreamReport>,
|
||||||
|
pub artifacts: Vec<ArtifactReport>,
|
||||||
|
pub errors: Vec<String>,
|
||||||
|
pub module_registry: Vec<ModuleRegistryEntry>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use goblin::elf::{Elf, header::EM_AARCH64};
|
||||||
|
|
||||||
|
use crate::error::{Error, Result, invalid};
|
||||||
|
|
||||||
|
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000;
|
||||||
|
pub const DEFAULT_CIPHER_CONSTANT: u32 = 0xbf20_165d;
|
||||||
|
pub const DEFAULT_OUTER_SIZE: usize = 0x23c;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) struct Stage1Header {
|
||||||
|
pub key: u32,
|
||||||
|
pub reserved: u32,
|
||||||
|
pub payload_offset: u32,
|
||||||
|
pub payload_size: u32,
|
||||||
|
pub payload_key: u32,
|
||||||
|
pub entry_offset: u32,
|
||||||
|
pub protect_size: u32,
|
||||||
|
pub size_copy: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Stage1Result {
|
||||||
|
pub section_index: usize,
|
||||||
|
pub section_offset: usize,
|
||||||
|
pub section_size: usize,
|
||||||
|
pub header_offset: usize,
|
||||||
|
pub payload_file_offset: usize,
|
||||||
|
pub remaining_file_offset: usize,
|
||||||
|
pub remaining_size: usize,
|
||||||
|
pub header: Stage1Header,
|
||||||
|
pub plaintext: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inspect(
|
||||||
|
data: &[u8],
|
||||||
|
path: &Path,
|
||||||
|
outer_size: usize,
|
||||||
|
cipher_constant: u32,
|
||||||
|
) -> Result<Stage1Result> {
|
||||||
|
let elf = Elf::parse(data).map_err(|source| Error::Elf {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
if elf.header.e_machine != EM_AARCH64 {
|
||||||
|
return invalid(format!(
|
||||||
|
"expected AArch64 ELF (machine 0x{EM_AARCH64:X}), got 0x{:X}",
|
||||||
|
elf.header.e_machine
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let matches = elf
|
||||||
|
.section_headers
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, section)| section.sh_type == SHT_LOUSER)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if matches.len() != 1 {
|
||||||
|
return invalid(format!(
|
||||||
|
"expected exactly one SHT_LOUSER section, found {}",
|
||||||
|
matches.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let (section_index, section) = matches[0];
|
||||||
|
let section_offset = usize::try_from(section.sh_offset)
|
||||||
|
.map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?;
|
||||||
|
let section_size = usize::try_from(section.sh_size)
|
||||||
|
.map_err(|_| Error::Invalid("SHT_LOUSER size exceeds usize".to_owned()))?;
|
||||||
|
let section_end = section_offset
|
||||||
|
.checked_add(section_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("SHT_LOUSER range overflows usize".to_owned()))?;
|
||||||
|
if section_end > data.len() {
|
||||||
|
return invalid("SHT_LOUSER range extends beyond the input file");
|
||||||
|
}
|
||||||
|
let header_relative = outer_size;
|
||||||
|
if outer_size
|
||||||
|
.checked_add(0x1000)
|
||||||
|
.is_none_or(|end| end > section_size)
|
||||||
|
{
|
||||||
|
return invalid("Stage 1 outer header leaves no complete parameter area");
|
||||||
|
}
|
||||||
|
let header_offset = section_offset
|
||||||
|
.checked_add(header_relative)
|
||||||
|
.ok_or_else(|| Error::Invalid("Stage 1 header offset overflow".to_owned()))?;
|
||||||
|
let header_raw = bytes(data, header_offset, 0x1000)?;
|
||||||
|
let header = decrypt_header(header_raw, cipher_constant)?;
|
||||||
|
if header.reserved != 0 {
|
||||||
|
return invalid(format!(
|
||||||
|
"Stage 1 header reserved word is nonzero: 0x{:x}",
|
||||||
|
header.reserved
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if header.size_copy != header.payload_size {
|
||||||
|
return invalid(format!(
|
||||||
|
"Stage 1 payload size copy 0x{:x} != size 0x{:x}",
|
||||||
|
header.size_copy, header.payload_size
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let private_size = section_size - outer_size;
|
||||||
|
let payload_offset = usize::try_from(header.payload_offset)
|
||||||
|
.map_err(|_| Error::Invalid("Stage 1 payload offset exceeds usize".to_owned()))?;
|
||||||
|
let payload_size = usize::try_from(header.payload_size)
|
||||||
|
.map_err(|_| Error::Invalid("Stage 1 payload size exceeds usize".to_owned()))?;
|
||||||
|
let payload_end = payload_offset
|
||||||
|
.checked_add(payload_size)
|
||||||
|
.ok_or_else(|| Error::Invalid("Stage 1 payload range overflow".to_owned()))?;
|
||||||
|
if payload_offset < 0x20 || payload_end > private_size {
|
||||||
|
return invalid(format!(
|
||||||
|
"Stage 1 payload range 0x{payload_offset:x}..0x{payload_end:x} exceeds private size 0x{private_size:x}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if payload_size == 0 || payload_size % 4 != 0 {
|
||||||
|
return invalid(format!(
|
||||||
|
"Stage 1 payload size must be nonzero and word aligned: 0x{payload_size:x}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let entry_offset = usize::try_from(header.entry_offset)
|
||||||
|
.map_err(|_| Error::Invalid("Stage 1 entry offset exceeds usize".to_owned()))?;
|
||||||
|
if entry_offset >= payload_size {
|
||||||
|
return invalid("Stage 1 entry offset is outside the payload");
|
||||||
|
}
|
||||||
|
let protect_size = usize::try_from(header.protect_size)
|
||||||
|
.map_err(|_| Error::Invalid("Stage 1 protect size exceeds usize".to_owned()))?;
|
||||||
|
if protect_size > payload_size {
|
||||||
|
return invalid("Stage 1 mprotect length exceeds the payload");
|
||||||
|
}
|
||||||
|
let payload_file_offset = header_offset
|
||||||
|
.checked_add(payload_offset)
|
||||||
|
.ok_or_else(|| Error::Invalid("Stage 1 payload file offset overflow".to_owned()))?;
|
||||||
|
let encrypted = bytes(data, payload_file_offset, payload_size)?;
|
||||||
|
let plaintext = decrypt_words(encrypted, header.payload_key, cipher_constant)?;
|
||||||
|
let aligned_payload_end = (payload_end + 3) & !3;
|
||||||
|
let remaining_relative = aligned_payload_end;
|
||||||
|
if remaining_relative > private_size {
|
||||||
|
return invalid("aligned Stage 2 cursor exceeds SHT_LOUSER");
|
||||||
|
}
|
||||||
|
let remaining_file_offset = section_offset
|
||||||
|
.checked_add(outer_size)
|
||||||
|
.and_then(|value| value.checked_add(remaining_relative))
|
||||||
|
.ok_or_else(|| Error::Invalid("Stage 2 stream offset overflow".to_owned()))?;
|
||||||
|
Ok(Stage1Result {
|
||||||
|
section_index,
|
||||||
|
section_offset,
|
||||||
|
section_size,
|
||||||
|
header_offset,
|
||||||
|
payload_file_offset,
|
||||||
|
remaining_file_offset,
|
||||||
|
remaining_size: private_size - remaining_relative,
|
||||||
|
header,
|
||||||
|
plaintext,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_header(raw: &[u8], constant: u32) -> Result<Stage1Header> {
|
||||||
|
let key = read_u32(raw, 0)?;
|
||||||
|
let mut decoded = decrypt_words(&raw[..0x20], key, constant)?;
|
||||||
|
decoded[..4].copy_from_slice(&key.to_le_bytes());
|
||||||
|
Ok(Stage1Header {
|
||||||
|
key,
|
||||||
|
reserved: read_u32(&decoded, 4)?,
|
||||||
|
payload_offset: read_u32(&decoded, 8)?,
|
||||||
|
payload_size: read_u32(&decoded, 12)?,
|
||||||
|
payload_key: read_u32(&decoded, 16)?,
|
||||||
|
entry_offset: read_u32(&decoded, 20)?,
|
||||||
|
protect_size: read_u32(&decoded, 24)?,
|
||||||
|
size_copy: read_u32(&decoded, 28)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_words(ciphertext: &[u8], key: u32, constant: u32) -> Result<Vec<u8>> {
|
||||||
|
if ciphertext.len() % 4 != 0 {
|
||||||
|
return invalid("Stage 1 word cipher input is not 4-byte aligned");
|
||||||
|
}
|
||||||
|
let mut plaintext = ciphertext.to_vec();
|
||||||
|
for (index, chunk) in plaintext.chunks_exact_mut(4).enumerate() {
|
||||||
|
let index = u32::try_from(index)
|
||||||
|
.map_err(|_| Error::Invalid("Stage 1 word index exceeds u32".to_owned()))?;
|
||||||
|
let mut word = u32::from_le_bytes(
|
||||||
|
chunk
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Invalid("Stage 1 word has an invalid size".to_owned()))?,
|
||||||
|
);
|
||||||
|
word = word.wrapping_add(index.wrapping_add(3).wrapping_mul(key));
|
||||||
|
word ^= constant.wrapping_mul(index.wrapping_add(1));
|
||||||
|
chunk.copy_from_slice(&word.to_le_bytes());
|
||||||
|
}
|
||||||
|
Ok(plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(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 outside the input"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
|
||||||
|
let bytes = bytes(data, offset, 4)?;
|
||||||
|
Ok(u32::from_le_bytes(bytes.try_into().map_err(|_| {
|
||||||
|
Error::Invalid("invalid u32 byte range".to_owned())
|
||||||
|
})?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stage1_word_transform_round_trips() {
|
||||||
|
let key = 0x1234_5678;
|
||||||
|
let constant = DEFAULT_CIPHER_CONSTANT;
|
||||||
|
let plain = [0x1122_3344_u32, 0xaabb_ccdd, 0x0102_0304];
|
||||||
|
let mut cipher = Vec::new();
|
||||||
|
for (index, value) in plain.into_iter().enumerate() {
|
||||||
|
let index = index as u32;
|
||||||
|
let word = (value ^ constant.wrapping_mul(index + 1))
|
||||||
|
.wrapping_sub((index + 3).wrapping_mul(key));
|
||||||
|
cipher.extend_from_slice(&word.to_le_bytes());
|
||||||
|
}
|
||||||
|
let decoded = decrypt_words(&cipher, key, constant).unwrap();
|
||||||
|
let expected = plain
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(u32::to_le_bytes)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(decoded, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
use senbei_android_crypto::gf32_mul_fixed;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result, invalid};
|
||||||
|
|
||||||
|
pub(crate) const RECORD_SIZE: usize = 0x5c;
|
||||||
|
pub(crate) const DIRECT_FLAG: u32 = 2;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) struct Record {
|
||||||
|
pub index: usize,
|
||||||
|
pub command_id: u32,
|
||||||
|
pub flags: u32,
|
||||||
|
pub image_offset: u32,
|
||||||
|
pub image_size: u32,
|
||||||
|
pub metadata_offset: u32,
|
||||||
|
pub metadata_size: u32,
|
||||||
|
pub id_copy: u32,
|
||||||
|
pub entry_offset: u32,
|
||||||
|
pub init_offset: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Record {
|
||||||
|
pub(crate) fn direct(self) -> bool {
|
||||||
|
self.flags & DIRECT_FLAG != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) struct StreamHeader {
|
||||||
|
pub encrypted_words: [u32; 2],
|
||||||
|
pub decrypted_words: [u32; 2],
|
||||||
|
pub record_state: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_record_stream(
|
||||||
|
stream: &[u8],
|
||||||
|
stream_id: u32,
|
||||||
|
) -> Result<(StreamHeader, Vec<Record>, usize)> {
|
||||||
|
if stream.len() < 8 {
|
||||||
|
return invalid(format!(
|
||||||
|
"stream 0x{stream_id:02X} is shorter than its 8-byte header"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let cipher0 = read_u32(stream, 0)?;
|
||||||
|
let cipher1 = read_u32(stream, 4)?;
|
||||||
|
let key = stream_id.wrapping_mul(0x9d32_3cd7);
|
||||||
|
let shift = stream_id & 7;
|
||||||
|
let base = (key >> shift)
|
||||||
|
.wrapping_add(0x5e72_7d74)
|
||||||
|
.wrapping_add(key.wrapping_shl(stream_id & 0xb))
|
||||||
|
.wrapping_add(0xf71e_3005);
|
||||||
|
let plain0 =
|
||||||
|
gf32_mul_fixed(cipher0.wrapping_add(0xcbf0_c1d8)) ^ 0xeb_e81dba_u32.wrapping_add(base);
|
||||||
|
let plain1 = gf32_mul_fixed(cipher1.wrapping_add(cipher0))
|
||||||
|
^ 0xeb_e81dba_u32.wrapping_mul(5).wrapping_add(base);
|
||||||
|
let header = StreamHeader {
|
||||||
|
encrypted_words: [cipher0, cipher1],
|
||||||
|
decrypted_words: [plain0, plain1],
|
||||||
|
record_state: plain1.wrapping_add(base),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut records = Vec::new();
|
||||||
|
let mut first_payload = stream.len();
|
||||||
|
for index in 0..256_usize {
|
||||||
|
let start =
|
||||||
|
8_usize
|
||||||
|
.checked_add(index.checked_mul(RECORD_SIZE).ok_or_else(|| {
|
||||||
|
Error::Invalid("record descriptor offset overflow".to_owned())
|
||||||
|
})?)
|
||||||
|
.ok_or_else(|| Error::Invalid("record descriptor offset overflow".to_owned()))?;
|
||||||
|
let end = start
|
||||||
|
.checked_add(RECORD_SIZE)
|
||||||
|
.ok_or_else(|| Error::Invalid("record descriptor end overflow".to_owned()))?;
|
||||||
|
if end > stream.len() {
|
||||||
|
return invalid(format!(
|
||||||
|
"stream 0x{stream_id:02X} descriptor table is truncated at record {index}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let record = decrypt_record(&stream[start..end], index, header.record_state)?;
|
||||||
|
if record.id_copy != 0 && record.command_id != record.id_copy {
|
||||||
|
return invalid(format!(
|
||||||
|
"stream 0x{stream_id:02X} record {index} command/id mismatch: 0x{:X} != 0x{:X}",
|
||||||
|
record.command_id, record.id_copy
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for (offset, size) in [
|
||||||
|
(record.image_offset, record.image_size),
|
||||||
|
(record.metadata_offset, record.metadata_size),
|
||||||
|
] {
|
||||||
|
if offset != 0 && size != 0 {
|
||||||
|
let offset = usize::try_from(offset).map_err(|_| {
|
||||||
|
Error::Invalid(format!(
|
||||||
|
"stream 0x{stream_id:02X} record {index} payload offset exceeds usize"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if offset >= stream.len() {
|
||||||
|
return invalid(format!(
|
||||||
|
"stream 0x{stream_id:02X} record {index} payload offset 0x{offset:x} exceeds stream 0x{:x}",
|
||||||
|
stream.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
first_payload = first_payload.min(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
records.push(record);
|
||||||
|
if end == first_payload {
|
||||||
|
return Ok((header, records, first_payload));
|
||||||
|
}
|
||||||
|
if end > first_payload {
|
||||||
|
return invalid(format!(
|
||||||
|
"stream 0x{stream_id:02X} descriptor table crosses first payload at 0x{first_payload:x}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalid(format!(
|
||||||
|
"stream 0x{stream_id:02X} has no descriptor boundary in 256 records"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_record(raw: &[u8], index: usize, state: u32) -> Result<Record> {
|
||||||
|
if raw.len() != RECORD_SIZE {
|
||||||
|
return invalid(format!(
|
||||||
|
"record {index} has size 0x{:x}, expected 0x{RECORD_SIZE:x}",
|
||||||
|
raw.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let product = state.wrapping_add(0x96f6_0b71).wrapping_mul(state);
|
||||||
|
let index_mask = product.wrapping_shl(((index + 1) & 3) as u32);
|
||||||
|
let mix = state.wrapping_mul(0x06a5_5bcc).wrapping_add(product);
|
||||||
|
let mut accumulator = 0x7993_4cf6_u32;
|
||||||
|
let mut feedback = 0xf02f_7685_u32;
|
||||||
|
let mut words = [0_u32; RECORD_SIZE / 4];
|
||||||
|
for (word_index, chunk) in raw.chunks_exact(4).enumerate() {
|
||||||
|
feedback = feedback.wrapping_mul(feedback);
|
||||||
|
let cipher = u32::from_le_bytes(
|
||||||
|
chunk
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::Invalid("record word has an invalid size".to_owned()))?,
|
||||||
|
);
|
||||||
|
let mut value = gf32_mul_fixed(cipher ^ (feedback >> 3)) ^ index_mask;
|
||||||
|
value = value.wrapping_add(accumulator).wrapping_add(state);
|
||||||
|
value = value.wrapping_sub(mix >> ((word_index * 4 + 3) & 5));
|
||||||
|
words[word_index] = value;
|
||||||
|
accumulator = accumulator.wrapping_add(0xe64d_33d8);
|
||||||
|
feedback = cipher;
|
||||||
|
}
|
||||||
|
Ok(Record {
|
||||||
|
index,
|
||||||
|
command_id: words[0],
|
||||||
|
flags: words[1],
|
||||||
|
image_offset: words[2],
|
||||||
|
image_size: words[3],
|
||||||
|
metadata_offset: words[4],
|
||||||
|
metadata_size: words[5],
|
||||||
|
id_copy: words[6],
|
||||||
|
entry_offset: words[7],
|
||||||
|
init_offset: words[8],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
|
||||||
|
let bytes = data.get(offset..offset + 4).ok_or_else(|| {
|
||||||
|
Error::Invalid(format!("record header range 0x{offset:x} is out of bounds"))
|
||||||
|
})?;
|
||||||
|
Ok(u32::from_le_bytes(bytes.try_into().map_err(|_| {
|
||||||
|
Error::Invalid("invalid record u32 range".to_owned())
|
||||||
|
})?))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user