mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
fix(unpacker): refine layout validation and diagnostics
This commit is contained in:
+13
-1
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
+245
-57
@@ -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;
|
||||
};
|
||||
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 magic_off: u32 = if stamp_at(104) {
|
||||
104
|
||||
} else if stamp_at(112) {
|
||||
112
|
||||
} else {
|
||||
104
|
||||
};
|
||||
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}");
|
||||
}
|
||||
if !u.decrypt_and_decompress_data(at1, xor_acc ^ chk2 ^ accum, None) {
|
||||
return Err(UnpackError::StageDecompressionFailed(
|
||||
DecompressionStage::ExeStage3,
|
||||
));
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user