feat: add static stage extraction and metadata detection

This commit is contained in:
bfloat16
2026-08-16 01:47:25 +08:00
parent b1d3699df3
commit 131ced6db5
17 changed files with 1840 additions and 49 deletions
+20
View File
@@ -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
+63
View File
@@ -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()))
}
+529
View File
@@ -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())
}
+12
View File
@@ -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};
+115
View File
@@ -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>,
}
+231
View File
@@ -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);
}
}
+168
View File
@@ -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())
})?))
}