mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
feat: Optimize error reporting
This commit is contained in:
+7
-2
@@ -12,7 +12,7 @@ The crate is split into a pure core and a thin CLI shell:
|
|||||||
- **`src/unpacker/`** — the core. Pure functions over byte slices: no file
|
- **`src/unpacker/`** — the core. Pure functions over byte slices: no file
|
||||||
I/O, no environment access (beyond a few debugging overrides, see
|
I/O, no environment access (beyond a few debugging overrides, see
|
||||||
[development.md](development.md)), panic-free at the public boundary (all
|
[development.md](development.md)), panic-free at the public boundary (all
|
||||||
internal panics are trapped and converted to `UnpackError::Corrupt`). This
|
internal panics are trapped and converted to `UnpackError::InternalPanic`). This
|
||||||
is what the WebAssembly build embeds.
|
is what the WebAssembly build embeds.
|
||||||
- **`src/` (top level)** — the CLI shell: argument parsing, recursive folder
|
- **`src/` (top level)** — the CLI shell: argument parsing, recursive folder
|
||||||
scanning, per-run log file, progress bar, Explorer-friendly exit pause, and
|
scanning, per-run log file, progress bar, Explorer-friendly exit pause, and
|
||||||
@@ -120,7 +120,12 @@ threads (WebAssembly) the sequential path is used automatically.
|
|||||||
|
|
||||||
The public API never panics: every pipeline runs under a `catch_unwind`
|
The public API never panics: every pipeline runs under a `catch_unwind`
|
||||||
wrapper (`catch_unpack`) that converts a trapped panic to
|
wrapper (`catch_unpack`) that converts a trapped panic to
|
||||||
`UnpackError::Corrupt`, with the default panic hook transiently suppressed.
|
`UnpackError::InternalPanic`, including the Rust source location and panic
|
||||||
|
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.
|
||||||
Size requests are bounds-checked against a 1 GiB `MAX_IMAGE_SIZE` before
|
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 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
|
allocation. In folder mode each file is isolated: one file's failure is logged
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ impl OpsLut {
|
|||||||
pub fn generate(data: &[u8], offset: u32) -> Option<Vec<Op>> {
|
pub fn generate(data: &[u8], offset: u32) -> Option<Vec<Op>> {
|
||||||
// Bounds-checked cursor: a corrupt `data_offset` (bad decrypt_data6 / the
|
// Bounds-checked cursor: a corrupt `data_offset` (bad decrypt_data6 / the
|
||||||
// alignment fallback) must yield `None`, not an out-of-bounds panic — the
|
// alignment fallback) must yield `None`, not an out-of-bounds panic — the
|
||||||
// panic path would surface as a misleading `UnpackError::Corrupt` instead
|
// panic path would surface as a misleading `UnpackError::InternalPanic` instead
|
||||||
// of the precise `BytecodeGenFailed`, and any future caller without a
|
// of the precise `BytecodeGenerationFailed`, and any future caller without a
|
||||||
// `catch_unwind` wrapper would abort outright.
|
// `catch_unwind` wrapper would abort outright.
|
||||||
let mut pos = offset as usize;
|
let mut pos = offset as usize;
|
||||||
let mut next = move || {
|
let mut next = move || {
|
||||||
|
|||||||
+61
-25
@@ -13,9 +13,12 @@
|
|||||||
//! CalculateChecksumWithSizeXor -> primitives::calculate_checksum
|
//! CalculateChecksumWithSizeXor -> primitives::calculate_checksum
|
||||||
//! CalculateCrc32 -> crc32::compute (via above)
|
//! CalculateCrc32 -> crc32::compute (via above)
|
||||||
|
|
||||||
use super::UnpackError;
|
|
||||||
use super::bytecode::{Op, OpsLut, generate};
|
use super::bytecode::{Op, OpsLut, generate};
|
||||||
use super::primitives::{self, *};
|
use super::primitives::{self, *};
|
||||||
|
use super::{
|
||||||
|
BufferOperation, BytecodeStage, DecompressionStage, DescriptorTable, SectionPipeline,
|
||||||
|
UnpackError,
|
||||||
|
};
|
||||||
|
|
||||||
/// Read a signed 32-bit little-endian value.
|
/// Read a signed 32-bit little-endian value.
|
||||||
fn get_i32(d: &[u8], offset: i32) -> i32 {
|
fn get_i32(d: &[u8], offset: i32) -> i32 {
|
||||||
@@ -68,6 +71,7 @@ fn decrypt_data4(
|
|||||||
key: i32,
|
key: i32,
|
||||||
decomp_params: &[i32; 4],
|
decomp_params: &[i32; 4],
|
||||||
transform: Option<&[Op]>,
|
transform: Option<&[Op]>,
|
||||||
|
stage: DecompressionStage,
|
||||||
) -> Result<(), UnpackError> {
|
) -> Result<(), UnpackError> {
|
||||||
let addr = get_i32(d, offset);
|
let addr = get_i32(d, offset);
|
||||||
let size = get_i32(d, offset + 4);
|
let size = get_i32(d, offset + 4);
|
||||||
@@ -95,7 +99,7 @@ fn decrypt_data4(
|
|||||||
size as u32,
|
size as u32,
|
||||||
decompressed_size as u32,
|
decompressed_size as u32,
|
||||||
) {
|
) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::StageDecompressionFailed(stage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -218,7 +222,11 @@ fn decrypt_and_decompress_data(
|
|||||||
// Guard: need 16 bytes at section_data_offset in `d`
|
// Guard: need 16 bytes at section_data_offset in `d`
|
||||||
let off = section_data_offset as usize;
|
let off = section_data_offset as usize;
|
||||||
if off.saturating_add(16) > d.len() {
|
if off.saturating_add(16) > d.len() {
|
||||||
return Err(UnpackError::OutOfBounds(off));
|
return Err(UnpackError::DescriptorOutOfBounds {
|
||||||
|
table: DescriptorTable::DllSectionBlocks,
|
||||||
|
offset: off,
|
||||||
|
image_len: d.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
decrypt_data6_shift6(d, section_data_offset, 16);
|
decrypt_data6_shift6(d, section_data_offset, 16);
|
||||||
let dest_offset = get_i32(d, section_data_offset);
|
let dest_offset = get_i32(d, section_data_offset);
|
||||||
@@ -246,10 +254,10 @@ fn decrypt_and_decompress_data(
|
|||||||
let lut = OpsLut::new(decrypt_func);
|
let lut = OpsLut::new(decrypt_func);
|
||||||
let ko0 = decomp_params[0];
|
let ko0 = decomp_params[0];
|
||||||
let ko2 = decomp_params[2];
|
let ko2 = decomp_params[2];
|
||||||
let ks_snap =
|
let ks_snap = primitives::aes_schedule_snapshot(d, ko2 as u32)
|
||||||
primitives::aes_schedule_snapshot(d, ko2 as u32).ok_or(UnpackError::Corrupt)?;
|
.ok_or(UnpackError::InvalidAesKeySchedule { offset: ko2 as u32 })?;
|
||||||
let tab_snap = primitives::huffman_table_snapshot(d, ko0 as u32)
|
let tab_snap = primitives::huffman_table_snapshot(d, ko0 as u32)
|
||||||
.ok_or(UnpackError::DecompressFailed)?;
|
.ok_or(UnpackError::InvalidHuffmanTable { offset: ko0 as u32 })?;
|
||||||
let spans: Vec<(usize, usize)> = blocks
|
let spans: Vec<(usize, usize)> = blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|b| {
|
.map(|b| {
|
||||||
@@ -277,7 +285,10 @@ fn decrypt_and_decompress_data(
|
|||||||
b.size as u32,
|
b.size as u32,
|
||||||
b.expected_crc as u32,
|
b.expected_crc as u32,
|
||||||
) {
|
) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::SectionDecompressionFailed {
|
||||||
|
pipeline: SectionPipeline::Dll,
|
||||||
|
block: i,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -292,7 +303,11 @@ fn decrypt_and_decompress_data(
|
|||||||
// decrypts 16 too, so guard 16 (an 8-byte guard would let
|
// decrypts 16 too, so guard 16 (an 8-byte guard would let
|
||||||
// decrypt_data6_shift6 index past the end of a truncated descriptor).
|
// decrypt_data6_shift6 index past the end of a truncated descriptor).
|
||||||
if off.saturating_add(16) > d.len() {
|
if off.saturating_add(16) > d.len() {
|
||||||
return Err(UnpackError::OutOfBounds(off));
|
return Err(UnpackError::DescriptorOutOfBounds {
|
||||||
|
table: DescriptorTable::DllZeroFill,
|
||||||
|
offset: off,
|
||||||
|
image_len: d.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
decrypt_data6_shift6(d, section_data_offset, 16);
|
decrypt_data6_shift6(d, section_data_offset, 16);
|
||||||
let zero_offset = get_i32(d, section_data_offset);
|
let zero_offset = get_i32(d, section_data_offset);
|
||||||
@@ -305,7 +320,12 @@ fn decrypt_and_decompress_data(
|
|||||||
for i in 0..zero_size {
|
for i in 0..zero_size {
|
||||||
let idx = (zero_offset + i) as usize;
|
let idx = (zero_offset + i) as usize;
|
||||||
if idx >= d.len() {
|
if idx >= d.len() {
|
||||||
return Err(UnpackError::OutOfBounds(idx));
|
return Err(UnpackError::BufferRangeOutOfBounds {
|
||||||
|
operation: BufferOperation::ZeroFill,
|
||||||
|
offset: idx,
|
||||||
|
size: 1,
|
||||||
|
buffer_len: d.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
d[idx] = 0;
|
d[idx] = 0;
|
||||||
}
|
}
|
||||||
@@ -328,8 +348,12 @@ pub fn unpack_dll_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
||||||
if input.len() < 4096 {
|
const HEADER_LEN: usize = 4128;
|
||||||
return Err(UnpackError::InputTooShort(input.len()));
|
if input.len() < HEADER_LEN {
|
||||||
|
return Err(UnpackError::InputTooShort {
|
||||||
|
actual: input.len(),
|
||||||
|
required: HEADER_LEN,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// `file_data` and `original_file_data` both borrow the same protected input.
|
// `file_data` and `original_file_data` both borrow the same protected input.
|
||||||
@@ -348,14 +372,17 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !super::is_supported_magic(keys[1] as u32) {
|
if !super::is_supported_magic(keys[1] as u32) {
|
||||||
return Err(UnpackError::DllUnpack(
|
return Err(UnpackError::HeaderMagicMismatch {
|
||||||
"Not a Crackproof protected file (KONN magic mismatch)".into(),
|
found: keys[1] as u32,
|
||||||
));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let pe_offset = get_i32(file_data, 60);
|
let pe_offset = get_i32(file_data, 60);
|
||||||
if pe_offset < 0 || (pe_offset as usize).saturating_add(84) > file_data.len() {
|
if pe_offset < 0 || (pe_offset as usize).saturating_add(84) > file_data.len() {
|
||||||
return Err(UnpackError::DllUnpack("implausible PE offset".into()));
|
return Err(UnpackError::InvalidPeOffset {
|
||||||
|
offset: i64::from(pe_offset),
|
||||||
|
input_len: file_data.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// This pipeline is PE32+-only: its header fixups write the data
|
// This pipeline is PE32+-only: its header fixups write the data
|
||||||
// directories at PE32+ offsets (pe+144..180, pe+136 for the DD blob). On a
|
// directories at PE32+ offsets (pe+144..180, pe+136 for the DD blob). On a
|
||||||
@@ -363,14 +390,18 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
|||||||
// structurally plausible but unloadable file. Reject early with a clear
|
// structurally plausible but unloadable file. Reject early with a clear
|
||||||
// error so `unpack_auto`'s EXE-pipeline fallback handles PE32 DLLs (that
|
// error so `unpack_auto`'s EXE-pipeline fallback handles PE32 DLLs (that
|
||||||
// path is PE32-aware — see run_pe32), instead of us mangling them here.
|
// path is PE32-aware — see run_pe32), instead of us mangling them here.
|
||||||
if get_i32(file_data, pe_offset + 24) & 0xFFFF != 0x20B {
|
let optional_magic = get_u16(file_data, (pe_offset + 24) as u32);
|
||||||
return Err(UnpackError::DllUnpack(
|
if optional_magic != 0x20B {
|
||||||
"not a PE32+ image (the DLL pipeline handles 64-bit only)".into(),
|
return Err(UnpackError::UnsupportedDllPeMagic {
|
||||||
));
|
found: optional_magic,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let size_of_image = get_i32(file_data, pe_offset + 80);
|
let size_of_image = get_i32(file_data, pe_offset + 80);
|
||||||
if size_of_image <= 0 || size_of_image as u64 > super::MAX_IMAGE_SIZE {
|
if size_of_image <= 0 || size_of_image as u64 > super::MAX_IMAGE_SIZE {
|
||||||
return Err(UnpackError::DllUnpack("implausible SizeOfImage".into()));
|
return Err(UnpackError::InvalidImageSize {
|
||||||
|
size: i64::from(size_of_image),
|
||||||
|
max: super::MAX_IMAGE_SIZE,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let mut out = vec![0u8; size_of_image as usize];
|
let mut out = vec![0u8; size_of_image as usize];
|
||||||
let base_offset = keys[6] - keys[3] + 0x2000;
|
let base_offset = keys[6] - keys[3] + 0x2000;
|
||||||
@@ -512,6 +543,7 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
|||||||
table_val ^ checksum2 ^ (xor_accumulator as i32),
|
table_val ^ checksum2 ^ (xor_accumulator as i32),
|
||||||
&decomp_params,
|
&decomp_params,
|
||||||
None,
|
None,
|
||||||
|
DecompressionStage::DllCodeBlock1,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let addr3b = get_i32(&out, decrypted_addr1 + 3728);
|
let addr3b = get_i32(&out, decrypted_addr1 + 3728);
|
||||||
@@ -537,6 +569,7 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
|||||||
crc_xored ^ (xor_accumulator as i32) ^ trailing_val,
|
crc_xored ^ (xor_accumulator as i32) ^ trailing_val,
|
||||||
&decomp_params,
|
&decomp_params,
|
||||||
None,
|
None,
|
||||||
|
DecompressionStage::DllCodeBlock2,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let checksum3 = calculate_checksum(&out, (decrypted_addr1 + 3480) as u32) as i32;
|
let checksum3 = calculate_checksum(&out, (decrypted_addr1 + 3480) as u32) as i32;
|
||||||
@@ -549,6 +582,7 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
|||||||
(not_val ^ (xor_key as u32)) as i32,
|
(not_val ^ (xor_key as u32)) as i32,
|
||||||
&decomp_params,
|
&decomp_params,
|
||||||
None,
|
None,
|
||||||
|
DecompressionStage::DllCodeBlock3,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let addr4 = get_i32(&out, addr4_offset);
|
let addr4 = get_i32(&out, addr4_offset);
|
||||||
@@ -574,8 +608,9 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
|||||||
lfsr_seed_val = lfsr_seed_val.wrapping_add(k);
|
lfsr_seed_val = lfsr_seed_val.wrapping_add(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
let decrypt_func = generate(&out, lfsr as u32)
|
let decrypt_func = generate(&out, lfsr as u32).ok_or(UnpackError::BytecodeGenerationFailed(
|
||||||
.ok_or_else(|| UnpackError::DllUnpack("Failed to build decryption expression".into()))?;
|
BytecodeStage::DllPrimaryDecryptor,
|
||||||
|
))?;
|
||||||
|
|
||||||
let addr5_offset = decrypted_addr1 + 3840;
|
let addr5_offset = decrypted_addr1 + 3840;
|
||||||
let addr5 = get_i32(&out, addr5_offset);
|
let addr5 = get_i32(&out, addr5_offset);
|
||||||
@@ -585,6 +620,7 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
|||||||
lfsr_seed_val ^ xor_key ^ checksum4,
|
lfsr_seed_val ^ xor_key ^ checksum4,
|
||||||
&decomp_params,
|
&decomp_params,
|
||||||
Some(&decrypt_func),
|
Some(&decrypt_func),
|
||||||
|
DecompressionStage::DllCodeBlock4,
|
||||||
)?;
|
)?;
|
||||||
if verbose {
|
if verbose {
|
||||||
println!("[7/9] Decrypting code block 4 (addr5)...");
|
println!("[7/9] Decrypting code block 4 (addr5)...");
|
||||||
@@ -603,9 +639,9 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
|||||||
let lfsr2 = metadata_offset + 88;
|
let lfsr2 = metadata_offset + 88;
|
||||||
decrypt_data6(&mut out, lfsr2 as u32);
|
decrypt_data6(&mut out, lfsr2 as u32);
|
||||||
|
|
||||||
let decrypt_func2 = generate(&out, lfsr2 as u32).ok_or_else(|| {
|
let decrypt_func2 = generate(&out, lfsr2 as u32).ok_or(
|
||||||
UnpackError::DllUnpack("Failed to build second decryption expression".into())
|
UnpackError::BytecodeGenerationFailed(BytecodeStage::DllSectionDecryptor),
|
||||||
})?;
|
)?;
|
||||||
|
|
||||||
let section_image_base = 4095 - get_i32(original_file_data, 4224);
|
let section_image_base = 4095 - get_i32(original_file_data, 4224);
|
||||||
let section_data_offset = get_i32(&out, addr5 + 11976);
|
let section_data_offset = get_i32(&out, addr5 + 11976);
|
||||||
|
|||||||
+293
-53
@@ -2,13 +2,121 @@ use super::bytecode::{Op, OpsLut, generate};
|
|||||||
use super::primitives;
|
use super::primitives;
|
||||||
use super::primitives::*;
|
use super::primitives::*;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum UnpackError {
|
pub enum DecompressionStage {
|
||||||
#[error("input too short for header (need at least 4096 bytes, got {0})")]
|
ExeStage3,
|
||||||
InputTooShort(usize),
|
ExeStage3Secondary,
|
||||||
|
ExeStage4,
|
||||||
|
ExeStage5,
|
||||||
|
Pe32FourthStage,
|
||||||
|
Pe32FifthStage,
|
||||||
|
Pe32SeventhStage,
|
||||||
|
DllCodeBlock1,
|
||||||
|
DllCodeBlock2,
|
||||||
|
DllCodeBlock3,
|
||||||
|
DllCodeBlock4,
|
||||||
|
}
|
||||||
|
|
||||||
#[error("info[1] mismatch — corrupt data or wrong offset")]
|
impl std::fmt::Display for DecompressionStage {
|
||||||
HeaderMismatch,
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::ExeStage3 => "EXE stage3",
|
||||||
|
Self::ExeStage3Secondary => "EXE secondary stage3",
|
||||||
|
Self::ExeStage4 => "EXE stage4",
|
||||||
|
Self::ExeStage5 => "EXE stage5",
|
||||||
|
Self::Pe32FourthStage => "PE32 fourth stage",
|
||||||
|
Self::Pe32FifthStage => "PE32 fifth stage",
|
||||||
|
Self::Pe32SeventhStage => "PE32 seventh stage",
|
||||||
|
Self::DllCodeBlock1 => "DLL code block 1",
|
||||||
|
Self::DllCodeBlock2 => "DLL code block 2",
|
||||||
|
Self::DllCodeBlock3 => "DLL code block 3",
|
||||||
|
Self::DllCodeBlock4 => "DLL code block 4",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BytecodeStage {
|
||||||
|
ExeStage4,
|
||||||
|
ExeStage5,
|
||||||
|
Pe32CustomDecryptor,
|
||||||
|
Pe32FileDecryptor,
|
||||||
|
DllPrimaryDecryptor,
|
||||||
|
DllSectionDecryptor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for BytecodeStage {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::ExeStage4 => "EXE stage4",
|
||||||
|
Self::ExeStage5 => "EXE stage5",
|
||||||
|
Self::Pe32CustomDecryptor => "PE32 custom decryptor",
|
||||||
|
Self::Pe32FileDecryptor => "PE32 file decryptor",
|
||||||
|
Self::DllPrimaryDecryptor => "DLL primary decryptor",
|
||||||
|
Self::DllSectionDecryptor => "DLL section decryptor",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SectionPipeline {
|
||||||
|
ExePe32Plus,
|
||||||
|
ExePe32,
|
||||||
|
Dll,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SectionPipeline {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::ExePe32Plus => "PE32+ EXE",
|
||||||
|
Self::ExePe32 => "PE32 EXE",
|
||||||
|
Self::Dll => "DLL",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DescriptorTable {
|
||||||
|
DllSectionBlocks,
|
||||||
|
DllZeroFill,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DescriptorTable {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::DllSectionBlocks => "DLL section-block",
|
||||||
|
Self::DllZeroFill => "DLL zero-fill",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BufferOperation {
|
||||||
|
Read,
|
||||||
|
CopySource,
|
||||||
|
CopyDestination,
|
||||||
|
ZeroFill,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for BufferOperation {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::Read => "read",
|
||||||
|
Self::CopySource => "copy source",
|
||||||
|
Self::CopyDestination => "copy destination",
|
||||||
|
Self::ZeroFill => "zero-fill",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum UnpackError {
|
||||||
|
#[error("input too short (need at least {required} bytes, got {actual})")]
|
||||||
|
InputTooShort { actual: usize, required: usize },
|
||||||
|
|
||||||
|
#[error("decrypted header magic mismatch (got 0x{found:08X})")]
|
||||||
|
HeaderMagicMismatch { found: u32 },
|
||||||
|
|
||||||
#[error("anchor field not found — corrupt data or wrong offset")]
|
#[error("anchor field not found — corrupt data or wrong offset")]
|
||||||
AnchorNotFound,
|
AnchorNotFound,
|
||||||
@@ -22,23 +130,42 @@ pub enum UnpackError {
|
|||||||
#[error("table_start not found — corrupt data or wrong offset")]
|
#[error("table_start not found — corrupt data or wrong offset")]
|
||||||
TableStartNotFound,
|
TableStartNotFound,
|
||||||
|
|
||||||
#[error("stage4 bytecode generation failed — corrupt data or wrong offset")]
|
#[error("{0} bytecode generation failed — corrupt data or wrong offset")]
|
||||||
BytecodeGenFailed,
|
BytecodeGenerationFailed(BytecodeStage),
|
||||||
|
|
||||||
#[error("stage5 marker not found — this build's layout is not supported by this unpacker")]
|
#[error("stage5 marker not found — this build's layout is not supported by this unpacker")]
|
||||||
Stage5MarkerNotFound,
|
Stage5MarkerNotFound,
|
||||||
|
|
||||||
#[error("stage5 bytecode generation failed — corrupt data or wrong offset")]
|
|
||||||
Stage5BytecodeGenFailed,
|
|
||||||
|
|
||||||
#[error("DLL unpack failed: {0}")]
|
|
||||||
DllUnpack(String),
|
|
||||||
|
|
||||||
#[error("not a Crackproof-protected file")]
|
#[error("not a Crackproof-protected file")]
|
||||||
NotCrackproof,
|
NotCrackproof,
|
||||||
|
|
||||||
#[error("out-of-bounds access at offset {0}")]
|
#[error("invalid PE header offset {offset} for {input_len}-byte input")]
|
||||||
OutOfBounds(usize),
|
InvalidPeOffset { offset: i64, input_len: usize },
|
||||||
|
|
||||||
|
#[error("DLL pipeline requires PE32+ optional-header magic, got 0x{found:04X}")]
|
||||||
|
UnsupportedDllPeMagic { found: u16 },
|
||||||
|
|
||||||
|
#[error("invalid SizeOfImage {size}; expected 1..={max}")]
|
||||||
|
InvalidImageSize { size: i64, max: u64 },
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"{operation} range out of bounds (offset {offset}, size {size}, buffer length {buffer_len})"
|
||||||
|
)]
|
||||||
|
BufferRangeOutOfBounds {
|
||||||
|
operation: BufferOperation,
|
||||||
|
offset: usize,
|
||||||
|
size: usize,
|
||||||
|
buffer_len: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"{table} descriptor out of bounds (offset {offset}, size 16, image length {image_len})"
|
||||||
|
)]
|
||||||
|
DescriptorOutOfBounds {
|
||||||
|
table: DescriptorTable,
|
||||||
|
offset: usize,
|
||||||
|
image_len: usize,
|
||||||
|
},
|
||||||
|
|
||||||
#[error("PE32 tbl not found — corrupt data or wrong offset")]
|
#[error("PE32 tbl not found — corrupt data or wrong offset")]
|
||||||
Pe32TblNotFound,
|
Pe32TblNotFound,
|
||||||
@@ -49,20 +176,52 @@ pub enum UnpackError {
|
|||||||
#[error("PE32 customDecryptor not found in sevenStage")]
|
#[error("PE32 customDecryptor not found in sevenStage")]
|
||||||
Pe32CustomDecryptorNotFound,
|
Pe32CustomDecryptorNotFound,
|
||||||
|
|
||||||
#[error("PE32 stage bytecode generation failed")]
|
|
||||||
Pe32BytecodeGenFailed,
|
|
||||||
|
|
||||||
#[error("PE32 eighthStageKey not found")]
|
#[error("PE32 eighthStageKey not found")]
|
||||||
Pe32EighthKeyNotFound,
|
Pe32EighthKeyNotFound,
|
||||||
|
|
||||||
#[error("PE32 file LFSR not found in eighthStage")]
|
#[error("PE32 file LFSR not found in eighthStage")]
|
||||||
Pe32FileLfsrNotFound,
|
Pe32FileLfsrNotFound,
|
||||||
|
|
||||||
#[error("decompression failed — corrupt data or wrong offset")]
|
#[error("{0} decompression failed — corrupt data or wrong offset")]
|
||||||
DecompressFailed,
|
StageDecompressionFailed(DecompressionStage),
|
||||||
|
|
||||||
#[error("input is corrupt or not a supported Crackproof layout")]
|
#[error("{pipeline} section block {block} decompression failed")]
|
||||||
Corrupt,
|
SectionDecompressionFailed {
|
||||||
|
pipeline: SectionPipeline,
|
||||||
|
block: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("AES key schedule is outside the image at offset {offset}")]
|
||||||
|
InvalidAesKeySchedule { offset: u32 },
|
||||||
|
|
||||||
|
#[error("Huffman table is outside the image at offset {offset}")]
|
||||||
|
InvalidHuffmanTable { offset: u32 },
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"PE32 second-stage range is invalid (offset {offset}, size {size}, image length {image_len})"
|
||||||
|
)]
|
||||||
|
Pe32SecondStageRangeInvalid {
|
||||||
|
offset: u32,
|
||||||
|
size: u32,
|
||||||
|
image_len: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("PE32 relocation-data descriptor not found")]
|
||||||
|
Pe32RelocationDataNotFound,
|
||||||
|
|
||||||
|
#[error("file decryptor candidate failed structural validation")]
|
||||||
|
FileDecryptorValidationFailed,
|
||||||
|
|
||||||
|
#[error("PE32 memory image could not be rebuilt as a file-layout PE")]
|
||||||
|
Pe32OutputLayoutInvalid,
|
||||||
|
|
||||||
|
#[error("internal panic at {file}:{line}:{column}: {message}")]
|
||||||
|
InternalPanic {
|
||||||
|
message: String,
|
||||||
|
file: String,
|
||||||
|
line: u32,
|
||||||
|
column: u32,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unpack(input: &[u8]) -> Result<Vec<u8>, UnpackError> {
|
pub fn unpack(input: &[u8]) -> Result<Vec<u8>, UnpackError> {
|
||||||
@@ -93,8 +252,12 @@ fn prot_rva_to_off(file_data: &[u8], pe_header: u32, rva: u32) -> Option<u32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
||||||
if input.len() < 4096 {
|
const HEADER_LEN: usize = 4128;
|
||||||
return Err(UnpackError::InputTooShort(input.len()));
|
if input.len() < HEADER_LEN {
|
||||||
|
return Err(UnpackError::InputTooShort {
|
||||||
|
actual: input.len(),
|
||||||
|
required: HEADER_LEN,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// The pipeline chases offsets read out of the decrypted image; on a
|
// The pipeline chases offsets read out of the decrypted image; on a
|
||||||
// truncated/garbled-but-detected file those run out of bounds. Trap any
|
// truncated/garbled-but-detected file those run out of bounds. Trap any
|
||||||
@@ -376,13 +539,25 @@ impl<'a> Unpacker<'a> {
|
|||||||
println!(" info[7] = 0x{:08X}", u.info[7]);
|
println!(" info[7] = 0x{:08X}", u.info[7]);
|
||||||
}
|
}
|
||||||
if !super::is_supported_magic(u.info[1]) {
|
if !super::is_supported_magic(u.info[1]) {
|
||||||
return Err(UnpackError::HeaderMismatch);
|
return Err(UnpackError::HeaderMagicMismatch { found: u.info[1] });
|
||||||
}
|
}
|
||||||
|
|
||||||
let pe_off = get_u32(u.file_data, 60);
|
let pe_off = get_u32(u.file_data, 60);
|
||||||
|
if (pe_off as usize)
|
||||||
|
.checked_add(84)
|
||||||
|
.is_none_or(|end| end > u.file_data.len())
|
||||||
|
{
|
||||||
|
return Err(UnpackError::InvalidPeOffset {
|
||||||
|
offset: i64::from(pe_off),
|
||||||
|
input_len: u.file_data.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
let size_of_image = get_u32(u.file_data, pe_off.wrapping_add(80));
|
let size_of_image = get_u32(u.file_data, pe_off.wrapping_add(80));
|
||||||
if size_of_image == 0 || size_of_image as u64 > super::MAX_IMAGE_SIZE {
|
if size_of_image == 0 || size_of_image as u64 > super::MAX_IMAGE_SIZE {
|
||||||
return Err(UnpackError::Corrupt);
|
return Err(UnpackError::InvalidImageSize {
|
||||||
|
size: i64::from(size_of_image),
|
||||||
|
max: super::MAX_IMAGE_SIZE,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
u.decompressed = vec![0u8; size_of_image as usize];
|
u.decompressed = vec![0u8; size_of_image as usize];
|
||||||
u.decrypt_size = u.info[6].wrapping_sub(u.info[3]).wrapping_add(8192);
|
u.decrypt_size = u.info[6].wrapping_sub(u.info[3]).wrapping_add(8192);
|
||||||
@@ -671,7 +846,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
println!(" stage3 = 0x{:08X}", stage3_field);
|
println!(" stage3 = 0x{:08X}", stage3_field);
|
||||||
}
|
}
|
||||||
if !u.decrypt_and_decompress_data(at1, xor_acc ^ chk2 ^ accum, None) {
|
if !u.decrypt_and_decompress_data(at1, xor_acc ^ chk2 ^ accum, None) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::StageDecompressionFailed(
|
||||||
|
DecompressionStage::ExeStage3,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let at2 = stage1.wrapping_add(stage2_off.wrapping_add(104));
|
let at2 = stage1.wrapping_add(stage2_off.wrapping_add(104));
|
||||||
@@ -689,7 +866,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
.unwrap_or_else(|| stage3_field.wrapping_add(4692));
|
.unwrap_or_else(|| stage3_field.wrapping_add(4692));
|
||||||
let v4_val = get_u32(&u.decompressed, v4);
|
let v4_val = get_u32(&u.decompressed, v4);
|
||||||
if !u.decrypt_and_decompress_data(at2, xor_acc ^ chk3 ^ v4_val, None) {
|
if !u.decrypt_and_decompress_data(at2, xor_acc ^ chk3 ^ v4_val, None) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::StageDecompressionFailed(
|
||||||
|
DecompressionStage::ExeStage3Secondary,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let chk4 = u.calculate_checksum(stage1.wrapping_add(chk_src_start.wrapping_add(16)));
|
let chk4 = u.calculate_checksum(stage1.wrapping_add(chk_src_start.wrapping_add(16)));
|
||||||
@@ -708,7 +887,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
println!(" stage4 = 0x{:08X}", stage4_field);
|
println!(" stage4 = 0x{:08X}", stage4_field);
|
||||||
}
|
}
|
||||||
if !u.decrypt_and_decompress_data(at3, xor_acc ^ chk4 ^ v5_val, None) {
|
if !u.decrypt_and_decompress_data(at3, xor_acc ^ chk4 ^ v5_val, None) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::StageDecompressionFailed(
|
||||||
|
DecompressionStage::ExeStage4,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inside stage4, two locations vary by build:
|
// Inside stage4, two locations vary by build:
|
||||||
@@ -758,7 +939,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
let ops1 = match generate(&u.decompressed, data_offset) {
|
let ops1 = match generate(&u.decompressed, data_offset) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => {
|
||||||
return Err(UnpackError::BytecodeGenFailed);
|
return Err(UnpackError::BytecodeGenerationFailed(
|
||||||
|
BytecodeStage::ExeStage4,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -771,7 +954,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
println!(" stage5 = 0x{:08X}", stage5_field);
|
println!(" stage5 = 0x{:08X}", stage5_field);
|
||||||
}
|
}
|
||||||
if !u.decrypt_and_decompress_data(at4, xor_acc ^ chk4 ^ chk5 ^ accum2, Some(&ops1)) {
|
if !u.decrypt_and_decompress_data(at4, xor_acc ^ chk4 ^ chk5 ^ accum2, Some(&ops1)) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::StageDecompressionFailed(
|
||||||
|
DecompressionStage::ExeStage5,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inside stage5, the loader stores a table of (ptr, size) pairs at a
|
// Inside stage5, the loader stores a table of (ptr, size) pairs at a
|
||||||
@@ -921,7 +1106,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
let ops2 = match generate(&u.decompressed, data_offset2) {
|
let ops2 = match generate(&u.decompressed, data_offset2) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => {
|
||||||
return Err(UnpackError::Stage5BytecodeGenFailed);
|
return Err(UnpackError::BytecodeGenerationFailed(
|
||||||
|
BytecodeStage::ExeStage5,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// The new layout picked its file decryptor by distance (no marker, no
|
// The new layout picked its file decryptor by distance (no marker, no
|
||||||
@@ -930,7 +1117,7 @@ impl<'a> Unpacker<'a> {
|
|||||||
// garbling into the output without any error (see the validator).
|
// garbling into the output without any error (see the validator).
|
||||||
let rebase = (!get_u32(u.file_data, 4224)).wrapping_add(4096);
|
let rebase = (!get_u32(u.file_data, 4224)).wrapping_add(4096);
|
||||||
if new_layout && !u.new_layout_file_ops_validate(walk4_slot, &ops2, rebase) {
|
if new_layout && !u.new_layout_file_ops_validate(walk4_slot, &ops2, rebase) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::FileDecryptorValidationFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
let at6 = walk4_slot;
|
let at6 = walk4_slot;
|
||||||
@@ -980,9 +1167,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
// Snapshot the shared tables before the fan-out: workers get
|
// Snapshot the shared tables before the fan-out: workers get
|
||||||
// disjoint span slices, not the whole buffer.
|
// disjoint span slices, not the whole buffer.
|
||||||
let ks_snap = primitives::aes_schedule_snapshot(&u.decompressed, ko[2])
|
let ks_snap = primitives::aes_schedule_snapshot(&u.decompressed, ko[2])
|
||||||
.ok_or(UnpackError::Corrupt)?;
|
.ok_or(UnpackError::InvalidAesKeySchedule { offset: ko[2] })?;
|
||||||
let tab_snap = primitives::huffman_table_snapshot(&u.decompressed, ko[0])
|
let tab_snap = primitives::huffman_table_snapshot(&u.decompressed, ko[0])
|
||||||
.ok_or(UnpackError::DecompressFailed)?;
|
.ok_or(UnpackError::InvalidHuffmanTable { offset: ko[0] })?;
|
||||||
let spans: Vec<(usize, usize)> = blocks
|
let spans: Vec<(usize, usize)> = blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|b| {
|
.map(|b| {
|
||||||
@@ -1009,7 +1196,10 @@ impl<'a> Unpacker<'a> {
|
|||||||
b.len,
|
b.len,
|
||||||
b.plain_len,
|
b.plain_len,
|
||||||
) {
|
) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::SectionDecompressionFailed {
|
||||||
|
pipeline: SectionPipeline::ExePe32Plus,
|
||||||
|
block: i,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1475,7 +1665,7 @@ impl<'a> Unpacker<'a> {
|
|||||||
/// whose fileCS pointer sits just past `info[3]`), not by content. A
|
/// whose fileCS pointer sits just past `info[3]`), not by content. A
|
||||||
/// coincidental LFSR-shaped block at a shorter distance would decode to a
|
/// coincidental LFSR-shaped block at a shorter distance would decode to a
|
||||||
/// wrong `ops2` translate and silently garble every section block — raw
|
/// wrong `ops2` translate and silently garble every section block — raw
|
||||||
/// blocks never hit `DecompressFailed`, so the failure would ship as a
|
/// raw blocks never enter the decompressor, so the failure would ship as a
|
||||||
/// plausible but wrong image. Replay the first *compressed* block's full
|
/// plausible but wrong image. Replay the first *compressed* block's full
|
||||||
/// transform (raw copy, AES, translate, decompress) on a snapshot and
|
/// transform (raw copy, AES, translate, decompress) on a snapshot and
|
||||||
/// require decompression to succeed; restore the region afterwards.
|
/// require decompression to succeed; restore the region afterwards.
|
||||||
@@ -1837,7 +2027,11 @@ impl<'a> Unpacker<'a> {
|
|||||||
let ss_lo = ss as usize;
|
let ss_lo = ss as usize;
|
||||||
let ss_hi = ss_lo.wrapping_add(ss_size as usize);
|
let ss_hi = ss_lo.wrapping_add(ss_size as usize);
|
||||||
if ss_hi < ss_lo || ss_hi > self.decompressed.len() {
|
if ss_hi < ss_lo || ss_hi > self.decompressed.len() {
|
||||||
return Err(UnpackError::Corrupt);
|
return Err(UnpackError::Pe32SecondStageRangeInvalid {
|
||||||
|
offset: ss,
|
||||||
|
size: ss_size,
|
||||||
|
image_len: self.decompressed.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let ss_ct: Vec<u8> = self.decompressed[ss_lo..ss_hi].to_vec();
|
let ss_ct: Vec<u8> = self.decompressed[ss_lo..ss_hi].to_vec();
|
||||||
// PE32 data dir 5 (BaseReloc) = optional_header(pe+24) + 0x60 + 5*8 = pe+0xA0.
|
// PE32 data dir 5 (BaseReloc) = optional_header(pe+24) + 0x60 + 5*8 = pe+0xA0.
|
||||||
@@ -1872,7 +2066,7 @@ impl<'a> Unpacker<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
return Err(UnpackError::Corrupt);
|
return Err(UnpackError::Pe32RelocationDataNotFound);
|
||||||
}
|
}
|
||||||
if verbose {
|
if verbose {
|
||||||
println!(
|
println!(
|
||||||
@@ -1996,7 +2190,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
let forth_addr = dp_base.wrapping_add(0x40);
|
let forth_addr = dp_base.wrapping_add(0x40);
|
||||||
let fk = header_checksum ^ second_stage_cs ^ forth_stage_key;
|
let fk = header_checksum ^ second_stage_cs ^ forth_stage_key;
|
||||||
if !self.decrypt_and_decompress_data(forth_addr, fk, None) {
|
if !self.decrypt_and_decompress_data(forth_addr, fk, None) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::StageDecompressionFailed(
|
||||||
|
DecompressionStage::Pe32FourthStage,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- FifthStage ----
|
// ---- FifthStage ----
|
||||||
@@ -2012,7 +2208,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
);
|
);
|
||||||
let fk5 = header_checksum ^ forth_cs ^ fifth_key;
|
let fk5 = header_checksum ^ forth_cs ^ fifth_key;
|
||||||
if !self.decrypt_and_decompress_data(fifth_addr, fk5, None) {
|
if !self.decrypt_and_decompress_data(fifth_addr, fk5, None) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::StageDecompressionFailed(
|
||||||
|
DecompressionStage::Pe32FifthStage,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- SevenStage ----
|
// ---- SevenStage ----
|
||||||
@@ -2035,7 +2233,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
);
|
);
|
||||||
let fk7 = header_checksum ^ fifth_cs ^ seven_key;
|
let fk7 = header_checksum ^ fifth_cs ^ seven_key;
|
||||||
if !self.decrypt_and_decompress_data(seven_addr, fk7, None) {
|
if !self.decrypt_and_decompress_data(seven_addr, fk7, None) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::StageDecompressionFailed(
|
||||||
|
DecompressionStage::Pe32SeventhStage,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- EighthStage ----
|
// ---- EighthStage ----
|
||||||
@@ -2063,8 +2263,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
.ok_or(UnpackError::Pe32CustomDecryptorNotFound)?;
|
.ok_or(UnpackError::Pe32CustomDecryptorNotFound)?;
|
||||||
let custom_dec_addr = seven_start_actual.wrapping_add(custom_dec_off);
|
let custom_dec_addr = seven_start_actual.wrapping_add(custom_dec_off);
|
||||||
self.decrypt_data6(custom_dec_addr);
|
self.decrypt_data6(custom_dec_addr);
|
||||||
let custom_ops = generate(&self.decompressed, custom_dec_addr)
|
let custom_ops = generate(&self.decompressed, custom_dec_addr).ok_or(
|
||||||
.ok_or(UnpackError::Pe32BytecodeGenFailed)?;
|
UnpackError::BytecodeGenerationFailed(BytecodeStage::Pe32CustomDecryptor),
|
||||||
|
)?;
|
||||||
|
|
||||||
let seven_cs = self.calculate_checksum(seven_stage_cs_addr);
|
let seven_cs = self.calculate_checksum(seven_stage_cs_addr);
|
||||||
let eighth_addr = dp_base.wrapping_add(0xC0);
|
let eighth_addr = dp_base.wrapping_add(0xC0);
|
||||||
@@ -2323,7 +2524,7 @@ impl<'a> Unpacker<'a> {
|
|||||||
// "legacy loose scan" picked the nearest LFSR-shaped block
|
// "legacy loose scan" picked the nearest LFSR-shaped block
|
||||||
// by offset distance without any validation — that is
|
// by offset distance without any validation — that is
|
||||||
// exactly how a wrong file_ops got applied to every data
|
// exactly how a wrong file_ops got applied to every data
|
||||||
// block (uncompressed blocks never hit DecompressFailed),
|
// block (uncompressed blocks never enter the decompressor),
|
||||||
// producing a plausible but fully wrong image (the PE32
|
// producing a plausible but fully wrong image (the PE32
|
||||||
// .text scramble root cause). Trial-and-validate or error.
|
// .text scramble root cause). Trial-and-validate or error.
|
||||||
return Err(UnpackError::Pe32FileLfsrNotFound);
|
return Err(UnpackError::Pe32FileLfsrNotFound);
|
||||||
@@ -2362,8 +2563,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
};
|
};
|
||||||
let file_dec_addr = eighth_start.wrapping_add(lfsr_off);
|
let file_dec_addr = eighth_start.wrapping_add(lfsr_off);
|
||||||
self.decrypt_data6(file_dec_addr);
|
self.decrypt_data6(file_dec_addr);
|
||||||
let file_ops = generate(&self.decompressed, file_dec_addr)
|
let file_ops = generate(&self.decompressed, file_dec_addr).ok_or(
|
||||||
.ok_or(UnpackError::Pe32BytecodeGenFailed)?;
|
UnpackError::BytecodeGenerationFailed(BytecodeStage::Pe32FileDecryptor),
|
||||||
|
)?;
|
||||||
|
|
||||||
// ---- PE32 metadata: EP and data dirs from info[3] ----
|
// ---- PE32 metadata: EP and data dirs from info[3] ----
|
||||||
let test_val = get_u32(&self.decompressed, info3.wrapping_add(0x10));
|
let test_val = get_u32(&self.decompressed, info3.wrapping_add(0x10));
|
||||||
@@ -2448,9 +2650,9 @@ impl<'a> Unpacker<'a> {
|
|||||||
let clean = &self.file_data;
|
let clean = &self.file_data;
|
||||||
let ko = self.key_offsets;
|
let ko = self.key_offsets;
|
||||||
let ks_snap = primitives::aes_schedule_snapshot(&self.decompressed, ko[2])
|
let ks_snap = primitives::aes_schedule_snapshot(&self.decompressed, ko[2])
|
||||||
.ok_or(UnpackError::Corrupt)?;
|
.ok_or(UnpackError::InvalidAesKeySchedule { offset: ko[2] })?;
|
||||||
let tab_snap = primitives::huffman_table_snapshot(&self.decompressed, ko[0])
|
let tab_snap = primitives::huffman_table_snapshot(&self.decompressed, ko[0])
|
||||||
.ok_or(UnpackError::DecompressFailed)?;
|
.ok_or(UnpackError::InvalidHuffmanTable { offset: ko[0] })?;
|
||||||
let spans: Vec<(usize, usize)> = blocks
|
let spans: Vec<(usize, usize)> = blocks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|b| {
|
.map(|b| {
|
||||||
@@ -2472,7 +2674,10 @@ impl<'a> Unpacker<'a> {
|
|||||||
if !primitives::decompress_tbl(
|
if !primitives::decompress_tbl(
|
||||||
&tab_snap, span, rel as u32, rel as u32, b.ssz, b.dsz,
|
&tab_snap, span, rel as u32, rel as u32, b.ssz, b.dsz,
|
||||||
) {
|
) {
|
||||||
return Err(UnpackError::DecompressFailed);
|
return Err(UnpackError::SectionDecompressionFailed {
|
||||||
|
pipeline: SectionPipeline::ExePe32,
|
||||||
|
block: i,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -2805,8 +3010,43 @@ impl<'a> Unpacker<'a> {
|
|||||||
if !is_dll && !primitives::pe32_imports_already_match_idata_layout(&mut out, pe_off) {
|
if !is_dll && !primitives::pe32_imports_already_match_idata_layout(&mut out, pe_off) {
|
||||||
primitives::move_pe32_imports_to_kmiat(&mut out, pe_off);
|
primitives::move_pe32_imports_to_kmiat(&mut out, pe_off);
|
||||||
}
|
}
|
||||||
let compact =
|
let compact = primitives::compact_memory_image_to_pe(&out, pe_off)
|
||||||
primitives::compact_memory_image_to_pe(&out, pe_off).ok_or(UnpackError::Corrupt)?;
|
.ok_or(UnpackError::Pe32OutputLayoutInvalid)?;
|
||||||
Ok(compact)
|
Ok(compact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod error_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_input_reports_actual_and_required_lengths() {
|
||||||
|
let error = unpack(&[0; 4096]).expect_err("header must be rejected");
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
UnpackError::InputTooShort {
|
||||||
|
actual: 4096,
|
||||||
|
required: 4128,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_errors_include_stage_and_block_context() {
|
||||||
|
let stage = UnpackError::StageDecompressionFailed(DecompressionStage::ExeStage4);
|
||||||
|
assert_eq!(
|
||||||
|
stage.to_string(),
|
||||||
|
"EXE stage4 decompression failed — corrupt data or wrong offset"
|
||||||
|
);
|
||||||
|
|
||||||
|
let block = UnpackError::SectionDecompressionFailed {
|
||||||
|
pipeline: SectionPipeline::ExePe32,
|
||||||
|
block: 7,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
block.to_string(),
|
||||||
|
"PE32 EXE section block 7 decompression failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+207
-16
@@ -9,8 +9,14 @@ pub(crate) mod parallel;
|
|||||||
pub(crate) mod primitives;
|
pub(crate) mod primitives;
|
||||||
mod tables;
|
mod tables;
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
pub use dll::{unpack_dll, unpack_dll_v};
|
pub use dll::{unpack_dll, unpack_dll_v};
|
||||||
pub use exe::{UnpackError, unpack as unpack_exe, unpack_v as unpack_exe_v};
|
pub use exe::{
|
||||||
|
BufferOperation, BytecodeStage, DecompressionStage, DescriptorTable, SectionPipeline,
|
||||||
|
UnpackError, unpack as unpack_exe, unpack_v as unpack_exe_v,
|
||||||
|
};
|
||||||
pub use integrity::{IntegrityReport, check as check_integrity};
|
pub use integrity::{IntegrityReport, check as check_integrity};
|
||||||
|
|
||||||
/// Maximum plausible PE `SizeOfImage` we are willing to allocate a zero buffer
|
/// Maximum plausible PE `SizeOfImage` we are willing to allocate a zero buffer
|
||||||
@@ -20,11 +26,139 @@ pub use integrity::{IntegrityReport, check as check_integrity};
|
|||||||
/// Real protected binaries are far below this.
|
/// Real protected binaries are far below this.
|
||||||
pub(crate) const MAX_IMAGE_SIZE: u64 = 1 << 30; // 1 GiB
|
pub(crate) const MAX_IMAGE_SIZE: u64 = 1 << 30; // 1 GiB
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct PanicCapture(Arc<Mutex<Option<PanicDetails>>>);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PanicDetails {
|
||||||
|
message: String,
|
||||||
|
file: String,
|
||||||
|
line: u32,
|
||||||
|
column: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static ACTIVE_PANIC_CAPTURE: RefCell<Option<PanicCapture>> = const { RefCell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PanicCaptureGuard(Option<PanicCapture>);
|
||||||
|
|
||||||
|
impl Drop for PanicCaptureGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
ACTIVE_PANIC_CAPTURE.with(|slot| {
|
||||||
|
slot.replace(self.0.take());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PanicCapture {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self(Arc::new(Mutex::new(None)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn record(&self, info: &std::panic::PanicHookInfo<'_>) {
|
||||||
|
let location = info.location();
|
||||||
|
let details = PanicDetails {
|
||||||
|
message: panic_message(info.payload()),
|
||||||
|
file: location
|
||||||
|
.map(|value| value.file().to_owned())
|
||||||
|
.unwrap_or_else(|| "<unknown>".to_owned()),
|
||||||
|
line: location.map_or(0, std::panic::Location::line),
|
||||||
|
column: location.map_or(0, std::panic::Location::column),
|
||||||
|
};
|
||||||
|
let mut captured = self
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
if captured.is_none() {
|
||||||
|
*captured = Some(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_error(self, payload: &(dyn std::any::Any + Send)) -> UnpackError {
|
||||||
|
let details = self
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| PanicDetails {
|
||||||
|
message: panic_message(payload),
|
||||||
|
file: "<unknown>".to_owned(),
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
});
|
||||||
|
UnpackError::InternalPanic {
|
||||||
|
message: details.message,
|
||||||
|
file: details.file,
|
||||||
|
line: details.line,
|
||||||
|
column: details.column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_from(&self, other: &Self) {
|
||||||
|
let details = other
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.clone();
|
||||||
|
let Some(details) = details else { return };
|
||||||
|
let mut captured = self
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
if captured.is_none() {
|
||||||
|
*captured = Some(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
|
||||||
|
if let Some(message) = payload.downcast_ref::<&str>() {
|
||||||
|
(*message).to_owned()
|
||||||
|
} else if let Some(message) = payload.downcast_ref::<String>() {
|
||||||
|
message.clone()
|
||||||
|
} else {
|
||||||
|
"non-string panic payload".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn install_panic_capture_hook() {
|
||||||
|
static INSTALL: std::sync::Once = std::sync::Once::new();
|
||||||
|
INSTALL.call_once(|| {
|
||||||
|
let previous = std::panic::take_hook();
|
||||||
|
std::panic::set_hook(Box::new(move |info| {
|
||||||
|
let capture = ACTIVE_PANIC_CAPTURE
|
||||||
|
.try_with(|slot| slot.borrow().clone())
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
if let Some(capture) = capture {
|
||||||
|
capture.record(info);
|
||||||
|
} else {
|
||||||
|
previous(info);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn install_panic_capture_hook() {}
|
||||||
|
|
||||||
|
pub(crate) fn current_panic_capture() -> Option<PanicCapture> {
|
||||||
|
ACTIVE_PANIC_CAPTURE.with(|slot| slot.borrow().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_panic_capture<R>(capture: Option<PanicCapture>, f: impl FnOnce() -> R) -> R {
|
||||||
|
let previous = ACTIVE_PANIC_CAPTURE.with(|slot| slot.replace(capture));
|
||||||
|
let _guard = PanicCaptureGuard(previous);
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
/// Run an unpack pipeline, converting any internal panic into a clean
|
/// Run an unpack pipeline, converting any internal panic into a clean
|
||||||
/// [`UnpackError::Corrupt`] so the public API stays panic-free on any input
|
/// [`UnpackError::InternalPanic`] so the public API stays panic-free on any input
|
||||||
/// (truncated/garbled files chase offsets out of bounds). The default panic
|
/// (truncated/garbled files chase offsets out of bounds). The panic location and
|
||||||
/// hook is suppressed transiently so a trapped panic does not spill a
|
/// payload are captured for diagnostics without printing a backtrace to stderr.
|
||||||
/// backtrace to stderr.
|
|
||||||
///
|
///
|
||||||
/// Note: allocation *failures* abort the process and are NOT caught here; size
|
/// Note: allocation *failures* abort the process and are NOT caught here; size
|
||||||
/// requests are bounds-checked against [`MAX_IMAGE_SIZE`] before allocating.
|
/// requests are bounds-checked against [`MAX_IMAGE_SIZE`] before allocating.
|
||||||
@@ -32,17 +166,17 @@ pub(crate) fn catch_unpack<F>(f: F) -> Result<Vec<u8>, UnpackError>
|
|||||||
where
|
where
|
||||||
F: FnOnce() -> Result<Vec<u8>, UnpackError>,
|
F: FnOnce() -> Result<Vec<u8>, UnpackError>,
|
||||||
{
|
{
|
||||||
// Hook suppression is skipped on wasm: the prebuilt std cannot unwind
|
// Hook capture is skipped on wasm: the prebuilt std cannot unwind there,
|
||||||
// there, so a panic traps immediately — and the suppressed hook would
|
// so a panic traps immediately. The Web Worker boundary reports that trap.
|
||||||
// hide the panic message, leaving a bare `unreachable` with no clue.
|
install_panic_capture_hook();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
let capture = PanicCapture::new();
|
||||||
let prev = std::panic::take_hook();
|
let r = with_panic_capture(Some(capture.clone()), || {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
|
||||||
std::panic::set_hook(Box::new(|_| {}));
|
});
|
||||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
|
match r {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
Ok(result) => result,
|
||||||
std::panic::set_hook(prev);
|
Err(payload) => Err(capture.into_error(payload.as_ref())),
|
||||||
r.unwrap_or(Err(UnpackError::Corrupt))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crackproof header magic stored in `keys[1]`/`info[1]`.
|
/// Crackproof header magic stored in `keys[1]`/`info[1]`.
|
||||||
@@ -208,3 +342,60 @@ pub fn unpack_auto_v(input: &[u8], verbose: bool) -> Result<(Kind, Vec<u8>), Unp
|
|||||||
};
|
};
|
||||||
Ok((detected.kind, out))
|
Ok((detected.kind, out))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[test]
|
||||||
|
fn caught_panic_reports_location_and_message() {
|
||||||
|
let error = catch_unpack(|| -> Result<Vec<u8>, UnpackError> {
|
||||||
|
panic!("test panic");
|
||||||
|
})
|
||||||
|
.expect_err("panic must become an error");
|
||||||
|
let UnpackError::InternalPanic {
|
||||||
|
message,
|
||||||
|
file,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
} = error
|
||||||
|
else {
|
||||||
|
panic!("unexpected error: {error}");
|
||||||
|
};
|
||||||
|
assert_eq!(message, "test panic");
|
||||||
|
assert!(file.ends_with("src/unpacker/mod.rs") || file.ends_with("src\\unpacker\\mod.rs"));
|
||||||
|
assert!(line > 0);
|
||||||
|
assert!(column > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[test]
|
||||||
|
fn worker_panic_keeps_the_worker_source_location() {
|
||||||
|
let error = catch_unpack(|| -> Result<Vec<u8>, UnpackError> {
|
||||||
|
let capture = current_panic_capture();
|
||||||
|
let result = std::thread::spawn(move || {
|
||||||
|
with_panic_capture(capture, || panic!("worker panic"));
|
||||||
|
})
|
||||||
|
.join();
|
||||||
|
if let Err(payload) = result {
|
||||||
|
std::panic::resume_unwind(payload);
|
||||||
|
}
|
||||||
|
Ok(Vec::new())
|
||||||
|
})
|
||||||
|
.expect_err("worker panic must become an error");
|
||||||
|
let UnpackError::InternalPanic {
|
||||||
|
message,
|
||||||
|
file,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
} = error
|
||||||
|
else {
|
||||||
|
panic!("unexpected error: {error}");
|
||||||
|
};
|
||||||
|
assert_eq!(message, "worker panic");
|
||||||
|
assert!(file.ends_with("src/unpacker/mod.rs") || file.ends_with("src\\unpacker\\mod.rs"));
|
||||||
|
assert!(line > 0);
|
||||||
|
assert!(column > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ pub(crate) fn thread_cap() -> usize {
|
|||||||
///
|
///
|
||||||
/// Returns the first `Err` any block produces; re-raises the first block panic
|
/// Returns the first `Err` any block produces; re-raises the first block panic
|
||||||
/// on the calling thread (so the pipeline's existing `catch_unpack` still
|
/// on the calling thread (so the pipeline's existing `catch_unpack` still
|
||||||
/// converts it to `UnpackError::Corrupt`).
|
/// converts it to `UnpackError::InternalPanic`).
|
||||||
pub(crate) fn parallel_for<E, F>(
|
pub(crate) fn parallel_for<E, F>(
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
spans: &[(usize, usize)],
|
spans: &[(usize, usize)],
|
||||||
@@ -125,6 +125,7 @@ where
|
|||||||
let stop = AtomicBool::new(false);
|
let stop = AtomicBool::new(false);
|
||||||
let first_err: Mutex<Option<E>> = Mutex::new(None);
|
let first_err: Mutex<Option<E>> = Mutex::new(None);
|
||||||
let first_panic: Mutex<Option<Box<dyn std::any::Any + Send>>> = Mutex::new(None);
|
let first_panic: Mutex<Option<Box<dyn std::any::Any + Send>>> = Mutex::new(None);
|
||||||
|
let panic_capture = super::current_panic_capture();
|
||||||
|
|
||||||
std::thread::scope(|scope| {
|
std::thread::scope(|scope| {
|
||||||
for _ in 0..workers {
|
for _ in 0..workers {
|
||||||
@@ -133,6 +134,7 @@ where
|
|||||||
let first_err = &first_err;
|
let first_err = &first_err;
|
||||||
let first_panic = &first_panic;
|
let first_panic = &first_panic;
|
||||||
let f = &f;
|
let f = &f;
|
||||||
|
let panic_capture = panic_capture.clone();
|
||||||
scope.spawn(move || {
|
scope.spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
if stop.load(Ordering::Relaxed) {
|
if stop.load(Ordering::Relaxed) {
|
||||||
@@ -141,9 +143,15 @@ where
|
|||||||
let next = iter.lock().unwrap().next();
|
let next = iter.lock().unwrap().next();
|
||||||
let Some((i, piece)) = next else { break };
|
let Some((i, piece)) = next else { break };
|
||||||
let span = piece.unwrap();
|
let span = piece.unwrap();
|
||||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
// Keep details local until this panic wins `first_panic`;
|
||||||
|
// otherwise simultaneous workers could pair one worker's
|
||||||
|
// location with another worker's propagated payload.
|
||||||
|
let block_capture = panic_capture.as_ref().map(|_| super::PanicCapture::new());
|
||||||
|
let r = super::with_panic_capture(block_capture.clone(), || {
|
||||||
|
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||||
f(i, spans[i].0, span)
|
f(i, spans[i].0, span)
|
||||||
}));
|
}))
|
||||||
|
});
|
||||||
match r {
|
match r {
|
||||||
Ok(Ok(())) => {}
|
Ok(Ok(())) => {}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
@@ -157,6 +165,11 @@ where
|
|||||||
Err(panic) => {
|
Err(panic) => {
|
||||||
let mut slot = first_panic.lock().unwrap();
|
let mut slot = first_panic.lock().unwrap();
|
||||||
if slot.is_none() {
|
if slot.is_none() {
|
||||||
|
if let (Some(parent), Some(block)) =
|
||||||
|
(&panic_capture, &block_capture)
|
||||||
|
{
|
||||||
|
parent.merge_from(block);
|
||||||
|
}
|
||||||
*slot = Some(panic);
|
*slot = Some(panic);
|
||||||
}
|
}
|
||||||
stop.store(true, Ordering::Relaxed);
|
stop.store(true, Ordering::Relaxed);
|
||||||
|
|||||||
@@ -69,9 +69,22 @@ pub(crate) fn write_u32(data: &mut [u8], offset: u32, value: u32) {
|
|||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn try_u32(d: &[u8], off: usize) -> Result<u32, super::UnpackError> {
|
pub(crate) fn try_u32(d: &[u8], off: usize) -> Result<u32, super::UnpackError> {
|
||||||
d.get(off..off + 4)
|
let end = off
|
||||||
|
.checked_add(4)
|
||||||
|
.ok_or(super::UnpackError::BufferRangeOutOfBounds {
|
||||||
|
operation: super::BufferOperation::Read,
|
||||||
|
offset: off,
|
||||||
|
size: 4,
|
||||||
|
buffer_len: d.len(),
|
||||||
|
})?;
|
||||||
|
d.get(off..end)
|
||||||
.map(|s| u32::from_le_bytes(s.try_into().unwrap()))
|
.map(|s| u32::from_le_bytes(s.try_into().unwrap()))
|
||||||
.ok_or(super::UnpackError::OutOfBounds(off))
|
.ok_or(super::UnpackError::BufferRangeOutOfBounds {
|
||||||
|
operation: super::BufferOperation::Read,
|
||||||
|
offset: off,
|
||||||
|
size: 4,
|
||||||
|
buffer_len: d.len(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -79,7 +92,7 @@ pub(crate) fn try_i32(d: &[u8], off: usize) -> Result<i32, super::UnpackError> {
|
|||||||
try_u32(d, off).map(|v| v as i32)
|
try_u32(d, off).map(|v| v as i32)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checked copy: returns OutOfBounds if src or dst ranges exceed their respective slices.
|
/// Checked copy with distinct source and destination range errors.
|
||||||
pub(crate) fn try_copy_from_slice(
|
pub(crate) fn try_copy_from_slice(
|
||||||
dst: &mut [u8],
|
dst: &mut [u8],
|
||||||
dst_off: usize,
|
dst_off: usize,
|
||||||
@@ -87,17 +100,39 @@ pub(crate) fn try_copy_from_slice(
|
|||||||
src: &[u8],
|
src: &[u8],
|
||||||
src_off: usize,
|
src_off: usize,
|
||||||
) -> Result<(), super::UnpackError> {
|
) -> Result<(), super::UnpackError> {
|
||||||
let dst_end = dst_off
|
let dst_end =
|
||||||
|
dst_off
|
||||||
.checked_add(dst_len)
|
.checked_add(dst_len)
|
||||||
.ok_or(super::UnpackError::OutOfBounds(dst_off))?;
|
.ok_or(super::UnpackError::BufferRangeOutOfBounds {
|
||||||
let src_end = src_off
|
operation: super::BufferOperation::CopyDestination,
|
||||||
|
offset: dst_off,
|
||||||
|
size: dst_len,
|
||||||
|
buffer_len: dst.len(),
|
||||||
|
})?;
|
||||||
|
let src_end =
|
||||||
|
src_off
|
||||||
.checked_add(dst_len)
|
.checked_add(dst_len)
|
||||||
.ok_or(super::UnpackError::OutOfBounds(src_off))?;
|
.ok_or(super::UnpackError::BufferRangeOutOfBounds {
|
||||||
|
operation: super::BufferOperation::CopySource,
|
||||||
|
offset: src_off,
|
||||||
|
size: dst_len,
|
||||||
|
buffer_len: src.len(),
|
||||||
|
})?;
|
||||||
if dst_end > dst.len() {
|
if dst_end > dst.len() {
|
||||||
return Err(super::UnpackError::OutOfBounds(dst_off));
|
return Err(super::UnpackError::BufferRangeOutOfBounds {
|
||||||
|
operation: super::BufferOperation::CopyDestination,
|
||||||
|
offset: dst_off,
|
||||||
|
size: dst_len,
|
||||||
|
buffer_len: dst.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if src_end > src.len() {
|
if src_end > src.len() {
|
||||||
return Err(super::UnpackError::OutOfBounds(src_off));
|
return Err(super::UnpackError::BufferRangeOutOfBounds {
|
||||||
|
operation: super::BufferOperation::CopySource,
|
||||||
|
offset: src_off,
|
||||||
|
size: dst_len,
|
||||||
|
buffer_len: src.len(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
dst[dst_off..dst_end].copy_from_slice(&src[src_off..src_end]);
|
dst[dst_off..dst_end].copy_from_slice(&src[src_off..src_end]);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -2005,6 +2040,37 @@ fn score_dd8_shift(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checked_copy_distinguishes_source_and_destination_ranges() {
|
||||||
|
let mut short_destination = [0u8; 2];
|
||||||
|
let source = [1u8; 4];
|
||||||
|
let error = try_copy_from_slice(&mut short_destination, 0, 3, &source, 0)
|
||||||
|
.expect_err("destination must be rejected");
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
super::super::UnpackError::BufferRangeOutOfBounds {
|
||||||
|
operation: super::super::BufferOperation::CopyDestination,
|
||||||
|
offset: 0,
|
||||||
|
size: 3,
|
||||||
|
buffer_len: 2,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut destination = [0u8; 4];
|
||||||
|
let short_source = [1u8; 2];
|
||||||
|
let error = try_copy_from_slice(&mut destination, 0, 3, &short_source, 0)
|
||||||
|
.expect_err("source must be rejected");
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
super::super::UnpackError::BufferRangeOutOfBounds {
|
||||||
|
operation: super::super::BufferOperation::CopySource,
|
||||||
|
offset: 0,
|
||||||
|
size: 3,
|
||||||
|
buffer_len: 2,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aes_ks_variant_matches_single_buffer() {
|
fn aes_ks_variant_matches_single_buffer() {
|
||||||
// Random-ish key schedule at ko and data block; both variants must
|
// Random-ish key schedule at ko and data block; both variants must
|
||||||
|
|||||||
Reference in New Issue
Block a user