fix(unpacker): refine layout validation and diagnostics

This commit is contained in:
bfloat16
2026-08-11 11:42:12 +08:00
parent 67178d34af
commit a89900a812
7 changed files with 393 additions and 105 deletions
+13 -1
View File
@@ -93,6 +93,15 @@ Several protected stages are themselves little bytecode programs. The core
includes a small VM (`bytecode.rs`) that generates and interprets those
programs rather than hardcoding each variant's constants.
The PE32+ configuration block has two observed anchor-relative alignments.
Senbei selects between them by validating the stage1 `(RVA, length)`
descriptor against the image, rather than relying on a version-like word whose
value is not stable across build families. Stage3 seed advancement also varies:
the usual four-round result is tried first, then bounded alternatives are
replayed from the untouched ciphertext and accepted only when decompression
writes the exact target size and the recovered stage has its expected function
tail structure.
## Integrity check
Every produced image passes through `integrity::check` — a static, execution-
@@ -125,7 +134,10 @@ payload. The capture context is propagated into section worker threads; panics
outside an active unpack continue through the previously installed panic hook.
Expected validation failures use structured variants carrying the failed stage,
block index, table kind, or invalid range instead of collapsing unrelated causes
into a generic corruption error.
into a generic corruption error. Huffman/LZ failures distinguish invalid code
lengths, tree traversal, pending-length overflow, invalid back-references,
output overflow, and size mismatch. When both DLL parsing and the EXE-layout
fallback fail, the returned error retains both pipeline errors.
Size requests are bounds-checked against a 1 GiB `MAX_IMAGE_SIZE` before
allocation so a crafted header cannot abort the process with a huge
allocation. In folder mode each file is isolated: one file's failure is logged
+5 -1
View File
@@ -8,7 +8,8 @@ committed.
## What to put here
Place protected inputs directly in this folder:
Place protected inputs in this folder, either directly or grouped in
subdirectories:
- `*.exe` — Crackproof-protected executables (PE32 or PE32+)
- `*.dll` — Crackproof-protected DLLs (native or managed)
@@ -19,6 +20,9 @@ keeping the exact `._` suffix on the full file name. The test splices it the
same way the CLI does; without it the loader stub alone is meaningless and the
splice / export-overlay / TLS-restore code is never exercised.
The corpus scan is recursive and skips directories named `unpack`, matching the
CLI's output-directory rule.
Optionally, place a **golden** next to each input — the known-good unpacked
output, named `<base>.golden.<ext>`:
+15 -7
View File
@@ -88,19 +88,17 @@ fn decrypt_data4(
OpsLut::new(ops).map_region(d, addr as usize, size as usize);
}
if size != decompressed_size {
// decompress reports corruption (after partial writes) via its bool;
// surface it instead of shipping a garbage block.
if !decompress(
if size != decompressed_size
&& let Err(reason) = primitives::decompress_detailed(
d,
addr as u32,
compressed_addr as u32,
decomp_params[1] as u32,
size as u32,
decompressed_size as u32,
) {
return Err(UnpackError::StageDecompressionFailed(stage));
}
)
{
return Err(UnpackError::StageDecompressionFailed { stage, reason });
}
Ok(())
}
@@ -469,6 +467,16 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
println!(" checksum1 = 0x{:08X}", checksum1 as u32);
println!(" decrypted_addr1 = 0x{:08X}", decrypted_addr1 as u32);
}
let primary_end = decrypted_addr1.checked_add(3856);
if decrypted_addr1 < keys[3]
|| primary_end.is_none_or(|end| end < 0 || end as usize > out.len())
{
return Err(UnpackError::InvalidDllPrimaryDescriptor {
address: decrypted_addr1 as u32,
minimum: keys[3] as u32,
image_len: out.len(),
});
}
let import_offset = get_i32(&out, decrypted_addr1 + 3444);
let decrypted_addr2_size = get_i32(&out, decrypted_addr1 + 3632);
decrypt_data3(
+244 -56
View File
@@ -35,6 +35,42 @@ impl std::fmt::Display for DecompressionStage {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DecompressionFailure {
#[error("compressed source size {size} exceeds limit {max}")]
SourceTooLarge { size: u32, max: u64 },
#[error("Huffman code length {bits} is invalid")]
InvalidCodeLength { bits: u8 },
#[error("Huffman tree traversal exceeded 64 levels")]
HuffmanTraversalLimit,
#[error("pending length accumulator overflowed at {pending}")]
PendingLengthOverflow { pending: u32 },
#[error("output step {step} at byte {written} exceeds expected size {expected}")]
OutputOverflow {
written: u32,
step: u32,
expected: u32,
},
#[error("run-fill width {width} reads before output offset 0x{destination:08X}")]
RunFillBeforeOutput { width: u32, destination: u32 },
#[error("run-fill width {width} is unsupported")]
InvalidRunFillWidth { width: u32 },
#[error("back-reference distance {distance} exceeds {written} written bytes")]
InvalidBackReference { distance: u32, written: u32 },
#[error("Huffman symbol consumed no input and produced no output")]
NoProgress,
#[error(
"output size mismatch (wrote {written}/{expected} bytes after consuming {consumed}/{source_size})"
)]
OutputSizeMismatch {
written: u32,
expected: u32,
consumed: u32,
source_size: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BytecodeStage {
ExeStage4,
@@ -121,6 +157,9 @@ pub enum UnpackError {
#[error("anchor field not found — corrupt data or wrong offset")]
AnchorNotFound,
#[error("stage1 descriptor not found near anchor 0x{anchor:08X}")]
Stage1DescriptorNotFound { anchor: u32 },
#[error("stage2 field not found — corrupt data or wrong offset")]
Stage2NotFound,
@@ -145,6 +184,15 @@ pub enum UnpackError {
#[error("DLL pipeline requires PE32+ optional-header magic, got 0x{found:04X}")]
UnsupportedDllPeMagic { found: u16 },
#[error(
"DLL primary descriptor address 0x{address:08X} is below layout base 0x{minimum:08X} or outside {image_len}-byte image"
)]
InvalidDllPrimaryDescriptor {
address: u32,
minimum: u32,
image_len: usize,
},
#[error("invalid SizeOfImage {size}; expected 1..={max}")]
InvalidImageSize { size: i64, max: u64 },
@@ -182,8 +230,11 @@ pub enum UnpackError {
#[error("PE32 file LFSR not found in eighthStage")]
Pe32FileLfsrNotFound,
#[error("{0} decompression failed — corrupt data or wrong offset")]
StageDecompressionFailed(DecompressionStage),
#[error("{stage} decompression failed: {reason}")]
StageDecompressionFailed {
stage: DecompressionStage,
reason: DecompressionFailure,
},
#[error("{pipeline} section block {block} decompression failed")]
SectionDecompressionFailed {
@@ -197,6 +248,12 @@ pub enum UnpackError {
#[error("Huffman table is outside the image at offset {offset}")]
InvalidHuffmanTable { offset: u32 },
#[error("DLL pipeline failed: {dll}; EXE fallback failed: {exe}")]
PipelineFallbackFailed {
dll: Box<UnpackError>,
exe: Box<UnpackError>,
},
#[error(
"PE32 second-stage range is invalid (offset {offset}, size {size}, image length {image_len})"
)]
@@ -391,8 +448,13 @@ impl<'a> Unpacker<'a> {
}
// Strategy (a): delegate to primitives::decrypt_and_decompress_data
fn decrypt_and_decompress_data(&mut self, pos: u32, key: u32, custom: Option<&[Op]>) -> bool {
primitives::decrypt_and_decompress_data(
fn decrypt_and_decompress_data(
&mut self,
pos: u32,
key: u32,
custom: Option<&[Op]>,
) -> Result<(), DecompressionFailure> {
primitives::decrypt_and_decompress_data_detailed(
&mut self.decompressed,
pos,
key,
@@ -620,26 +682,34 @@ impl<'a> Unpacker<'a> {
}
};
// Detect config-block layout version. Newer Crackproof builds (observed
// across several EXE families) shift every anchor-relative field from
// offset 40 onward by +8 bytes. The config-version stamp sits at
// anchor+104 in the old layout and anchor+112 in the new one. Across
// the whole corpus the stamp's top nibble is always 0x4 (top byte 0x40
// or 0x44), whereas the +8 layout's anchor+104 holds an inserted small
// count (top nibble 0), so the stamp position is a reliable layout
// discriminator.
let stamp_at = |off: u32| -> bool {
(anchor + off + 4) as usize <= u.decompressed.len()
&& (get_u32(&u.decompressed, anchor + off) >> 28) == 0x4
// Layouts shift the anchor-relative fields by either zero or eight
// bytes. The nearby version-like word is not stable across all build
// families, so validate the stage1 (RVA, length) descriptor itself.
let descriptor_is_valid = |extra: u32| -> bool {
let pos = anchor.wrapping_add(120 + extra);
let Some(end) = (pos as usize).checked_add(8) else {
return false;
};
let magic_off: u32 = if stamp_at(104) {
104
} else if stamp_at(112) {
112
} else {
104
if end > u.decompressed.len() {
return false;
}
let base = get_u32(&u.decompressed, pos);
let length = get_u32(&u.decompressed, pos.wrapping_add(4));
base >= u.info[3]
&& length >= 16
&& (base as usize)
.checked_add(length as usize)
.is_some_and(|stage_end| stage_end <= u.decompressed.len())
};
let anchor_extra: u32 = magic_off - 104;
let anchor_extra = [0u32, 8]
.into_iter()
.find(|&extra| descriptor_is_valid(extra))
.ok_or(UnpackError::Stage1DescriptorNotFound { anchor })?;
if verbose {
println!(" anchor = 0x{anchor:08X}");
println!(" anchor layout offset = +0x{anchor_extra:X}");
}
let p1 = get_u32(&u.decompressed, anchor.wrapping_add(8));
let p2 = get_u32(&u.decompressed, anchor.wrapping_add(4));
@@ -667,11 +737,22 @@ impl<'a> Unpacker<'a> {
let v_at = anchor.wrapping_add(20);
let v = get_u32(&u.decompressed, v_at);
let tgt = anchor.wrapping_add(120 + anchor_extra);
let stage1_descriptor = [
get_u32(&u.decompressed, tgt),
get_u32(&u.decompressed, tgt.wrapping_add(4)),
];
u.decrypt_data3(tgt, xor_acc ^ chk1 ^ v, 21);
let stage1 = get_u32(&u.decompressed, tgt);
let stage1_len = get_u32(&u.decompressed, tgt.wrapping_add(4));
if verbose {
println!("[3/9] Locating config layout...");
println!(" stage1 = 0x{:08X}", stage1);
println!(" stage1_len = 0x{stage1_len:08X}");
println!(
" stage1 descriptor = [0x{:08X}, 0x{:08X}]",
stage1_descriptor[0], stage1_descriptor[1]
);
println!(" stage1 key = xor 0x{xor_acc:08X} ^ chk 0x{chk1:08X} ^ val 0x{v:08X}");
}
// Field offsets inside stage1 vary between Crackproof versions. Locate
@@ -680,7 +761,6 @@ impl<'a> Unpacker<'a> {
// derive every other field as fixed offsets from there. Observed
// stage2_off: 3632 (older EXE builds), 3616 (another old-layout build),
// 3624 (managed-assembly builds).
let stage1_len = get_u32(&u.decompressed, tgt.wrapping_add(4));
let info3 = u.info[3];
let info5 = u.info[5];
// Use the full info[3]..info[3]+info[5] range: stage entries may live in
@@ -752,6 +832,9 @@ impl<'a> Unpacker<'a> {
if verbose {
println!("[4/9] Decrypting stage2...");
println!(" stage2 = 0x{:08X}", stage2);
println!(" stage2_off = 0x{stage2_off:04X}");
println!(" checksum table = stage1+0x{chk_src_start:04X}");
println!(" stage2 key = 0x{key2:08X}");
}
// The stage2 head/walk2 tables shift between Crackproof versions. The 4-entry
@@ -783,6 +866,20 @@ impl<'a> Unpacker<'a> {
};
let head_off = table_start.wrapping_add(32);
let walk2_off = head_off.wrapping_sub(88);
if verbose {
println!(" operation table = stage2+0x{table_start:04X}");
println!(" head/walk = +0x{head_off:04X}/+0x{walk2_off:04X}");
for index in 0..2u32 {
let entry = stage2.wrapping_add(head_off + index * 16);
println!(
" operation[{index}] = [0x{:08X}, 0x{:08X}, 0x{:08X}, 0x{:08X}]",
get_u32(&u.decompressed, entry),
get_u32(&u.decompressed, entry.wrapping_add(4)),
get_u32(&u.decompressed, entry.wrapping_add(8)),
get_u32(&u.decompressed, entry.wrapping_add(12)),
);
}
}
let mut head = stage2.wrapping_add(head_off);
for _iter in 0..2 {
@@ -825,30 +922,110 @@ impl<'a> Unpacker<'a> {
}
walk2 = walk2.wrapping_add(32);
}
if verbose {
println!(
" key offsets = [0x{:08X}, 0x{:08X}, 0x{:08X}, 0x{:08X}]",
u.key_offsets[0], u.key_offsets[1], u.key_offsets[2], u.key_offsets[3]
);
}
let chk2 = u.calculate_checksum(anchor.wrapping_add(48 + anchor_extra));
let accum_at = stage1.wrapping_add(chk_src_start.wrapping_sub(16));
let mut accum = get_u32(&u.decompressed, accum_at);
for l in 0..4u32 {
let accum_seed = get_u32(&u.decompressed, accum_at);
let mut running_accum = accum_seed;
let mut accum_candidates = vec![(0u32, accum_seed)];
for l in 0..8u32 {
let bound = (l + 1).wrapping_mul(25) << 2;
let mut i: u32 = 1;
while i <= bound {
accum = accum.wrapping_add(i);
running_accum = running_accum.wrapping_add(i);
i = i.wrapping_add(1);
}
accum_candidates.push((l + 1, running_accum));
}
let accum = accum_candidates[4].1;
let at1 = stage1.wrapping_add(stage2_off.wrapping_add(88));
let stage3_field = get_u32(&u.decompressed, at1);
let stage3_slen = get_u32(&u.decompressed, at1.wrapping_add(4));
let stage3_dest = get_u32(&u.decompressed, at1.wrapping_add(8));
let stage3_dlen = get_u32(&u.decompressed, at1.wrapping_add(12));
if verbose {
println!("[5/9] Decrypting stages 3-5...");
println!(" stage3 = 0x{:08X}", stage3_field);
println!(
" stage3 descriptor = [0x{stage3_field:08X}, 0x{stage3_slen:08X}, 0x{stage3_dest:08X}, 0x{stage3_dlen:08X}]"
);
println!(" stage3 key = xor 0x{xor_acc:08X} ^ chk 0x{chk2:08X} ^ val 0x{accum:08X}");
println!(" stage3 accum seed = 0x{accum_seed:08X}");
}
let stage3_key = xor_acc ^ chk2 ^ accum;
let stage3_source_end = (stage3_field as usize)
.checked_add(stage3_slen as usize)
.filter(|&end| end <= u.decompressed.len())
.ok_or(UnpackError::BufferRangeOutOfBounds {
operation: BufferOperation::Read,
offset: stage3_field as usize,
size: stage3_slen as usize,
buffer_len: u.decompressed.len(),
})?;
let stage3_dest_end = (stage3_dest as usize)
.checked_add(stage3_dlen as usize)
.filter(|&end| end <= u.decompressed.len())
.ok_or(UnpackError::BufferRangeOutOfBounds {
operation: BufferOperation::CopyDestination,
offset: stage3_dest as usize,
size: stage3_dlen as usize,
buffer_len: u.decompressed.len(),
})?;
const MAX_STAGE3_TRIAL_BYTES: usize = 16 * 1024 * 1024;
let trial_size = (stage3_slen as usize).checked_add(stage3_dlen as usize);
let stage3_backups = trial_size
.filter(|&size| size <= MAX_STAGE3_TRIAL_BYTES)
.map(|_| {
(
u.decompressed[stage3_field as usize..stage3_source_end].to_vec(),
u.decompressed[stage3_dest as usize..stage3_dest_end].to_vec(),
)
});
let default_result = u.decrypt_and_decompress_data(at1, stage3_key, None);
if let Err(reason) = default_result {
let Some((source_backup, dest_backup)) = stage3_backups else {
return Err(UnpackError::StageDecompressionFailed {
stage: DecompressionStage::ExeStage3,
reason,
});
};
let restore_stage3 = |data: &mut [u8]| {
data[stage3_field as usize..stage3_source_end].copy_from_slice(&source_backup);
data[stage3_dest as usize..stage3_dest_end].copy_from_slice(&dest_backup);
};
let mut selected = None;
for (rounds, candidate_accum) in &accum_candidates {
if *rounds == 4 {
continue;
}
restore_stage3(&mut u.decompressed);
let candidate_key = xor_acc ^ chk2 ^ candidate_accum;
let result = u.decrypt_and_decompress_data(at1, candidate_key, None);
if result.is_ok()
&& find_v4_offset(&u.decompressed, stage3_field, stage3_dlen).is_some()
{
selected = Some(*rounds);
break;
}
}
if let Some(rounds) = selected {
if verbose {
println!(" selected stage3 accumulator rounds = {rounds}");
}
} else {
restore_stage3(&mut u.decompressed);
return Err(UnpackError::StageDecompressionFailed {
stage: DecompressionStage::ExeStage3,
reason,
});
}
if !u.decrypt_and_decompress_data(at1, xor_acc ^ chk2 ^ accum, None) {
return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::ExeStage3,
));
}
let at2 = stage1.wrapping_add(stage2_off.wrapping_add(104));
@@ -865,10 +1042,11 @@ impl<'a> Unpacker<'a> {
let v4 = find_v4_offset(&u.decompressed, stage3_field, stage3_dlen)
.unwrap_or_else(|| stage3_field.wrapping_add(4692));
let v4_val = get_u32(&u.decompressed, v4);
if !u.decrypt_and_decompress_data(at2, xor_acc ^ chk3 ^ v4_val, None) {
return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::ExeStage3Secondary,
));
if let Err(reason) = u.decrypt_and_decompress_data(at2, xor_acc ^ chk3 ^ v4_val, None) {
return Err(UnpackError::StageDecompressionFailed {
stage: DecompressionStage::ExeStage3Secondary,
reason,
});
}
let chk4 = u.calculate_checksum(stage1.wrapping_add(chk_src_start.wrapping_add(16)));
@@ -886,10 +1064,11 @@ impl<'a> Unpacker<'a> {
if verbose {
println!(" stage4 = 0x{:08X}", stage4_field);
}
if !u.decrypt_and_decompress_data(at3, xor_acc ^ chk4 ^ v5_val, None) {
return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::ExeStage4,
));
if let Err(reason) = u.decrypt_and_decompress_data(at3, xor_acc ^ chk4 ^ v5_val, None) {
return Err(UnpackError::StageDecompressionFailed {
stage: DecompressionStage::ExeStage4,
reason,
});
}
// Inside stage4, two locations vary by build:
@@ -953,10 +1132,13 @@ impl<'a> Unpacker<'a> {
if verbose {
println!(" stage5 = 0x{:08X}", stage5_field);
}
if !u.decrypt_and_decompress_data(at4, xor_acc ^ chk4 ^ chk5 ^ accum2, Some(&ops1)) {
return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::ExeStage5,
));
if let Err(reason) =
u.decrypt_and_decompress_data(at4, xor_acc ^ chk4 ^ chk5 ^ accum2, Some(&ops1))
{
return Err(UnpackError::StageDecompressionFailed {
stage: DecompressionStage::ExeStage5,
reason,
});
}
// Inside stage5, the loader stores a table of (ptr, size) pairs at a
@@ -2189,10 +2371,11 @@ impl<'a> Unpacker<'a> {
let dp_base = ss.wrapping_add(dp_base_off);
let forth_addr = dp_base.wrapping_add(0x40);
let fk = header_checksum ^ second_stage_cs ^ forth_stage_key;
if !self.decrypt_and_decompress_data(forth_addr, fk, None) {
return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::Pe32FourthStage,
));
if let Err(reason) = self.decrypt_and_decompress_data(forth_addr, fk, None) {
return Err(UnpackError::StageDecompressionFailed {
stage: DecompressionStage::Pe32FourthStage,
reason,
});
}
// ---- FifthStage ----
@@ -2207,10 +2390,11 @@ impl<'a> Unpacker<'a> {
.wrapping_sub(4),
);
let fk5 = header_checksum ^ forth_cs ^ fifth_key;
if !self.decrypt_and_decompress_data(fifth_addr, fk5, None) {
return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::Pe32FifthStage,
));
if let Err(reason) = self.decrypt_and_decompress_data(fifth_addr, fk5, None) {
return Err(UnpackError::StageDecompressionFailed {
stage: DecompressionStage::Pe32FifthStage,
reason,
});
}
// ---- SevenStage ----
@@ -2232,10 +2416,11 @@ impl<'a> Unpacker<'a> {
cs1_addr.wrapping_add(cs1_size).wrapping_sub(0x10),
);
let fk7 = header_checksum ^ fifth_cs ^ seven_key;
if !self.decrypt_and_decompress_data(seven_addr, fk7, None) {
return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::Pe32SeventhStage,
));
if let Err(reason) = self.decrypt_and_decompress_data(seven_addr, fk7, None) {
return Err(UnpackError::StageDecompressionFailed {
stage: DecompressionStage::Pe32SeventhStage,
reason,
});
}
// ---- EighthStage ----
@@ -3034,10 +3219,13 @@ mod error_tests {
#[test]
fn structured_errors_include_stage_and_block_context() {
let stage = UnpackError::StageDecompressionFailed(DecompressionStage::ExeStage4);
let stage = UnpackError::StageDecompressionFailed {
stage: DecompressionStage::ExeStage4,
reason: DecompressionFailure::NoProgress,
};
assert_eq!(
stage.to_string(),
"EXE stage4 decompression failed — corrupt data or wrong offset"
"EXE stage4 decompression failed: Huffman symbol consumed no input and produced no output"
);
let block = UnpackError::SectionDecompressionFailed {
+8 -6
View File
@@ -14,8 +14,8 @@ use std::sync::{Arc, Mutex};
pub use dll::{unpack_dll, unpack_dll_v};
pub use exe::{
BufferOperation, BytecodeStage, DecompressionStage, DescriptorTable, SectionPipeline,
UnpackError, unpack as unpack_exe, unpack_v as unpack_exe_v,
BufferOperation, BytecodeStage, DecompressionFailure, DecompressionStage, DescriptorTable,
SectionPipeline, UnpackError, unpack as unpack_exe, unpack_v as unpack_exe_v,
};
pub use integrity::{IntegrityReport, check as check_integrity};
@@ -332,10 +332,12 @@ pub fn unpack_auto_v(input: &[u8], verbose: bool) -> Result<(Kind, Vec<u8>), Unp
Ok(out) => out,
Err(dll_err) => match exe::unpack_v(input, verbose) {
Ok(out) => out,
// Surface the DLL-pipeline error, not the EXE one: for a
// genuinely corrupt DLL the DLL error is the more relevant
// diagnostic, and the EXE fallback is best-effort.
Err(_) => return Err(dll_err),
Err(exe_err) => {
return Err(UnpackError::PipelineFallbackFailed {
dll: Box::new(dll_err),
exe: Box::new(exe_err),
});
}
},
}
}
+88 -28
View File
@@ -610,24 +610,28 @@ pub(crate) fn calculate_checksum2(d: &[u8], clean: &[u8], pos: u32, start: u32)
/// Reads `s_size` bytes from `src`, writes `d_size` bytes to `dest`.
/// The Huffman table lives at `key_offset` within `d`.
///
/// Returns `true` when exactly `d_size` bytes were written (full success),
/// `false` on any corruption-triggered early exit. The PE32 eighth-stage key
/// brute force uses this status to discriminate the correct key.
pub(crate) fn decompress(
/// Returns a structured reason when the stream cannot produce exactly
/// `d_size` bytes.
pub(crate) fn decompress_detailed(
d: &mut [u8],
src: u32,
mut dest: u32,
key_offset: u32,
s_size: u32,
d_size: u32,
) -> bool {
) -> Result<(), super::DecompressionFailure> {
use super::DecompressionFailure;
// Bound the scratch allocation: a corrupt descriptor could request a
// multi-gigabyte source size, and an allocation failure aborts the process
// (uncatchable). Real payloads are far below this.
if s_size as u64 > super::MAX_IMAGE_SIZE {
return false;
return Err(DecompressionFailure::SourceTooLarge {
size: s_size,
max: super::MAX_IMAGE_SIZE,
});
}
DECOMPRESS_SCRATCH.with_borrow_mut(|buf| {
DECOMPRESS_SCRATCH.with_borrow_mut(|buf| -> Result<(), DecompressionFailure> {
let mut bit_pos: i32 = 0;
let need = (s_size as usize).saturating_add(3);
if buf.len() < need {
@@ -661,7 +665,7 @@ pub(crate) fn decompress(
// length byte comes from a corrupt table, and `1 << b2` would
// panic (debug) or wrap (release) on it.
if b2 >= 32 {
return false;
return Err(DecompressionFailure::InvalidCodeLength { bits: b2 });
}
let mut mask: u32 = 1u32 << b2;
b2 = b2.wrapping_add(1);
@@ -673,7 +677,7 @@ pub(crate) fn decompress(
while (t2 & 0x8000) == 0 {
depth += 1;
if depth > 64 {
return false;
return Err(DecompressionFailure::HuffmanTraversalLimit);
}
mask <<= 1;
b2 = b2.wrapping_add(1);
@@ -700,8 +704,7 @@ pub(crate) fn decompress(
0x100 => {
step = 0;
if pending >= 256 {
// corrupt input: stop decompressing (diagnostics go to caller/log, not stdout)
return false;
return Err(DecompressionFailure::PendingLengthOverflow { pending });
}
pending = if pending == 0 {
payload
@@ -715,7 +718,11 @@ pub(crate) fn decompress(
}
step = pending.wrapping_mul(payload);
if step.wrapping_add(written) > d_size {
return false;
return Err(DecompressionFailure::OutputOverflow {
written,
step,
expected: d_size,
});
}
// Run-fill replicates the unit just written before `dest`. A
// corrupt stream can emit one of these before anything has been
@@ -724,7 +731,10 @@ pub(crate) fn decompress(
match payload {
1 => {
if dest < 1 {
return false;
return Err(DecompressionFailure::RunFillBeforeOutput {
width: payload,
destination: dest,
});
}
let v = d[(dest as usize) - 1];
for k in 0..pending {
@@ -733,7 +743,10 @@ pub(crate) fn decompress(
}
2 => {
if dest < 2 {
return false;
return Err(DecompressionFailure::RunFillBeforeOutput {
width: payload,
destination: dest,
});
}
let v = get_u16(d, dest.wrapping_sub(2));
for k in 0..pending {
@@ -742,7 +755,10 @@ pub(crate) fn decompress(
}
4 => {
if dest < 4 {
return false;
return Err(DecompressionFailure::RunFillBeforeOutput {
width: payload,
destination: dest,
});
}
let v = get_u32(d, dest.wrapping_sub(4));
for k in 0..pending {
@@ -755,7 +771,9 @@ pub(crate) fn decompress(
// yet still counted `step` bytes as written, leaving
// stale-buffer holes that later stages treated as
// plaintext. Report corruption instead.
return false;
return Err(DecompressionFailure::InvalidRunFillWidth {
width: payload,
});
}
}
pending = 0;
@@ -765,7 +783,18 @@ pub(crate) fn decompress(
if written.wrapping_add(payload) > d_size
|| pending.wrapping_add(payload) > written
{
return false;
let distance = pending.wrapping_add(payload);
if distance > written {
return Err(DecompressionFailure::InvalidBackReference {
distance,
written,
});
}
return Err(DecompressionFailure::OutputOverflow {
written,
step: payload,
expected: d_size,
});
}
let back = pending.wrapping_add(payload);
for k in 0..payload {
@@ -783,18 +812,34 @@ pub(crate) fn decompress(
// infinite loop (and `catch_unpack` traps panics, not hangs).
// Every real symbol consumes ≥ 1 bit, so a valid stream can
// never hit this.
return false;
return Err(DecompressionFailure::NoProgress);
}
}
src_consumed += if bit_pos != 0 { 1 } else { 0 };
// Mismatch in consumed/written sizes indicates corrupt input; the unpack
// result will then fail downstream checks. No stdout diagnostics here —
// the pure core stays I/O-free; surface errors via the caller/logfile.
let _ = src_consumed;
written == d_size
if written != d_size {
return Err(DecompressionFailure::OutputSizeMismatch {
written,
expected: d_size,
consumed: src_consumed.max(0) as u32,
source_size: s_size,
});
}
Ok(())
})
}
/// Boolean compatibility wrapper used by candidate searches and block fan-out.
pub(crate) fn decompress(
d: &mut [u8],
src: u32,
dest: u32,
key_offset: u32,
s_size: u32,
d_size: u32,
) -> bool {
decompress_detailed(d, src, dest, key_offset, s_size, d_size).is_ok()
}
/// Walk the Huffman table at `key_offset` and snapshot its bytes for
/// [`decompress_tbl`]. The table is a forest of 256 root entries (3 bytes
/// each); non-terminal entries point at a child index pair. Returns `None`
@@ -1874,14 +1919,14 @@ pub(crate) fn decrypt_data7(d: &mut [u8], pos: u32, mut key: u8) {
///
/// Returns the decompression success status (always `true` when no
/// decompression was needed). The PE32 eighth-stage key search relies on this.
pub(crate) fn decrypt_and_decompress_data(
pub(crate) fn decrypt_and_decompress_data_detailed(
d: &mut [u8],
pos: u32,
key: u32,
key1_offset: u32,
key3_offset: u32,
ops: Option<&[Op]>,
) -> bool {
) -> Result<(), super::DecompressionFailure> {
let src = get_u32(d, pos);
let src_len = get_u32(d, pos.wrapping_add(4));
aes_decrypt(d, src, src_len, key3_offset);
@@ -1894,9 +1939,21 @@ pub(crate) fn decrypt_and_decompress_data(
let dest = get_u32(d, pos.wrapping_add(8));
let dest_len = get_u32(d, pos.wrapping_add(12));
if src_len != dest_len {
return decompress(d, src, dest, key1_offset, src_len, dest_len);
return decompress_detailed(d, src, dest, key1_offset, src_len, dest_len);
}
true
Ok(())
}
/// Boolean compatibility wrapper used by key searches that trial candidates.
pub(crate) fn decrypt_and_decompress_data(
d: &mut [u8],
pos: u32,
key: u32,
key1_offset: u32,
key3_offset: u32,
ops: Option<&[Op]>,
) -> bool {
decrypt_and_decompress_data_detailed(d, pos, key, key1_offset, key3_offset, ops).is_ok()
}
// ---------------------------------------------------------------------------
@@ -2202,7 +2259,10 @@ mod tests {
d[0..2].copy_from_slice(&sym.to_le_bytes());
d[2] = 8;
// All-zero source -> symbol index 0 -> the invalid run-fill.
assert!(!decompress(&mut d, 0x40, 0x80, 0, 4, 3));
assert_eq!(
decompress_detailed(&mut d, 0x40, 0x80, 0, 4, 3),
Err(super::super::DecompressionFailure::InvalidRunFillWidth { width: 3 })
);
}
/// Control for the above: a width-1 run-fill is legal and succeeds.
+19 -5
View File
@@ -27,6 +27,7 @@
mod common;
use common::samples_dir;
use std::path::Path;
use walkdir::WalkDir;
/// An input is a `.exe`/`.dll`/`.dat` whose name doesn't carry the `.golden.`
/// marker — those are goldens, not inputs. External companions (`<name>._`)
@@ -85,10 +86,19 @@ fn samples_unpack_against_goldens() {
return;
}
let mut inputs: Vec<_> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read {}: {e}", dir.display()))
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.is_file() && is_input(p))
let mut inputs: Vec<_> = WalkDir::new(&dir)
.follow_links(false)
.into_iter()
.filter_entry(|entry| {
entry.depth() == 0
|| !entry
.file_name()
.to_str()
.is_some_and(|name| name.eq_ignore_ascii_case("unpack"))
})
.map(|entry| entry.unwrap_or_else(|e| panic!("walk {}: {e}", dir.display())))
.filter(|entry| entry.file_type().is_file() && is_input(entry.path()))
.map(|entry| entry.into_path())
.collect();
inputs.sort();
@@ -107,7 +117,11 @@ fn samples_unpack_against_goldens() {
let mut failures: Vec<String> = Vec::new();
for input in &inputs {
let name = input.file_name().unwrap().to_string_lossy().to_string();
let name = input
.strip_prefix(&dir)
.unwrap_or(input)
.to_string_lossy()
.to_string();
let bytes = match std::fs::read(input) {
Ok(b) => b,
Err(e) => {