mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
2032 lines
95 KiB
Rust
2032 lines
95 KiB
Rust
use senbei_crypto::bytecode::{Op, OpsLut, generate};
|
|
use senbei_crypto::primitives;
|
|
use senbei_crypto::primitives::*;
|
|
|
|
use super::super::error::*;
|
|
use super::super::layout::{self, *};
|
|
|
|
mod pe32;
|
|
|
|
pub fn unpack(input: &[u8]) -> Result<Vec<u8>, UnpackError> {
|
|
unpack_v(input, false)
|
|
}
|
|
|
|
/// Map an RVA to a file offset using the protected file's section table.
|
|
/// Used by the new-layout managed (CLR) metadata restore to locate the COR20
|
|
/// header and BSJB MetaData stream in the original protected file.
|
|
fn prot_rva_to_off(file_data: &[u8], pe_header: u32, rva: u32) -> Option<u32> {
|
|
let headers = senbei_pe::parse(file_data).ok()?;
|
|
if headers.pe_offset != pe_header as usize {
|
|
return None;
|
|
}
|
|
let offset = senbei_pe::rva_to_offset(file_data, headers, rva).ok()?;
|
|
u32::try_from(offset).ok()
|
|
}
|
|
|
|
pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
|
const HEADER_LEN: usize = 4128;
|
|
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
|
|
// truncated/garbled-but-detected file those run out of bounds. Trap any
|
|
// such panic and report it as corrupt input so the library never unwinds
|
|
// into the caller (same role as the DLL path's explicit bounds checks).
|
|
// `input` is read-only for the entire unpack (the payload is decrypted into a
|
|
// separate `decompressed` buffer), so the unpacker borrows it directly — no
|
|
// owned copy is made here. catch_unwind uses AssertUnwindSafe, so a borrowing
|
|
// (non-'static) closure is fine.
|
|
super::super::catch_unpack(move || Unpacker::run(input, verbose))
|
|
}
|
|
|
|
struct Unpacker<'a> {
|
|
file_data: &'a [u8],
|
|
decompressed: Vec<u8>,
|
|
info: [u32; 8],
|
|
key_offsets: [u32; 4],
|
|
decrypt_size: u32,
|
|
}
|
|
|
|
impl<'a> Unpacker<'a> {
|
|
// Strategy (a): delegate to primitives::aes_decrypt
|
|
fn aes_decrypt(&mut self, pos: u32, size: u32, key_offset: u32) {
|
|
primitives::aes_decrypt(&mut self.decompressed, pos, size, key_offset);
|
|
}
|
|
|
|
// Strategy (a): delegate to primitives::calculate_checksum
|
|
fn calculate_checksum(&self, pos: u32) -> u32 {
|
|
primitives::calculate_checksum(&self.decompressed, pos)
|
|
}
|
|
|
|
// Strategy (a): delegate to primitives::calculate_checksum2
|
|
fn calculate_checksum2(&self, pos: u32, start: u32) -> Result<u32, UnpackError> {
|
|
primitives::calculate_checksum2(&self.decompressed, self.file_data, pos, start).map_err(
|
|
|error| match error {
|
|
senbei_crypto::Error::BufferRangeOutOfBounds {
|
|
offset,
|
|
size,
|
|
buffer_len,
|
|
..
|
|
} => UnpackError::ExeChecksumRangeOutOfBounds {
|
|
descriptor: pos,
|
|
offset,
|
|
size,
|
|
image_len: buffer_len,
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
// Strategy (a): delegate to primitives::decrypt_data1
|
|
fn decrypt_data(&mut self) {
|
|
primitives::decrypt_data1(self.file_data, &mut self.info);
|
|
}
|
|
|
|
// Strategy (b): decrypt_data2 is tightly coupled to Unpacker fields
|
|
// (file_data, decompressed, info, decrypt_size); left on Unpacker.
|
|
// The DLL port will need a different driver state anyway.
|
|
fn decrypt_data2(&mut self) {
|
|
let base_src = self.info[4].wrapping_add(4096);
|
|
let mut k = self.info[0].wrapping_add(!self.decrypt_size);
|
|
let words = self.decrypt_size >> 2;
|
|
for i in 0..words {
|
|
let off = i.wrapping_mul(4);
|
|
let cell = get_u32(self.file_data, base_src.wrapping_add(off));
|
|
write_u32(
|
|
&mut self.decompressed,
|
|
self.info[3].wrapping_add(off),
|
|
k ^ cell,
|
|
);
|
|
k = i.wrapping_mul(i) ^ (k.wrapping_add(cell).wrapping_add(i));
|
|
}
|
|
}
|
|
|
|
// Strategy (a): delegate to primitives::decrypt_data3
|
|
fn decrypt_data3(&mut self, pos: u32, key: u32, shift: u32) {
|
|
primitives::decrypt_data3(&mut self.decompressed, pos, key, shift);
|
|
}
|
|
|
|
// Strategy (b): decrypt_data4 is specific to EXE key-offset layout;
|
|
// left on Unpacker. DLL port will need its own variant.
|
|
fn decrypt_data4(&mut self, pos: u32) {
|
|
let base_addr = get_u32(&self.decompressed, pos);
|
|
let length = get_u32(&self.decompressed, pos.wrapping_add(4));
|
|
let mut b: u8 = (((base_addr >> 8).wrapping_add(base_addr)) & 0xFF) as u8;
|
|
let mut b2: u8 = b.wrapping_add(1);
|
|
for i in 0..length {
|
|
let idx = (base_addr + i) as usize;
|
|
let b3 = self.decompressed[idx];
|
|
let b4 = b3.rotate_left(3) ^ b2;
|
|
let b5 = b4.rotate_left(3) ^ b;
|
|
self.decompressed[idx] = b5.rotate_left(3);
|
|
b = b.wrapping_add(1);
|
|
b2 = b2.wrapping_add(1);
|
|
}
|
|
}
|
|
|
|
// Strategy (b): decrypt_data5 is frequently called with the EXE's virtual
|
|
// address convention; kept on Unpacker to avoid signature changes at call
|
|
// sites. DLL port can call primitives directly with explicit slice.
|
|
fn decrypt_data5(&mut self, va: u32, size: u32) {
|
|
let mut b: u8 = va as u8;
|
|
let mut b2: u8 = b.wrapping_add(1);
|
|
for i in 0..size {
|
|
let idx = (va + i) as usize;
|
|
let b3 = self.decompressed[idx];
|
|
let b4 = b3.rotate_left(2) ^ b2;
|
|
let b5 = b4.rotate_left(2) ^ b;
|
|
self.decompressed[idx] = b5.rotate_left(2);
|
|
b = b.wrapping_add(1);
|
|
b2 = b2.wrapping_add(1);
|
|
}
|
|
}
|
|
|
|
// Strategy (a): delegate to primitives::decrypt_data6
|
|
fn decrypt_data6(&mut self, pos: u32) {
|
|
primitives::decrypt_data6(&mut self.decompressed, pos);
|
|
}
|
|
|
|
// Strategy (a): delegate to primitives::decrypt_data7
|
|
fn decrypt_data7(&mut self, pos: u32, key: u8) {
|
|
primitives::decrypt_data7(&mut self.decompressed, pos, key);
|
|
}
|
|
|
|
// Strategy (b): decrypt_data8 is specific to the EXE's .text page-level
|
|
// XOR pass; left on Unpacker. DLL port won't need this.
|
|
fn decrypt_data8(&mut self, va: u32, size: u32, mut key: u32) {
|
|
let blocks = size >> 4;
|
|
for i in 0..blocks {
|
|
let mixed = key.rotate_right(15).wrapping_add(i);
|
|
key = mixed.wrapping_add(i);
|
|
// The packer's dd8 loop does not XOR block i=0; its key state still
|
|
// advances. For shift-0 builds the i=0 XOR value is always 0 (a
|
|
// no-op), so this is byte-identical for the older EXE-64 family;
|
|
// for shift-15 builds it is the difference between a clean
|
|
// .text and one corrupt byte per page.
|
|
if i == 0 {
|
|
continue;
|
|
}
|
|
let target = va
|
|
.wrapping_add(i.wrapping_mul(16))
|
|
.wrapping_add(mixed & 0xF);
|
|
self.decompressed[target as usize] ^= key as u8;
|
|
}
|
|
}
|
|
|
|
// Strategy (a): delegate to 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,
|
|
self.key_offsets[1],
|
|
self.key_offsets[3],
|
|
custom,
|
|
)
|
|
}
|
|
|
|
/// New-layout (marker-less) import reconstruction from the PE Import Directory.
|
|
/// Walks each IMAGE_IMPORT_DESCRIPTOR at `import_rva`, decrypt_data7-decrypts
|
|
/// and lowercases the DLL name, then walks the (OFT|IFT) thunk array
|
|
/// decrypting each by-name import's hint/name string, and recovers the IAT
|
|
/// directory (DD[12]) bounds.
|
|
fn process_imports_idt(&mut self, import_rva: u32, mut import_size: u32, pe_off2: u32) {
|
|
let len = self.decompressed.len() as u32;
|
|
let mut dll_count: u32 = 0;
|
|
let mut iat_min: u32 = 0xFFFF_FFFF;
|
|
let mut iat_max: u32 = 0;
|
|
let mut pos = import_rva;
|
|
let find_nul = |d: &[u8], from: u32, to: u32| -> u32 {
|
|
let hi = (to as usize).min(d.len());
|
|
let mut i = from as usize;
|
|
while i < hi {
|
|
if d[i] == 0 {
|
|
return i as u32;
|
|
}
|
|
i += 1;
|
|
}
|
|
hi as u32
|
|
};
|
|
let all_ascii = |d: &[u8], a: u32, b: u32| -> bool {
|
|
d[a as usize..b as usize]
|
|
.iter()
|
|
.all(|&c| (0x20..0x7F).contains(&c))
|
|
};
|
|
while pos + 20 <= len {
|
|
let name_rva = get_u32(&self.decompressed, pos + 12);
|
|
if name_rva == 0 || name_rva >= len {
|
|
break;
|
|
}
|
|
// Decrypt the DLL name unless it already reads as a plain ASCII
|
|
// *.dll / *.exe string.
|
|
let end = find_nul(&self.decompressed, name_rva, name_rva + 64);
|
|
let mut already_plain = false;
|
|
if end > name_rva
|
|
&& end - name_rva <= 60
|
|
&& all_ascii(&self.decompressed, name_rva, end)
|
|
{
|
|
let s = &self.decompressed[name_rva as usize..end as usize];
|
|
let lower: Vec<u8> = s.iter().map(|c| c.to_ascii_lowercase()).collect();
|
|
if lower.ends_with(b".dll") || lower.ends_with(b".exe") {
|
|
already_plain = true;
|
|
}
|
|
}
|
|
if !already_plain {
|
|
self.decrypt_data7(name_rva, name_rva as u8);
|
|
}
|
|
// Normalize to lowercase.
|
|
let end = find_nul(&self.decompressed, name_rva, name_rva + 64);
|
|
if end > name_rva && all_ascii(&self.decompressed, name_rva, end) {
|
|
for b in &mut self.decompressed[name_rva as usize..end as usize] {
|
|
b.make_ascii_lowercase();
|
|
}
|
|
}
|
|
|
|
let oft = get_u32(&self.decompressed, pos);
|
|
let ift = get_u32(&self.decompressed, pos + 16);
|
|
let mut thunk = if oft != 0 { oft } else { ift };
|
|
if 0 < ift && ift < len {
|
|
iat_min = iat_min.min(ift);
|
|
}
|
|
while thunk != 0 && thunk + 8 <= len {
|
|
let v = get_u64(&self.decompressed, thunk);
|
|
if v == 0 {
|
|
break;
|
|
}
|
|
if (v & 0x8000_0000_0000_0000) == 0 {
|
|
let r = (v & 0xFFFF_FFFF) as u32;
|
|
if r + 2 < len {
|
|
let fend = find_nul(&self.decompressed, r + 2, r + 2 + 256);
|
|
let already = fend > r + 2
|
|
&& fend - (r + 2) <= 250
|
|
&& all_ascii(&self.decompressed, r + 2, fend);
|
|
if !already {
|
|
self.decrypt_data7(r + 2, r as u8);
|
|
write_u16(&mut self.decompressed, r, 0);
|
|
}
|
|
}
|
|
}
|
|
thunk += 8;
|
|
}
|
|
if 0 < ift && ift < len {
|
|
let mut tp = ift;
|
|
while tp + 8 <= len {
|
|
let v2 = get_u64(&self.decompressed, tp);
|
|
tp += 8;
|
|
if v2 == 0 {
|
|
break;
|
|
}
|
|
}
|
|
iat_max = iat_max.max(tp);
|
|
}
|
|
dll_count += 1;
|
|
pos += 20;
|
|
}
|
|
if import_size == 0 {
|
|
import_size = (dll_count + 1) * 20;
|
|
write_u32(
|
|
&mut self.decompressed,
|
|
pe_off2.wrapping_add(0x94),
|
|
import_size,
|
|
);
|
|
}
|
|
if iat_min < iat_max {
|
|
write_u32(&mut self.decompressed, pe_off2.wrapping_add(0xE8), iat_min);
|
|
write_u32(
|
|
&mut self.decompressed,
|
|
pe_off2.wrapping_add(0xEC),
|
|
iat_max - iat_min,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn run(file_data: &'a [u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
|
let mut u = Unpacker {
|
|
file_data,
|
|
decompressed: Vec::new(),
|
|
info: [0u32; 8],
|
|
key_offsets: [0u32; 4],
|
|
decrypt_size: 0,
|
|
};
|
|
|
|
u.decrypt_data();
|
|
if verbose {
|
|
println!("[1/9] Decrypting file header...");
|
|
println!(" info[0] key = 0x{:08X}", u.info[0]);
|
|
println!(" info[1] signature = 0x{:08X}", u.info[1]);
|
|
println!(" info[2] = 0x{:08X}", u.info[2]);
|
|
println!(" info[3] base = 0x{:08X}", u.info[3]);
|
|
println!(" info[4] src_off = 0x{:08X}", u.info[4]);
|
|
println!(" info[5] total_size = 0x{:08X}", u.info[5]);
|
|
println!(" info[6] end_mark = 0x{:08X}", u.info[6]);
|
|
println!(" info[7] = 0x{:08X}", u.info[7]);
|
|
}
|
|
if !super::super::is_supported_magic(u.info[1]) {
|
|
return Err(UnpackError::HeaderMagicMismatch { found: u.info[1] });
|
|
}
|
|
|
|
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));
|
|
if size_of_image == 0 || size_of_image as u64 > super::super::MAX_IMAGE_SIZE {
|
|
return Err(UnpackError::InvalidImageSize {
|
|
size: i64::from(size_of_image),
|
|
max: super::super::MAX_IMAGE_SIZE,
|
|
});
|
|
}
|
|
u.decompressed = vec![0u8; size_of_image as usize];
|
|
u.decrypt_size = u.info[6].wrapping_sub(u.info[3]).wrapping_add(8192);
|
|
if verbose {
|
|
println!("[2/9] Decrypting payload...");
|
|
}
|
|
u.decrypt_data2();
|
|
|
|
{
|
|
let src_start = u.info[4].wrapping_add(4096).wrapping_add(u.decrypt_size) as usize;
|
|
let dst_start = u.info[3].wrapping_add(u.decrypt_size) as usize;
|
|
let len = u.info[5].wrapping_sub(u.decrypt_size) as usize;
|
|
u.decompressed[dst_start..dst_start + len]
|
|
.copy_from_slice(&u.file_data[src_start..src_start + len]);
|
|
}
|
|
write_u32(&mut u.decompressed, u.info[3], 4096);
|
|
u.decompressed[..4096].copy_from_slice(&u.file_data[..4096]);
|
|
|
|
// PE32 (32-bit) images use an entirely different config layout and
|
|
// final-output transform than PE32+ (64-bit). Dispatch here, after the
|
|
// shared header/payload setup (the PE32 branch follows the common
|
|
// Stage 1/2 work). pe_magic at pe_off+24: 0x10B = PE32, 0x20B = PE32+.
|
|
if get_u16(u.file_data, pe_off.wrapping_add(24)) == 0x10B {
|
|
return u.run_pe32(pe_off, verbose);
|
|
}
|
|
|
|
// The config block layout in the decrypted region varies between Crackproof
|
|
// versions. Find the anchor field — a dword equal to info[3], immediately
|
|
// followed by 0x28 and then info[6]-0x200 — and derive every other field
|
|
// offset relative to it. Observed anchor positions: +6728 (older EXE
|
|
// builds), +6744 (another old-layout build, +16), +5592 (managed-assembly
|
|
// builds, -1136).
|
|
let info6 = u.info[6];
|
|
let mut anchor: Option<u32> = None;
|
|
let mut probe = info6.wrapping_add(1000);
|
|
let probe_end = info6.wrapping_add(8000);
|
|
while probe + 12 <= probe_end && (probe as usize + 12) <= u.decompressed.len() {
|
|
if get_u32(&u.decompressed, probe) == u.info[3] {
|
|
let v8 = get_u32(&u.decompressed, probe.wrapping_add(8));
|
|
// v8 must be slightly less than info[6], aligned to 0x200, and
|
|
// close to it. Older EXE builds stay within 0x800; the
|
|
// external-companion DLLs reach 0xC00, so the window is 0x1000.
|
|
// The dword==info[3] equality is already a 32-bit match on the
|
|
// base RVA, making this secondary bound's exact value
|
|
// non-critical for false-positive rejection.
|
|
if v8 < info6 {
|
|
let delta = info6.wrapping_sub(v8);
|
|
if delta <= 0x1000 && delta % 0x200 == 0 {
|
|
anchor = Some(probe);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
probe = probe.wrapping_add(4);
|
|
}
|
|
let anchor = match anchor {
|
|
Some(a) => a,
|
|
None => {
|
|
return Err(UnpackError::AnchorNotFound);
|
|
}
|
|
};
|
|
|
|
// 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 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));
|
|
let p3 = get_u32(&u.decompressed, anchor.wrapping_add(40 + anchor_extra));
|
|
let p4 = get_u32(&u.decompressed, anchor.wrapping_add(44 + anchor_extra));
|
|
// Save the anchor-stage import dir (`saved_import_rva` / `saved_import_size`).
|
|
// On the new layout the metadata data-dirs may carry a zero import entry
|
|
// (esp. managed DLLs); this anchor value is the fallback.
|
|
let saved_import_rva = p1;
|
|
let saved_import_size = p2;
|
|
write_u32(&mut u.decompressed, pe_off.wrapping_add(144), p1);
|
|
write_u32(&mut u.decompressed, pe_off.wrapping_add(148), p2);
|
|
write_u32(&mut u.decompressed, pe_off.wrapping_add(152), p3);
|
|
write_u32(&mut u.decompressed, pe_off.wrapping_add(156), p4);
|
|
write_u32(&mut u.decompressed, pe_off.wrapping_add(176), 0);
|
|
write_u32(&mut u.decompressed, pe_off.wrapping_add(180), 0);
|
|
|
|
let mut walk = anchor.wrapping_add(184 + anchor_extra);
|
|
let mut xor_acc: u32 = 0;
|
|
while get_u32(&u.decompressed, walk.wrapping_add(4)) != 0 {
|
|
xor_acc ^= u.calculate_checksum(walk);
|
|
walk = walk.wrapping_add(8);
|
|
}
|
|
let chk1 = u.calculate_checksum(anchor.wrapping_add(56 + anchor_extra));
|
|
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
|
|
// stage2 (the only 16-byte entry where dword[0]==dword[2], dword[1] is the
|
|
// large encrypted size and dword[3] is a smaller decompressed size), then
|
|
// 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 info3 = u.info[3];
|
|
let info5 = u.info[5];
|
|
// Use the full info[3]..info[3]+info[5] range: stage entries may live in
|
|
// either the decrypt_data2 zone or the raw-copy zone (managed-assembly
|
|
// builds' stage3 sits just before the raw-copy boundary).
|
|
let raw_lo = info3;
|
|
let raw_hi = info3.wrapping_add(info5);
|
|
let mut stage2_off: Option<u32> = None;
|
|
let scan_lo: u32 = 3000;
|
|
let scan_hi: u32 = stage1_len.saturating_sub(16);
|
|
let mut off = scan_lo;
|
|
while off < scan_hi {
|
|
let p = stage1.wrapping_add(off);
|
|
if (p as usize + 16) <= u.decompressed.len() {
|
|
let d0 = get_u32(&u.decompressed, p);
|
|
let d1 = get_u32(&u.decompressed, p.wrapping_add(4));
|
|
let d2 = get_u32(&u.decompressed, p.wrapping_add(8));
|
|
let d3 = get_u32(&u.decompressed, p.wrapping_add(12));
|
|
if d0 == d2 && d0 >= raw_lo && d0 < raw_hi && d1 > 0x10000 && d3 > 0 && d3 < d1 {
|
|
stage2_off = Some(off);
|
|
break;
|
|
}
|
|
}
|
|
off = off.wrapping_add(4);
|
|
}
|
|
let stage2_off = match stage2_off {
|
|
Some(x) => x,
|
|
None => {
|
|
return Err(UnpackError::Stage2NotFound);
|
|
}
|
|
};
|
|
|
|
// Find chk_src_start by walking back from stage2 looking for the first
|
|
// position where 4 consecutive (src, len) entries all sit in the raw-copy
|
|
// range with sensible lengths. The table holds chks for stage5/4/3b/3 in
|
|
// that order; stage1+3472/3480/3488 correspond to chk_src_start + 8 / +16
|
|
// / +24.
|
|
let mut chk_src_start: Option<u32> = None;
|
|
let mut probe = stage2_off.wrapping_sub(200);
|
|
while probe + 32 <= stage2_off {
|
|
let mut all_valid = true;
|
|
for i in 0..4u32 {
|
|
let pp = stage1.wrapping_add(probe + i * 8);
|
|
let s = get_u32(&u.decompressed, pp);
|
|
let l = get_u32(&u.decompressed, pp.wrapping_add(4));
|
|
if s < raw_lo || s >= raw_hi || l == 0 || l >= 0x10000 {
|
|
all_valid = false;
|
|
break;
|
|
}
|
|
}
|
|
if all_valid {
|
|
chk_src_start = Some(probe);
|
|
break;
|
|
}
|
|
probe = probe.wrapping_add(4);
|
|
}
|
|
let chk_src_start = match chk_src_start {
|
|
Some(x) => x,
|
|
None => {
|
|
return Err(UnpackError::ChkSrcStartNotFound);
|
|
}
|
|
};
|
|
|
|
let key_at = stage1.wrapping_add(chk_src_start.wrapping_sub(20));
|
|
let key2 = get_u32(&u.decompressed, key_at);
|
|
let stage1b = stage1.wrapping_add(stage2_off);
|
|
u.decrypt_data3(stage1b, key2, 19);
|
|
let stage2 = get_u32(&u.decompressed, stage1b);
|
|
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
|
|
// table starts with a `kind=1, 0, info[3], 0` 16-byte entry; head is two
|
|
// entries (32 bytes) past that, walk2 sits 88 bytes before head. Observed
|
|
// table_start: 9136 (older EXE builds) and 9216 (another old-layout build
|
|
// / managed-assembly builds).
|
|
let mut table_start: Option<u32> = None;
|
|
let mut sc = 8000u32;
|
|
while sc + 16 < 12000 {
|
|
let p = stage2.wrapping_add(sc);
|
|
if (p as usize + 16) <= u.decompressed.len() {
|
|
let d0 = get_u32(&u.decompressed, p);
|
|
let d1 = get_u32(&u.decompressed, p.wrapping_add(4));
|
|
let d2 = get_u32(&u.decompressed, p.wrapping_add(8));
|
|
let d3 = get_u32(&u.decompressed, p.wrapping_add(12));
|
|
if d0 == 1 && d1 == 0 && d2 == info3 && d3 == 0 {
|
|
table_start = Some(sc);
|
|
break;
|
|
}
|
|
}
|
|
sc = sc.wrapping_add(4);
|
|
}
|
|
let table_start = match table_start {
|
|
Some(x) => x,
|
|
None => {
|
|
return Err(UnpackError::TableStartNotFound);
|
|
}
|
|
};
|
|
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 {
|
|
let kind = get_u32(&u.decompressed, head);
|
|
// Some builds use kind=0x11 in place of kind=1 for the same operation
|
|
// (the upper nibble appears to be a build-version stamp).
|
|
if kind & 0x0F == 1 {
|
|
u.decrypt_data4(head.wrapping_add(4));
|
|
} else if kind == 2 {
|
|
let mut p = get_u32(&u.decompressed, head.wrapping_add(4));
|
|
loop {
|
|
u.decrypt_data5(p, 16);
|
|
let p0 = p;
|
|
let s = get_u32(&u.decompressed, p0);
|
|
let n = get_u32(&u.decompressed, p0.wrapping_add(4));
|
|
let dst = get_u32(&u.decompressed, p0.wrapping_add(8));
|
|
let chk = get_u32(&u.decompressed, p0.wrapping_add(12));
|
|
p = p.wrapping_add(16);
|
|
if s != 0 && n != 0 && dst != 0 && chk == n {
|
|
let ss = s as usize;
|
|
let dd = dst as usize;
|
|
let nn = n as usize;
|
|
u.decompressed.copy_within(ss..ss + nn, dd);
|
|
}
|
|
if get_u32(&u.decompressed, p.wrapping_sub(16).wrapping_add(4)) == 0 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
head = head.wrapping_add(16);
|
|
}
|
|
|
|
let mut walk2 = stage2.wrapping_add(walk2_off);
|
|
for j in 0..2usize {
|
|
let mut p = walk2;
|
|
for k in 0..2usize {
|
|
u.decrypt_data4(p);
|
|
u.key_offsets[j * 2 + k] = get_u32(&u.decompressed, p);
|
|
p = p.wrapping_add(8);
|
|
}
|
|
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 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 {
|
|
running_accum = running_accum.wrapping_add(i);
|
|
i = i.wrapping_add(1);
|
|
}
|
|
accum_candidates.push((l + 1, running_accum));
|
|
}
|
|
let accum = accum_candidates[4].1;
|
|
|
|
let at1 = stage1.wrapping_add(stage2_off.wrapping_add(88));
|
|
let stage3_field = get_u32(&u.decompressed, at1);
|
|
let stage3_slen = get_u32(&u.decompressed, at1.wrapping_add(4));
|
|
let stage3_dest = get_u32(&u.decompressed, at1.wrapping_add(8));
|
|
let stage3_dlen = get_u32(&u.decompressed, at1.wrapping_add(12));
|
|
if verbose {
|
|
println!("[5/9] Decrypting stages 3-5...");
|
|
println!(" stage3 = 0x{:08X}", stage3_field);
|
|
println!(
|
|
" stage3 descriptor = [0x{stage3_field:08X}, 0x{stage3_slen:08X}, 0x{stage3_dest:08X}, 0x{stage3_dlen:08X}]"
|
|
);
|
|
println!(" stage3 key = xor 0x{xor_acc:08X} ^ chk 0x{chk2:08X} ^ val 0x{accum:08X}");
|
|
println!(" stage3 accum seed = 0x{accum_seed:08X}");
|
|
}
|
|
let stage3_key = xor_acc ^ chk2 ^ accum;
|
|
let stage3_source_end = (stage3_field as usize)
|
|
.checked_add(stage3_slen as usize)
|
|
.filter(|&end| end <= u.decompressed.len())
|
|
.ok_or(UnpackError::BufferRangeOutOfBounds {
|
|
operation: BufferOperation::Read,
|
|
offset: stage3_field as usize,
|
|
size: stage3_slen as usize,
|
|
buffer_len: u.decompressed.len(),
|
|
})?;
|
|
let stage3_dest_end = (stage3_dest as usize)
|
|
.checked_add(stage3_dlen as usize)
|
|
.filter(|&end| end <= u.decompressed.len())
|
|
.ok_or(UnpackError::BufferRangeOutOfBounds {
|
|
operation: BufferOperation::CopyDestination,
|
|
offset: stage3_dest as usize,
|
|
size: stage3_dlen as usize,
|
|
buffer_len: u.decompressed.len(),
|
|
})?;
|
|
const MAX_STAGE3_TRIAL_BYTES: usize = 16 * 1024 * 1024;
|
|
let trial_size = (stage3_slen as usize).checked_add(stage3_dlen as usize);
|
|
let stage3_backups = trial_size
|
|
.filter(|&size| size <= MAX_STAGE3_TRIAL_BYTES)
|
|
.map(|_| {
|
|
(
|
|
u.decompressed[stage3_field as usize..stage3_source_end].to_vec(),
|
|
u.decompressed[stage3_dest as usize..stage3_dest_end].to_vec(),
|
|
)
|
|
});
|
|
let default_result = u.decrypt_and_decompress_data(at1, stage3_key, None);
|
|
if let Err(reason) = default_result {
|
|
let Some((source_backup, dest_backup)) = stage3_backups else {
|
|
return Err(UnpackError::StageDecompressionFailed {
|
|
stage: DecompressionStage::ExeStage3,
|
|
reason,
|
|
});
|
|
};
|
|
let restore_stage3 = |data: &mut [u8]| {
|
|
data[stage3_field as usize..stage3_source_end].copy_from_slice(&source_backup);
|
|
data[stage3_dest as usize..stage3_dest_end].copy_from_slice(&dest_backup);
|
|
};
|
|
let mut selected = None;
|
|
for (rounds, candidate_accum) in &accum_candidates {
|
|
if *rounds == 4 {
|
|
continue;
|
|
}
|
|
restore_stage3(&mut u.decompressed);
|
|
let candidate_key = xor_acc ^ chk2 ^ candidate_accum;
|
|
let result = u.decrypt_and_decompress_data(at1, candidate_key, None);
|
|
if result.is_ok()
|
|
&& find_v4_offset(&u.decompressed, stage3_field, stage3_dlen).is_some()
|
|
{
|
|
selected = Some(*rounds);
|
|
break;
|
|
}
|
|
}
|
|
if let Some(rounds) = selected {
|
|
if verbose {
|
|
println!(" selected stage3 accumulator rounds = {rounds}");
|
|
}
|
|
} else {
|
|
restore_stage3(&mut u.decompressed);
|
|
return Err(UnpackError::StageDecompressionFailed {
|
|
stage: DecompressionStage::ExeStage3,
|
|
reason,
|
|
});
|
|
}
|
|
}
|
|
|
|
let at2 = stage1.wrapping_add(stage2_off.wrapping_add(104));
|
|
let stage3b_field = get_u32(&u.decompressed, at2);
|
|
let stage3b_dlen = get_u32(&u.decompressed, at2.wrapping_add(12));
|
|
if verbose {
|
|
println!(" stage3b = 0x{:08X}", stage3b_field);
|
|
}
|
|
let chk3 = u.calculate_checksum(stage1.wrapping_add(chk_src_start.wrapping_add(24)));
|
|
// v4_val lives at the end of stage3, immediately following `C3 CC CC CC`
|
|
// (function epilogue + 3-byte int3 padding), with zeros to end of buffer.
|
|
// Scan stage3 from the end for the last non-zero dword anchored by the
|
|
// C3+CC pattern.
|
|
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 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)));
|
|
// v5_val sits 8 bytes before the first API name string in stage3b's
|
|
// hash/slot/name table. The table starts with hash(VirtualFree) +
|
|
// slot(8 bytes total before the "Virtual..." ASCII). The hardcoded
|
|
// offset 1960 corresponds to (string_pos - 8) for older EXE builds.
|
|
let v5 = find_str_pos(&u.decompressed, stage3b_field, stage3b_dlen, b"Virtual")
|
|
.map(|p| p.wrapping_sub(8))
|
|
.unwrap_or_else(|| stage3b_field.wrapping_add(1960));
|
|
let v5_val = !get_u32(&u.decompressed, v5);
|
|
let at3 = stage1.wrapping_add(stage2_off.wrapping_add(136));
|
|
let stage4_field = get_u32(&u.decompressed, at3);
|
|
let stage4_dlen = get_u32(&u.decompressed, at3.wrapping_add(12));
|
|
if verbose {
|
|
println!(" stage4 = 0x{:08X}", stage4_field);
|
|
}
|
|
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:
|
|
// accum2 source (offset 3504) sits 24 bytes before "IsDebuggerPresent"
|
|
// bytecode block (offset 3584) sits at the first 16-byte aligned
|
|
// boundary at or after the end of "CheckRemoteDebuggerPresent\0".
|
|
let idb_pos = find_str_pos(
|
|
&u.decompressed,
|
|
stage4_field,
|
|
stage4_dlen,
|
|
b"IsDebuggerPresent",
|
|
)
|
|
.unwrap_or_else(|| stage4_field.wrapping_add(3528));
|
|
let crdp_pos = find_str_pos(
|
|
&u.decompressed,
|
|
stage4_field,
|
|
stage4_dlen,
|
|
b"CheckRemoteDebuggerPresent",
|
|
)
|
|
.unwrap_or_else(|| stage4_field.wrapping_add(3553));
|
|
// Trial-decrypt every 16-byte aligned position to find the bytecode block.
|
|
// Works whether or not the build has IsDebuggerPresent/CRDP API strings.
|
|
let data_offset = find_bytecode_offset(&u.decompressed, stage4_field, stage4_dlen)
|
|
.or_else(|| {
|
|
find_bytecode_offset(&u.decompressed, stage4_field, stage4_dlen.saturating_mul(2))
|
|
})
|
|
.unwrap_or_else(|| {
|
|
let crdp_end = crdp_pos.wrapping_add("CheckRemoteDebuggerPresent\0".len() as u32);
|
|
(crdp_end.wrapping_add(15)) & !15u32
|
|
});
|
|
u.decrypt_data6(data_offset);
|
|
let chk5 = u.calculate_checksum(stage1.wrapping_add(chk_src_start.wrapping_add(8)));
|
|
// accum2 sits 4 bytes past the function-end `48 EB 01 B9` + any CC padding
|
|
// (i.e., the 4 bytes immediately after the last instance of that pattern).
|
|
let v6 = find_v_after_pad(&u.decompressed, stage4_field, stage4_dlen)
|
|
.unwrap_or_else(|| idb_pos.wrapping_sub(24));
|
|
let accum2_seed = get_u32(&u.decompressed, v6);
|
|
let mut accum2 = accum2_seed;
|
|
let mut accum2_candidates = vec![(0u32, accum2_seed)];
|
|
for m in 0..8u32 {
|
|
let bound = (m + 1).wrapping_mul(25) << 2;
|
|
let mut i: u32 = 1;
|
|
while i <= bound {
|
|
accum2 = accum2.wrapping_add(i);
|
|
i = i.wrapping_add(1);
|
|
}
|
|
accum2_candidates.push((m + 1, accum2));
|
|
}
|
|
|
|
let ops1 = match generate(&u.decompressed, data_offset) {
|
|
Some(v) => v,
|
|
None => {
|
|
return Err(UnpackError::BytecodeGenerationFailed(
|
|
BytecodeStage::ExeStage4,
|
|
));
|
|
}
|
|
};
|
|
|
|
let at4 = stage1.wrapping_add(stage2_off.wrapping_add(216));
|
|
let stage5_field = get_u32(&u.decompressed, at4);
|
|
let stage5_slen = get_u32(&u.decompressed, at4.wrapping_add(4));
|
|
let stage5_dest = get_u32(&u.decompressed, at4.wrapping_add(8));
|
|
let stage5_dlen = get_u32(&u.decompressed, at4.wrapping_add(12));
|
|
if verbose {
|
|
println!(" stage5 = 0x{:08X}", stage5_field);
|
|
println!(
|
|
" stage5 descriptor = [0x{stage5_field:08X}, 0x{stage5_slen:08X}, 0x{stage5_dest:08X}, 0x{stage5_dlen:08X}]"
|
|
);
|
|
println!(" stage5 bytecode = 0x{data_offset:08X}");
|
|
println!(" stage5 accumulator seed = 0x{accum2_seed:08X} at 0x{v6:08X}");
|
|
}
|
|
let stage5_lo = stage5_field.min(stage5_dest) as usize;
|
|
let stage5_hi = stage5_field
|
|
.checked_add(stage5_slen)
|
|
.zip(stage5_dest.checked_add(stage5_dlen))
|
|
.map(|(source_end, dest_end)| source_end.max(dest_end) as usize)
|
|
.filter(|&end| stage5_lo <= end && end <= u.decompressed.len())
|
|
.ok_or(UnpackError::BufferRangeOutOfBounds {
|
|
operation: BufferOperation::Read,
|
|
offset: stage5_lo,
|
|
size: stage5_slen.max(stage5_dlen) as usize,
|
|
buffer_len: u.decompressed.len(),
|
|
})?;
|
|
let stage5_backup = u.decompressed[stage5_lo..stage5_hi].to_vec();
|
|
let mut first_failure = None;
|
|
let mut selected_rounds = None;
|
|
for rounds in std::iter::once(3u32).chain((0..=8).filter(|&rounds| rounds != 3)) {
|
|
u.decompressed[stage5_lo..stage5_hi].copy_from_slice(&stage5_backup);
|
|
let candidate_accum = accum2_candidates[rounds as usize].1;
|
|
match u.decrypt_and_decompress_data(
|
|
at4,
|
|
xor_acc ^ chk4 ^ chk5 ^ candidate_accum,
|
|
Some(&ops1),
|
|
) {
|
|
Ok(()) => {
|
|
selected_rounds = Some(rounds);
|
|
break;
|
|
}
|
|
Err(reason) => {
|
|
first_failure.get_or_insert(reason);
|
|
}
|
|
}
|
|
}
|
|
let Some(selected_rounds) = selected_rounds else {
|
|
u.decompressed[stage5_lo..stage5_hi].copy_from_slice(&stage5_backup);
|
|
return Err(UnpackError::StageDecompressionFailed {
|
|
stage: DecompressionStage::ExeStage5,
|
|
reason: first_failure.expect("at least one Stage5 candidate was tried"),
|
|
});
|
|
};
|
|
if verbose {
|
|
println!(" selected stage5 accumulator rounds = {selected_rounds}");
|
|
}
|
|
|
|
// Inside stage5, the loader stores a table of (ptr, size) pairs at a
|
|
// fixed offset from the `70 6D 00 00 63 6D 00 00` marker. The first two
|
|
// pairs are walk4 (raw data load) and walk3 (chk2 chain). Then a variable
|
|
// number of additional entries, followed by a `0x40000000, 0x1` kind
|
|
// marker, then walk5 (string-table pointer). The secondary custom
|
|
// decryptor bytecode sits at marker+960.
|
|
let s5_marker_opt = find_str_pos(
|
|
&u.decompressed,
|
|
stage5_field,
|
|
stage5_dlen,
|
|
&[0x70, 0x6D, 0x00, 0x00, 0x63, 0x6D, 0x00, 0x00],
|
|
);
|
|
// Older builds embed the `pm\0\0cm\0\0` marker right before the stage5
|
|
// (ptr,size) table, so it's a tight search base. Newer builds
|
|
// (marker-less native/managed DLLs) omit the marker entirely — the slots
|
|
// are discovered by scanning the whole eighthStage instead. When the
|
|
// marker is absent, fall back to the start of the stage5 region as the
|
|
// search base; the downstream kind-marker + bytecode scans (which derive
|
|
// the real walk3/walk4/walk5 slots) then run over the full region.
|
|
let s5_marker = s5_marker_opt.unwrap_or(stage5_field);
|
|
// The (ptr, size) entry table after the marker ends with a fixed tail:
|
|
// walk4 (raw data load) = kind_pos - 0x20
|
|
// walk3 (checksum chain) = kind_pos - 0x18
|
|
// <large raw-section entry> = kind_pos - 0x10
|
|
// <0x40000000, 0x1 kind> = kind_pos (walk5 = kind_pos + 8)
|
|
// The kind-marker is the only stable anchor: older builds place it at
|
|
// marker+0x58, newer ones at marker+0x60, and walk3's entry size differs
|
|
// (0x50 vs 0x30). Deriving walk3/walk4 relative to the kind-marker
|
|
// handles every observed variant.
|
|
let kind_pos_opt = find_str_pos(
|
|
&u.decompressed,
|
|
s5_marker,
|
|
stage5_dlen.saturating_sub(s5_marker.wrapping_sub(stage5_field)),
|
|
&[0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x00],
|
|
);
|
|
// Layout discriminator. Older builds (the 7 EXE + 2 DLL goldens) embed
|
|
// the `00 00 00 40 01 00 00 00` kind-marker, from which walk3/walk4/walk5
|
|
// are derived at fixed offsets. Newer builds (marker-less native +
|
|
// managed DLLs) omit BOTH the `pm\0\0cm\0\0` and kind markers; their
|
|
// eighthStage slots are discovered structurally (LFSR scan +
|
|
// trial-decrypt), and imports are rebuilt from the PE Import Directory
|
|
// rather than the walk5 encrypted-pointer table. `new_layout` selects
|
|
// that path.
|
|
let new_layout = kind_pos_opt.is_none();
|
|
let kind_pos = kind_pos_opt.unwrap_or_else(|| s5_marker.wrapping_add(88));
|
|
let walk4_slot = kind_pos.wrapping_sub(0x20);
|
|
let walk3_slot = kind_pos.wrapping_sub(0x18);
|
|
let walk5_slot = kind_pos.wrapping_add(8);
|
|
// Stage5 bytecode block. Anchor by trial-decryption + parse; fall back to
|
|
// marker+960 (older EXE build layout).
|
|
let bc2_search_base = s5_marker;
|
|
let bc2_search_len = stage5_dlen.saturating_sub(s5_marker.wrapping_sub(stage5_field));
|
|
let mut bc2_off = find_bytecode_offset(&u.decompressed, bc2_search_base, bc2_search_len)
|
|
.or_else(|| {
|
|
find_bytecode_offset(
|
|
&u.decompressed,
|
|
bc2_search_base,
|
|
bc2_search_len.saturating_mul(2),
|
|
)
|
|
})
|
|
.unwrap_or_else(|| s5_marker.wrapping_add(960));
|
|
// walk4 is the section-load (compressedInfo) descriptor table; in the
|
|
// old layout it sits at a fixed offset from the kind-marker.
|
|
let mut walk4_slot = walk4_slot;
|
|
// New-layout discovery: the kind-marker is absent, so derive the
|
|
// compressedInfo table pointer (walk4) and file-decryptor LFSR (bc2)
|
|
// structurally. fileCS stays at bc2_off-0x58 as in the old layout.
|
|
if new_layout {
|
|
let compress_data_offset = (!get_u32(u.file_data, 0x1080)).wrapping_add(0x1000);
|
|
let slots = layout::discover_eighth_slots(
|
|
&u.decompressed,
|
|
stage5_field,
|
|
stage5_dlen,
|
|
u.info[3],
|
|
compress_data_offset,
|
|
u.file_data.len() as u32,
|
|
)
|
|
.ok_or(UnpackError::Stage5MarkerNotFound)?;
|
|
walk4_slot = slots.compressed_info_ptr;
|
|
bc2_off = slots.file_lfsr;
|
|
}
|
|
let at5 = walk3_slot;
|
|
let mut walk3 = get_u32(&u.decompressed, at5);
|
|
// walk3 is a checksum-validation walk: its chain_crc is computed and
|
|
// discarded. Each 16-byte entry is decrypted only transiently (to feed
|
|
// the checksum); the on-disk bytes stay encrypted. Back up each block
|
|
// before decrypting and restore after, so the output matches the golden
|
|
// (which keeps this chain encrypted). The new layout has no walk3 chain
|
|
// (the kind-marker that anchors it is absent), so this whole transient
|
|
// checksum walk is skipped there.
|
|
let mut walk3_backups: Vec<(usize, [u8; 16])> = Vec::new();
|
|
let snap16 = |buf: &[u8], addr: u32| -> [u8; 16] {
|
|
let s = addr as usize;
|
|
let mut b = [0u8; 16];
|
|
b.copy_from_slice(&buf[s..s + 16]);
|
|
b
|
|
};
|
|
if !new_layout {
|
|
walk3_backups.push((walk3 as usize, snap16(&u.decompressed, walk3)));
|
|
u.decrypt_data5(walk3, 16);
|
|
walk3 = walk3.wrapping_add(16);
|
|
let mut chain_crc: u32 = 0;
|
|
loop {
|
|
walk3_backups.push((walk3 as usize, snap16(&u.decompressed, walk3)));
|
|
u.decrypt_data5(walk3, 16);
|
|
let p = walk3;
|
|
let n = get_u32(&u.decompressed, p.wrapping_add(4));
|
|
walk3 = walk3.wrapping_add(16);
|
|
if n != 0 {
|
|
match u.calculate_checksum2(walk3.wrapping_sub(16), chain_crc) {
|
|
Ok(next) => chain_crc = next,
|
|
Err(UnpackError::ExeChecksumRangeOutOfBounds { .. }) => {
|
|
if verbose {
|
|
println!(
|
|
" checksum chain terminates at 0x{:08X}: descriptor payload is outside protected input",
|
|
walk3.wrapping_sub(16)
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
if get_u32(&u.decompressed, walk3.wrapping_sub(16).wrapping_add(4)) == 0 {
|
|
break;
|
|
}
|
|
}
|
|
if verbose {
|
|
println!("[6/9] Decrypting section data...");
|
|
println!(
|
|
" integrity = 0x{:08X}",
|
|
get_u32(u.file_data, 56).wrapping_add(1985229329)
|
|
);
|
|
println!(" chain_crc = 0x{:08X}", chain_crc);
|
|
}
|
|
for (addr, bytes) in walk3_backups {
|
|
u.decompressed[addr..addr + 16].copy_from_slice(&bytes);
|
|
}
|
|
} else if verbose {
|
|
println!("[6/9] Decrypting section data (new layout)...");
|
|
}
|
|
|
|
let data_offset2 = bc2_off;
|
|
// fileCS chain: the file-checksum table is permanently decrypted in
|
|
// place. Its pointer lives 0x58 bytes before the file LFSR/decryptor
|
|
// block (fileDecryptorAddress = fileChecksumAddresses + 0x58). Walk
|
|
// 16-byte entries until the size dword (offset +4) is zero, decrypting
|
|
// each.
|
|
{
|
|
let mut fcs = get_u32(&u.decompressed, bc2_off.wrapping_sub(0x58));
|
|
while get_u32(&u.decompressed, fcs.wrapping_add(4)) != 0 {
|
|
u.decrypt_data5(fcs, 16);
|
|
fcs = fcs.wrapping_add(16);
|
|
}
|
|
}
|
|
u.decrypt_data6(data_offset2);
|
|
let ops2 = match generate(&u.decompressed, data_offset2) {
|
|
Some(v) => v,
|
|
None => {
|
|
return Err(UnpackError::BytecodeGenerationFailed(
|
|
BytecodeStage::ExeStage5,
|
|
));
|
|
}
|
|
};
|
|
// The new layout picked its file decryptor by distance (no marker, no
|
|
// content check). Prove the choice before shipping it: a wrong pick
|
|
// would garble every section block, and raw blocks would carry that
|
|
// garbling into the output without any error (see the validator).
|
|
let rebase = (!get_u32(u.file_data, 4224)).wrapping_add(4096);
|
|
if new_layout && !u.new_layout_file_ops_validate(walk4_slot, &ops2, rebase) {
|
|
return Err(UnpackError::FileDecryptorValidationFailed);
|
|
}
|
|
|
|
let at6 = walk4_slot;
|
|
if verbose {
|
|
println!("[7/9] Loading and decompressing sections...");
|
|
}
|
|
let mut walk4 = get_u32(&u.decompressed, at6);
|
|
// Pass 1 (sequential): the 16-byte descriptors are decrypted in a
|
|
// position-keyed chain (`decrypt_data5`) that terminates on the next
|
|
// record's length field, so collection cannot be parallelized.
|
|
struct Blk {
|
|
src: u32,
|
|
len: u32,
|
|
dst: u32,
|
|
plain_len: u32,
|
|
}
|
|
let mut blocks: Vec<Blk> = Vec::new();
|
|
loop {
|
|
u.decrypt_data5(walk4, 16);
|
|
let p = walk4;
|
|
let src = get_u32(&u.decompressed, p);
|
|
let len = get_u32(&u.decompressed, p.wrapping_add(4));
|
|
let dst = get_u32(&u.decompressed, p.wrapping_add(8));
|
|
let plain_len = get_u32(&u.decompressed, p.wrapping_add(12));
|
|
walk4 = walk4.wrapping_add(16);
|
|
if len != 0 {
|
|
blocks.push(Blk {
|
|
src,
|
|
len,
|
|
dst,
|
|
plain_len,
|
|
});
|
|
}
|
|
if get_u32(&u.decompressed, walk4.wrapping_sub(16).wrapping_add(4)) == 0 {
|
|
break;
|
|
}
|
|
}
|
|
// Pass 2: each block writes only its own [dst, dst+max(len,plain_len))
|
|
// span and reads only immutable input + the (snapshotted) key tables,
|
|
// so blocks with disjoint dst spans are independent. `parallel_for`
|
|
// carves the spans into safe disjoint &mut slices and fans out when
|
|
// worthwhile (see its docs for the soundness argument).
|
|
{
|
|
let lut = OpsLut::new(&ops2);
|
|
let clean = &u.file_data;
|
|
let ko = u.key_offsets;
|
|
// Snapshot the shared tables before the fan-out: workers get
|
|
// disjoint span slices, not the whole buffer.
|
|
let ks_snap = primitives::aes_schedule_snapshot(&u.decompressed, ko[2])
|
|
.ok_or(UnpackError::InvalidAesKeySchedule { offset: ko[2] })?;
|
|
let tab_snap = primitives::huffman_table_snapshot(&u.decompressed, ko[0])
|
|
.ok_or(UnpackError::InvalidHuffmanTable { offset: ko[0] })?;
|
|
let spans: Vec<(usize, usize)> = blocks
|
|
.iter()
|
|
.map(|b| {
|
|
let s = b.dst as usize;
|
|
(s, s + b.len.max(b.plain_len) as usize)
|
|
})
|
|
.collect();
|
|
let do_block = |i: usize, base: usize, span: &mut [u8]| -> Result<(), UnpackError> {
|
|
let b = &blocks[i];
|
|
let cs = b.src.wrapping_add(rebase) as usize;
|
|
let rel = b.dst as usize - base;
|
|
let ll = b.len as usize;
|
|
span[rel..rel + ll].copy_from_slice(&clean[cs..cs + ll]);
|
|
primitives::aes_decrypt_ks(&ks_snap, span, rel as u32, b.len);
|
|
lut.map_region(span, rel, ll);
|
|
if b.len != b.plain_len {
|
|
// decompress reports corruption (after partial writes) via
|
|
// its bool; surface it instead of shipping a garbage block.
|
|
if !primitives::decompress_tbl(
|
|
&tab_snap,
|
|
span,
|
|
rel as u32,
|
|
rel as u32,
|
|
b.len,
|
|
b.plain_len,
|
|
) {
|
|
return Err(UnpackError::SectionDecompressionFailed {
|
|
pipeline: SectionPipeline::ExePe32Plus,
|
|
block: i,
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
};
|
|
super::super::parallel::parallel_for(&mut u.decompressed, &spans, 1, do_block)?;
|
|
}
|
|
loop {
|
|
u.decrypt_data5(walk4, 16);
|
|
let p = walk4;
|
|
let dst = get_u32(&u.decompressed, p);
|
|
let len = get_u32(&u.decompressed, p.wrapping_add(4));
|
|
walk4 = walk4.wrapping_add(16);
|
|
if len != 0 {
|
|
for k in 0..len {
|
|
u.decompressed[(dst + k) as usize] = 0;
|
|
}
|
|
}
|
|
if get_u32(&u.decompressed, walk4.wrapping_sub(16).wrapping_add(4)) == 0 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let at7 = walk5_slot;
|
|
if verbose {
|
|
println!("[8/9] Decrypting import strings...");
|
|
}
|
|
// walk5 is the old layout's encrypted import-name pointer table. The new
|
|
// layout has no such table — imports are rebuilt from the PE Import
|
|
// Directory after the header is reconstructed (see `process_imports_idt`
|
|
// below). Skip the walk5 pass entirely for the new layout.
|
|
//
|
|
// Managed assemblies leave the walk5 slot null as well: their imports
|
|
// are just the CLR bootstrap stub, so there is no encrypted name table
|
|
// to walk. Reading the table at address 0 would chase header garbage as
|
|
// a pointer chain, so a null slot means "nothing to decrypt".
|
|
let mut walk5 = get_u32(&u.decompressed, at7);
|
|
if !new_layout && walk5 != 0 {
|
|
loop {
|
|
let outer = get_u32(&u.decompressed, walk5.wrapping_add(12));
|
|
if outer == 0 {
|
|
break;
|
|
}
|
|
u.decrypt_data7(outer, outer as u8);
|
|
// Normalize the DLL name to lowercase (e.g. 'UnityPlayer.dll' ->
|
|
// 'unityplayer.dll'). Only when the name is all printable ASCII.
|
|
{
|
|
let start = outer as usize;
|
|
let mut end = start;
|
|
while end < u.decompressed.len() && u.decompressed[end] != 0 {
|
|
end += 1;
|
|
}
|
|
if end > start
|
|
&& u.decompressed[start..end]
|
|
.iter()
|
|
.all(|&b| (0x20..0x7F).contains(&b))
|
|
{
|
|
for b in &mut u.decompressed[start..end] {
|
|
b.make_ascii_lowercase();
|
|
}
|
|
}
|
|
}
|
|
let a = get_u32(&u.decompressed, walk5);
|
|
let b = get_u32(&u.decompressed, walk5.wrapping_add(16));
|
|
let mut chain = if a == 0 { b } else { a };
|
|
loop {
|
|
// PE32+ thunks are 8 bytes: an ordinal import carries bit 63
|
|
// with the ordinal in the low word; only a by-name thunk holds
|
|
// a hint/name RVA (in the low dword). Reading just the low
|
|
// dword would mistake an ordinal for a tiny RVA and scribble
|
|
// over the image header.
|
|
let v = get_u64(&u.decompressed, chain);
|
|
if v == 0 {
|
|
break;
|
|
}
|
|
if (v & 0x8000_0000_0000_0000) == 0 {
|
|
let inner = v as u32;
|
|
u.decrypt_data7(inner.wrapping_add(2), inner as u8);
|
|
write_u16(&mut u.decompressed, inner, 0);
|
|
}
|
|
chain = chain.wrapping_add(8);
|
|
}
|
|
walk5 = walk5.wrapping_add(20);
|
|
}
|
|
} // end !new_layout (walk5)
|
|
|
|
if verbose {
|
|
println!("[9/9] Reconstructing PE headers...");
|
|
}
|
|
u.decompressed[..4096].copy_from_slice(&u.file_data[..4096]);
|
|
let opt_hdr_size = get_u16(u.file_data, pe_off.wrapping_add(20));
|
|
let sect_table = pe_off.wrapping_add(24).wrapping_add(opt_hdr_size as u32);
|
|
let payload_va = get_u32(
|
|
u.file_data,
|
|
pe_off
|
|
.wrapping_add(24)
|
|
.wrapping_add(opt_hdr_size as u32)
|
|
.wrapping_sub(128),
|
|
);
|
|
let payload_size = get_u32(
|
|
u.file_data,
|
|
pe_off
|
|
.wrapping_add(24)
|
|
.wrapping_add(opt_hdr_size as u32)
|
|
.wrapping_sub(124),
|
|
);
|
|
// Walk the section table by NumberOfSections, not "until a zero
|
|
// VirtualSize": PE has no sentinel entry, so a section whose
|
|
// VirtualSize is legitimately 0 (or a corrupt early field) would
|
|
// silently truncate the fixups — .text, and the export payload below,
|
|
// would then be missed entirely. The all-zero-name break guards the
|
|
// other direction (a corrupt, overstated NumberOfSections): real
|
|
// sections always have a name, header padding is all zero.
|
|
let num_sections = get_u16(u.file_data, pe_off.wrapping_add(6)) as u32;
|
|
let mut text_va: u32 = 0;
|
|
let mut text_size: u32 = 0;
|
|
let mut payload_off: u32 = 0;
|
|
for i in 0..num_sections.min(96) {
|
|
let sect = sect_table.wrapping_add(i.wrapping_mul(40));
|
|
if u.file_data[sect as usize..sect as usize + 8]
|
|
.iter()
|
|
.all(|&b| b == 0)
|
|
{
|
|
break;
|
|
}
|
|
let sec_va = get_u32(u.file_data, sect.wrapping_add(12));
|
|
let sec_size = get_u32(u.file_data, sect.wrapping_add(8));
|
|
let sec_raw = get_u32(u.file_data, sect.wrapping_add(20));
|
|
if section_name(u.file_data, sect) == ".text" {
|
|
text_size = sec_size;
|
|
text_va = sec_va;
|
|
}
|
|
if payload_size != 0
|
|
&& payload_va >= sec_va
|
|
&& payload_va.wrapping_add(payload_size) <= sec_va.wrapping_add(sec_size)
|
|
{
|
|
payload_off = payload_va.wrapping_sub(sec_va).wrapping_add(sec_raw);
|
|
}
|
|
write_u32(&mut u.decompressed, sect.wrapping_add(16), sec_size);
|
|
write_u32(&mut u.decompressed, sect.wrapping_add(20), sec_va);
|
|
// NOTE: do NOT flag .rdata writable. The Windows loader already
|
|
// makes the IAT pages temporarily writable while snapping imports
|
|
// (it knows the range from the IAT data directory DD[12]), so the
|
|
// original read-only .rdata characteristics are sufficient.
|
|
//
|
|
// Marking .rdata MEM_WRITE actively breaks statically-linked
|
|
// MSVC/UCRT EXEs: the CRT's float-format init (`_cfltcvt_init`,
|
|
// which populates `_cfltcvt_tab`) is gated by a security check that
|
|
// refuses to call an init function whose descriptor lives in a
|
|
// *writable* section. With .rdata writable that init is skipped,
|
|
// `_cfltcvt_tab` keeps its R6002 stubs, and the first `%f` aborts
|
|
// with "R6002 - floating point support not loaded" (observed on some
|
|
// statically-linked MSVC/UCRT EXEs).
|
|
}
|
|
// Guard `payload_off != 0` like the PE32 path does: if no section
|
|
// contained the export range, offset 0 would copy the DOS stub over
|
|
// the image's export directory.
|
|
if payload_size != 0 && payload_off != 0 {
|
|
let s = payload_off as usize;
|
|
let d = payload_va as usize;
|
|
let n = payload_size as usize;
|
|
u.decompressed[d..d + n].copy_from_slice(&u.file_data[s..s + n]);
|
|
}
|
|
// EP/data-directory layout. For the new layout (marker-less), the
|
|
// metadata block (EP@info[3]+0x20, dirs@info[3]+0x30, "Layout B") is read
|
|
// BEFORE running the .text dd8 pass, then the encrypted bytes are
|
|
// restored so dd8 operates on them like the golden. Because info[3]
|
|
// sits inside .text on these builds, reading after dd8 (as the old
|
|
// layout does) would see dd8-corrupted metadata. So capture EP + 128
|
|
// dir bytes here, pre-dd8.
|
|
let new_ep_dirs: Option<(u32, [u8; 128])> = if new_layout {
|
|
let ms = u.info[3].wrapping_add(32) as usize;
|
|
let backup: Vec<u8> = u.decompressed[ms..ms + 144].to_vec();
|
|
u.decrypt_data5(u.info[3].wrapping_add(32), 144);
|
|
let ep = get_u32(&u.decompressed, u.info[3].wrapping_add(32));
|
|
let mut dirs = [0u8; 128];
|
|
dirs.copy_from_slice(
|
|
&u.decompressed[(u.info[3] + 48) as usize..(u.info[3] + 48 + 128) as usize],
|
|
);
|
|
u.decompressed[ms..ms + 144].copy_from_slice(&backup);
|
|
Some((ep, dirs))
|
|
} else {
|
|
None
|
|
};
|
|
// Old layout runs dd8 here (its metadata/.text regions don't overlap the
|
|
// not-yet-restored COR20/BSJB metadata). The new layout defers dd8 until
|
|
// AFTER the CLR metadata restore + section fixup (dd8 is the final .text
|
|
// step), because on managed DLLs the restored BSJB stream lives inside
|
|
// .text and must itself be dd8-processed to match the golden — and the
|
|
// shift is re-selected there over the restored bytes, so selecting it
|
|
// here would be wasted work on stale content.
|
|
if !new_layout {
|
|
// The packer keys the dd8 page-XOR with `page_idx << shift`. Most
|
|
// builds use shift 0; some newer builds use shift 15. The shift is
|
|
// NOT recorded in any header/config field — some older and newer
|
|
// builds carry byte-identical config-version stamps yet need
|
|
// different shifts — so it must be derived from the .text content
|
|
// (see `select_dd8_shift`). An explicit DD8_SHIFT env var (incl.
|
|
// 99 = skip) overrides for analysis.
|
|
let dd8_shift: u32 = match std::env::var("DD8_SHIFT").ok().and_then(|s| s.parse().ok())
|
|
{
|
|
Some(s) => s,
|
|
None => layout::select_dd8_shift(&u.decompressed, text_va, text_size, u.info[3]),
|
|
};
|
|
if dd8_shift != 99 {
|
|
let mut page = text_va >> 12;
|
|
let end_page = text_va.wrapping_add(text_size) >> 12;
|
|
while page < end_page {
|
|
u.decrypt_data8(page << 12, 4096, page << dd8_shift);
|
|
page = page.wrapping_add(1);
|
|
}
|
|
}
|
|
}
|
|
// EP/DD layout in info[3] varies between Crackproof versions. Old
|
|
// layout: EP at info[3]+64, ImageBase at info[3]+68, DD[0..15] at
|
|
// info[3]+80..info[3]+208 — total 144 bytes encrypted starting at +64.
|
|
// New layout: everything shifts 32 bytes earlier — EP at info[3]+32,
|
|
// DD at info[3]+48, encrypted region at info[3]+32..info[3]+176. Probe
|
|
// both candidates and pick the one whose ImageBase-low matches info[3]
|
|
// (the dword right after EP in the PE optional header).
|
|
let ep_off: u32 = if new_layout {
|
|
// New-layout (marker-less) builds always use "Layout B": EP at
|
|
// info[3]+0x20, data dirs at info[3]+0x30. The old ImageBase-low
|
|
// probe assumes ImageBase==info[3], which does not hold for these
|
|
// builds (esp. managed DLLs), so pin the offset directly.
|
|
32
|
|
} else {
|
|
[32u32, 64]
|
|
.iter()
|
|
.copied()
|
|
.find(|&off| {
|
|
trial_decrypt5_u32(&u.decompressed, u.info[3].wrapping_add(off + 4))
|
|
== u.info[3]
|
|
})
|
|
.unwrap_or(64)
|
|
};
|
|
let dd_off = ep_off.wrapping_add(16);
|
|
// The metadata block (EP + data directories) is read transiently: decrypt
|
|
// it, copy EP and dirs into the PE header, then RESTORE the original
|
|
// encrypted bytes. The packer leaves this region encrypted in its output,
|
|
// so leaving it decrypted in-place would diverge from the golden (the
|
|
// first byte at info[3]+ep_off would carry the decrypted EP low byte).
|
|
let pe_off2 = get_u32(&u.decompressed, 60);
|
|
if let Some((ep, dirs)) = new_ep_dirs {
|
|
// New layout: EP and dirs were captured pre-dd8 (Layout B). Write the
|
|
// EP and the 128-byte data-directory block into the PE header.
|
|
write_u32(&mut u.decompressed, pe_off2.wrapping_add(40), ep);
|
|
u.decompressed[(pe_off2 + 136) as usize..(pe_off2 + 136) as usize + 128]
|
|
.copy_from_slice(&dirs);
|
|
// Import-RVA selection. Prefer the metadata import dir (dirs[1] @ +8)
|
|
// when it points at a plausible IDT; otherwise fall back to the
|
|
// anchor-stage value saved earlier whenever it is set at all (a
|
|
// non-zero-but-implausible anchor value still beats a metadata dir
|
|
// we already rejected). Managed DLLs leave the metadata import dir
|
|
// zero, so the anchor value is used there.
|
|
let image_size = get_u32(&u.decompressed, pe_off2.wrapping_add(80));
|
|
let meta_imp_rva = u32::from_le_bytes([dirs[8], dirs[9], dirs[10], dirs[11]]);
|
|
let meta_imp_size = u32::from_le_bytes([dirs[12], dirs[13], dirs[14], dirs[15]]);
|
|
let idt_plausible = |rva: u32, sz: u32, d: &[u8]| -> bool {
|
|
if !(0x1000 < rva && rva < image_size && 0 < sz && sz < 0x10000) {
|
|
return false;
|
|
}
|
|
((rva + 12) as usize + 4) <= d.len() && get_u32(d, rva + 12) != 0
|
|
};
|
|
let (imp_rva, imp_size) = if idt_plausible(meta_imp_rva, meta_imp_size, &u.decompressed)
|
|
|| saved_import_rva == 0
|
|
{
|
|
(meta_imp_rva, meta_imp_size)
|
|
} else {
|
|
(saved_import_rva, saved_import_size)
|
|
};
|
|
write_u32(&mut u.decompressed, pe_off2.wrapping_add(0x90), imp_rva);
|
|
write_u32(&mut u.decompressed, pe_off2.wrapping_add(0x94), imp_size);
|
|
} else {
|
|
// The metadata block (EP + data directories) is read transiently:
|
|
// decrypt it, copy EP and dirs into the PE header, then RESTORE the
|
|
// original encrypted bytes. The packer leaves this region encrypted
|
|
// in its output, so leaving it decrypted in-place would diverge from
|
|
// the golden (the first byte at info[3]+ep_off would carry the
|
|
// decrypted EP low byte).
|
|
let meta_start = u.info[3].wrapping_add(ep_off) as usize;
|
|
let backup: Vec<u8> = u.decompressed[meta_start..meta_start + 144].to_vec();
|
|
u.decrypt_data5(u.info[3].wrapping_add(ep_off), 144);
|
|
let ep = get_u32(&u.decompressed, u.info[3].wrapping_add(ep_off));
|
|
// Managed assemblies store 0 as the entry point here (their EP is a
|
|
// property of the CLR header, not the PE). Keep the protected
|
|
// header's EP in that case — overwriting with 0 would produce an
|
|
// image whose entry point is the DOS header.
|
|
if ep != 0 {
|
|
write_u32(&mut u.decompressed, pe_off2.wrapping_add(40), ep);
|
|
}
|
|
for n in 0..128 {
|
|
u.decompressed[(pe_off2 + 136 + n) as usize] =
|
|
u.decompressed[(u.info[3] + dd_off + n) as usize];
|
|
}
|
|
u.decompressed[meta_start..meta_start + 144].copy_from_slice(&backup);
|
|
}
|
|
|
|
// New-layout import reconstruction runs AFTER the deferred .text dd8
|
|
// pass (see below): dd8 precedes the IDT name/thunk decryption. The
|
|
// import RVA lives inside .text on these builds, so decrypting names
|
|
// before dd8 would let dd8 re-scramble them. The actual call is placed
|
|
// after the dd8 block.
|
|
|
|
// New-layout managed (CLR) metadata restore happens AFTER the .text dd8
|
|
// pass below. CrackProof preserves the COR20 header + BSJB MetaData
|
|
// verbatim in the protected file; both live inside .text on these
|
|
// builds. Restoring them before dd8 would let dd8 corrupt the metadata
|
|
// (~1 byte per 16) and leave an invalid COR20 header signature. We
|
|
// restore after dd8 so the copied-back bytes are final. `restored_clr`
|
|
// (set in that later block) suppresses the native COR20-directory
|
|
// clearing.
|
|
let mut restored_clr = false;
|
|
|
|
// Deferred .text dd8 for the new layout (dd8 is the final .text step).
|
|
// The shift/formula is re-selected here over the now-fully-restored
|
|
// .text so the 0xCC-padding heuristic scores the real post-restore
|
|
// bytes. Only run dd8 when the entry point falls inside .text.
|
|
if new_layout {
|
|
let ep_final = get_u32(&u.decompressed, pe_off2.wrapping_add(40));
|
|
let ep_in_text = text_size > 0
|
|
&& text_va > 0
|
|
&& text_va <= ep_final
|
|
&& ep_final < text_va + text_size;
|
|
if ep_in_text {
|
|
let shift = match std::env::var("DD8_SHIFT").ok().and_then(|s| s.parse().ok()) {
|
|
Some(s) => s,
|
|
None => {
|
|
layout::select_dd8_shift(&u.decompressed, text_va, text_size, u.info[3])
|
|
}
|
|
};
|
|
if shift != 99 {
|
|
let mut page = text_va >> 12;
|
|
let end_page = text_va.wrapping_add(text_size) >> 12;
|
|
while page < end_page {
|
|
u.decrypt_data8(page << 12, 4096, page << shift);
|
|
page = page.wrapping_add(1);
|
|
}
|
|
}
|
|
}
|
|
// IDT name/thunk decryption, after dd8. The PE Import Directory
|
|
// points at the IDT; decrypt+lowercase each DLL name and each
|
|
// by-name import's hint/name, and recover the IAT (DD[12]).
|
|
let import_rva = get_u32(&u.decompressed, pe_off2.wrapping_add(0x90));
|
|
let import_size = get_u32(&u.decompressed, pe_off2.wrapping_add(0x94));
|
|
if import_rva != 0 {
|
|
u.process_imports_idt(import_rva, import_size, pe_off2);
|
|
}
|
|
|
|
// Managed (CLR) COR20 + BSJB MetaData restore — AFTER dd8 so the
|
|
// verbatim-copied bytes are final. CrackProof preserves these regions
|
|
// in the protected file at their RVA-mapped offsets; they live inside
|
|
// .text but must NOT be dd8-processed (they are not packer-encrypted
|
|
// code, just copied through). Restoring post-dd8 overwrites whatever
|
|
// dd8 scribbled, yielding a valid CLR header + BSJB stream.
|
|
let clr_rva = get_u32(&u.decompressed, pe_off2.wrapping_add(0xF8));
|
|
let clr_size = get_u32(&u.decompressed, pe_off2.wrapping_add(0xFC));
|
|
if clr_rva != 0
|
|
&& clr_size != 0
|
|
&& (clr_rva as u64 + clr_size as u64) <= u.decompressed.len() as u64
|
|
&& let Some(cor_off) = prot_rva_to_off(u.file_data, pe_off, clr_rva)
|
|
&& (cor_off as u64 + 0x48) <= u.file_data.len() as u64
|
|
&& get_u32(u.file_data, cor_off) == 0x48
|
|
{
|
|
// Restore the COR20 header from the protected file (its
|
|
// MetaData RVA/size fields are authoritative).
|
|
let s = cor_off as usize;
|
|
let d = clr_rva as usize;
|
|
u.decompressed[d..d + 0x48].copy_from_slice(&u.file_data[s..s + 0x48]);
|
|
restored_clr = true;
|
|
// Read MetaData RVA/size from the just-restored header.
|
|
let md_rva = get_u32(&u.decompressed, clr_rva + 0x08);
|
|
let md_size = get_u32(&u.decompressed, clr_rva + 0x0C);
|
|
if md_rva != 0
|
|
&& md_size != 0
|
|
&& (md_rva as u64 + md_size as u64) <= u.decompressed.len() as u64
|
|
&& let Some(md_off) = prot_rva_to_off(u.file_data, pe_off, md_rva)
|
|
&& (md_off as u64 + md_size as u64) <= u.file_data.len() as u64
|
|
&& &u.file_data[md_off as usize..md_off as usize + 4] == b"BSJB"
|
|
{
|
|
let s = md_off as usize;
|
|
let d = md_rva as usize;
|
|
let n = md_size as usize;
|
|
u.decompressed[d..d + n].copy_from_slice(&u.file_data[s..s + n]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Old-layout managed (CLR) restore: same verbatim regions as the
|
|
// new-layout restore above (COR20 header + BSJB MetaData stream) plus
|
|
// the COR20 resources blob. Crackproof preserves only these regions
|
|
// verbatim in the protected file — the IL method bodies between the
|
|
// COR20 header and the resources ARE packer-encrypted and arrive via
|
|
// the section-block pass, so copying the whole section's raw data (as
|
|
// the older-DLL pipeline does for its layout) would clobber them with
|
|
// the placeholder zeros the protected file carries there. Runs after
|
|
// the old-layout dd8 pass, so the restored bytes are final.
|
|
if !new_layout {
|
|
let clr_rva = get_u32(&u.decompressed, pe_off2.wrapping_add(0xF8));
|
|
let clr_size = get_u32(&u.decompressed, pe_off2.wrapping_add(0xFC));
|
|
if clr_rva != 0
|
|
&& clr_size != 0
|
|
&& (clr_rva as u64 + clr_size as u64) <= u.decompressed.len() as u64
|
|
&& let Some(cor_off) = prot_rva_to_off(u.file_data, pe_off, clr_rva)
|
|
&& (cor_off as u64 + 0x48) <= u.file_data.len() as u64
|
|
&& get_u32(u.file_data, cor_off) == 0x48
|
|
{
|
|
let s = cor_off as usize;
|
|
let d = clr_rva as usize;
|
|
u.decompressed[d..d + 0x48].copy_from_slice(&u.file_data[s..s + 0x48]);
|
|
restored_clr = true;
|
|
// MetaData RVA/size from the just-restored COR20 header.
|
|
let md_rva = get_u32(&u.decompressed, clr_rva + 0x08);
|
|
let md_size = get_u32(&u.decompressed, clr_rva + 0x0C);
|
|
if md_rva != 0
|
|
&& md_size != 0
|
|
&& (md_rva as u64 + md_size as u64) <= u.decompressed.len() as u64
|
|
&& let Some(md_off) = prot_rva_to_off(u.file_data, pe_off, md_rva)
|
|
&& (md_off as u64 + md_size as u64) <= u.file_data.len() as u64
|
|
&& &u.file_data[md_off as usize..md_off as usize + 4] == b"BSJB"
|
|
{
|
|
let s = md_off as usize;
|
|
let d = md_rva as usize;
|
|
let n = md_size as usize;
|
|
u.decompressed[d..d + n].copy_from_slice(&u.file_data[s..s + n]);
|
|
}
|
|
// COR20 resources (managed .resources blob), verbatim too.
|
|
let res_rva = get_u32(&u.decompressed, clr_rva + 0x18);
|
|
let res_size = get_u32(&u.decompressed, clr_rva + 0x1C);
|
|
if res_rva != 0
|
|
&& res_size != 0
|
|
&& (res_rva as u64 + res_size as u64) <= u.decompressed.len() as u64
|
|
&& let Some(res_off) = prot_rva_to_off(u.file_data, pe_off, res_rva)
|
|
&& (res_off as u64 + res_size as u64) <= u.file_data.len() as u64
|
|
{
|
|
let s = res_off as usize;
|
|
let d = res_rva as usize;
|
|
let n = res_size as usize;
|
|
u.decompressed[d..d + n].copy_from_slice(&u.file_data[s..s + n]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The payload's TLS directory (DD[9]) arrives blanked: Crackproof strips
|
|
// the struct and re-installs TLS itself when it maps the module. Prefer
|
|
// recovering the real one from the stub's plaintext `.rdata` — dropping
|
|
// DD[9] leaves `_tls_index` unwritten, so every `thread_local` access
|
|
// resolves through TLS slot 0 (another module's block, or NULL). Only if
|
|
// the stub can't supply it do we clear the entry, which at least stops
|
|
// the loader writing through a NULL `AddressOfIndex`.
|
|
//
|
|
// Old builds have no TLS directory at all (DD[9] already 0), so this is
|
|
// a no-op for them. Restricted to the old, `/FIXED` layout: the caller
|
|
// zeroes BaseReloc/DllCharacteristics below, so the restored absolute
|
|
// VAs stay valid without relocations. The new-layout companion case is
|
|
// handled by `job::restore_tls_from_stub`.
|
|
let tls_dd_off = pe_off2.wrapping_add(136 + 9 * 8);
|
|
let tls_rva = get_u32(&u.decompressed, tls_dd_off);
|
|
if tls_rva != 0 && (tls_rva as usize + 40) <= u.decompressed.len() {
|
|
let tls_all_zero = u.decompressed[tls_rva as usize..tls_rva as usize + 40]
|
|
.iter()
|
|
.all(|&b| b == 0);
|
|
if tls_all_zero {
|
|
let image_base = get_u64(&u.decompressed, pe_off2.wrapping_add(48));
|
|
if new_layout || !u.restore_pe64_tls_from_stub(pe_off, tls_rva, image_base) {
|
|
write_u32(&mut u.decompressed, tls_dd_off, 0);
|
|
write_u32(&mut u.decompressed, tls_dd_off.wrapping_add(4), 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the COR20 (CLR) directory points at a zero-cb header, clear it so
|
|
// the loader treats the image as native instead of handing off to
|
|
// mscoree._CorExeMain (which crashes on the empty header). Skipped when
|
|
// the new-layout managed restore above repopulated a real COR20 header.
|
|
let cor20_dd_off = pe_off2.wrapping_add(0xF8);
|
|
let cor20_rva = get_u32(&u.decompressed, cor20_dd_off);
|
|
if !restored_clr
|
|
&& cor20_rva != 0
|
|
&& (cor20_rva as usize + 4) <= u.decompressed.len()
|
|
&& get_u32(&u.decompressed, cor20_rva) == 0
|
|
{
|
|
write_u32(&mut u.decompressed, cor20_dd_off, 0);
|
|
write_u32(&mut u.decompressed, cor20_dd_off.wrapping_add(4), 0);
|
|
}
|
|
|
|
// Old-layout CrackProof binaries are /FIXED: the shell discards the
|
|
// relocation table and clears DllCharacteristics, so the loader needs no
|
|
// relocations (exe_pe+0x5E / +0xB0). The new-layout (external-companion)
|
|
// modules are *not* /FIXED — they keep a real DllCharacteristics
|
|
// (ASLR/high-entropy) and a valid BaseReloc table (DD[5]); zeroing those
|
|
// produces a DLL that the loader can only place at its preferred base,
|
|
// and any rebase leaves every pointer — including the restored TLS
|
|
// directory — unrelocated and the module crashes. So preserve both for
|
|
// the new layout.
|
|
//
|
|
// The same applies to a *DLL* on the old layout: EXEs load at their
|
|
// preferred base, but a DLL is almost always rebased, so stripping its
|
|
// BaseReloc/DllCharacteristics makes it unloadable. The PE32 pipeline
|
|
// already gates this on IMAGE_FILE_DLL (see run_pe32); this is the
|
|
// PE32+ counterpart of that fix. EXEs keep the original /FIXED zeroing.
|
|
let is_dll = (get_u16(&u.decompressed, pe_off2.wrapping_add(22)) & 0x2000) != 0;
|
|
if !new_layout && !is_dll {
|
|
write_u16(&mut u.decompressed, pe_off2.wrapping_add(0x5E), 0);
|
|
write_u32(&mut u.decompressed, pe_off2.wrapping_add(0xB0), 0);
|
|
write_u32(&mut u.decompressed, pe_off2.wrapping_add(0xB4), 0);
|
|
} else if !new_layout {
|
|
// Old-layout DLL: keep whatever BaseReloc the header restore
|
|
// produced and make sure DYNAMIC_BASE is set (same as run_pe32).
|
|
let mut dll_chars = get_u16(&u.decompressed, pe_off2.wrapping_add(0x5E));
|
|
if dll_chars == 0 {
|
|
dll_chars = 0x0040; // IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
|
|
}
|
|
write_u16(
|
|
&mut u.decompressed,
|
|
pe_off2.wrapping_add(0x5E),
|
|
dll_chars as u32,
|
|
);
|
|
}
|
|
|
|
Ok(u.decompressed)
|
|
}
|
|
|
|
/// Validate the new-layout (marker-less) file-decryptor choice by
|
|
/// trial-decompression, same approach as [`Self::pe32_file_lfsr_validates`].
|
|
///
|
|
/// The marker-less discovery picks the file LFSR by *distance* (the LFSR
|
|
/// whose fileCS pointer sits just past `info[3]`), not by content. A
|
|
/// coincidental LFSR-shaped block at a shorter distance would decode to a
|
|
/// wrong `ops2` translate and silently garble every section block — raw
|
|
/// raw blocks never enter the decompressor, so the failure would ship as a
|
|
/// plausible but wrong image. Replay the first *compressed* block's full
|
|
/// transform (raw copy, AES, translate, decompress) on a snapshot and
|
|
/// require decompression to succeed; restore the region afterwards.
|
|
///
|
|
/// `walk4_slot` holds the compressedInfo table pointer; entries are read
|
|
/// with the non-mutating `trial_decrypt5_u32` (the position-keyed cipher
|
|
/// has no cross-byte state, so trial reads equal the real pass's
|
|
/// in-place decrypts). Returns `true` when no compressed block exists
|
|
/// (nothing to validate against).
|
|
fn new_layout_file_ops_validate(&mut self, walk4_slot: u32, ops: &[Op], rebase: u32) -> bool {
|
|
let mut walk4 = get_u32(&self.decompressed, walk4_slot);
|
|
for _ in 0..4096 {
|
|
if walk4 as usize + 16 > self.decompressed.len() {
|
|
return false;
|
|
}
|
|
let src = trial_decrypt5_u32(&self.decompressed, walk4);
|
|
let len = trial_decrypt5_u32(&self.decompressed, walk4.wrapping_add(4));
|
|
let dst = trial_decrypt5_u32(&self.decompressed, walk4.wrapping_add(8));
|
|
let plain_len = trial_decrypt5_u32(&self.decompressed, walk4.wrapping_add(12));
|
|
if len == 0 {
|
|
return true; // terminator: no compressed block to validate against
|
|
}
|
|
if len != plain_len {
|
|
let cs = src.wrapping_add(rebase) as usize;
|
|
let dd = dst as usize;
|
|
let ll = len as usize;
|
|
let touch = len.max(plain_len) as usize;
|
|
if dd < 0x1000
|
|
|| cs.checked_add(ll).is_none_or(|e| e > self.file_data.len())
|
|
|| dd
|
|
.checked_add(touch)
|
|
.is_none_or(|e| e > self.decompressed.len())
|
|
{
|
|
return false;
|
|
}
|
|
let snap: Vec<u8> = self.decompressed[dd..dd + touch].to_vec();
|
|
self.decompressed[dd..dd + ll].copy_from_slice(&self.file_data[cs..cs + ll]);
|
|
self.aes_decrypt(dst, len, self.key_offsets[2]);
|
|
for k in 0..len {
|
|
let idx = (dst + k) as usize;
|
|
self.decompressed[idx] =
|
|
senbei_crypto::bytecode::apply(ops, self.decompressed[idx]);
|
|
}
|
|
let ok = primitives::decompress(
|
|
&mut self.decompressed,
|
|
dst,
|
|
dst,
|
|
self.key_offsets[0],
|
|
len,
|
|
plain_len,
|
|
);
|
|
self.decompressed[dd..dd + touch].copy_from_slice(&snap);
|
|
return ok;
|
|
}
|
|
walk4 = walk4.wrapping_add(16);
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Validate a candidate PE32 file-decryptor LFSR block by trial-decompression.
|
|
///
|
|
/// `file_dec_addr` is the absolute address of the candidate bytecode block;
|
|
/// `ci_slot` is the absolute address of the compressedInfo pointer slot
|
|
/// (`eighth_start + off_compressed_info`). Decodes the candidate's `file_ops`
|
|
/// (the per-byte translate applied to every data block before Huffman
|
|
/// decompression), then replays the first *compressed* data block's full
|
|
/// transform — raw copy, AES, translate, decompress — on a snapshot and
|
|
/// reports whether decompression succeeded. The correct fileLFSR yields a
|
|
/// translate that lets every block decompress; a coincidental valid-opcode
|
|
/// block decodes to a bogus translate that makes decompression fail.
|
|
///
|
|
/// Non-destructive: the 96-byte LFSR block and the touched destination
|
|
/// region are snapshotted and restored before returning.
|
|
fn pe32_file_lfsr_validates(&mut self, file_dec_addr: u32, ci_slot: u32) -> bool {
|
|
let da = file_dec_addr as usize;
|
|
if da + 96 > self.decompressed.len() {
|
|
return false;
|
|
}
|
|
// Decode file_ops transiently (decrypt_data6 mutates 96 bytes in place).
|
|
let lfsr_snap: Vec<u8> = self.decompressed[da..da + 96].to_vec();
|
|
self.decrypt_data6(file_dec_addr);
|
|
let ops = generate(&self.decompressed, file_dec_addr);
|
|
self.decompressed[da..da + 96].copy_from_slice(&lfsr_snap);
|
|
let ops = match ops {
|
|
Some(o) => o,
|
|
None => return false,
|
|
};
|
|
|
|
let cdo = (!get_u32(self.file_data, 0x1080)).wrapping_add(0x1000);
|
|
let table = get_u32(&self.decompressed, ci_slot);
|
|
if table == 0 || table as usize + 16 > self.decompressed.len() {
|
|
return false;
|
|
}
|
|
// Walk the compressedInfo table (entries read via the non-mutating
|
|
// trial decrypt) to the first compressed block, then test it.
|
|
let mut entry = table;
|
|
for _ in 0..256 {
|
|
if entry as usize + 16 > self.decompressed.len() {
|
|
return false;
|
|
}
|
|
let src2 = trial_decrypt5_u32(&self.decompressed, entry);
|
|
let s_sz2 = trial_decrypt5_u32(&self.decompressed, entry.wrapping_add(4));
|
|
let dst2 = trial_decrypt5_u32(&self.decompressed, entry.wrapping_add(8));
|
|
let d_sz2 = trial_decrypt5_u32(&self.decompressed, entry.wrapping_add(12));
|
|
if s_sz2 == 0 {
|
|
return false; // terminator: no compressed block to validate against
|
|
}
|
|
if s_sz2 != d_sz2 {
|
|
let file_src = src2.wrapping_add(cdo) as usize;
|
|
let dd = dst2 as usize;
|
|
let n = s_sz2 as usize;
|
|
let dlen = self.decompressed.len();
|
|
// The trial transform writes `s_sz2` bytes starting at dd
|
|
// (copy + AES + translate) before decompress reads them, so
|
|
// the snapshot/restore must cover max(s_sz2, d_sz2) — restoring
|
|
// only d_sz2 leaves [dd+d_sz2, dd+s_sz2) permanently corrupted
|
|
// for the wrong-candidate case (s_sz2 > d_sz2), and every
|
|
// later candidate is then validated against a polluted buffer.
|
|
let touch = (s_sz2.max(d_sz2)) as usize;
|
|
if dst2 < 0x1000
|
|
|| file_src
|
|
.checked_add(n)
|
|
.is_none_or(|e| e > self.file_data.len())
|
|
|| dd.checked_add(touch).is_none_or(|e| e > dlen)
|
|
{
|
|
return false;
|
|
}
|
|
let dst_snap: Vec<u8> = self.decompressed[dd..dd + touch].to_vec();
|
|
self.decompressed[dd..dd + n]
|
|
.copy_from_slice(&self.file_data[file_src..file_src + n]);
|
|
self.aes_decrypt(dst2, s_sz2, self.key_offsets[2]);
|
|
for k in 0..s_sz2 {
|
|
let idx = (dst2 + k) as usize;
|
|
self.decompressed[idx] =
|
|
senbei_crypto::bytecode::apply(&ops, self.decompressed[idx]);
|
|
}
|
|
let ok = primitives::decompress(
|
|
&mut self.decompressed,
|
|
dst2,
|
|
dst2,
|
|
self.key_offsets[0],
|
|
s_sz2,
|
|
d_sz2,
|
|
);
|
|
self.decompressed[dd..dd + touch].copy_from_slice(&dst_snap);
|
|
return ok;
|
|
}
|
|
entry = entry.wrapping_add(16);
|
|
}
|
|
false
|
|
}
|
|
|
|
/// PE32+ counterpart of [`Self::restore_pe32_tls_from_stub`]: recover the
|
|
/// genuine 40-byte `IMAGE_TLS_DIRECTORY64` and its raw-data template from the
|
|
/// loader stub, which keeps `.rdata` in plaintext at the same RVAs.
|
|
///
|
|
/// Only meaningful for the old (single-file, `/FIXED`) layout, where the
|
|
/// caller goes on to zero `DllCharacteristics` and `BaseReloc` — the image
|
|
/// then loads at `image_base`, so the struct's absolute VAs are already
|
|
/// correct and need no relocations. The external-companion layout keeps its
|
|
/// relocations and is handled separately by `job::restore_tls_from_stub`,
|
|
/// which also synthesizes the four DIR64 fixups.
|
|
///
|
|
/// Returns `false` without touching the image if the stub cannot supply a
|
|
/// plausible directory, so the caller can fall back to clearing DD[9].
|
|
fn restore_pe64_tls_from_stub(
|
|
&mut self,
|
|
pe_off: u32,
|
|
tls_dir_rva: u32,
|
|
image_base: u64,
|
|
) -> bool {
|
|
let src = match prot_rva_to_off(self.file_data, pe_off, tls_dir_rva) {
|
|
Some(o) => o as usize,
|
|
None => return false,
|
|
};
|
|
if src.checked_add(40).is_none_or(|e| e > self.file_data.len()) {
|
|
return false;
|
|
}
|
|
let field = |i: u32| get_u64(self.file_data, (src as u32).wrapping_add(i));
|
|
let (start_va, end_va, idx_va, cb_va) = (field(0), field(8), field(16), field(24));
|
|
|
|
let img_len = self.decompressed.len() as u64;
|
|
let in_image = |va: u64| va > image_base && (va - image_base) < img_len;
|
|
if !in_image(start_va) || !in_image(idx_va) || !in_image(cb_va) {
|
|
return false;
|
|
}
|
|
if end_va < start_va || (end_va - start_va) > 0x10_0000 {
|
|
return false;
|
|
}
|
|
|
|
// Template first: bail before touching the struct so a failure leaves the
|
|
// caller's all-zero directory intact.
|
|
let tpl_len = (end_va - start_va) as usize;
|
|
if tpl_len > 0 {
|
|
let tpl_rva = (start_va - image_base) as u32;
|
|
let ts = match prot_rva_to_off(self.file_data, pe_off, tpl_rva) {
|
|
Some(o) => o as usize,
|
|
None => return false,
|
|
};
|
|
let td = tpl_rva as usize;
|
|
if ts
|
|
.checked_add(tpl_len)
|
|
.is_none_or(|e| e > self.file_data.len())
|
|
|| td
|
|
.checked_add(tpl_len)
|
|
.is_none_or(|e| e > self.decompressed.len())
|
|
{
|
|
return false;
|
|
}
|
|
self.decompressed[td..td + tpl_len].copy_from_slice(&self.file_data[ts..ts + tpl_len]);
|
|
}
|
|
|
|
let d = tls_dir_rva as usize;
|
|
self.decompressed[d..d + 40].copy_from_slice(&self.file_data[src..src + 40]);
|
|
true
|
|
}
|
|
|
|
/// Restore the genuine `IMAGE_TLS_DIRECTORY32` — and the raw-data template
|
|
/// it points at — from the loader stub.
|
|
///
|
|
/// Crackproof zeroes the TLS directory struct inside the encrypted payload
|
|
/// and re-installs TLS itself when it maps the module, so a statically
|
|
/// unpacked image reaches the ordinary Windows loader with a blank struct.
|
|
/// Synthesizing a placeholder (empty template, index/callbacks aimed at
|
|
/// scratch) makes the image *load*, but it is not equivalent to the
|
|
/// original: the module's initialized thread-local bytes are never copied,
|
|
/// `_tls_index` is written somewhere the code never reads, and the TLS
|
|
/// callback array — which is where the CRT runs `__dyn_tls_init` — is empty.
|
|
/// Every `thread_local` access then hits a garbage slot (the same class of
|
|
/// `0xC0000005` documented for the companion-DLL path in `job.rs`).
|
|
///
|
|
/// The stub keeps the module's original `.rdata` and `.tls` in plaintext, so
|
|
/// both the 24-byte struct and its template are copied back byte-for-byte at
|
|
/// their RVAs. EXE images are emitted without base relocations and therefore
|
|
/// load at `image_base`, so the struct's absolute VAs stay correct as-is;
|
|
/// DLLs keep the original relocation table, which already covered these four
|
|
/// fields before packing.
|
|
///
|
|
/// Returns `false` without touching the image when the stub cannot supply a
|
|
/// plausible directory, so the caller can fall back to the placeholder.
|
|
fn restore_pe32_tls_from_stub(
|
|
&mut self,
|
|
pe_off: u32,
|
|
tls_dir_rva: u32,
|
|
image_base: u32,
|
|
) -> bool {
|
|
let src = match prot_rva_to_off(self.file_data, pe_off, tls_dir_rva) {
|
|
Some(o) => o as usize,
|
|
None => return false,
|
|
};
|
|
if src.checked_add(24).is_none_or(|e| e > self.file_data.len()) {
|
|
return false;
|
|
}
|
|
let field = |i: u32| get_u32(self.file_data, (src as u32).wrapping_add(i));
|
|
let (start_va, end_va, idx_va, cb_va) = (field(0), field(4), field(8), field(12));
|
|
|
|
// Sanity-check before trusting it: a stub whose `.rdata` is not plaintext
|
|
// at this RVA yields noise, and installing noise is worse than the
|
|
// placeholder. All three pointers must land inside the image at its
|
|
// preferred base, and the template must be a sane, non-inverted range.
|
|
let img_len = self.decompressed.len() as u64;
|
|
let in_image = |va: u32| va > image_base && ((va - image_base) as u64) < img_len;
|
|
if !in_image(start_va) || !in_image(idx_va) || !in_image(cb_va) {
|
|
return false;
|
|
}
|
|
if end_va < start_va || (end_va - start_va) as u64 > 0x10_0000 {
|
|
return false;
|
|
}
|
|
|
|
// Template first: a failure here must leave the struct untouched so the
|
|
// caller's fallback still sees an all-zero directory.
|
|
let tpl_len = (end_va - start_va) as usize;
|
|
if tpl_len > 0 {
|
|
let tpl_rva = start_va - image_base;
|
|
let ts = match prot_rva_to_off(self.file_data, pe_off, tpl_rva) {
|
|
Some(o) => o as usize,
|
|
None => return false,
|
|
};
|
|
let td = tpl_rva as usize;
|
|
if ts
|
|
.checked_add(tpl_len)
|
|
.is_none_or(|e| e > self.file_data.len())
|
|
|| td
|
|
.checked_add(tpl_len)
|
|
.is_none_or(|e| e > self.decompressed.len())
|
|
{
|
|
return false;
|
|
}
|
|
self.decompressed[td..td + tpl_len].copy_from_slice(&self.file_data[ts..ts + tpl_len]);
|
|
}
|
|
|
|
let d = tls_dir_rva as usize;
|
|
self.decompressed[d..d + 24].copy_from_slice(&self.file_data[src..src + 24]);
|
|
true
|
|
}
|
|
}
|
|
|
|
#[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 {
|
|
stage: DecompressionStage::ExeStage4,
|
|
reason: DecompressionFailure::NoProgress,
|
|
};
|
|
assert_eq!(
|
|
stage.to_string(),
|
|
"EXE stage4 decompression failed: Huffman symbol consumed no input and produced no output"
|
|
);
|
|
|
|
let block = UnpackError::SectionDecompressionFailed {
|
|
pipeline: SectionPipeline::ExePe32,
|
|
block: 7,
|
|
};
|
|
assert_eq!(
|
|
block.to_string(),
|
|
"PE32 EXE section block 7 decompression failed"
|
|
);
|
|
}
|
|
}
|