mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-03 10:31:28 -05:00
Compare commits
2
Commits
6e233bbf18
...
v1.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31a0c4139c | ||
|
|
ea92d26d28 |
No files matched your search
Generated
+7
-7
@@ -402,7 +402,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-cli"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"senbei-engine",
|
||||
"senbei-io",
|
||||
@@ -413,7 +413,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-crypto"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"thiserror",
|
||||
@@ -421,7 +421,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-elf"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"goblin",
|
||||
"thiserror",
|
||||
@@ -429,7 +429,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-engine"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"memmap2",
|
||||
"senbei-crypto",
|
||||
@@ -444,7 +444,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-io"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indicatif",
|
||||
@@ -465,7 +465,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-metadata"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
@@ -473,7 +473,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-pe"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ exclude = ["senbei-wasm"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.98.1"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
@@ -104,6 +104,45 @@ impl<'a> Unpacker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_payload(&mut self) -> Option<u32> {
|
||||
self.decrypt_size = self.info[6].wrapping_sub(self.info[3]).wrapping_add(8192);
|
||||
self.decrypt_data2();
|
||||
let src_start = self.info[4]
|
||||
.wrapping_add(4096)
|
||||
.wrapping_add(self.decrypt_size) as usize;
|
||||
let dst_start = self.info[3].wrapping_add(self.decrypt_size) as usize;
|
||||
let len = self.info[5].wrapping_sub(self.decrypt_size) as usize;
|
||||
self.decompressed[dst_start..dst_start + len]
|
||||
.copy_from_slice(&self.file_data[src_start..src_start + len]);
|
||||
|
||||
write_u32(&mut self.decompressed, self.info[3], 4096);
|
||||
self.decompressed[..4096].copy_from_slice(&self.file_data[..4096]);
|
||||
let pe = get_u32(self.file_data, 60);
|
||||
let is_pe32 = get_u16(self.file_data, pe.wrapping_add(24)) == 0x10B;
|
||||
let mut table = if is_pe32 {
|
||||
self.find_pe32_table()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Older PE32 loaders encrypt the entire payload; the length also seeds
|
||||
// the cipher, so the prefix-size key cannot decode even their shell.
|
||||
if is_pe32 && self.info[5] > self.decrypt_size && table.is_none() {
|
||||
let prefix_size = self.decrypt_size;
|
||||
let start = self.info[3] as usize;
|
||||
let end = start + self.info[5] as usize;
|
||||
let backup = self.decompressed[start..end].to_vec();
|
||||
self.decrypt_size = self.info[5];
|
||||
self.decrypt_data2();
|
||||
write_u32(&mut self.decompressed, self.info[3], 4096);
|
||||
table = self.find_pe32_table();
|
||||
if table.is_none() {
|
||||
self.decompressed[start..end].copy_from_slice(&backup);
|
||||
self.decrypt_size = prefix_size;
|
||||
}
|
||||
}
|
||||
table
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -351,28 +390,18 @@ impl<'a> Unpacker<'a> {
|
||||
});
|
||||
}
|
||||
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]);
|
||||
let pe32_table = u.load_payload();
|
||||
|
||||
// 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);
|
||||
let table = pe32_table.ok_or(UnpackError::Pe32TblNotFound)?;
|
||||
return u.run_pe32(pe_off, table, verbose);
|
||||
}
|
||||
|
||||
// The config block layout in the decrypted region varies between Crackproof
|
||||
@@ -1992,6 +2021,146 @@ impl<'a> Unpacker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod payload_tests {
|
||||
use super::*;
|
||||
|
||||
fn check_payload(full: bool, valid_table: bool) {
|
||||
let info = [
|
||||
0x1234_5678,
|
||||
0x4E4E_4F4B,
|
||||
0,
|
||||
0x2000,
|
||||
0x200,
|
||||
0x7000,
|
||||
0x2400,
|
||||
0,
|
||||
];
|
||||
let mut plain = vec![0u8; 0x10000];
|
||||
let table = 0x2800;
|
||||
let (shell_field, header_field) = if full { (0x78, 0x50) } else { (0x88, 0x58) };
|
||||
write_u32(&mut plain, table + shell_field, info[6]);
|
||||
write_u32(&mut plain, table + shell_field + 4, 0x1800);
|
||||
write_u32(&mut plain, table + header_field, 0x80);
|
||||
write_u32(
|
||||
&mut plain,
|
||||
table + header_field + 4,
|
||||
if valid_table { 0x58 } else { 0 },
|
||||
);
|
||||
let ss_size = if full { 0xAEC } else { 0xBC0 };
|
||||
write_u32(&mut plain, table + shell_field + 0x10, 0x6000);
|
||||
write_u32(&mut plain, table + shell_field + 0x14, ss_size);
|
||||
write_u32(&mut plain, table + shell_field + 0x20, info[6]);
|
||||
write_u32(&mut plain, table + shell_field + 0x24, 0x1800);
|
||||
write_u32(&mut plain, table + 0x40, 0x9ABC_DEF0);
|
||||
let third_pair = 0x6000 + if full { 0xAB4 } else { 0xB8C };
|
||||
write_u32(&mut plain, third_pair, 0x8000);
|
||||
write_u32(&mut plain, third_pair + 4, 0x20);
|
||||
if full {
|
||||
// This earlier table has plausible descriptors but a wrong stage key.
|
||||
for (field, value) in [
|
||||
(0x40, 0x1122_3344),
|
||||
(0x50, 0x80),
|
||||
(0x54, 0x58),
|
||||
(0x78, info[6]),
|
||||
(0x7C, 0x1800),
|
||||
(0x88, 0x7000),
|
||||
(0x8C, 0xAEC),
|
||||
(0x98, info[6]),
|
||||
(0x9C, 0x1800),
|
||||
] {
|
||||
write_u32(&mut plain, 0x2500 + field, value);
|
||||
}
|
||||
}
|
||||
write_u32(&mut plain, 0x8100, 0xAABB_CCDD);
|
||||
let encrypted_size = if full {
|
||||
info[5]
|
||||
} else {
|
||||
info[6] - info[3] + 8192
|
||||
};
|
||||
let src = 4096 + info[4];
|
||||
let mut input = vec![0u8; (src + info[5]) as usize];
|
||||
write_u32(&mut input, 0x3C, 0x80);
|
||||
write_u16(&mut input, 0x98, 0x10B);
|
||||
plain[..4096].copy_from_slice(&input[..4096]);
|
||||
let mut key = primitives::calculate_checksum(&plain, table + header_field)
|
||||
^ primitives::calculate_checksum(&plain, table + shell_field + 0x20)
|
||||
^ get_u32(&plain, table + 0x40);
|
||||
for i in 0..ss_size / 4 {
|
||||
let word = get_u32(&plain, 0x6000 + i * 4);
|
||||
write_u32(
|
||||
&mut plain,
|
||||
0x6000 + i * 4,
|
||||
word.wrapping_add(i).rotate_left(21) ^ key,
|
||||
);
|
||||
key = key.wrapping_add(i);
|
||||
}
|
||||
let mut key = info[0].wrapping_add(!encrypted_size);
|
||||
for i in 0..info[5] / 4 {
|
||||
let word = get_u32(&plain, info[3] + i * 4);
|
||||
let cell = if i * 4 < encrypted_size {
|
||||
word ^ key
|
||||
} else {
|
||||
word
|
||||
};
|
||||
write_u32(&mut input, src + i * 4, cell);
|
||||
key = i.wrapping_mul(i) ^ key.wrapping_add(cell).wrapping_add(i);
|
||||
}
|
||||
let mut unpacker = Unpacker {
|
||||
file_data: &input,
|
||||
decompressed: vec![0u8; plain.len()],
|
||||
info,
|
||||
key_offsets: [0; 4],
|
||||
decrypt_size: 0,
|
||||
};
|
||||
let selected = unpacker.load_payload();
|
||||
assert_eq!(selected, valid_table.then_some(table));
|
||||
if !valid_table {
|
||||
let prefix_size = info[6] - info[3] + 8192;
|
||||
assert_eq!(unpacker.decrypt_size, prefix_size);
|
||||
assert_eq!(
|
||||
get_u32(&unpacker.decompressed, info[3] + prefix_size - 4),
|
||||
{
|
||||
let mut key = info[0].wrapping_add(!prefix_size);
|
||||
let mut word = 0;
|
||||
for i in 0..prefix_size / 4 {
|
||||
let cell = get_u32(&input, src + i * 4);
|
||||
word = key ^ cell;
|
||||
key = i.wrapping_mul(i) ^ key.wrapping_add(cell).wrapping_add(i);
|
||||
}
|
||||
word
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
&unpacker.decompressed
|
||||
[(info[3] + prefix_size) as usize..(info[3] + info[5]) as usize],
|
||||
&input[(src + prefix_size) as usize..(src + info[5]) as usize]
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert_eq!(unpacker.decrypt_size, encrypted_size);
|
||||
assert_eq!(
|
||||
&unpacker.decompressed[info[3] as usize + 4..(info[3] + info[5]) as usize],
|
||||
&plain[info[3] as usize + 4..(info[3] + info[5]) as usize]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pe32_full_payload_encryption_is_trial_validated() {
|
||||
check_payload(true, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pe32_prefix_payload_encryption_preserves_plaintext_tail() {
|
||||
check_payload(false, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pe32_invalid_full_payload_trial_restores_prefix() {
|
||||
check_payload(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod error_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -23,10 +23,153 @@ fn stage_key_rounds(data: &[u8], table: u32, slots: usize) -> Result<u32, Unpack
|
||||
}
|
||||
|
||||
impl<'a> Unpacker<'a> {
|
||||
pub(super) fn find_pe32_table(&mut self) -> Option<u32> {
|
||||
let pe_off = get_u32(self.file_data, 60);
|
||||
let header = self.decompressed[..4096].to_vec();
|
||||
for tbl in layout::find_tbl_pe32_candidates(&self.decompressed, &self.info) {
|
||||
let compact = get_u32(&self.decompressed, tbl + 0x78) == self.info[6];
|
||||
let pair = tbl + if compact { 0x88 } else { 0x98 };
|
||||
let start = get_u32(&self.decompressed, pair) as usize;
|
||||
let size = get_u32(&self.decompressed, pair + 4) as usize;
|
||||
let Some(end) = start
|
||||
.checked_add(size)
|
||||
.filter(|&end| end <= self.decompressed.len())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let ciphertext = self.decompressed[start..end].to_vec();
|
||||
let valid = self.decrypt_pe32_second_stage(tbl, pe_off).is_ok();
|
||||
self.decompressed[start..end].copy_from_slice(&ciphertext);
|
||||
self.decompressed[..4096].copy_from_slice(&header);
|
||||
if valid {
|
||||
return Some(tbl);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn decrypt_pe32_second_stage(&mut self, tbl: u32, pe_off: u32) -> Result<u32, UnpackError> {
|
||||
let compact = get_u32(&self.decompressed, tbl.wrapping_add(0x78)) == self.info[6];
|
||||
let header_cs_field = if compact { 0x50 } else { 0x58 };
|
||||
let stage_field = if compact { 0x88 } else { 0x98 };
|
||||
let resource_field = if compact { 0xB0 } else { 0xC8 };
|
||||
if (pe_off as usize)
|
||||
.checked_add(0xB8)
|
||||
.is_none_or(|end| end > 0x1000)
|
||||
{
|
||||
return Err(UnpackError::Pe32RelocationDataNotFound);
|
||||
}
|
||||
let import_rva = get_u32(&self.decompressed, tbl + stage_field + 0x24);
|
||||
let resource_rva = get_u32(&self.decompressed, tbl + resource_field);
|
||||
let resource_size = get_u32(&self.decompressed, tbl + resource_field + 4);
|
||||
write_u32(&mut self.decompressed, pe_off + 0x80, import_rva);
|
||||
write_u32(&mut self.decompressed, pe_off + 0x88, resource_rva);
|
||||
write_u32(&mut self.decompressed, pe_off + 0x8C, resource_size);
|
||||
write_u32(&mut self.decompressed, pe_off + 0xB0, 0);
|
||||
write_u32(&mut self.decompressed, pe_off + 0xB4, 0);
|
||||
|
||||
let cs = tbl + stage_field + 0x10;
|
||||
let cs_start = get_u32(&self.decompressed, cs) as usize;
|
||||
let cs_size = get_u32(&self.decompressed, cs + 4) as usize;
|
||||
if cs_size == 0
|
||||
|| cs_start
|
||||
.checked_add(cs_size)
|
||||
.is_none_or(|end| end > self.decompressed.len())
|
||||
{
|
||||
return Err(UnpackError::Pe32RelocationDataNotFound);
|
||||
}
|
||||
let first_stage_cs = self.calculate_checksum(cs);
|
||||
let second_stage_key = get_u32(&self.decompressed, tbl + 0x40);
|
||||
let ss_pair = tbl + stage_field;
|
||||
let ss = get_u32(&self.decompressed, ss_pair);
|
||||
let ss_size = get_u32(&self.decompressed, ss_pair + 4);
|
||||
let ss_shift = ss_size.wrapping_sub(if compact { 0xAEC } else { 0xBC0 });
|
||||
let third_pair_off = (if compact { 0xAB4u32 } else { 0xB8C }).wrapping_add(ss_shift);
|
||||
let ss_lo = ss as usize;
|
||||
let ss_hi = ss_lo.saturating_add(ss_size as usize);
|
||||
if ss_hi > self.decompressed.len() || third_pair_off.saturating_add(8) > ss_size {
|
||||
return Err(UnpackError::Pe32SecondStageRangeInvalid {
|
||||
offset: ss,
|
||||
size: ss_size,
|
||||
image_len: self.decompressed.len(),
|
||||
});
|
||||
}
|
||||
let ss_ct = self.decompressed[ss_lo..ss_hi].to_vec();
|
||||
// Header checksums refer to the original PE header. Compact EXEs and
|
||||
// native DLLs can carry a packer-added BaseReloc entry: trial both keys.
|
||||
for zero_reloc in [false, true] {
|
||||
if zero_reloc {
|
||||
write_u32(&mut self.decompressed, pe_off + 0xA0, 0);
|
||||
write_u32(&mut self.decompressed, pe_off + 0xA4, 0);
|
||||
}
|
||||
let mut hcs_addr = tbl + header_cs_field;
|
||||
let mut header_checksum = 0;
|
||||
loop {
|
||||
if hcs_addr + 8 > tbl + stage_field - 0x10 {
|
||||
return Err(UnpackError::Pe32RelocationDataNotFound);
|
||||
}
|
||||
let size = get_u32(&self.decompressed, hcs_addr + 4) as usize;
|
||||
if size == 0 {
|
||||
break;
|
||||
}
|
||||
let start = get_u32(&self.decompressed, hcs_addr) as usize;
|
||||
if start.checked_add(size).is_none_or(|end| end > 0x1000) {
|
||||
return Err(UnpackError::Pe32RelocationDataNotFound);
|
||||
}
|
||||
header_checksum ^= self.calculate_checksum(hcs_addr);
|
||||
hcs_addr += 8;
|
||||
}
|
||||
self.decompressed[ss_lo..ss_hi].copy_from_slice(&ss_ct);
|
||||
self.decrypt_data3(
|
||||
ss_pair,
|
||||
header_checksum ^ first_stage_cs ^ second_stage_key,
|
||||
21,
|
||||
);
|
||||
let pair = ss + third_pair_off;
|
||||
let off = get_u32(&self.decompressed, pair) as usize;
|
||||
let size = get_u32(&self.decompressed, pair + 4) as usize;
|
||||
if off > 0x1000
|
||||
&& size >= 4
|
||||
&& off
|
||||
.checked_add(size)
|
||||
.is_some_and(|end| end <= self.decompressed.len())
|
||||
{
|
||||
return Ok(header_checksum);
|
||||
}
|
||||
}
|
||||
Err(UnpackError::Pe32RelocationDataNotFound)
|
||||
}
|
||||
|
||||
fn read_pe32_metadata(&mut self, compact: bool) -> (u32, [u8; 128]) {
|
||||
let base = self.info[3];
|
||||
let layout_b = get_u32(&self.decompressed, base.wrapping_add(0x10)) > 0x10000;
|
||||
let (offset, size, ep_offset, dirs_offset) = if layout_b {
|
||||
// Compact metadata has an extra 16-byte prefix before the EP/dirs.
|
||||
let shift = if compact { 0x10 } else { 0 };
|
||||
(0x10, 0x290, 0x20 + shift, 0x30 + shift)
|
||||
} else {
|
||||
(0x40, 144, 0x40, 0x50)
|
||||
};
|
||||
let start = base.wrapping_add(offset) as usize;
|
||||
let backup = self.decompressed[start..start + size].to_vec();
|
||||
self.decrypt_data5(base.wrapping_add(offset), size as u32);
|
||||
let ep = get_u32(&self.decompressed, base.wrapping_add(ep_offset));
|
||||
let mut dirs = [0u8; 128];
|
||||
let dirs_start = base.wrapping_add(dirs_offset) as usize;
|
||||
dirs.copy_from_slice(&self.decompressed[dirs_start..dirs_start + 128]);
|
||||
self.decompressed[start..start + size].copy_from_slice(&backup);
|
||||
(ep, dirs)
|
||||
}
|
||||
|
||||
/// PE32 (32-bit) unpack pipeline. The shared Stage 1/2 setup (info decrypt,
|
||||
/// payload decrypt, raw copy, header restore) has already run in `run()`
|
||||
/// before dispatch; this takes over from "Locating shell offsets".
|
||||
pub(super) fn run_pe32(&mut self, pe_off: u32, verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
||||
pub(super) fn run_pe32(
|
||||
&mut self,
|
||||
pe_off: u32,
|
||||
tbl: u32,
|
||||
verbose: bool,
|
||||
) -> Result<Vec<u8>, UnpackError> {
|
||||
let info = self.info;
|
||||
let info3 = info[3];
|
||||
|
||||
@@ -43,94 +186,21 @@ impl<'a> Unpacker<'a> {
|
||||
key
|
||||
};
|
||||
|
||||
// ---- Locate tbl in shell ----
|
||||
let tbl =
|
||||
layout::find_tbl_pe32(&self.decompressed, &info).ok_or(UnpackError::Pe32TblNotFound)?;
|
||||
// ---- Validated shell table ----
|
||||
let compact = get_u32(&self.decompressed, tbl.wrapping_add(0x78)) == info[6];
|
||||
let stage_field = if compact { 0x88 } else { 0x98 };
|
||||
if verbose {
|
||||
println!("[3/9] Locating config layout (PE32)...");
|
||||
println!(" tbl = 0x{:X}", tbl);
|
||||
println!(" tbl = 0x{:X}, compact = {}", tbl, compact);
|
||||
}
|
||||
|
||||
// ---- PE header restore ----
|
||||
let val_bc = get_u32(&self.decompressed, tbl.wrapping_add(0xBC));
|
||||
let val_c8 = get_u32(&self.decompressed, tbl.wrapping_add(0xC8));
|
||||
let val_cc = get_u32(&self.decompressed, tbl.wrapping_add(0xCC));
|
||||
write_u32(&mut self.decompressed, pe_off.wrapping_add(0x80), val_bc);
|
||||
write_u32(&mut self.decompressed, pe_off.wrapping_add(0x88), val_c8);
|
||||
write_u32(&mut self.decompressed, pe_off.wrapping_add(0x8C), val_cc);
|
||||
write_u32(&mut self.decompressed, pe_off.wrapping_add(0xB0), 0);
|
||||
write_u32(&mut self.decompressed, pe_off.wrapping_add(0xB4), 0);
|
||||
|
||||
// ---- Header-independent checksum inputs ----
|
||||
let first_stage_cs = self.calculate_checksum(tbl.wrapping_add(0xA8));
|
||||
let second_stage_key = get_u32(&self.decompressed, tbl.wrapping_add(0x40));
|
||||
|
||||
// ---- Stage 3: SecondStage ----
|
||||
//
|
||||
// ss_key = headerChecksum ^ firstStageCS ^ secondStageKey, where the
|
||||
// header checksum (a XOR of crc32(region)^size over the sub-regions at
|
||||
// tbl+0x58) is taken over the *original* pre-pack PE header. For EXEs the
|
||||
// import/resource restore above reconstructs that header exactly. Native
|
||||
// DLLs additionally carry a packer-added BaseReloc data-directory entry
|
||||
// (dir 5) that was absent from the checksummed original, so the header
|
||||
// checksum only matches once that entry is treated as zero. EXEs have no
|
||||
// dir-5 entry, so zeroing it is a no-op for them.
|
||||
//
|
||||
// Rather than branch on EXE-vs-DLL, try the header as-is and, on failure,
|
||||
// with the BaseReloc entry zeroed; keep whichever ss_key decrypts a
|
||||
// SecondStage whose ThirdStage (off,size) pair lands inside the image.
|
||||
// This uses the same shift/key trial-and-validate the later stages
|
||||
// already use, and keeps EXE output byte-identical (the as-is variant
|
||||
// wins first).
|
||||
let ss_pair = tbl.wrapping_add(0x98);
|
||||
let ss_pair = tbl.wrapping_add(stage_field);
|
||||
let ss = get_u32(&self.decompressed, ss_pair);
|
||||
let ss_size = get_u32(&self.decompressed, ss_pair.wrapping_add(4));
|
||||
let ss_shift = ss_size.wrapping_sub(0xBC0);
|
||||
// Back up the SecondStage ciphertext so a failed trial can be retried.
|
||||
let ss_lo = ss as usize;
|
||||
let ss_hi = ss_lo.wrapping_add(ss_size as usize);
|
||||
if ss_hi < ss_lo || ss_hi > self.decompressed.len() {
|
||||
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();
|
||||
// PE32 data dir 5 (BaseReloc) = optional_header(pe+24) + 0x60 + 5*8 = pe+0xA0.
|
||||
let reloc_dir = pe_off.wrapping_add(0xA0);
|
||||
let len = self.decompressed.len() as u64;
|
||||
let pair_off = 0xB8Cu32.wrapping_add(ss_shift);
|
||||
let mut found = false;
|
||||
// Holds the winning variant's header checksum; the later stages
|
||||
// (Forth/Fifth/Seven/Eighth) reuse it as a key component.
|
||||
let mut header_checksum: u32 = 0;
|
||||
for zero_reloc in [false, true] {
|
||||
if zero_reloc {
|
||||
write_u32(&mut self.decompressed, reloc_dir, 0);
|
||||
write_u32(&mut self.decompressed, reloc_dir.wrapping_add(4), 0);
|
||||
}
|
||||
let mut hcs_addr = tbl.wrapping_add(0x58);
|
||||
header_checksum = 0;
|
||||
while get_u32(&self.decompressed, hcs_addr.wrapping_add(4)) != 0 {
|
||||
header_checksum ^= self.calculate_checksum(hcs_addr);
|
||||
hcs_addr = hcs_addr.wrapping_add(8);
|
||||
}
|
||||
let ss_key = header_checksum ^ first_stage_cs ^ second_stage_key;
|
||||
self.decompressed[ss_lo..ss_hi].copy_from_slice(&ss_ct);
|
||||
self.decrypt_data3(ss_pair, ss_key, 21);
|
||||
// Validate: the ThirdStage (off,size) pair must reference the image.
|
||||
let pair = ss.wrapping_add(pair_off);
|
||||
let off = get_u32(&self.decompressed, pair) as u64;
|
||||
let sz = get_u32(&self.decompressed, pair.wrapping_add(4)) as u64;
|
||||
if off > 0x1000 && off < len && sz >= 4 && off.saturating_add(sz) <= len {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return Err(UnpackError::Pe32RelocationDataNotFound);
|
||||
}
|
||||
let ss_shift = ss_size.wrapping_sub(if compact { 0xAEC } else { 0xBC0 });
|
||||
let third_pair_off = (if compact { 0xAB4u32 } else { 0xB8C }).wrapping_add(ss_shift);
|
||||
let header_checksum = self.decrypt_pe32_second_stage(tbl, pe_off)?;
|
||||
if verbose {
|
||||
println!(
|
||||
" ss = 0x{:08X}, size = 0x{:X}, shift = 0x{:X}",
|
||||
@@ -139,13 +209,12 @@ impl<'a> Unpacker<'a> {
|
||||
}
|
||||
|
||||
// ---- PE32 fixed offsets ----
|
||||
let third_key_off = 0x968u32.wrapping_add(ss_shift);
|
||||
let forth_key_off = 0x964u32.wrapping_add(ss_shift);
|
||||
let cs_base_off = 0x96Cu32.wrapping_add(ss_shift);
|
||||
let dp_base_off = 0xA9Cu32.wrapping_add(ss_shift);
|
||||
let third_key_off = (if compact { 0x950u32 } else { 0x968 }).wrapping_add(ss_shift);
|
||||
let forth_key_off = (if compact { 0x94Cu32 } else { 0x964 }).wrapping_add(ss_shift);
|
||||
let cs_base_off = (if compact { 0x954u32 } else { 0x96C }).wrapping_add(ss_shift);
|
||||
let dp_base_off = (if compact { 0x9C4u32 } else { 0xA9C }).wrapping_add(ss_shift);
|
||||
|
||||
// ---- Stage 4: ThirdStage (brute-force the rotate shift) ----
|
||||
let third_pair_off = 0xB8Cu32.wrapping_add(ss_shift);
|
||||
let key = get_u32(&self.decompressed, ss.wrapping_add(third_key_off));
|
||||
let pair_addr = ss.wrapping_add(third_pair_off);
|
||||
let ts_addr = get_u32(&self.decompressed, pair_addr);
|
||||
@@ -238,7 +307,7 @@ impl<'a> Unpacker<'a> {
|
||||
}
|
||||
|
||||
// ---- Checksum addresses ----
|
||||
let second_stage_cs_addr = tbl.wrapping_add(0xB0);
|
||||
let second_stage_cs_addr = tbl.wrapping_add(stage_field + 0x18);
|
||||
let forth_stage_cs_addr = ss.wrapping_add(cs_base_off);
|
||||
let fifth_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x08);
|
||||
let seven_stage_cs_addr = ss.wrapping_add(cs_base_off).wrapping_add(0x10);
|
||||
@@ -298,7 +367,9 @@ impl<'a> Unpacker<'a> {
|
||||
);
|
||||
let seven_key = !get_u32(
|
||||
&self.decompressed,
|
||||
cs1_addr.wrapping_add(cs1_size).wrapping_sub(0x10),
|
||||
cs1_addr
|
||||
.wrapping_add(cs1_size)
|
||||
.wrapping_sub(if compact { 0xC } else { 0x10 }),
|
||||
);
|
||||
let fk7 = header_checksum ^ fifth_cs ^ seven_key;
|
||||
if let Err(reason) = self.decrypt_and_decompress_data(seven_addr, fk7, None) {
|
||||
@@ -640,28 +711,7 @@ impl<'a> Unpacker<'a> {
|
||||
)?;
|
||||
|
||||
// ---- PE32 metadata: EP and data dirs from info[3] ----
|
||||
let test_val = get_u32(&self.decompressed, info3.wrapping_add(0x10));
|
||||
let metadata_ep: u32;
|
||||
let mut metadata_dirs = [0u8; 128];
|
||||
if test_val > 0x10000 {
|
||||
// Layout B
|
||||
let s = info3.wrapping_add(0x10) as usize;
|
||||
let backup_meta = self.decompressed[s..s + 0x290].to_vec();
|
||||
self.decrypt_data5(info3.wrapping_add(0x10), 0x290);
|
||||
metadata_ep = get_u32(&self.decompressed, info3.wrapping_add(0x20));
|
||||
let d = info3.wrapping_add(0x30) as usize;
|
||||
metadata_dirs.copy_from_slice(&self.decompressed[d..d + 128]);
|
||||
self.decompressed[s..s + 0x290].copy_from_slice(&backup_meta);
|
||||
} else {
|
||||
// Layout A
|
||||
let s = info3.wrapping_add(0x40) as usize;
|
||||
let backup_meta = self.decompressed[s..s + 144].to_vec();
|
||||
self.decrypt_data5(info3.wrapping_add(0x40), 144);
|
||||
metadata_ep = get_u32(&self.decompressed, info3.wrapping_add(0x40));
|
||||
let d = info3.wrapping_add(0x50) as usize;
|
||||
metadata_dirs.copy_from_slice(&self.decompressed[d..d + 128]);
|
||||
self.decompressed[s..s + 144].copy_from_slice(&backup_meta);
|
||||
}
|
||||
let (metadata_ep, metadata_dirs) = self.read_pe32_metadata(compact);
|
||||
|
||||
// ---- Zero-out list (runs BEFORE decompression) ----
|
||||
let zero_list_addr = eighth_start.wrapping_add(off_zero_list);
|
||||
@@ -1157,6 +1207,56 @@ impl<'a> Unpacker<'a> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn check_metadata(compact: bool, layout_b: bool) {
|
||||
let base = 0x1000u32;
|
||||
let (offset, size, ep_offset, dirs_offset) = if layout_b {
|
||||
if compact {
|
||||
(0x10, 0x290, 0x30, 0x40)
|
||||
} else {
|
||||
(0x10, 0x290, 0x20, 0x30)
|
||||
}
|
||||
} else {
|
||||
(0x40, 144, 0x40, 0x50)
|
||||
};
|
||||
let mut data = vec![0u8; 0x2000];
|
||||
let ep = 0x123456;
|
||||
let dirs = std::array::from_fn::<_, 128, _>(|i| i as u8 ^ 0x5A);
|
||||
write_u32(&mut data, base + ep_offset, ep);
|
||||
data[(base + dirs_offset) as usize..(base + dirs_offset + 128) as usize]
|
||||
.copy_from_slice(&dirs);
|
||||
for i in 0..size {
|
||||
let key = (base + offset + i) as u8;
|
||||
let byte = &mut data[(base + offset + i) as usize];
|
||||
let mixed = (byte.rotate_right(2) ^ key).rotate_right(2) ^ key.wrapping_add(1);
|
||||
*byte = mixed.rotate_right(2);
|
||||
}
|
||||
assert_eq!(get_u32(&data, base + 0x10) > 0x10000, layout_b);
|
||||
let backup = data.clone();
|
||||
let mut unpacker = Unpacker {
|
||||
file_data: &[],
|
||||
decompressed: data,
|
||||
info: [0, 0, 0, base, 0, 0, 0, 0],
|
||||
key_offsets: [0; 4],
|
||||
decrypt_size: 0,
|
||||
};
|
||||
assert_eq!(unpacker.read_pe32_metadata(compact), (ep, dirs));
|
||||
assert_eq!(
|
||||
unpacker.decompressed, backup,
|
||||
"metadata reads must be nondestructive"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pe32_compact_metadata_restores_entry_point_and_directories() {
|
||||
check_metadata(true, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pe32_standard_metadata_preserves_both_layouts() {
|
||||
check_metadata(false, false);
|
||||
check_metadata(false, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_key_rounds_follow_active_descriptor_prefix() {
|
||||
for (sizes, expected) in [
|
||||
|
||||
@@ -6,8 +6,9 @@ mod image;
|
||||
|
||||
pub(super) use dd8::{select_dd8_formula_pe32, select_dd8_shift};
|
||||
pub(super) use discovery::{
|
||||
discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos, find_tbl_pe32,
|
||||
find_v_after_pad, find_v4_offset, get_string_to_null, section_name, trial_decrypt5_u32,
|
||||
discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos,
|
||||
find_tbl_pe32_candidates, find_v_after_pad, find_v4_offset, get_string_to_null, section_name,
|
||||
trial_decrypt5_u32,
|
||||
};
|
||||
pub(super) use image::{
|
||||
compact_memory_image_to_pe, move_pe32_imports_to_kmiat, pe32_imports_already_match_idata_layout,
|
||||
|
||||
@@ -212,37 +212,54 @@ pub fn section_name(data: &[u8], offset: u32) -> String {
|
||||
// PE32 (32-bit) helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// PE32 shell-table locator. Walks the shell region (`info[6]`) for a dword
|
||||
/// equal to `info[6]` followed by a plausible shell size, returning the table
|
||||
/// base (`candidate = off - 0x88`) when `candidate+0x58` holds a valid pointer.
|
||||
pub fn find_tbl_pe32(data: &[u8], info: &[u32; 8]) -> Option<u32> {
|
||||
let shell = info[6];
|
||||
if (data.len() as u64) < 0x100 {
|
||||
return None;
|
||||
/// PE32 shell-table locator. The compact layout moves the shell descriptor
|
||||
/// from +0x88 to +0x78 and the header-checksum list from +0x58 to +0x50.
|
||||
/// Return structurally plausible candidates; the pipeline must trial-decrypt
|
||||
/// their second stages before selecting a table.
|
||||
pub fn find_tbl_pe32_candidates(data: &[u8], info: &[u32; 8]) -> Vec<u32> {
|
||||
let mut tables = Vec::new();
|
||||
let shell = info[6] as usize;
|
||||
let hi = shell.saturating_add(0x3000).min(data.len());
|
||||
for off in (shell..hi.saturating_sub(8)).step_by(4) {
|
||||
if get_u32(data, off as u32) != info[6] {
|
||||
continue;
|
||||
}
|
||||
let hi = (shell as u64)
|
||||
.saturating_add(0x3000)
|
||||
.min(data.len() as u64 - 0x100) as u32;
|
||||
let mut off = shell;
|
||||
while off < hi {
|
||||
if off as usize + 8 <= data.len() {
|
||||
let candidate = off.wrapping_sub(0x88);
|
||||
if candidate >= shell && get_u32(data, off) == info[6] {
|
||||
let shell_size_val = get_u32(data, off.wrapping_add(4));
|
||||
if shell_size_val > 0x1000 && shell_size_val < 0x100000 {
|
||||
let v58_off = candidate.wrapping_add(0x58);
|
||||
if (v58_off as usize + 4) <= data.len() {
|
||||
let v58 = get_u32(data, v58_off);
|
||||
if v58 > 0 && (v58 as usize) < data.len() {
|
||||
return Some(candidate);
|
||||
let shell_size = get_u32(data, off as u32 + 4) as usize;
|
||||
if !(0x1001..0x100000).contains(&shell_size)
|
||||
|| shell
|
||||
.checked_add(shell_size)
|
||||
.is_none_or(|end| end > data.len())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (shell_field, header_field) in [(0x88, 0x58), (0x78, 0x50)] {
|
||||
let Some(candidate) = off.checked_sub(shell_field).filter(|&base| base >= shell) else {
|
||||
continue;
|
||||
};
|
||||
let Some(table) = data.get(candidate..candidate + 0xD0) else {
|
||||
continue;
|
||||
};
|
||||
let header = get_u32(table, header_field) as usize;
|
||||
let header_size = get_u32(table, header_field + 4) as usize;
|
||||
let stage = get_u32(table, shell_field as u32 + 0x10) as usize;
|
||||
let stage_size = get_u32(table, shell_field as u32 + 0x14) as usize;
|
||||
if header > 0
|
||||
&& header_size > 0
|
||||
&& header
|
||||
.checked_add(header_size)
|
||||
.is_some_and(|end| end <= 0x1000)
|
||||
&& stage >= info[3] as usize
|
||||
&& stage_size >= 4
|
||||
&& stage
|
||||
.checked_add(stage_size)
|
||||
.is_some_and(|end| end <= data.len())
|
||||
&& !tables.contains(&(candidate as u32))
|
||||
{
|
||||
tables.push(candidate as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
off = off.wrapping_add(4);
|
||||
}
|
||||
None
|
||||
tables
|
||||
}
|
||||
|
||||
/// Locate an LFSR-encrypted bytecode block (decrypt_data6 form) in a region.
|
||||
@@ -449,6 +466,57 @@ pub fn discover_eighth_slots(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn pe32_table_fixture(compact: bool) -> (Vec<u8>, [u32; 8], u32) {
|
||||
let mut data = vec![0u8; 0x10000];
|
||||
let info = [0, 0, 0, 0x2000, 0, 0x7000, 0x2400, 0];
|
||||
let table = 0x2800;
|
||||
let (shell, header) = if compact { (0x78, 0x50) } else { (0x88, 0x58) };
|
||||
for (field, value) in [
|
||||
(header, 0x80),
|
||||
(header + 4, 0x58),
|
||||
(shell, info[6]),
|
||||
(shell + 4, 0x1800),
|
||||
(shell + 0x10, 0x6000),
|
||||
(shell + 0x14, 0xAEC),
|
||||
] {
|
||||
senbei_crypto::primitives::write_u32(&mut data, table + field, value);
|
||||
}
|
||||
(data, info, table)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pe32_table_supports_compact_header_descriptors() {
|
||||
for compact in [false, true] {
|
||||
let (data, info, table) = pe32_table_fixture(compact);
|
||||
assert_eq!(find_tbl_pe32_candidates(&data, &info), vec![table]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pe32_table_rejects_a_shell_reference_without_valid_descriptors() {
|
||||
let (mut data, info, table) = pe32_table_fixture(true);
|
||||
for (field, value) in [(0x58, 0x80), (0x5C, 0x58), (0x88, info[6]), (0x8C, 0x1800)] {
|
||||
senbei_crypto::primitives::write_u32(&mut data, 0x2500 + field, value);
|
||||
}
|
||||
assert_eq!(find_tbl_pe32_candidates(&data, &info), vec![table]);
|
||||
senbei_crypto::primitives::write_u32(&mut data, table + 0x54, 0);
|
||||
assert_eq!(find_tbl_pe32_candidates(&data, &info), Vec::<u32>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pe32_table_rejects_truncated_and_out_of_range_regions() {
|
||||
let (mut data, mut info, table) = pe32_table_fixture(true);
|
||||
assert_eq!(
|
||||
find_tbl_pe32_candidates(&data[..0x28C0], &info),
|
||||
Vec::<u32>::new()
|
||||
);
|
||||
senbei_crypto::primitives::write_u32(&mut data, table + 0x50, u32::MAX - 15);
|
||||
assert_eq!(find_tbl_pe32_candidates(&data, &info), Vec::<u32>::new());
|
||||
info[6] = u32::MAX;
|
||||
assert_eq!(find_tbl_pe32_candidates(&data, &info), Vec::<u32>::new());
|
||||
assert_eq!(find_tbl_pe32_candidates(&[], &info), Vec::<u32>::new());
|
||||
}
|
||||
|
||||
/// Task 4.1 regression: build a synthetic buffer whose valid bytecode block
|
||||
/// sits PAST `len` but within `len*2`. Assert that the smaller window misses
|
||||
/// it and the doubled window finds it.
|
||||
|
||||
Generated
+7
-7
@@ -412,7 +412,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-crypto"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"thiserror",
|
||||
@@ -420,7 +420,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-elf"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"goblin",
|
||||
"thiserror",
|
||||
@@ -428,7 +428,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-engine"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"memmap2",
|
||||
"senbei-crypto",
|
||||
@@ -443,7 +443,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-io"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indicatif",
|
||||
@@ -464,7 +464,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-metadata"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
@@ -472,14 +472,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "senbei-pe"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "senbei-wasm"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"senbei-engine",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "senbei-wasm"
|
||||
version = "1.3.2"
|
||||
version = "1.3.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.98.1"
|
||||
description = "WebAssembly bindings for senbei (browser frontend assets live in web/)"
|
||||
|
||||
Reference in new issue
Block a user