mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-20 06:18:01 -04:00
refactor: consolidate platform engines into senbei-engine
This commit is contained in:
@@ -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_crypto::android::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_crypto::android::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_crypto::android::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,12 @@
|
||||
mod error;
|
||||
mod pipeline;
|
||||
mod probe;
|
||||
mod report;
|
||||
mod stage1;
|
||||
mod stream;
|
||||
|
||||
pub use error::Error;
|
||||
pub use pipeline::{ExtractOptions, extract_stage2};
|
||||
pub use probe::is_protected_libil2cpp;
|
||||
pub use report::ExtractionReport;
|
||||
pub use stage1::{DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE};
|
||||
@@ -0,0 +1,538 @@
|
||||
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_crypto::android::{Module9bConfig, decode_container};
|
||||
use serde_json::to_vec_pretty;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use super::error::{Error, Result, invalid};
|
||||
use super::report::{
|
||||
ArtifactReport, DecoderReport, ExtractionReport, ModuleRegistryEntry, RecordReport,
|
||||
Stage1Report, StreamParent, StreamReport,
|
||||
};
|
||||
use super::stage1::{
|
||||
DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE, SHT_LOUSER, Stage1Result, inspect,
|
||||
};
|
||||
use super::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);
|
||||
hex_digest(&digest.finalize())
|
||||
}
|
||||
/// Lowercase hex of a digest output (sha2 0.11's `Array` no longer formats as
|
||||
/// hex directly).
|
||||
fn hex_digest(data: &[u8]) -> String {
|
||||
let mut out = String::with_capacity(data.len() * 2);
|
||||
for byte in data {
|
||||
out.push_str(&format!("{byte:02x}"));
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use std::path::Path;
|
||||
|
||||
use senbei_crypto::android::Module9bConfig;
|
||||
|
||||
use super::stage1::{self, DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE};
|
||||
|
||||
/// Return whether `data` has a supported protected AArch64 IL2CPP layout.
|
||||
#[must_use]
|
||||
pub fn is_protected_libil2cpp(data: &[u8]) -> bool {
|
||||
if !stage1::looks_protected(data) {
|
||||
return false;
|
||||
}
|
||||
let Ok(stage1) = stage1::inspect(
|
||||
data,
|
||||
Path::new("<probe>"),
|
||||
DEFAULT_OUTER_SIZE,
|
||||
DEFAULT_CIPHER_CONSTANT,
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
Module9bConfig::parse_embedded(&stage1.plaintext).is_ok()
|
||||
}
|
||||
@@ -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,262 @@
|
||||
use std::path::Path;
|
||||
|
||||
use goblin::elf::{Elf, header::EM_AARCH64};
|
||||
|
||||
use super::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;
|
||||
|
||||
pub(crate) fn looks_protected(data: &[u8]) -> bool {
|
||||
let Ok(elf) = Elf::parse(data) else {
|
||||
return false;
|
||||
};
|
||||
if elf.header.e_machine != EM_AARCH64
|
||||
|| elf
|
||||
.section_headers
|
||||
.iter()
|
||||
.filter(|section| section.sh_type == SHT_LOUSER)
|
||||
.count()
|
||||
!= 1
|
||||
{
|
||||
return false;
|
||||
}
|
||||
[
|
||||
".dynsym",
|
||||
".dynstr",
|
||||
".gnu.hash",
|
||||
".gnu.version",
|
||||
".gnu.version_r",
|
||||
]
|
||||
.into_iter()
|
||||
.all(|wanted| {
|
||||
elf.section_headers.iter().any(|section| {
|
||||
elf.shdr_strtab
|
||||
.get_at(section.sh_name)
|
||||
.is_some_and(|name| name == wanted)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[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_crypto::android::gf32_mul_fixed;
|
||||
|
||||
use super::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())
|
||||
})?))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//! Android AArch64 extraction and ELF restoration.
|
||||
|
||||
mod extract;
|
||||
mod restore;
|
||||
|
||||
pub use extract::{
|
||||
DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE, Error as ExtractionError, ExtractOptions,
|
||||
ExtractionReport, extract_stage2, is_protected_libil2cpp,
|
||||
};
|
||||
pub use restore::{Error as RestoreError, RestoreOptions, RestoreReport, restore_libil2cpp};
|
||||
@@ -0,0 +1,105 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::error::{Error, Result, invalid};
|
||||
|
||||
const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Artifact {
|
||||
pub path: PathBuf,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact>> {
|
||||
let text = std::fs::read_to_string(index_path)
|
||||
.map_err(|error| Error::io("read module index", index_path, error))?;
|
||||
let document: Value = serde_json::from_str(&text)?;
|
||||
let root = index_path.parent().unwrap_or_else(|| Path::new("."));
|
||||
let mut result = BTreeMap::new();
|
||||
|
||||
if let Some(items) = document.get("module_registry").and_then(Value::as_array) {
|
||||
for item in items {
|
||||
let Some(command_id) = item.get("command_id").and_then(Value::as_u64) else {
|
||||
continue;
|
||||
};
|
||||
let command_id = u32::try_from(command_id)
|
||||
.map_err(|_| Error::Invalid("module command ID exceeds u32".to_owned()))?;
|
||||
if !REQUIRED_IDS.contains(&command_id) {
|
||||
continue;
|
||||
}
|
||||
let Some(path) = item.get("image_path").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
let size = item
|
||||
.get("size")
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| Error::Invalid(format!("module 0x{command_id:02X} lacks size")))?;
|
||||
result.insert(
|
||||
command_id,
|
||||
Artifact {
|
||||
path: root.join(path),
|
||||
size,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(streams) = document.get("streams").and_then(Value::as_array) {
|
||||
for stream in streams {
|
||||
let Some(records) = stream.get("records").and_then(Value::as_array) else {
|
||||
continue;
|
||||
};
|
||||
for record in records {
|
||||
let Some(command_id) = record.get("command_id").and_then(Value::as_u64) else {
|
||||
continue;
|
||||
};
|
||||
let command_id = u32::try_from(command_id)
|
||||
.map_err(|_| Error::Invalid("record command ID exceeds u32".to_owned()))?;
|
||||
if !REQUIRED_IDS.contains(&command_id) {
|
||||
continue;
|
||||
}
|
||||
let Some(image) = record.get("image") else {
|
||||
continue;
|
||||
};
|
||||
let Some(path) = image.get("path").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
let size = image.get("size").and_then(Value::as_u64).ok_or_else(|| {
|
||||
Error::Invalid(format!("record 0x{command_id:02X} lacks image size"))
|
||||
})?;
|
||||
result.insert(
|
||||
command_id,
|
||||
Artifact {
|
||||
path: root.join(path),
|
||||
size,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let missing = REQUIRED_IDS
|
||||
.iter()
|
||||
.filter(|id| !result.contains_key(id))
|
||||
.map(|id| format!("0x{id:02X}"))
|
||||
.collect::<Vec<_>>();
|
||||
if !missing.is_empty() {
|
||||
return invalid(format!(
|
||||
"module index lacks required IDs: {}",
|
||||
missing.join(", ")
|
||||
));
|
||||
}
|
||||
for (&command_id, artifact) in &result {
|
||||
let metadata = std::fs::metadata(&artifact.path)
|
||||
.map_err(|error| Error::io("inspect artifact", &artifact.path, error))?;
|
||||
if !metadata.is_file() || metadata.len() != artifact.size {
|
||||
return invalid(format!(
|
||||
"invalid artifact for module 0x{command_id:02X}: {}",
|
||||
artifact.path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// ELF restoration failure.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{action} `{path}`: {source}")]
|
||||
Io {
|
||||
action: &'static str,
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("cannot parse module index: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Crypto(#[from] senbei_crypto::android::Error),
|
||||
#[error("{0}")]
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn io(action: &'static str, path: &Path, source: std::io::Error) -> Self {
|
||||
Self::Io {
|
||||
action,
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub(crate) fn invalid<T>(message: impl Into<String>) -> Result<T> {
|
||||
Err(Error::Invalid(message.into()))
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
use super::error::{Error, Result, invalid};
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn elf_hash(name: &[u8]) -> u32 {
|
||||
let mut value = 0_u32;
|
||||
for &byte in name {
|
||||
value = value.wrapping_shl(4).wrapping_add(u32::from(byte));
|
||||
let high = value & 0xf000_0000;
|
||||
if high != 0 {
|
||||
value ^= high >> 24;
|
||||
value &= !high;
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn gnu_hash(name: &[u8]) -> u32 {
|
||||
name.iter().fold(5381_u32, |value, &byte| {
|
||||
value.wrapping_mul(33).wrapping_add(u32::from(byte))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_sysv_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
|
||||
if names.len() < 2 {
|
||||
return invalid("dynamic symbol table is unexpectedly empty");
|
||||
}
|
||||
let bucket_count = names.len();
|
||||
let symbol_count = names.len();
|
||||
let mut buckets = vec![0_u32; bucket_count];
|
||||
let mut chains = vec![0_u32; symbol_count];
|
||||
for (symbol_index, name) in names.iter().enumerate().skip(1) {
|
||||
let bucket_index = elf_hash(name) as usize % bucket_count;
|
||||
let symbol_index32 = u32::try_from(symbol_index)
|
||||
.map_err(|_| Error::Invalid("dynamic symbol index exceeds u32".to_owned()))?;
|
||||
if buckets[bucket_index] == 0 {
|
||||
buckets[bucket_index] = symbol_index32;
|
||||
continue;
|
||||
}
|
||||
let mut chain_index = buckets[bucket_index] as usize;
|
||||
while chains[chain_index] != 0 {
|
||||
chain_index = chains[chain_index] as usize;
|
||||
}
|
||||
chains[chain_index] = symbol_index32;
|
||||
}
|
||||
let mut output = Vec::with_capacity((2 + bucket_count + symbol_count) * 4);
|
||||
output.extend_from_slice(
|
||||
&u32::try_from(bucket_count)
|
||||
.map_err(|_| Error::Invalid("SysV bucket count exceeds u32".to_owned()))?
|
||||
.to_le_bytes(),
|
||||
);
|
||||
output.extend_from_slice(
|
||||
&u32::try_from(symbol_count)
|
||||
.map_err(|_| Error::Invalid("SysV symbol count exceeds u32".to_owned()))?
|
||||
.to_le_bytes(),
|
||||
);
|
||||
for value in buckets.into_iter().chain(chains) {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn build_gnu_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
|
||||
let hashes = names
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|name| gnu_hash(name))
|
||||
.collect::<Vec<_>>();
|
||||
if hashes.is_empty() {
|
||||
return invalid("GNU hash requires at least one dynamic symbol");
|
||||
}
|
||||
let bloom_shift = 5_u32;
|
||||
let mut bloom_word = 0_u64;
|
||||
for &value in &hashes {
|
||||
bloom_word |= 1_u64 << (value & 63);
|
||||
bloom_word |= 1_u64 << ((value >> bloom_shift) & 63);
|
||||
}
|
||||
let mut chains = hashes
|
||||
.into_iter()
|
||||
.map(|value| value & !1)
|
||||
.collect::<Vec<_>>();
|
||||
let last = chains
|
||||
.last_mut()
|
||||
.ok_or_else(|| Error::Invalid("GNU hash chain is empty".to_owned()))?;
|
||||
*last |= 1;
|
||||
let mut output = Vec::with_capacity(28 + chains.len() * 4);
|
||||
for value in [1_u32, 1, 1, bloom_shift] {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
output.extend_from_slice(&bloom_word.to_le_bytes());
|
||||
output.extend_from_slice(&1_u32.to_le_bytes());
|
||||
for value in chains {
|
||||
output.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn standard_elf_hash_is_stable() {
|
||||
assert_eq!(elf_hash(b"printf"), 0x0779_05a6);
|
||||
assert_eq!(gnu_hash(b"printf"), 0x156b_2bb8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
use super::error::{Error, Result, invalid};
|
||||
|
||||
pub(crate) const SHT_NOBITS: u32 = 8;
|
||||
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000;
|
||||
pub(crate) const SHF_ALLOC: u64 = 2;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct LoadSegment {
|
||||
pub offset: u64,
|
||||
pub virtual_address: u64,
|
||||
pub file_size: u64,
|
||||
pub memory_size: u64,
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct SectionHeader {
|
||||
pub name: u32,
|
||||
pub section_type: u32,
|
||||
pub flags: u64,
|
||||
pub address: u64,
|
||||
pub offset: u64,
|
||||
pub size: u64,
|
||||
pub link: u32,
|
||||
pub info: u32,
|
||||
pub alignment: u64,
|
||||
pub entry_size: u64,
|
||||
}
|
||||
|
||||
impl SectionHeader {
|
||||
pub const SIZE: usize = 0x40;
|
||||
|
||||
fn parse(data: &[u8], offset: usize) -> Result<Self> {
|
||||
Ok(Self {
|
||||
name: read_u32(data, offset)?,
|
||||
section_type: read_u32(data, offset + 4)?,
|
||||
flags: read_u64(data, offset + 8)?,
|
||||
address: read_u64(data, offset + 0x10)?,
|
||||
offset: read_u64(data, offset + 0x18)?,
|
||||
size: read_u64(data, offset + 0x20)?,
|
||||
link: read_u32(data, offset + 0x28)?,
|
||||
info: read_u32(data, offset + 0x2c)?,
|
||||
alignment: read_u64(data, offset + 0x30)?,
|
||||
entry_size: read_u64(data, offset + 0x38)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(self) -> [u8; Self::SIZE] {
|
||||
let mut output = [0_u8; Self::SIZE];
|
||||
output[0..4].copy_from_slice(&self.name.to_le_bytes());
|
||||
output[4..8].copy_from_slice(&self.section_type.to_le_bytes());
|
||||
output[8..0x10].copy_from_slice(&self.flags.to_le_bytes());
|
||||
output[0x10..0x18].copy_from_slice(&self.address.to_le_bytes());
|
||||
output[0x18..0x20].copy_from_slice(&self.offset.to_le_bytes());
|
||||
output[0x20..0x28].copy_from_slice(&self.size.to_le_bytes());
|
||||
output[0x28..0x2c].copy_from_slice(&self.link.to_le_bytes());
|
||||
output[0x2c..0x30].copy_from_slice(&self.info.to_le_bytes());
|
||||
output[0x30..0x38].copy_from_slice(&self.alignment.to_le_bytes());
|
||||
output[0x38..0x40].copy_from_slice(&self.entry_size.to_le_bytes());
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ElfLayout {
|
||||
pub entrypoint: u64,
|
||||
pub program_headers: Vec<LoadSegment>,
|
||||
pub section_headers: Vec<SectionHeader>,
|
||||
pub section_name_index: usize,
|
||||
pub private_section_index: usize,
|
||||
}
|
||||
|
||||
impl ElfLayout {
|
||||
pub fn parse(data: &[u8], require_private: bool) -> Result<Self> {
|
||||
let ident = slice(data, 0, 6)?;
|
||||
if ident[..4] != *b"\x7fELF" || ident[4] != 2 || ident[5] != 1 {
|
||||
return invalid("input is not a little-endian ELF64 file");
|
||||
}
|
||||
if read_u16(data, 0x12)? != 0xb7 {
|
||||
return invalid("input is not an AArch64 ELF");
|
||||
}
|
||||
let entrypoint = read_u64(data, 0x18)?;
|
||||
let program_header_offset = usize_from_u64(read_u64(data, 0x20)?, "program header offset")?;
|
||||
let section_header_offset = usize_from_u64(read_u64(data, 0x28)?, "section header offset")?;
|
||||
let program_header_size = usize::from(read_u16(data, 0x36)?);
|
||||
let program_header_count = usize::from(read_u16(data, 0x38)?);
|
||||
let section_header_size = usize::from(read_u16(data, 0x3a)?);
|
||||
let section_header_count = usize::from(read_u16(data, 0x3c)?);
|
||||
let section_name_index = usize::from(read_u16(data, 0x3e)?);
|
||||
if program_header_size != 0x38 || section_header_size != SectionHeader::SIZE {
|
||||
return invalid("unexpected ELF program/section header size");
|
||||
}
|
||||
|
||||
let mut program_headers = Vec::new();
|
||||
for index in 0..program_header_count {
|
||||
let offset = checked_index(program_header_offset, index, program_header_size)?;
|
||||
if read_u32(data, offset)? != 1 {
|
||||
continue;
|
||||
}
|
||||
let segment = LoadSegment {
|
||||
flags: read_u32(data, offset + 4)?,
|
||||
offset: read_u64(data, offset + 8)?,
|
||||
virtual_address: read_u64(data, offset + 0x10)?,
|
||||
file_size: read_u64(data, offset + 0x20)?,
|
||||
memory_size: read_u64(data, offset + 0x28)?,
|
||||
};
|
||||
let file_end = segment
|
||||
.offset
|
||||
.checked_add(segment.file_size)
|
||||
.ok_or_else(|| Error::Invalid(format!("PT_LOAD {index} file range overflow")))?;
|
||||
if file_end > data.len() as u64 {
|
||||
return invalid(format!("PT_LOAD {index} exceeds input file"));
|
||||
}
|
||||
program_headers.push(segment);
|
||||
}
|
||||
if program_headers.is_empty() {
|
||||
return invalid("input ELF contains no PT_LOAD segments");
|
||||
}
|
||||
|
||||
let mut section_headers = Vec::with_capacity(section_header_count);
|
||||
for index in 0..section_header_count {
|
||||
let offset = checked_index(section_header_offset, index, section_header_size)?;
|
||||
section_headers.push(SectionHeader::parse(data, offset)?);
|
||||
}
|
||||
if section_name_index >= section_headers.len() {
|
||||
return invalid("ELF section-name index is out of range");
|
||||
}
|
||||
let private = section_headers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, section)| (section.section_type == SHT_LOUSER).then_some(index))
|
||||
.collect::<Vec<_>>();
|
||||
let private_section_index = match private.as_slice() {
|
||||
[index] => *index,
|
||||
[] if !require_private => usize::MAX,
|
||||
_ => {
|
||||
return invalid(format!(
|
||||
"expected {} SHT_LOUSER section, found {}",
|
||||
if require_private {
|
||||
"one"
|
||||
} else {
|
||||
"at most one"
|
||||
},
|
||||
private.len()
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
entrypoint,
|
||||
program_headers,
|
||||
section_headers,
|
||||
section_name_index,
|
||||
private_section_index,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn private_section(&self) -> Result<SectionHeader> {
|
||||
self.section_headers
|
||||
.get(self.private_section_index)
|
||||
.copied()
|
||||
.ok_or_else(|| Error::Invalid("ELF has no private section".to_owned()))
|
||||
}
|
||||
|
||||
pub fn load_end(&self) -> Result<u64> {
|
||||
self.program_headers
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
segment
|
||||
.virtual_address
|
||||
.checked_add(segment.memory_size)
|
||||
.ok_or_else(|| Error::Invalid("PT_LOAD memory end overflow".to_owned()))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.max()
|
||||
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD memory range".to_owned()))
|
||||
}
|
||||
|
||||
pub fn file_load_end(&self) -> Result<u64> {
|
||||
self.program_headers
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
segment
|
||||
.offset
|
||||
.checked_add(segment.file_size)
|
||||
.ok_or_else(|| Error::Invalid("PT_LOAD file end overflow".to_owned()))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.max()
|
||||
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD file range".to_owned()))
|
||||
}
|
||||
|
||||
pub fn section_names(&self, data: &[u8]) -> Result<Vec<String>> {
|
||||
let table = self.section_headers[self.section_name_index];
|
||||
let strings = slice_u64(data, table.offset, table.size)?;
|
||||
self.section_headers
|
||||
.iter()
|
||||
.map(|section| {
|
||||
let offset = section.name as usize;
|
||||
if offset >= strings.len() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
let end = strings[offset..]
|
||||
.iter()
|
||||
.position(|&byte| byte == 0)
|
||||
.map_or(strings.len(), |length| offset + length);
|
||||
Ok(String::from_utf8_lossy(&strings[offset..end]).into_owned())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn file_offset_to_virtual_address(&self, offset: u64, size: u64) -> Result<u64> {
|
||||
let end = offset
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| Error::Invalid("file range overflow".to_owned()))?;
|
||||
for segment in &self.program_headers {
|
||||
let segment_end = segment
|
||||
.offset
|
||||
.checked_add(segment.file_size)
|
||||
.ok_or_else(|| Error::Invalid("PT_LOAD file range overflow".to_owned()))?;
|
||||
if segment.offset <= offset && end <= segment_end {
|
||||
return segment
|
||||
.virtual_address
|
||||
.checked_add(offset - segment.offset)
|
||||
.ok_or_else(|| Error::Invalid("virtual address overflow".to_owned()));
|
||||
}
|
||||
}
|
||||
invalid(format!(
|
||||
"file range 0x{offset:x}..0x{end:x} is not in PT_LOAD"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
|
||||
let end = offset
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| Error::Invalid("byte range overflow".to_owned()))?;
|
||||
data.get(offset..end).ok_or_else(|| {
|
||||
Error::Invalid(format!(
|
||||
"byte range 0x{offset:x}..0x{end:x} is out of bounds"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn slice_u64(data: &[u8], offset: u64, size: u64) -> Result<&[u8]> {
|
||||
slice(
|
||||
data,
|
||||
usize_from_u64(offset, "file offset")?,
|
||||
usize_from_u64(size, "file size")?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn read_u16(data: &[u8], offset: usize) -> Result<u16> {
|
||||
let bytes: [u8; 2] = slice(data, offset, 2)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::Invalid("invalid u16 range".to_owned()))?;
|
||||
Ok(u16::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
|
||||
let bytes: [u8; 4] = slice(data, offset, 4)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::Invalid("invalid u32 range".to_owned()))?;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn read_u64(data: &[u8], offset: usize) -> Result<u64> {
|
||||
let bytes: [u8; 8] = slice(data, offset, 8)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::Invalid("invalid u64 range".to_owned()))?;
|
||||
Ok(u64::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn read_i64(data: &[u8], offset: usize) -> Result<i64> {
|
||||
let bytes: [u8; 8] = slice(data, offset, 8)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::Invalid("invalid i64 range".to_owned()))?;
|
||||
Ok(i64::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn usize_from_u64(value: u64, field: &str) -> Result<usize> {
|
||||
usize::try_from(value).map_err(|_| Error::Invalid(format!("{field} 0x{value:x} exceeds usize")))
|
||||
}
|
||||
|
||||
pub(crate) fn checked_index(base: usize, index: usize, stride: usize) -> Result<usize> {
|
||||
index
|
||||
.checked_mul(stride)
|
||||
.and_then(|value| base.checked_add(value))
|
||||
.ok_or_else(|| Error::Invalid("table index overflow".to_owned()))
|
||||
}
|
||||
|
||||
pub(crate) fn align_up(value: u64, alignment: u64) -> Result<u64> {
|
||||
if alignment == 0 || !alignment.is_power_of_two() {
|
||||
return invalid(format!("invalid alignment {alignment}"));
|
||||
}
|
||||
value
|
||||
.checked_add(alignment - 1)
|
||||
.map(|aligned| aligned & !(alignment - 1))
|
||||
.ok_or_else(|| Error::Invalid("alignment overflow".to_owned()))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mod artifact;
|
||||
mod error;
|
||||
mod hash;
|
||||
mod layout;
|
||||
mod pipeline;
|
||||
|
||||
pub use error::Error;
|
||||
pub use pipeline::{RestoreOptions, RestoreReport, restore_libil2cpp};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user