mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-04 10:31:29 -05:00
fix(android): bound archive extraction and decoder resources
This commit is contained in:
1 parent
22dd92982e
commit
a380031567
21 files changed
+2714
-181
No files matched your search
@@ -44,6 +44,8 @@ pub enum Error {
|
||||
},
|
||||
#[error("{0}")]
|
||||
Invalid(String),
|
||||
#[error(transparent)]
|
||||
Limit(#[from] crate::android::LimitExceeded),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
||||
@@ -6,7 +6,7 @@ mod stage1;
|
||||
mod stream;
|
||||
|
||||
pub use error::Error;
|
||||
pub use pipeline::{ExtractOptions, extract_stage2};
|
||||
pub use probe::is_protected_libil2cpp;
|
||||
pub use pipeline::{ExtractOptions, extract_stage2, extract_stage2_with_limits};
|
||||
pub use probe::{is_protected_libil2cpp, probe_libil2cpp_with_limits};
|
||||
pub use report::ExtractionReport;
|
||||
pub use stage1::{DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE};
|
||||
@@ -3,7 +3,9 @@ use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use memmap2::MmapOptions;
|
||||
use senbei_crypto::android::{Module9bConfig, decode_container};
|
||||
use senbei_crypto::android::{Module9bConfig, decode_container_with_limit};
|
||||
|
||||
use crate::android::{ByteBudget, EntryBudget, ResourceLimits};
|
||||
use serde_json::to_vec_pretty;
|
||||
|
||||
use super::super::common;
|
||||
@@ -61,6 +63,13 @@ struct ArtifactSpec<'a> {
|
||||
classification: &'a str,
|
||||
}
|
||||
|
||||
struct DecodeBudget {
|
||||
limits: ResourceLimits,
|
||||
bytes: ByteBudget,
|
||||
records: EntryBudget,
|
||||
config: Module9bConfig,
|
||||
}
|
||||
|
||||
struct Extractor {
|
||||
output_dir: PathBuf,
|
||||
streams: Vec<StreamReport>,
|
||||
@@ -70,6 +79,20 @@ struct Extractor {
|
||||
}
|
||||
|
||||
pub fn extract_stage2(options: &ExtractOptions) -> Result<ExtractionReport> {
|
||||
extract_stage2_with_limits(options, &ResourceLimits::default())
|
||||
}
|
||||
|
||||
/// [`extract_stage2`] with explicit ceilings for decoded records and nesting.
|
||||
///
|
||||
/// `limits.max_entries` counts decoded stage-2 records. `limits.max_depth`
|
||||
/// counts nested interpreter streams, with the bootstrap stream at depth 0.
|
||||
/// `limits.max_entry_bytes` bounds each decoded container and its writers, and
|
||||
/// `limits.max_total_bytes` sums those decoded payloads for this call. Neither
|
||||
/// budget is a peak resident-memory measurement.
|
||||
pub fn extract_stage2_with_limits(
|
||||
options: &ExtractOptions,
|
||||
limits: &ResourceLimits,
|
||||
) -> Result<ExtractionReport> {
|
||||
let input_path = absolute(&options.input)?;
|
||||
let output_dir = absolute(&options.output_dir)?;
|
||||
if !input_path.is_file() {
|
||||
@@ -98,6 +121,10 @@ pub fn extract_stage2(options: &ExtractOptions) -> Result<ExtractionReport> {
|
||||
&input_path,
|
||||
options.outer_size,
|
||||
options.cipher_constant,
|
||||
// Stage 1 plaintext is the first thing this task retains. Its copy
|
||||
// is capped by both the single-entry ceiling and whatever remains of
|
||||
// the task total, which is the whole total at this point.
|
||||
Some(limits.max_entry_bytes.min(limits.max_total_bytes)),
|
||||
)?;
|
||||
if let Some(stage2_output) = &options.stage2_output {
|
||||
write_atomic(&absolute(stage2_output)?, &stage1.plaintext)?;
|
||||
@@ -119,13 +146,22 @@ pub fn extract_stage2(options: &ExtractOptions) -> Result<ExtractionReport> {
|
||||
registry: BTreeMap::new(),
|
||||
seen_streams: HashSet::new(),
|
||||
};
|
||||
let mut budget = DecodeBudget {
|
||||
limits: *limits,
|
||||
bytes: ByteBudget::new(limits),
|
||||
records: EntryBudget::new(limits),
|
||||
config: core_config,
|
||||
};
|
||||
budget
|
||||
.bytes
|
||||
.charge(u64::try_from(stage1.plaintext.len()).unwrap_or(u64::MAX))?;
|
||||
extractor.extract_stream(
|
||||
bootstrap,
|
||||
0xe2,
|
||||
0,
|
||||
None,
|
||||
Some(stage1.remaining_file_offset),
|
||||
core_config,
|
||||
&mut budget,
|
||||
)?;
|
||||
|
||||
let module_registry = extractor
|
||||
@@ -170,8 +206,9 @@ impl Extractor {
|
||||
depth: usize,
|
||||
parent: Option<StreamParent>,
|
||||
source_file_offset: Option<usize>,
|
||||
config: Module9bConfig,
|
||||
budget: &mut DecodeBudget,
|
||||
) -> Result<()> {
|
||||
budget.limits.check_depth(depth)?;
|
||||
let digest = sha256(stream);
|
||||
if !self.seen_streams.insert((stream_id, digest.clone())) {
|
||||
return Ok(());
|
||||
@@ -200,10 +237,15 @@ impl Extractor {
|
||||
"decoded_interpreter"
|
||||
},
|
||||
(depth != 0).then_some(stream_id),
|
||||
&config,
|
||||
&budget.config,
|
||||
),
|
||||
records: Vec::with_capacity(records.len()),
|
||||
records: Vec::new(),
|
||||
};
|
||||
budget.records.charge(records.len())?;
|
||||
stream_report
|
||||
.records
|
||||
.try_reserve_exact(records.len())
|
||||
.map_err(|_| Error::Invalid("stage-2 record report reservation failed".to_owned()))?;
|
||||
let mut direct_records = Vec::new();
|
||||
let mut modules_at_level = BTreeSet::new();
|
||||
|
||||
@@ -213,16 +255,24 @@ impl Extractor {
|
||||
let mut metadata_data = None;
|
||||
|
||||
if !record.direct() && record.image_size != 0 {
|
||||
// Charge the declared size before the decoder reserves it.
|
||||
// A metadata-only record is charged below, on the same rule.
|
||||
budget.bytes.charge(u64::from(record.image_size))?;
|
||||
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 image = decode_container_with_limit(
|
||||
image_source,
|
||||
&budget.config,
|
||||
record.image_size as usize,
|
||||
budget.limits.output_limit(),
|
||||
)
|
||||
.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 {
|
||||
@@ -243,17 +293,22 @@ impl Extractor {
|
||||
image_data = Some((image, artifact));
|
||||
}
|
||||
if record.metadata_size != 0 {
|
||||
budget.bytes.charge(u64::from(record.metadata_size))?;
|
||||
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 metadata = decode_container_with_limit(
|
||||
metadata_source,
|
||||
&budget.config,
|
||||
record.metadata_size as usize,
|
||||
budget.limits.output_limit(),
|
||||
)
|
||||
.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,
|
||||
@@ -339,14 +394,17 @@ impl Extractor {
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
budget.limits.check_depth(depth + 1)?;
|
||||
let parent_config = std::mem::replace(&mut budget.config, interpreter_config);
|
||||
self.extract_stream(
|
||||
child_data,
|
||||
next_stream_id,
|
||||
depth + 1,
|
||||
Some(parent),
|
||||
None,
|
||||
interpreter_config,
|
||||
budget,
|
||||
)?;
|
||||
budget.config = parent_config;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -506,3 +564,197 @@ fn absolute(path: &Path) -> Result<PathBuf> {
|
||||
fn sha256(data: &[u8]) -> String {
|
||||
common::sha256(data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod limit_tests {
|
||||
use super::*;
|
||||
use crate::android::LimitExceeded;
|
||||
|
||||
fn config() -> Module9bConfig {
|
||||
Module9bConfig {
|
||||
header_seed: 0,
|
||||
container_seed: 0,
|
||||
aes_key: [0; 32],
|
||||
skip_aes: true,
|
||||
schedule_offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn tight(entries: usize, depth: usize) -> ResourceLimits {
|
||||
ResourceLimits {
|
||||
max_entry_bytes: 1024,
|
||||
max_total_bytes: 1024,
|
||||
max_entries: entries,
|
||||
max_depth: depth,
|
||||
}
|
||||
}
|
||||
|
||||
/// One direct record whose payload is `child`.
|
||||
///
|
||||
/// The command is `stream_id + 0x10`, which is the id a nested stream uses
|
||||
/// for its interpreter. The payload begins immediately after the single
|
||||
/// descriptor, so parsing stops at that boundary.
|
||||
fn direct_stream(stream_id: u32, child: &[u8]) -> Vec<u8> {
|
||||
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);
|
||||
// record_state = plain1 + base, and plain1 is chosen so that state is
|
||||
// zero. That keeps the record cipher's index mask zero as well.
|
||||
let plain1 = 0_u32.wrapping_sub(base);
|
||||
let cipher0 =
|
||||
unmultiply(1_u32 ^ 0xebe8_1dba_u32.wrapping_add(base)).wrapping_sub(0xcbf0_c1d8);
|
||||
let cipher1 = unmultiply(plain1 ^ 0xebe8_1dba_u32.wrapping_mul(5).wrapping_add(base))
|
||||
.wrapping_sub(cipher0);
|
||||
let mut stream = Vec::new();
|
||||
stream.extend_from_slice(&cipher0.to_le_bytes());
|
||||
stream.extend_from_slice(&cipher1.to_le_bytes());
|
||||
// `command - 0x10` is the interpreter id a nested stream looks up.
|
||||
// Using `stream_id + 0x10` makes a lone record direct, because that
|
||||
// interpreter was not decoded from this same stream.
|
||||
let command = stream_id.wrapping_add(0x10);
|
||||
let words = [
|
||||
command,
|
||||
super::super::stream::DIRECT_FLAG,
|
||||
(8 + super::super::stream::RECORD_SIZE) as u32,
|
||||
child.len() as u32,
|
||||
0,
|
||||
0,
|
||||
command,
|
||||
0,
|
||||
0,
|
||||
];
|
||||
stream.extend_from_slice(&encrypt_record(&words));
|
||||
stream.extend_from_slice(child);
|
||||
stream
|
||||
}
|
||||
|
||||
fn encrypt_record(words: &[u32]) -> Vec<u8> {
|
||||
// Inverse of `decrypt_record` at record state zero: replay its
|
||||
// feedback from each ciphertext word, then undo the field multiply.
|
||||
let mut feedback = 0xf02f_7685_u32;
|
||||
let mut accumulator = 0x7993_4cf6_u32;
|
||||
let mut raw = vec![0_u8; super::super::stream::RECORD_SIZE];
|
||||
for (word_index, plain) in words.iter().copied().enumerate() {
|
||||
feedback = feedback.wrapping_mul(feedback);
|
||||
let mixed = plain
|
||||
.wrapping_sub(accumulator)
|
||||
.wrapping_add(0 >> ((word_index * 4 + 3) & 5));
|
||||
let cipher = unmultiply(mixed) ^ (feedback >> 3);
|
||||
raw[word_index * 4..word_index * 4 + 4].copy_from_slice(&cipher.to_le_bytes());
|
||||
accumulator = accumulator.wrapping_add(0xe64d_33d8);
|
||||
feedback = cipher;
|
||||
}
|
||||
raw
|
||||
}
|
||||
|
||||
fn unmultiply(encoded: u32) -> u32 {
|
||||
let mut multiplier = 0xdc49_cd06_u32;
|
||||
let mut value = encoded;
|
||||
let mut result = 0_u32;
|
||||
while multiplier != 0 {
|
||||
if multiplier & 1 != 0 {
|
||||
result ^= value;
|
||||
}
|
||||
let carry = value >> 31;
|
||||
value = value.wrapping_shl(1);
|
||||
if carry != 0 {
|
||||
value ^= 0x5793_57eb;
|
||||
}
|
||||
multiplier >>= 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn extractor() -> Extractor {
|
||||
Extractor {
|
||||
output_dir: PathBuf::from("."),
|
||||
streams: Vec::new(),
|
||||
artifacts: Vec::new(),
|
||||
registry: BTreeMap::new(),
|
||||
seen_streams: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn budget(entries: usize, depth: usize) -> DecodeBudget {
|
||||
let limits = tight(entries, depth);
|
||||
DecodeBudget {
|
||||
limits,
|
||||
bytes: ByteBudget::new(&limits),
|
||||
records: EntryBudget::new(&limits),
|
||||
config: config(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_budget_rejects_one_past_the_ceiling_and_allows_the_boundary() {
|
||||
let stream = direct_stream(0xe2, &[0]);
|
||||
let error = extractor()
|
||||
.extract_stream(&stream, 0xe2, 0, None, None, &mut budget(0, 32))
|
||||
.expect_err("one record is already past a zero-record ceiling");
|
||||
assert!(
|
||||
matches!(
|
||||
error,
|
||||
Error::Limit(LimitExceeded::Entries {
|
||||
requested: 1,
|
||||
limit: 0
|
||||
})
|
||||
),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
|
||||
let directory = tempfile::tempdir().expect("temporary output directory");
|
||||
let mut at_ceiling = extractor();
|
||||
at_ceiling.output_dir = directory.path().to_path_buf();
|
||||
at_ceiling
|
||||
.extract_stream(&stream, 0xe2, 0, None, None, &mut budget(1, 0))
|
||||
.expect("one record at depth zero is the boundary");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nesting_one_past_the_ceiling_is_rejected_before_the_child_is_decoded() {
|
||||
let stream = direct_stream(0xe2, &[0]);
|
||||
let directory = tempfile::tempdir().expect("temporary output directory");
|
||||
let mut at_ceiling = extractor();
|
||||
at_ceiling.output_dir = directory.path().to_path_buf();
|
||||
at_ceiling
|
||||
.extract_stream(&stream, 0xe2, 32, None, None, &mut budget(1, 32))
|
||||
.expect("depth 32 is the nesting boundary");
|
||||
|
||||
let error = extractor()
|
||||
.extract_stream(&stream, 0xe2, 33, None, None, &mut budget(1, 32))
|
||||
.expect_err("depth 33 is rejected before the stream is parsed");
|
||||
match &error {
|
||||
Error::Limit(limit) => assert_eq!(
|
||||
limit,
|
||||
&LimitExceeded::Depth {
|
||||
requested: 33,
|
||||
limit: 32
|
||||
}
|
||||
),
|
||||
other => panic!("unexpected error: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoded_payloads_share_one_task_budget() {
|
||||
let stream = direct_stream(0xe2, &[0]);
|
||||
let directory = tempfile::tempdir().expect("temporary output directory");
|
||||
let mut budget = budget(1, 0);
|
||||
budget.limits.max_total_bytes = 1;
|
||||
budget.bytes = ByteBudget::new(&budget.limits);
|
||||
// A direct record has no decoded container, so it does not charge the
|
||||
// byte budget. Seed the only allowed byte to prove the budget is the
|
||||
// one the caller passed in and is not reset per stream.
|
||||
budget.bytes.charge(1).unwrap();
|
||||
let mut extractor = extractor();
|
||||
extractor.output_dir = directory.path().to_path_buf();
|
||||
extractor
|
||||
.extract_stream(&stream, 0xe2, 0, None, None, &mut budget)
|
||||
.unwrap();
|
||||
assert_eq!(budget.bytes.used(), 1);
|
||||
assert!(budget.bytes.charge(1024).is_err());
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,132 @@ use std::path::Path;
|
||||
|
||||
use senbei_crypto::android::Module9bConfig;
|
||||
|
||||
use super::error::Error;
|
||||
use super::stage1::{self, DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE};
|
||||
use crate::android::ResourceLimits;
|
||||
|
||||
/// Return whether `data` has a supported protected AArch64 IL2CPP layout.
|
||||
///
|
||||
/// This uses the default ceilings. A size past those ceilings is reported as
|
||||
/// not protected, matching a scan that has not opted into a custom limit.
|
||||
/// [`probe_libil2cpp_with_limits`] returns that limit error instead.
|
||||
#[must_use]
|
||||
pub fn is_protected_libil2cpp(data: &[u8]) -> bool {
|
||||
let Ok(stage1) = stage1::inspect(
|
||||
probe_libil2cpp_with_limits(data, &ResourceLimits::default()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Probe one image under explicit ceilings.
|
||||
///
|
||||
/// A parse or configuration mismatch is `Ok(false)`: the image is not a
|
||||
/// supported target. [`Error::Limit`] is returned as-is, because the declared
|
||||
/// Stage 1 payload was large enough that classifying it would require the
|
||||
/// allocation the ceiling exists to prevent. The payload copy is capped by
|
||||
/// `min(max_entry_bytes, max_total_bytes)` before it is reserved.
|
||||
pub fn probe_libil2cpp_with_limits(data: &[u8], limits: &ResourceLimits) -> Result<bool, Error> {
|
||||
let stage1 = match stage1::inspect(
|
||||
data,
|
||||
Path::new("<probe>"),
|
||||
DEFAULT_OUTER_SIZE,
|
||||
DEFAULT_CIPHER_CONSTANT,
|
||||
) else {
|
||||
return false;
|
||||
Some(limits.max_entry_bytes.min(limits.max_total_bytes)),
|
||||
) {
|
||||
Ok(stage1) => stage1,
|
||||
Err(Error::Limit(limit)) => return Err(Error::Limit(limit)),
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
Module9bConfig::parse_embedded(&stage1.plaintext).is_ok()
|
||||
Ok(Module9bConfig::parse_embedded(&stage1.plaintext).is_ok())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Stage 1 header words, encrypted with the default cipher constant.
|
||||
fn encrypt_header(words: [u32; 8]) -> [u8; 0x20] {
|
||||
let key = words[0];
|
||||
let mut raw = [0_u8; 0x20];
|
||||
raw[..4].copy_from_slice(&key.to_le_bytes());
|
||||
for (index, value) in words.into_iter().enumerate().skip(1) {
|
||||
let index = index as u32;
|
||||
let word = (value ^ DEFAULT_CIPHER_CONSTANT.wrapping_mul(index + 1))
|
||||
.wrapping_sub((index + 3).wrapping_mul(key));
|
||||
raw[index as usize * 4..index as usize * 4 + 4].copy_from_slice(&word.to_le_bytes());
|
||||
}
|
||||
raw
|
||||
}
|
||||
|
||||
fn put(data: &mut [u8], offset: usize, bytes: &[u8]) {
|
||||
data[offset..offset + bytes.len()].copy_from_slice(bytes);
|
||||
}
|
||||
|
||||
/// A minimal AArch64 ELF whose SHT_LOUSER payload declares `payload_size`.
|
||||
///
|
||||
/// The declared payload is not present in the file. Inspection rejects that
|
||||
/// size before copying it when the ceiling is smaller.
|
||||
fn elf_with_payload_size(payload_size: u32) -> Vec<u8> {
|
||||
let outer = DEFAULT_OUTER_SIZE;
|
||||
let section_file_size = outer + 0x1000;
|
||||
let section_offset = 64_usize;
|
||||
let shoff = section_offset + section_file_size;
|
||||
// Offsets into `names`: dynsym 1, dynstr 9, gnu.version 17,
|
||||
// gnu.version_r 30, hash 44, private 50, shstrtab 59.
|
||||
let names =
|
||||
b"\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_r\0.hash\0.private\0.shstrtab\0";
|
||||
let strtab = shoff + 64 * 8;
|
||||
let mut data = vec![0_u8; strtab + names.len()];
|
||||
put(&mut data, 0, b"\x7fELF\x02\x01\x01");
|
||||
put(&mut data, 16, &3_u16.to_le_bytes());
|
||||
put(&mut data, 18, &183_u16.to_le_bytes());
|
||||
put(&mut data, 20, &1_u32.to_le_bytes());
|
||||
put(&mut data, 40, &(shoff as u64).to_le_bytes());
|
||||
put(&mut data, 52, &64_u16.to_le_bytes());
|
||||
put(&mut data, 58, &64_u16.to_le_bytes());
|
||||
put(&mut data, 60, &8_u16.to_le_bytes());
|
||||
put(&mut data, 62, &7_u16.to_le_bytes());
|
||||
let header = encrypt_header([1, 0, 0x20, payload_size, 0, 0, payload_size, payload_size]);
|
||||
put(&mut data, section_offset + outer, &header);
|
||||
put(&mut data, strtab, names);
|
||||
let sections: [(u32, u32, u64, u64); 8] = [
|
||||
(0, 0, 0, 0),
|
||||
(1, 11, 0, 0),
|
||||
(9, 3, 0, 0),
|
||||
(17, 0x6fff_fff0, 0, 0),
|
||||
(30, 0x6fff_fffe, 0, 0),
|
||||
(45, 5, 0, 0),
|
||||
(
|
||||
51,
|
||||
0x8000_0000,
|
||||
section_offset as u64,
|
||||
(outer + 0x1000 + payload_size as usize) as u64,
|
||||
),
|
||||
(60, 3, strtab as u64, names.len() as u64),
|
||||
];
|
||||
for (index, (name, kind, offset, size)) in sections.into_iter().enumerate() {
|
||||
let at = shoff + index * 64;
|
||||
put(&mut data, at, &name.to_le_bytes());
|
||||
put(&mut data, at + 4, &kind.to_le_bytes());
|
||||
put(&mut data, at + 24, &offset.to_le_bytes());
|
||||
put(&mut data, at + 32, &size.to_le_bytes());
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_stage1_payload_is_a_limit_error_not_a_clean_miss() {
|
||||
let image = elf_with_payload_size(8);
|
||||
let limits = ResourceLimits {
|
||||
max_entry_bytes: 4,
|
||||
max_total_bytes: 4,
|
||||
..ResourceLimits::default()
|
||||
};
|
||||
let error = probe_libil2cpp_with_limits(&image, &limits);
|
||||
assert!(
|
||||
matches!(error, Err(Error::Limit(_))),
|
||||
"unexpected probe result: {error:?}"
|
||||
);
|
||||
assert!(
|
||||
!probe_libil2cpp_with_limits(&[], &limits).expect("an empty image is not protected")
|
||||
);
|
||||
assert!(!is_protected_libil2cpp(&[]));
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ pub(crate) fn inspect(
|
||||
path: &Path,
|
||||
outer_size: usize,
|
||||
cipher_constant: u32,
|
||||
max_payload_bytes: Option<u64>,
|
||||
) -> Result<Stage1Result> {
|
||||
let elf = parse(data).map_err(|source: ElfError| Error::Elf {
|
||||
path: path.to_path_buf(),
|
||||
@@ -133,6 +134,15 @@ pub(crate) fn inspect(
|
||||
"Stage 1 payload size must be nonzero and word aligned: 0x{payload_size:x}"
|
||||
));
|
||||
}
|
||||
if let Some(limit) = max_payload_bytes {
|
||||
let declared = u64::try_from(payload_size)
|
||||
.map_err(|_| Error::Invalid("Stage 1 payload size exceeds u64".to_owned()))?;
|
||||
crate::android::ResourceLimits {
|
||||
max_entry_bytes: limit,
|
||||
..crate::android::ResourceLimits::default()
|
||||
}
|
||||
.check_entry_bytes(declared)?;
|
||||
}
|
||||
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 {
|
||||
@@ -190,7 +200,11 @@ fn decrypt_words(ciphertext: &[u8], key: u32, constant: u32) -> Result<Vec<u8>>
|
||||
if !ciphertext.len().is_multiple_of(4) {
|
||||
return invalid("Stage 1 word cipher input is not 4-byte aligned");
|
||||
}
|
||||
let mut plaintext = ciphertext.to_vec();
|
||||
let mut plaintext = Vec::new();
|
||||
plaintext
|
||||
.try_reserve_exact(ciphertext.len())
|
||||
.map_err(|_| Error::Invalid("Stage 1 plaintext reservation failed".to_owned()))?;
|
||||
plaintext.extend_from_slice(ciphertext);
|
||||
for (index, chunk) in plaintext.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
let index = u32::try_from(index)
|
||||
.map_err(|_| Error::Invalid("Stage 1 word index exceeds u32".to_owned()))?;
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
//! Shared Android resource ceilings for extraction, restoration, and archives.
|
||||
//!
|
||||
//! These bounds reject an attacker-controlled size before the corresponding
|
||||
//! allocation or file growth. They are per-allocation and per-task accounting
|
||||
//! caps, not a measurement of peak resident memory: decoder scratch, Huffman
|
||||
//! tables, and mapped inputs can exist at the same time.
|
||||
|
||||
/// Default ceiling for one ZIP entry, one decoder output, one writer source or
|
||||
/// destination, one artifact read, and the restored output image.
|
||||
pub const DEFAULT_MAX_ENTRY_BYTES: u64 = 2 << 30;
|
||||
/// Default ceiling for the sum of retained or temporary bytes charged by one
|
||||
/// extraction, restoration, or archive task.
|
||||
pub const DEFAULT_MAX_TOTAL_BYTES: u64 = 8 << 30;
|
||||
/// Default ceiling for ZIP entries and decoded stage-2 records in one task.
|
||||
pub const DEFAULT_MAX_ENTRIES: usize = 100_000;
|
||||
/// Default nesting ceiling. Depth `0` is the outermost container or stream, and
|
||||
/// a depth equal to this value is still allowed.
|
||||
pub const DEFAULT_MAX_DEPTH: usize = 32;
|
||||
|
||||
/// Overridable ceilings shared by Android extraction, restoration, and I/O.
|
||||
///
|
||||
/// Archive code uses the same type: `max_entry_bytes` bounds one streamed ZIP
|
||||
/// entry, `max_total_bytes` bounds the sum of those entries, `max_entries`
|
||||
/// bounds the entry count including nested archives, and `max_depth` bounds
|
||||
/// archive nesting.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ResourceLimits {
|
||||
pub max_entry_bytes: u64,
|
||||
pub max_total_bytes: u64,
|
||||
pub max_entries: usize,
|
||||
pub max_depth: usize,
|
||||
}
|
||||
|
||||
impl Default for ResourceLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_entry_bytes: DEFAULT_MAX_ENTRY_BYTES,
|
||||
max_total_bytes: DEFAULT_MAX_TOTAL_BYTES,
|
||||
max_entries: DEFAULT_MAX_ENTRIES,
|
||||
max_depth: DEFAULT_MAX_DEPTH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceLimits {
|
||||
/// Numeric output ceiling passed into the crypto crate.
|
||||
#[must_use]
|
||||
pub fn output_limit(self) -> senbei_crypto::android::OutputLimit {
|
||||
senbei_crypto::android::OutputLimit {
|
||||
max_output_bytes: self.max_entry_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reject one allocation, entry, or output before it is reserved.
|
||||
pub fn check_entry_bytes(self, bytes: u64) -> std::result::Result<(), LimitExceeded> {
|
||||
if bytes > self.max_entry_bytes {
|
||||
return Err(LimitExceeded::Entry {
|
||||
requested: bytes,
|
||||
limit: self.max_entry_bytes,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reject a declared entry or record count before those records are stored.
|
||||
pub fn check_entries(self, count: usize) -> std::result::Result<(), LimitExceeded> {
|
||||
if count > self.max_entries {
|
||||
return Err(LimitExceeded::Entries {
|
||||
requested: count,
|
||||
limit: self.max_entries,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reject a nested container or stream whose depth exceeds the ceiling.
|
||||
///
|
||||
/// `depth` is zero for the outermost item. `max_depth` itself is accepted.
|
||||
pub fn check_depth(self, depth: usize) -> std::result::Result<(), LimitExceeded> {
|
||||
if depth > self.max_depth {
|
||||
return Err(LimitExceeded::Depth {
|
||||
requested: depth,
|
||||
limit: self.max_depth,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A size or count that would exceed a [`ResourceLimits`] ceiling.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum LimitExceeded {
|
||||
/// One entry or allocation is larger than `max_entry_bytes`.
|
||||
#[error("resource limit: entry of {requested} bytes exceeds the {limit}-byte ceiling")]
|
||||
Entry { requested: u64, limit: u64 },
|
||||
/// Charging more bytes would pass `max_total_bytes`.
|
||||
#[error(
|
||||
"resource limit: task total of {requested} bytes exceeds the {limit}-byte ceiling (adding {adding} bytes)"
|
||||
)]
|
||||
Total {
|
||||
requested: u64,
|
||||
adding: u64,
|
||||
limit: u64,
|
||||
},
|
||||
/// An entry or record count passed `max_entries`.
|
||||
#[error("resource limit: {requested} entries exceed the ceiling of {limit}")]
|
||||
Entries { requested: usize, limit: usize },
|
||||
/// Nesting passed `max_depth`.
|
||||
#[error("resource limit: nesting depth {requested} exceeds the ceiling of {limit}")]
|
||||
Depth { requested: usize, limit: usize },
|
||||
}
|
||||
|
||||
/// Running sum of retained or temporary bytes for one task.
|
||||
///
|
||||
/// Each call starts its own budget at zero. Charging checks the single-entry
|
||||
/// ceiling and the task total, but it does not account for buffers that were
|
||||
/// never charged, so it is not a peak-RSS budget.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ByteBudget {
|
||||
limits: ResourceLimits,
|
||||
used: u64,
|
||||
}
|
||||
|
||||
impl ByteBudget {
|
||||
#[must_use]
|
||||
pub fn new(limits: &ResourceLimits) -> Self {
|
||||
Self {
|
||||
limits: *limits,
|
||||
used: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn used(&self) -> u64 {
|
||||
self.used
|
||||
}
|
||||
|
||||
/// Reject `bytes` against the single-entry ceiling without charging it.
|
||||
pub fn check_entry(&self, bytes: u64) -> std::result::Result<(), LimitExceeded> {
|
||||
self.limits.check_entry_bytes(bytes)
|
||||
}
|
||||
|
||||
/// Charge `bytes` against both the single-entry ceiling and the task total.
|
||||
pub fn charge(&mut self, bytes: u64) -> std::result::Result<(), LimitExceeded> {
|
||||
self.check_entry(bytes)?;
|
||||
self.charge_total(bytes)
|
||||
}
|
||||
|
||||
/// Charge `bytes` against the task total only.
|
||||
///
|
||||
/// Use this for a sum of pieces that were each already checked against the
|
||||
/// single-entry ceiling, such as several decoded records in one stream.
|
||||
pub fn charge_total(&mut self, bytes: u64) -> std::result::Result<(), LimitExceeded> {
|
||||
let requested = self
|
||||
.used
|
||||
.checked_add(bytes)
|
||||
.filter(|total| *total <= self.limits.max_total_bytes);
|
||||
match requested {
|
||||
Some(total) => {
|
||||
self.used = total;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(LimitExceeded::Total {
|
||||
requested: self.used.saturating_add(bytes),
|
||||
adding: bytes,
|
||||
limit: self.limits.max_total_bytes,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Running count of entries or decoded records for one task.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EntryBudget {
|
||||
limits: ResourceLimits,
|
||||
used: usize,
|
||||
}
|
||||
|
||||
impl EntryBudget {
|
||||
#[must_use]
|
||||
pub fn new(limits: &ResourceLimits) -> Self {
|
||||
Self {
|
||||
limits: *limits,
|
||||
used: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn used(&self) -> usize {
|
||||
self.used
|
||||
}
|
||||
|
||||
/// Charge `count` additional entries against `max_entries`.
|
||||
pub fn charge(&mut self, count: usize) -> std::result::Result<(), LimitExceeded> {
|
||||
let requested = self
|
||||
.used
|
||||
.checked_add(count)
|
||||
.filter(|total| *total <= self.limits.max_entries);
|
||||
match requested {
|
||||
Some(total) => {
|
||||
self.used = total;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(LimitExceeded::Entries {
|
||||
requested: self.used.saturating_add(count),
|
||||
limit: self.limits.max_entries,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn defaults_match_the_documented_ceilings() {
|
||||
let limits = ResourceLimits::default();
|
||||
assert_eq!(limits.max_entry_bytes, 2 << 30);
|
||||
assert_eq!(limits.max_total_bytes, 8 << 30);
|
||||
assert_eq!(limits.max_entries, 100_000);
|
||||
assert_eq!(limits.max_depth, 32);
|
||||
assert!(limits.check_entry_bytes(limits.max_entry_bytes).is_ok());
|
||||
assert!(limits.check_entries(limits.max_entries).is_ok());
|
||||
assert!(limits.check_depth(limits.max_depth).is_ok());
|
||||
assert!(
|
||||
limits
|
||||
.check_entry_bytes(limits.max_entry_bytes + 1)
|
||||
.is_err()
|
||||
);
|
||||
assert!(limits.check_depth(limits.max_depth + 1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_budget_accepts_the_boundary_and_rejects_the_next_byte() {
|
||||
let limits = ResourceLimits {
|
||||
max_entry_bytes: 4,
|
||||
max_total_bytes: 8,
|
||||
max_entries: 2,
|
||||
max_depth: 1,
|
||||
};
|
||||
let mut budget = ByteBudget::new(&limits);
|
||||
budget.charge(4).unwrap();
|
||||
budget.charge_total(4).unwrap();
|
||||
assert_eq!(budget.used(), 8);
|
||||
assert!(matches!(
|
||||
budget.charge(5),
|
||||
Err(LimitExceeded::Entry { requested: 5, .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
budget.charge_total(1),
|
||||
Err(LimitExceeded::Total { .. })
|
||||
));
|
||||
assert_eq!(budget.used(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_budget_rejects_the_count_past_the_ceiling() {
|
||||
let limits = ResourceLimits {
|
||||
max_entries: 2,
|
||||
..ResourceLimits::default()
|
||||
};
|
||||
let mut budget = EntryBudget::new(&limits);
|
||||
budget.charge(2).unwrap();
|
||||
assert!(matches!(
|
||||
budget.charge(1),
|
||||
Err(LimitExceeded::Entries {
|
||||
requested: 3,
|
||||
limit: 2
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,19 @@
|
||||
|
||||
mod common;
|
||||
mod extract;
|
||||
mod limits;
|
||||
mod restore;
|
||||
|
||||
pub use extract::{
|
||||
DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE, Error as ExtractionError, ExtractOptions,
|
||||
ExtractionReport, extract_stage2, is_protected_libil2cpp,
|
||||
ExtractionReport, extract_stage2, extract_stage2_with_limits, is_protected_libil2cpp,
|
||||
probe_libil2cpp_with_limits,
|
||||
};
|
||||
pub use limits::{
|
||||
ByteBudget, DEFAULT_MAX_DEPTH, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_ENTRY_BYTES,
|
||||
DEFAULT_MAX_TOTAL_BYTES, EntryBudget, LimitExceeded, ResourceLimits,
|
||||
};
|
||||
pub use restore::{
|
||||
Error as RestoreError, RestoreOptions, RestoreReport, restore_libil2cpp,
|
||||
restore_libil2cpp_with_limits,
|
||||
};
|
||||
pub use restore::{Error as RestoreError, RestoreOptions, RestoreReport, restore_libil2cpp};
|
||||
@@ -18,6 +18,8 @@ pub enum Error {
|
||||
Elf(#[from] senbei_elf::Error),
|
||||
#[error("{0}")]
|
||||
Invalid(String),
|
||||
#[error(transparent)]
|
||||
Limit(#[from] crate::android::LimitExceeded),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
||||
@@ -4,4 +4,6 @@ mod error;
|
||||
mod pipeline;
|
||||
|
||||
pub use error::Error;
|
||||
pub use pipeline::{RestoreOptions, RestoreReport, restore_libil2cpp};
|
||||
pub use pipeline::{
|
||||
RestoreOptions, RestoreReport, restore_libil2cpp, restore_libil2cpp_with_limits,
|
||||
};
|
||||
@@ -15,6 +15,7 @@ use tempfile::NamedTempFile;
|
||||
use super::super::common;
|
||||
use super::artifact::load_artifacts;
|
||||
use super::error::{Error, Result, invalid};
|
||||
use crate::android::{ByteBudget, ResourceLimits};
|
||||
use senbei_elf::{
|
||||
DT_GNU_HASH, DT_HASH, DT_JMPREL, DT_PLTRELSZ, DT_RELA, DT_RELACOUNT, DT_RELASZ, DT_STRSZ,
|
||||
DT_STRTAB, DT_SYMTAB, DT_VERNEED, DT_VERSYM, ELF64_RELA_SIZE, ELF64_SYMBOL_SIZE, ElfLayout,
|
||||
@@ -169,8 +170,68 @@ fn map_mut(file: &File, length: usize, path: &Path) -> Result<MmapMut> {
|
||||
.map_err(|error| Error::io("map temporary output", path, error))
|
||||
}
|
||||
|
||||
fn read_file(path: &Path) -> Result<Vec<u8>> {
|
||||
std::fs::read(path).map_err(|error| Error::io("read", path, error))
|
||||
/// Reserve the flat auxiliary image.
|
||||
///
|
||||
/// `output_size` is the size declared by the auxiliary container header. It
|
||||
/// is charged and rejected before `try_reserve_exact`, so an over-limit size
|
||||
/// never becomes an allocation.
|
||||
fn allocate_auxiliary(
|
||||
output_size: u32,
|
||||
limits: &ResourceLimits,
|
||||
bytes: &mut ByteBudget,
|
||||
) -> Result<Vec<u8>> {
|
||||
let declared = u64::from(output_size);
|
||||
limits.check_entry_bytes(declared)?;
|
||||
// The auxiliary image is retained for the rest of the task, so it counts
|
||||
// against the task total as well as the single-entry ceiling. The task
|
||||
// total is also the cap for the restored output file below.
|
||||
bytes.charge(declared)?;
|
||||
let auxiliary_size = output_size as usize;
|
||||
let mut decoded = Vec::new();
|
||||
decoded
|
||||
.try_reserve_exact(auxiliary_size)
|
||||
.map_err(|_| Error::Invalid("auxiliary output reservation failed".to_owned()))?;
|
||||
decoded.resize(auxiliary_size, 0);
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
/// Reject one scratch reservation against the single-entry ceiling.
|
||||
fn reserve_within(limits: &ResourceLimits, bytes: usize) -> Result<()> {
|
||||
let declared = u64::try_from(bytes)
|
||||
.map_err(|_| Error::Invalid("scratch reservation exceeds u64".to_owned()))?;
|
||||
limits.check_entry_bytes(declared)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_artifact(path: &Path, limits: &ResourceLimits, bytes: &mut ByteBudget) -> Result<Vec<u8>> {
|
||||
let data = read_file_limited(path, limits, bytes)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn read_file_limited(
|
||||
path: &Path,
|
||||
limits: &ResourceLimits,
|
||||
bytes: &mut ByteBudget,
|
||||
) -> Result<Vec<u8>> {
|
||||
let mut file = File::open(path).map_err(|error| Error::io("read", path, error))?;
|
||||
let length = file
|
||||
.metadata()
|
||||
.map_err(|error| Error::io("inspect", path, error))?
|
||||
.len();
|
||||
// Charge the length the open file reports before reserving it. This is
|
||||
// the file's own metadata, not a directory lookup that can be swapped
|
||||
// before the open.
|
||||
limits.check_entry_bytes(length)?;
|
||||
bytes.charge(length)?;
|
||||
let capacity = usize::try_from(length)
|
||||
.map_err(|_| Error::Invalid("artifact length exceeds usize".to_owned()))?;
|
||||
let mut data = Vec::new();
|
||||
data.try_reserve_exact(capacity)
|
||||
.map_err(|_| Error::Invalid("artifact reservation failed".to_owned()))?;
|
||||
data.resize(capacity, 0);
|
||||
file.read_exact(&mut data)
|
||||
.map_err(|error| Error::io("read", path, error))?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn sha256_bytes(data: &[u8]) -> String {
|
||||
@@ -214,8 +275,9 @@ struct FileLayoutWriter<'a> {
|
||||
}
|
||||
|
||||
impl FileLayoutWriter<'_> {
|
||||
fn write(&mut self, virtual_address: u64, data: &[u8]) -> Result<usize> {
|
||||
let data_len = u64::try_from(data.len())
|
||||
/// Reject a declared write before its bytes have been decompressed.
|
||||
fn validate(&self, virtual_address: u64, length: usize) -> Result<()> {
|
||||
let data_len = u64::try_from(length)
|
||||
.map_err(|_| Error::Invalid("decoded write length exceeds u64".to_owned()))?;
|
||||
let end = virtual_address
|
||||
.checked_add(data_len)
|
||||
@@ -225,6 +287,16 @@ impl FileLayoutWriter<'_> {
|
||||
"decoded write 0x{virtual_address:x}..0x{end:x} exceeds target load image"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write(&mut self, virtual_address: u64, data: &[u8]) -> Result<usize> {
|
||||
self.validate(virtual_address, data.len())?;
|
||||
let data_len = u64::try_from(data.len())
|
||||
.map_err(|_| Error::Invalid("decoded write length exceeds u64".to_owned()))?;
|
||||
let end = virtual_address
|
||||
.checked_add(data_len)
|
||||
.ok_or_else(|| Error::Invalid("decoded write range overflow".to_owned()))?;
|
||||
let mut written = 0_u64;
|
||||
let mut covered_memory = 0_u64;
|
||||
for segment in &self.layout.program_headers {
|
||||
@@ -275,21 +347,31 @@ impl FileLayoutWriter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_container<F>(
|
||||
struct DecodeContext<'a, V> {
|
||||
limits: &'a ResourceLimits,
|
||||
bytes: &'a mut ByteBudget,
|
||||
records: &'a mut crate::android::EntryBudget,
|
||||
validate: V,
|
||||
}
|
||||
|
||||
fn decode_container<F, V>(
|
||||
payload: &[u8],
|
||||
header: &ContainerHeader,
|
||||
config: &Module9bConfig,
|
||||
verbose: bool,
|
||||
context: &mut DecodeContext<'_, V>,
|
||||
mut writer: F,
|
||||
) -> Result<DecodeStatistics>
|
||||
where
|
||||
F: FnMut(u64, &[u8]) -> Result<usize>,
|
||||
V: FnMut(u64, usize) -> Result<()>,
|
||||
{
|
||||
let decoder = HuffmanLzDecoder::new(&header.tree)?;
|
||||
let mut statistics = DecodeStatistics {
|
||||
segments: header.segments.len(),
|
||||
..DecodeStatistics::default()
|
||||
};
|
||||
context.records.charge(header.segments.len())?;
|
||||
let decrypt_aes = !(config.skip_aes || header.skip_aes);
|
||||
for (segment_index, encoded) in header.segments.iter().enumerate() {
|
||||
let start = checked_add(
|
||||
@@ -298,6 +380,7 @@ where
|
||||
"encoded segment start",
|
||||
)?;
|
||||
let encoded_data = slice(payload, start, encoded.size as usize)?;
|
||||
context.limits.check_entry_bytes(u64::from(encoded.size))?;
|
||||
let transformed = transform_segment(
|
||||
encoded_data,
|
||||
config.container_seed,
|
||||
@@ -325,6 +408,7 @@ where
|
||||
"decoded segment {segment_index} has invalid writer offsets"
|
||||
));
|
||||
}
|
||||
context.records.charge(writer_count)?;
|
||||
let mut data_cursor = data_offset;
|
||||
for writer_index in 0..writer_count {
|
||||
let record = table_offset + writer_index * 16;
|
||||
@@ -341,16 +425,32 @@ where
|
||||
));
|
||||
}
|
||||
let source = &transformed[data_cursor..encoded_end];
|
||||
let encoded_bytes = u64::try_from(encoded_size)
|
||||
.map_err(|_| Error::Invalid("writer source length exceeds u64".to_owned()))?;
|
||||
let decoded_bytes = u64::try_from(output_size)
|
||||
.map_err(|_| Error::Invalid("writer output length exceeds u64".to_owned()))?;
|
||||
// The compressed source and the declared destination are both
|
||||
// rejected before Huffman decoding reserves the output buffer.
|
||||
// Only a writer that actually decompresses retains a new buffer,
|
||||
// so only that length is charged to the task total.
|
||||
context.limits.check_entry_bytes(encoded_bytes)?;
|
||||
context.limits.check_entry_bytes(decoded_bytes)?;
|
||||
let target = base_offset
|
||||
.checked_add(output_offset)
|
||||
.ok_or_else(|| Error::Invalid("writer target address overflow".to_owned()))?;
|
||||
// The destination is checked against the real output before a
|
||||
// compressed writer reserves its buffer. An in-range declared
|
||||
// length that still falls past the image is a bounds error, not
|
||||
// a decoder error.
|
||||
(context.validate)(target, output_size)?;
|
||||
let decoded = if encoded_size == output_size {
|
||||
None
|
||||
} else {
|
||||
statistics.compressed_writers += 1;
|
||||
context.bytes.charge(decoded_bytes)?;
|
||||
Some(decoder.decode(source, output_size)?)
|
||||
};
|
||||
let decoded_slice = decoded.as_deref().unwrap_or(source);
|
||||
let target = base_offset
|
||||
.checked_add(output_offset)
|
||||
.ok_or_else(|| Error::Invalid("writer target address overflow".to_owned()))?;
|
||||
statistics.file_bytes_written += writer(target, decoded_slice)? as u64;
|
||||
statistics.writers += 1;
|
||||
statistics.encoded_bytes += encoded_size as u64;
|
||||
@@ -1585,14 +1685,19 @@ fn table_end(tables: &[TablePayload], start: u64) -> Result<u64> {
|
||||
table_placements(tables, start).map(|(_, end)| end)
|
||||
}
|
||||
|
||||
struct MaterializeInput<'a> {
|
||||
auxiliary_symbol_seed: Option<u32>,
|
||||
constructor_module: Option<(&'a [u8], u32)>,
|
||||
limits: &'a ResourceLimits,
|
||||
}
|
||||
|
||||
fn materialize_static_elf_tables(
|
||||
output: &mut [u8],
|
||||
source: &[u8],
|
||||
layout: &ElfLayout,
|
||||
symbol_patch_data: &[u8],
|
||||
auxiliary_data: &[u8],
|
||||
auxiliary_symbol_seed: Option<u32>,
|
||||
constructor_module: Option<(&[u8], u32)>,
|
||||
input: MaterializeInput<'_>,
|
||||
) -> Result<(ElfLayout, ElfMaterializationReport, u64)> {
|
||||
let names = layout.section_names(source)?;
|
||||
let indices = required_section_indices(&names)?;
|
||||
@@ -1651,7 +1756,14 @@ fn materialize_static_elf_tables(
|
||||
Error::Invalid("auxiliary symbol table has no null entry".to_owned())
|
||||
})?)
|
||||
.map_err(|_| Error::Invalid("auxiliary symbol count exceeds usize".to_owned()))?;
|
||||
let mut appended_symbols = Vec::with_capacity(auxiliary_appended_count * ELF64_SYMBOL_SIZE);
|
||||
let appended_symbol_bytes = auxiliary_appended_count
|
||||
.checked_mul(ELF64_SYMBOL_SIZE)
|
||||
.ok_or_else(|| Error::Invalid("appended symbol reservation overflow".to_owned()))?;
|
||||
reserve_within(input.limits, appended_symbol_bytes)?;
|
||||
let mut appended_symbols = Vec::new();
|
||||
appended_symbols
|
||||
.try_reserve_exact(appended_symbol_bytes)
|
||||
.map_err(|_| Error::Invalid("appended symbol reservation failed".to_owned()))?;
|
||||
for index in 1..auxiliary.dynsym_count as usize {
|
||||
let offset = auxiliary.dynsym_offset as usize + index * ELF64_SYMBOL_SIZE;
|
||||
let symbol = slice(auxiliary_data, offset, ELF64_SYMBOL_SIZE)?;
|
||||
@@ -1670,10 +1782,27 @@ fn materialize_static_elf_tables(
|
||||
appended_symbols.extend_from_slice(&merged_name_offset.to_le_bytes());
|
||||
appended_symbols.extend_from_slice(&symbol[4..]);
|
||||
}
|
||||
let mut merged_symbols = Vec::with_capacity(old_symbols.len() + appended_symbols.len());
|
||||
let merged_symbol_bytes = old_symbols
|
||||
.len()
|
||||
.checked_add(appended_symbols.len())
|
||||
.ok_or_else(|| Error::Invalid("merged symbol reservation overflow".to_owned()))?;
|
||||
reserve_within(input.limits, merged_symbol_bytes)?;
|
||||
let mut merged_symbols = Vec::new();
|
||||
merged_symbols
|
||||
.try_reserve_exact(merged_symbol_bytes)
|
||||
.map_err(|_| Error::Invalid("merged symbol reservation failed".to_owned()))?;
|
||||
merged_symbols.extend_from_slice(&old_symbols);
|
||||
merged_symbols.extend_from_slice(&appended_symbols);
|
||||
let mut merged_strings = Vec::with_capacity(old_strings.len() + auxiliary_strings.len() + 64);
|
||||
let merged_string_bytes = old_strings
|
||||
.len()
|
||||
.checked_add(auxiliary_strings.len())
|
||||
.and_then(|len| len.checked_add(64))
|
||||
.ok_or_else(|| Error::Invalid("merged string reservation overflow".to_owned()))?;
|
||||
reserve_within(input.limits, merged_string_bytes)?;
|
||||
let mut merged_strings = Vec::new();
|
||||
merged_strings
|
||||
.try_reserve_exact(merged_string_bytes)
|
||||
.map_err(|_| Error::Invalid("merged string reservation failed".to_owned()))?;
|
||||
merged_strings.extend_from_slice(&old_strings);
|
||||
merged_strings.extend_from_slice(auxiliary_strings);
|
||||
let mut merged_versions =
|
||||
@@ -1708,7 +1837,7 @@ fn materialize_static_elf_tables(
|
||||
remap: Some(RelocationRemap {
|
||||
base_symbol_count: old_symbol_count,
|
||||
auxiliary_symbol_count: auxiliary.dynsym_count,
|
||||
permutation_seed: auxiliary_symbol_seed,
|
||||
permutation_seed: input.auxiliary_symbol_seed,
|
||||
}),
|
||||
};
|
||||
let auxiliary2 = RelocationTable {
|
||||
@@ -1718,7 +1847,7 @@ fn materialize_static_elf_tables(
|
||||
remap: Some(RelocationRemap {
|
||||
base_symbol_count: old_symbol_count,
|
||||
auxiliary_symbol_count: auxiliary.dynsym_count,
|
||||
permutation_seed: auxiliary_symbol_seed,
|
||||
permutation_seed: input.auxiliary_symbol_seed,
|
||||
}),
|
||||
};
|
||||
old_dyn.validate(
|
||||
@@ -1783,7 +1912,7 @@ fn materialize_static_elf_tables(
|
||||
let new_symbol_count = merged_names.len();
|
||||
let new_dynstr_size = merged_strings.len();
|
||||
|
||||
let constructors = match constructor_module {
|
||||
let constructors = match input.constructor_module {
|
||||
Some(module) => {
|
||||
let init_index = names
|
||||
.iter()
|
||||
@@ -1951,11 +2080,27 @@ fn materialize_static_elf_tables(
|
||||
.map_err(|_| Error::Invalid("fini RVA exceeds i64".to_owned()))?;
|
||||
}
|
||||
|
||||
let mut merged_rela_dyn = Vec::with_capacity(dyn_relocations.len() * ELF64_RELA_SIZE);
|
||||
let rela_dyn_bytes = dyn_relocations
|
||||
.len()
|
||||
.checked_mul(ELF64_RELA_SIZE)
|
||||
.ok_or_else(|| Error::Invalid("merged relocation reservation overflow".to_owned()))?;
|
||||
reserve_within(input.limits, rela_dyn_bytes)?;
|
||||
let mut merged_rela_dyn = Vec::new();
|
||||
merged_rela_dyn
|
||||
.try_reserve_exact(rela_dyn_bytes)
|
||||
.map_err(|_| Error::Invalid("merged relocation reservation failed".to_owned()))?;
|
||||
for relocation in dyn_relocations {
|
||||
relocation.encode(&mut merged_rela_dyn);
|
||||
}
|
||||
let mut merged_rela_plt = Vec::with_capacity(plt_relocations.len() * ELF64_RELA_SIZE);
|
||||
let rela_plt_bytes = plt_relocations
|
||||
.len()
|
||||
.checked_mul(ELF64_RELA_SIZE)
|
||||
.ok_or_else(|| Error::Invalid("merged PLT reservation overflow".to_owned()))?;
|
||||
reserve_within(input.limits, rela_plt_bytes)?;
|
||||
let mut merged_rela_plt = Vec::new();
|
||||
merged_rela_plt
|
||||
.try_reserve_exact(rela_plt_bytes)
|
||||
.map_err(|_| Error::Invalid("merged PLT reservation failed".to_owned()))?;
|
||||
for relocation in plt_relocations {
|
||||
relocation.encode(&mut merged_rela_plt);
|
||||
}
|
||||
@@ -2358,6 +2503,20 @@ fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
|
||||
|
||||
/// Restore the current protected `libil2cpp.so` without executing protector code.
|
||||
pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
restore_libil2cpp_with_limits(options, &ResourceLimits::default())
|
||||
}
|
||||
|
||||
/// [`restore_libil2cpp`] with explicit ceilings.
|
||||
///
|
||||
/// `max_entry_bytes` bounds each artifact read, the auxiliary decoded image,
|
||||
/// every writer source and destination, and growth of the temporary output.
|
||||
/// `max_total_bytes` sums the stage-2 artifact reads and decompressed writer
|
||||
/// outputs for this call. It is not a peak resident-memory budget: the mapped
|
||||
/// input and the output image are checked individually and are not added in.
|
||||
pub fn restore_libil2cpp_with_limits(
|
||||
options: &RestoreOptions,
|
||||
limits: &ResourceLimits,
|
||||
) -> Result<RestoreReport> {
|
||||
let started = Instant::now();
|
||||
let input_path = absolute(&options.input)?;
|
||||
let output_path = absolute(&options.output)?;
|
||||
@@ -2369,11 +2528,13 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
return invalid("refusing to overwrite the protected input in place");
|
||||
}
|
||||
let artifacts = load_artifacts(&index_path)?;
|
||||
let module = read_file(&artifacts[&0x9b].path)?;
|
||||
let symbol_patch_data = read_file(&artifacts[&0x9e].path)?;
|
||||
let mut bytes = ByteBudget::new(limits);
|
||||
let mut records = crate::android::EntryBudget::new(limits);
|
||||
let module = read_artifact(&artifacts[&0x9b].path, limits, &mut bytes)?;
|
||||
let symbol_patch_data = read_artifact(&artifacts[&0x9e].path, limits, &mut bytes)?;
|
||||
let auxiliary_symbol_seed = artifacts
|
||||
.get(&0x96)
|
||||
.map(|artifact| read_file(&artifact.path))
|
||||
.map(|artifact| read_artifact(&artifact.path, limits, &mut bytes))
|
||||
.transpose()?
|
||||
.as_deref()
|
||||
.map(discover_auxiliary_symbol_seed)
|
||||
@@ -2389,7 +2550,7 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
"constructor module 0x98 lacks its descriptor entry offset".to_owned(),
|
||||
)
|
||||
})?;
|
||||
Ok((read_file(&artifact.path)?, entry))
|
||||
Ok((read_artifact(&artifact.path, limits, &mut bytes)?, entry))
|
||||
})
|
||||
.transpose()?;
|
||||
let config = Module9bConfig::parse(&module)?;
|
||||
@@ -2445,6 +2606,12 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
if auxiliary_header.output_size != descriptor.auxiliary_expected_size {
|
||||
return invalid("auxiliary container output size does not match the 0x9D descriptor");
|
||||
}
|
||||
// The auxiliary image is one flat allocation. Reject its declared size
|
||||
// before that buffer is reserved, even when outer-only mode will not
|
||||
// decode it: a hostile size must not depend on a later branch to be safe.
|
||||
// The same check repeats inside `allocate_auxiliary`, which is also what
|
||||
// charges the buffer; this one only guards the outer-only path.
|
||||
limits.check_entry_bytes(u64::from(auxiliary_header.output_size))?;
|
||||
if outer.encoded_end()? != descriptor.auxiliary_offset as usize {
|
||||
return invalid("primary and auxiliary 0x9D containers are not contiguous");
|
||||
}
|
||||
@@ -2456,6 +2623,19 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
.map_err(|error| Error::io("create temporary output", parent, error))?;
|
||||
let temporary_path = temporary.path().to_path_buf();
|
||||
let private_size = usize_from_u64(private.offset, "private section offset")?;
|
||||
// The temporary output is one file. It is capped by the smaller of the
|
||||
// single-entry ceiling and the task total, but it is not added to the
|
||||
// decoder byte budget: that budget counts retained decoded bytes, and the
|
||||
// output file is the image those bytes are written into.
|
||||
limits.check_entry_bytes(private.offset.min(limits.max_total_bytes))?;
|
||||
if private.offset > limits.max_total_bytes {
|
||||
return Err(crate::android::LimitExceeded::Total {
|
||||
requested: private.offset,
|
||||
adding: private.offset,
|
||||
limit: limits.max_total_bytes,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
copy_range(
|
||||
&source,
|
||||
temporary.as_file_mut(),
|
||||
@@ -2486,11 +2666,30 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
layout: &layout,
|
||||
load_end,
|
||||
};
|
||||
let mut context = DecodeContext {
|
||||
limits,
|
||||
bytes: &mut bytes,
|
||||
records: &mut records,
|
||||
validate: |address: u64, length: usize| {
|
||||
let data_len = u64::try_from(length)
|
||||
.map_err(|_| Error::Invalid("decoded write length exceeds u64".to_owned()))?;
|
||||
let end = address
|
||||
.checked_add(data_len)
|
||||
.ok_or_else(|| Error::Invalid("decoded write range overflow".to_owned()))?;
|
||||
if end > load_end {
|
||||
return invalid(format!(
|
||||
"decoded write 0x{address:x}..0x{end:x} exceeds target load image"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
};
|
||||
primary_stats = decode_container(
|
||||
&payload,
|
||||
&outer,
|
||||
&config,
|
||||
options.verbose,
|
||||
&mut context,
|
||||
|address, data| writer.write(address, data),
|
||||
)?;
|
||||
output
|
||||
@@ -2502,12 +2701,29 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
if options.verbose {
|
||||
eprintln!("Decoding auxiliary 0x9D ELF materialization container...");
|
||||
}
|
||||
let mut decoded = vec![0_u8; auxiliary_header.output_size as usize];
|
||||
let mut decoded = allocate_auxiliary(auxiliary_header.output_size, limits, &mut bytes)?;
|
||||
let auxiliary_len = decoded.len();
|
||||
let mut context = DecodeContext {
|
||||
limits,
|
||||
bytes: &mut bytes,
|
||||
records: &mut records,
|
||||
validate: |offset: u64, length: usize| {
|
||||
let start = usize_from_u64(offset, "auxiliary write offset")?;
|
||||
let end = start
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| Error::Invalid("auxiliary decoded write overflow".to_owned()))?;
|
||||
if end > auxiliary_len {
|
||||
return invalid("auxiliary decoded write is out of range".to_owned());
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
};
|
||||
let stats = decode_container(
|
||||
&payload,
|
||||
&auxiliary_header,
|
||||
&config,
|
||||
options.verbose,
|
||||
&mut context,
|
||||
|offset, data| {
|
||||
let start = usize_from_u64(offset, "auxiliary write offset")?;
|
||||
let end = start
|
||||
@@ -2527,9 +2743,20 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
if mapping_length < private_size {
|
||||
return invalid("dynamic-table mapping is shorter than the ELF image");
|
||||
}
|
||||
let mapping_bytes = u64::try_from(mapping_length)
|
||||
.map_err(|_| Error::Invalid("dynamic-table mapping length exceeds u64".to_owned()))?;
|
||||
limits.check_entry_bytes(mapping_bytes.min(limits.max_total_bytes))?;
|
||||
if mapping_bytes > limits.max_total_bytes {
|
||||
return Err(crate::android::LimitExceeded::Total {
|
||||
requested: mapping_bytes,
|
||||
adding: mapping_bytes,
|
||||
limit: limits.max_total_bytes,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
temporary
|
||||
.as_file()
|
||||
.set_len(mapping_length as u64)
|
||||
.set_len(mapping_bytes)
|
||||
.map_err(|error| Error::io("extend temporary output", &temporary_path, error))?;
|
||||
if options.verbose {
|
||||
eprintln!("Rebuilding static ELF dynamic-linker tables...");
|
||||
@@ -2542,10 +2769,13 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
&layout,
|
||||
&symbol_patch_data,
|
||||
&decoded,
|
||||
auxiliary_symbol_seed,
|
||||
constructor_module
|
||||
.as_ref()
|
||||
.map(|(image, entry)| (image.as_slice(), *entry)),
|
||||
MaterializeInput {
|
||||
auxiliary_symbol_seed,
|
||||
constructor_module: constructor_module
|
||||
.as_ref()
|
||||
.map(|(image, entry)| (image.as_slice(), *entry)),
|
||||
limits,
|
||||
},
|
||||
)?;
|
||||
restored_layout = new_layout;
|
||||
materialization = Some(report);
|
||||
@@ -2616,6 +2846,198 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `decode_container` rejects a writer whose declared length fits the entry
|
||||
/// ceiling but whose target ends past the destination. The writer closure
|
||||
/// counts calls and must stay at zero.
|
||||
#[test]
|
||||
fn decode_rejects_an_in_cap_writer_past_the_destination_before_writing() {
|
||||
let limits = ResourceLimits {
|
||||
max_entry_bytes: 64,
|
||||
max_total_bytes: 64,
|
||||
max_entries: 8,
|
||||
..ResourceLimits::default()
|
||||
};
|
||||
let destination_len = 8_usize;
|
||||
let mut writes = 0_usize;
|
||||
let mut records = crate::android::EntryBudget::new(&limits);
|
||||
let mut bytes = ByteBudget::new(&limits);
|
||||
let container = past_end_container();
|
||||
let header = senbei_crypto::android::ContainerHeader::parse(&container, 0, 0)
|
||||
.expect("synthetic container header");
|
||||
let config = senbei_crypto::android::Module9bConfig {
|
||||
header_seed: 0,
|
||||
container_seed: 0,
|
||||
aes_key: [0; 32],
|
||||
skip_aes: true,
|
||||
schedule_offset: 0,
|
||||
};
|
||||
let mut context = DecodeContext {
|
||||
limits: &limits,
|
||||
bytes: &mut bytes,
|
||||
records: &mut records,
|
||||
validate: |offset: u64, length: usize| {
|
||||
limits.check_entry_bytes(length as u64)?;
|
||||
let start = usize_from_u64(offset, "auxiliary write offset")?;
|
||||
let end = start
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| Error::Invalid("auxiliary decoded write overflow".to_owned()))?;
|
||||
if end > destination_len {
|
||||
return invalid("auxiliary decoded write is out of range".to_owned());
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
};
|
||||
let error = decode_container(
|
||||
&container,
|
||||
&header,
|
||||
&config,
|
||||
false,
|
||||
&mut context,
|
||||
|_offset, _data| {
|
||||
writes += 1;
|
||||
Ok(0)
|
||||
},
|
||||
)
|
||||
.expect_err("the writer target ends past the destination");
|
||||
assert!(error.to_string().contains("out of range"), "{error}");
|
||||
assert_eq!(writes, 0, "the writer ran before the range check");
|
||||
}
|
||||
|
||||
/// Container whose one writer declares 4 bytes at offset 8. The destination
|
||||
/// used by the test is 8 bytes, so the range check fails before the writer.
|
||||
fn past_end_container() -> Vec<u8> {
|
||||
fn unmultiply(encoded: u32) -> u32 {
|
||||
let mut multiplier = 0xdc49_cd06_u32;
|
||||
let mut value = encoded;
|
||||
let mut result = 0_u32;
|
||||
while multiplier != 0 {
|
||||
if multiplier & 1 != 0 {
|
||||
result ^= value;
|
||||
}
|
||||
let carry = value >> 31;
|
||||
value = value.wrapping_shl(1);
|
||||
if carry != 0 {
|
||||
value ^= 0x5793_57eb;
|
||||
}
|
||||
multiplier >>= 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
let state = 0_u32;
|
||||
let output_size = 8_u32;
|
||||
let raw0 = unmultiply(
|
||||
output_size
|
||||
.wrapping_sub(0xa21d_fb3a_u32.wrapping_shl(state & 7))
|
||||
.wrapping_sub(state.wrapping_mul(0xf87b_337c)),
|
||||
);
|
||||
let flag_word = 1_u32 | (1_u32 << 8);
|
||||
let raw1 = unmultiply(
|
||||
flag_word
|
||||
^ state
|
||||
.wrapping_add(0xbd19_c63c)
|
||||
.wrapping_add(0x416e_2af2_u32.wrapping_shr(state & 0x0d)),
|
||||
);
|
||||
let mut tree = vec![0_u8; 256 * 3];
|
||||
tree[1] = 0x80;
|
||||
tree[2] = 1;
|
||||
let raw2 = unmultiply(
|
||||
(tree.len() as u32)
|
||||
.wrapping_add(state ^ 0x3b2b_f538)
|
||||
.wrapping_sub(0x643a_3a3b_u32.wrapping_shl(state & 0x0b)),
|
||||
);
|
||||
let mut header = Vec::new();
|
||||
header.extend_from_slice(&raw0.to_le_bytes());
|
||||
header.extend_from_slice(&raw1.to_le_bytes());
|
||||
header.extend_from_slice(&raw2.to_le_bytes());
|
||||
for offset in (0..tree.len() & !3).step_by(4) {
|
||||
let value = u32::from_le_bytes(tree[offset..offset + 4].try_into().unwrap());
|
||||
tree[offset..offset + 4].copy_from_slice(&unmultiply(value).to_le_bytes());
|
||||
}
|
||||
header.extend_from_slice(&tree);
|
||||
let mut plain = vec![0_u8; 36];
|
||||
plain[4..8].copy_from_slice(&1_u32.to_le_bytes());
|
||||
plain[8..12].copy_from_slice(&16_u32.to_le_bytes());
|
||||
plain[12..16].copy_from_slice(&32_u32.to_le_bytes());
|
||||
plain[16..20].copy_from_slice(&8_u32.to_le_bytes());
|
||||
plain[20..24].copy_from_slice(&4_u32.to_le_bytes());
|
||||
plain[24..28].copy_from_slice(&4_u32.to_le_bytes());
|
||||
let mut state_word = 0_u32;
|
||||
let mut left = 0xe34e_ac63_u32;
|
||||
let mut right = 0x07b4_8238_u32;
|
||||
for (index, chunk) in plain.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
let index32 = index as u32;
|
||||
left = state_word
|
||||
.wrapping_add(0x72f6_fcbe)
|
||||
.wrapping_add(left.wrapping_add(0x4f8b_1bca).wrapping_mul(left))
|
||||
.wrapping_shr(index32.wrapping_mul(index32) & 0x0f);
|
||||
right = state_word
|
||||
.wrapping_sub(0x71b6_a98d)
|
||||
.wrapping_add(right.wrapping_sub(0x1605_a81c).wrapping_mul(right))
|
||||
.wrapping_shl(index32 & 7);
|
||||
state_word = left ^ right;
|
||||
let mut value = u32::from_le_bytes(*chunk);
|
||||
value ^= state_word;
|
||||
value = value.wrapping_add(state_word);
|
||||
value ^= 0xaf57_f7fb_u32.wrapping_mul(index32 & 3);
|
||||
value = value.wrapping_sub(0xb43b_9baf_u32.wrapping_mul(index32 & 0x0d));
|
||||
chunk.copy_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
let segment_offset = (header.len() + 8) as u32;
|
||||
let table_state = state.wrapping_add(0xb31f_451c).wrapping_mul(state);
|
||||
let table_xor = table_state.wrapping_shl(3);
|
||||
let table_add = table_state.wrapping_sub(0x822f_e82d_u32.wrapping_mul(state));
|
||||
for (index, word) in [segment_offset, plain.len() as u32].into_iter().enumerate() {
|
||||
let adjustment = table_add.wrapping_shr((((index * 4) & 7) + 5) as u32);
|
||||
let encoded = unmultiply(word.wrapping_sub(adjustment)) ^ table_xor;
|
||||
header.extend_from_slice(&encoded.to_le_bytes());
|
||||
}
|
||||
header.extend_from_slice(&plain);
|
||||
header
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auxiliary_allocation_rejects_one_byte_over_the_ceiling() {
|
||||
let limits = ResourceLimits {
|
||||
max_entry_bytes: 4,
|
||||
max_total_bytes: 8,
|
||||
..ResourceLimits::default()
|
||||
};
|
||||
let mut budget = ByteBudget::new(&limits);
|
||||
let error = allocate_auxiliary(5, &limits, &mut budget)
|
||||
.expect_err("five bytes is over the four-byte ceiling");
|
||||
assert!(matches!(
|
||||
error,
|
||||
Error::Limit(crate::android::LimitExceeded::Entry {
|
||||
requested: 5,
|
||||
limit: 4
|
||||
})
|
||||
));
|
||||
assert_eq!(budget.used(), 0, "a rejected size is not charged");
|
||||
|
||||
let decoded = allocate_auxiliary(4, &limits, &mut budget).expect("the ceiling is allowed");
|
||||
assert_eq!(decoded.len(), 4);
|
||||
assert_eq!(budget.used(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auxiliary_allocations_share_the_task_budget() {
|
||||
let limits = ResourceLimits {
|
||||
max_entry_bytes: 4,
|
||||
max_total_bytes: 8,
|
||||
..ResourceLimits::default()
|
||||
};
|
||||
let mut budget = ByteBudget::new(&limits);
|
||||
allocate_auxiliary(4, &limits, &mut budget).unwrap();
|
||||
allocate_auxiliary(4, &limits, &mut budget).expect("eight bytes is the total ceiling");
|
||||
let error = allocate_auxiliary(1, &limits, &mut budget)
|
||||
.expect_err("the next byte is over the total");
|
||||
assert!(matches!(
|
||||
error,
|
||||
Error::Limit(crate::android::LimitExceeded::Total { .. })
|
||||
));
|
||||
assert_eq!(budget.used(), 8);
|
||||
}
|
||||
|
||||
fn auxiliary_image(symbol_count: u32) -> Vec<u8> {
|
||||
let mut data = vec![0_u8; 0x279];
|
||||
let words = [
|
||||
|
||||
@@ -52,7 +52,7 @@ where
|
||||
line: details.line,
|
||||
column: details.column,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,12 +215,12 @@ where
|
||||
{
|
||||
match validate_dll_layout(dll_layout()) {
|
||||
Ok(output) => Ok(output),
|
||||
Err(dll) => validate_dll_layout(exe_layout()).map_err(|exe| {
|
||||
UnpackError::PipelineFallbackFailed {
|
||||
Err(dll) => {
|
||||
validate_dll_layout(exe_layout()).map_err(|exe| UnpackError::PipelineFallbackFailed {
|
||||
dll: Box::new(dll),
|
||||
exe: Box::new(exe),
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +228,9 @@ fn validate_dll_layout(output: Result<Vec<u8>, UnpackError>) -> Result<Vec<u8>,
|
||||
let output = output?;
|
||||
if let Some(report) = dll_layout_invalid(&output) {
|
||||
return Err(UnpackError::InvalidLayoutCandidate {
|
||||
issues: report.findings.into_iter()
|
||||
issues: report
|
||||
.findings
|
||||
.into_iter()
|
||||
.filter(|finding| finding.severity == integrity::IntegritySeverity::Fatal)
|
||||
.map(|finding| finding.message)
|
||||
.collect(),
|
||||
|
||||
Reference in new issue
Block a user