10 Commits
Author SHA1 Message Date
Momoko-Ayase 31a0c4139c bump: 1.3.3 2026-10-24 21:33:59 +08:00
Momoko-Ayase ea92d26d28 fix(windows): support legacy PE32 payload layouts
Handle fully encrypted PE32 payloads and compact shell, stage, and metadata layouts. Trial-decrypt each table candidate before selecting it, restoring the header and stage ciphertext between attempts.

Add synthetic regressions for full and prefix encryption, false table candidates, rollback, metadata placement, and malformed ranges. Preserve existing corpus outputs.
2026-10-24 20:40:23 +08:00
bfloat16 6e233bbf18 fix(windows): preserve companion export tables
Guard PE32 export copies when the companion omits plaintext stub data. Reserve the complete export directory during compaction and validate file backing before overlaying it to prevent writes into the next section.

Add regression coverage for zero-filled export space, invalid directory ranges, and truncated source or destination sections.
2026-10-08 23:49:36 +08:00
Momoko-Ayase 5bd1b0b07d chore: bump version to 1.3.2 2026-09-27 14:12:42 +08:00
Momoko-Ayase 01fdc58d2b Remove CLAUDE.md symlink
Starting Claude Code 2.1.277 if no CLAUDE.md it will check for AGENTS.md.
2026-09-27 14:05:04 +08:00
Momoko-Ayase f7d2d8b7e1 Merge pull request #8 from y52en/chatgpt/android-runtime-restoration-v2
fix(android): restore protected IL2CPP runtime state
2026-09-27 13:54:37 +08:00
y52 7728c6e7c2 refactor(android): remove sample-specific runtime fingerprint 2026-09-27 13:52:44 +08:00
y52 921cd4bfb4 fix(android): address runtime restoration review gaps 2026-09-27 13:52:44 +08:00
y52 53f31d2210 fix(android): restore newer runtime protection profiles 2026-09-27 13:52:44 +08:00
y52 9083c60d45 fix(android): restore protected runtime ELF state 2026-09-27 13:52:44 +08:00
13 changed files with 1697 additions and 239 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
AGENTS.md
Generated
+7 -7
View File
@@ -402,7 +402,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-cli" name = "senbei-cli"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"senbei-engine", "senbei-engine",
"senbei-io", "senbei-io",
@@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-crypto" name = "senbei-crypto"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"aes", "aes",
"thiserror", "thiserror",
@@ -421,7 +421,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-elf" name = "senbei-elf"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"goblin", "goblin",
"thiserror", "thiserror",
@@ -429,7 +429,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-engine" name = "senbei-engine"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"memmap2", "memmap2",
"senbei-crypto", "senbei-crypto",
@@ -444,7 +444,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-io" name = "senbei-io"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"indicatif", "indicatif",
@@ -465,7 +465,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-metadata" name = "senbei-metadata"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@@ -473,7 +473,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-pe" name = "senbei-pe"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
+1 -1
View File
@@ -15,7 +15,7 @@ exclude = ["senbei-wasm"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "1.3.1" version = "1.3.3"
edition = "2024" edition = "2024"
rust-version = "1.98.1" rust-version = "1.98.1"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
@@ -6,6 +6,11 @@ use serde_json::Value;
use super::error::{Error, Result, invalid}; use super::error::{Error, Result, invalid};
const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e]; const REQUIRED_IDS: [u32; 3] = [0x9b, 0x9d, 0x9e];
const OPTIONAL_IDS: [u32; 2] = [0x96, 0x98];
fn wanted_id(command_id: u32) -> bool {
REQUIRED_IDS.contains(&command_id) || OPTIONAL_IDS.contains(&command_id)
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Artifact { pub(crate) struct Artifact {
@@ -27,7 +32,7 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
}; };
let command_id = u32::try_from(command_id) let command_id = u32::try_from(command_id)
.map_err(|_| Error::Invalid("module command ID exceeds u32".to_owned()))?; .map_err(|_| Error::Invalid("module command ID exceeds u32".to_owned()))?;
if !REQUIRED_IDS.contains(&command_id) { if !wanted_id(command_id) {
continue; continue;
} }
let Some(path) = item.get("image_path").and_then(Value::as_str) else { let Some(path) = item.get("image_path").and_then(Value::as_str) else {
@@ -57,7 +62,7 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
}; };
let command_id = u32::try_from(command_id) let command_id = u32::try_from(command_id)
.map_err(|_| Error::Invalid("record command ID exceeds u32".to_owned()))?; .map_err(|_| Error::Invalid("record command ID exceeds u32".to_owned()))?;
if !REQUIRED_IDS.contains(&command_id) { if !wanted_id(command_id) {
continue; continue;
} }
let Some(image) = record.get("image") else { let Some(image) = record.get("image") else {
File diff suppressed because it is too large. Load diff
+182 -13
View File
@@ -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 // Strategy (a): delegate to primitives::decrypt_data3
fn decrypt_data3(&mut self, pos: u32, key: u32, shift: u32) { fn decrypt_data3(&mut self, pos: u32, key: u32, shift: u32) {
primitives::decrypt_data3(&mut self.decompressed, pos, key, shift); 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.decompressed = vec![0u8; size_of_image as usize];
u.decrypt_size = u.info[6].wrapping_sub(u.info[3]).wrapping_add(8192);
if verbose { if verbose {
println!("[2/9] Decrypting payload..."); println!("[2/9] Decrypting payload...");
} }
u.decrypt_data2(); let pe32_table = u.load_payload();
{
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 // PE32 (32-bit) images use an entirely different config layout and
// final-output transform than PE32+ (64-bit). Dispatch here, after the // final-output transform than PE32+ (64-bit). Dispatch here, after the
// shared header/payload setup (the PE32 branch follows the common // shared header/payload setup (the PE32 branch follows the common
// Stage 1/2 work). pe_magic at pe_off+24: 0x10B = PE32, 0x20B = PE32+. // 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 { 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 // 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)] #[cfg(test)]
mod error_tests { mod error_tests {
use super::*; use super::*;
+220 -112
View File
@@ -23,10 +23,153 @@ fn stage_key_rounds(data: &[u8], table: u32, slots: usize) -> Result<u32, Unpack
} }
impl<'a> Unpacker<'a> { 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, /// PE32 (32-bit) unpack pipeline. The shared Stage 1/2 setup (info decrypt,
/// payload decrypt, raw copy, header restore) has already run in `run()` /// payload decrypt, raw copy, header restore) has already run in `run()`
/// before dispatch; this takes over from "Locating shell offsets". /// 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 info = self.info;
let info3 = info[3]; let info3 = info[3];
@@ -43,94 +186,21 @@ impl<'a> Unpacker<'a> {
key key
}; };
// ---- Locate tbl in shell ---- // ---- Validated shell table ----
let tbl = let compact = get_u32(&self.decompressed, tbl.wrapping_add(0x78)) == info[6];
layout::find_tbl_pe32(&self.decompressed, &info).ok_or(UnpackError::Pe32TblNotFound)?; let stage_field = if compact { 0x88 } else { 0x98 };
if verbose { if verbose {
println!("[3/9] Locating config layout (PE32)..."); 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 ---- // ---- Stage 3: SecondStage ----
// let ss_pair = tbl.wrapping_add(stage_field);
// 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 = get_u32(&self.decompressed, ss_pair); let ss = get_u32(&self.decompressed, ss_pair);
let ss_size = get_u32(&self.decompressed, ss_pair.wrapping_add(4)); let ss_size = get_u32(&self.decompressed, ss_pair.wrapping_add(4));
let ss_shift = ss_size.wrapping_sub(0xBC0); let ss_shift = ss_size.wrapping_sub(if compact { 0xAEC } else { 0xBC0 });
// Back up the SecondStage ciphertext so a failed trial can be retried. let third_pair_off = (if compact { 0xAB4u32 } else { 0xB8C }).wrapping_add(ss_shift);
let ss_lo = ss as usize; let header_checksum = self.decrypt_pe32_second_stage(tbl, pe_off)?;
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);
}
if verbose { if verbose {
println!( println!(
" ss = 0x{:08X}, size = 0x{:X}, shift = 0x{:X}", " ss = 0x{:08X}, size = 0x{:X}, shift = 0x{:X}",
@@ -139,13 +209,12 @@ impl<'a> Unpacker<'a> {
} }
// ---- PE32 fixed offsets ---- // ---- PE32 fixed offsets ----
let third_key_off = 0x968u32.wrapping_add(ss_shift); let third_key_off = (if compact { 0x950u32 } else { 0x968 }).wrapping_add(ss_shift);
let forth_key_off = 0x964u32.wrapping_add(ss_shift); let forth_key_off = (if compact { 0x94Cu32 } else { 0x964 }).wrapping_add(ss_shift);
let cs_base_off = 0x96Cu32.wrapping_add(ss_shift); let cs_base_off = (if compact { 0x954u32 } else { 0x96C }).wrapping_add(ss_shift);
let dp_base_off = 0xA9Cu32.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) ---- // ---- 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 key = get_u32(&self.decompressed, ss.wrapping_add(third_key_off));
let pair_addr = ss.wrapping_add(third_pair_off); let pair_addr = ss.wrapping_add(third_pair_off);
let ts_addr = get_u32(&self.decompressed, pair_addr); let ts_addr = get_u32(&self.decompressed, pair_addr);
@@ -238,7 +307,7 @@ impl<'a> Unpacker<'a> {
} }
// ---- Checksum addresses ---- // ---- 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 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 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); 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( let seven_key = !get_u32(
&self.decompressed, &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; let fk7 = header_checksum ^ fifth_cs ^ seven_key;
if let Err(reason) = self.decrypt_and_decompress_data(seven_addr, fk7, None) { 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] ---- // ---- PE32 metadata: EP and data dirs from info[3] ----
let test_val = get_u32(&self.decompressed, info3.wrapping_add(0x10)); let (metadata_ep, metadata_dirs) = self.read_pe32_metadata(compact);
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);
}
// ---- Zero-out list (runs BEFORE decompression) ---- // ---- Zero-out list (runs BEFORE decompression) ----
let zero_list_addr = eighth_start.wrapping_add(off_zero_list); let zero_list_addr = eighth_start.wrapping_add(off_zero_list);
@@ -823,7 +873,15 @@ impl<'a> Unpacker<'a> {
let d = export_va as usize; let d = export_va as usize;
let s = export_file_off as usize; let s = export_file_off as usize;
let n = export_size as usize; let n = export_size as usize;
self.decompressed[d..d + n].copy_from_slice(&self.file_data[s..s + n]); // Spliced companions may omit the stub's plaintext exports;
// the I/O adapter overlays those from the original stub later.
if s.checked_add(n)
.is_some_and(|end| end <= self.file_data.len())
&& d.checked_add(n)
.is_some_and(|end| end <= self.decompressed.len())
{
self.decompressed[d..d + n].copy_from_slice(&self.file_data[s..s + n]);
}
} }
// ---- .text decrypt with decrypt_data8 (PE32 auto-detected formula) ---- // ---- .text decrypt with decrypt_data8 (PE32 auto-detected formula) ----
@@ -1149,6 +1207,56 @@ impl<'a> Unpacker<'a> {
mod tests { mod tests {
use super::*; 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] #[test]
fn stage_key_rounds_follow_active_descriptor_prefix() { fn stage_key_rounds_follow_active_descriptor_prefix() {
for (sizes, expected) in [ for (sizes, expected) in [
+3 -2
View File
@@ -6,8 +6,9 @@ mod image;
pub(super) use dd8::{select_dd8_formula_pe32, select_dd8_shift}; pub(super) use dd8::{select_dd8_formula_pe32, select_dd8_shift};
pub(super) use discovery::{ pub(super) use discovery::{
discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos, find_tbl_pe32, discover_eighth_slots, find_bytecode_offset, find_lfsr_block, find_str_pos,
find_v_after_pad, find_v4_offset, get_string_to_null, section_name, trial_decrypt5_u32, find_tbl_pe32_candidates, find_v_after_pad, find_v4_offset, get_string_to_null, section_name,
trial_decrypt5_u32,
}; };
pub(super) use image::{ pub(super) use image::{
compact_memory_image_to_pe, move_pe32_imports_to_kmiat, pe32_imports_already_match_idata_layout, compact_memory_image_to_pe, move_pe32_imports_to_kmiat, pe32_imports_already_match_idata_layout,
+96 -28
View File
@@ -212,37 +212,54 @@ pub fn section_name(data: &[u8], offset: u32) -> String {
// PE32 (32-bit) helpers // PE32 (32-bit) helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// PE32 shell-table locator. Walks the shell region (`info[6]`) for a dword /// PE32 shell-table locator. The compact layout moves the shell descriptor
/// equal to `info[6]` followed by a plausible shell size, returning the table /// from +0x88 to +0x78 and the header-checksum list from +0x58 to +0x50.
/// base (`candidate = off - 0x88`) when `candidate+0x58` holds a valid pointer. /// Return structurally plausible candidates; the pipeline must trial-decrypt
pub fn find_tbl_pe32(data: &[u8], info: &[u32; 8]) -> Option<u32> { /// their second stages before selecting a table.
let shell = info[6]; pub fn find_tbl_pe32_candidates(data: &[u8], info: &[u32; 8]) -> Vec<u32> {
if (data.len() as u64) < 0x100 { let mut tables = Vec::new();
return None; let shell = info[6] as usize;
} let hi = shell.saturating_add(0x3000).min(data.len());
let hi = (shell as u64) for off in (shell..hi.saturating_sub(8)).step_by(4) {
.saturating_add(0x3000) if get_u32(data, off as u32) != info[6] {
.min(data.len() as u64 - 0x100) as u32; continue;
let mut off = shell; }
while off < hi { let shell_size = get_u32(data, off as u32 + 4) as usize;
if off as usize + 8 <= data.len() { if !(0x1001..0x100000).contains(&shell_size)
let candidate = off.wrapping_sub(0x88); || shell
if candidate >= shell && get_u32(data, off) == info[6] { .checked_add(shell_size)
let shell_size_val = get_u32(data, off.wrapping_add(4)); .is_none_or(|end| end > data.len())
if shell_size_val > 0x1000 && shell_size_val < 0x100000 { {
let v58_off = candidate.wrapping_add(0x58); continue;
if (v58_off as usize + 4) <= data.len() { }
let v58 = get_u32(data, v58_off); for (shell_field, header_field) in [(0x88, 0x58), (0x78, 0x50)] {
if v58 > 0 && (v58 as usize) < data.len() { let Some(candidate) = off.checked_sub(shell_field).filter(|&base| base >= shell) else {
return Some(candidate); 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. /// Locate an LFSR-encrypted bytecode block (decrypt_data6 form) in a region.
@@ -449,6 +466,57 @@ pub fn discover_eighth_slots(
mod tests { mod tests {
use super::*; 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 /// 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 /// sits PAST `len` but within `len*2`. Assert that the smaller window misses
/// it and the doubled window finds it. /// it and the doubled window finds it.
+57 -1
View File
@@ -331,6 +331,13 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
let opt_hdr = pe_header.wrapping_add(24); let opt_hdr = pe_header.wrapping_add(24);
let sec_table = opt_hdr.wrapping_add(opt_hdr_size); let sec_table = opt_hdr.wrapping_add(opt_hdr_size);
let num_sections = get_u16(data, pe_header.wrapping_add(6)) as u32; let num_sections = get_u16(data, pe_header.wrapping_add(6)) as u32;
let export_dir = opt_hdr.wrapping_add(if get_u16(data, opt_hdr) == 0x20B {
112
} else {
96
});
let export_rva = get_u32(data, export_dir);
let export_size = get_u32(data, export_dir.wrapping_add(4));
struct SecLayout { struct SecLayout {
sec_off: u32, sec_off: u32,
@@ -365,11 +372,21 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
break; break;
} }
} }
let meaningful = if last_nonzero >= 0 { let mut meaningful = if last_nonzero >= 0 {
(last_nonzero + 1) as u32 (last_nonzero + 1) as u32
} else { } else {
0 0
}; };
// Companion exports are overlaid after compaction. Keep their whole
// range file-backed even when the payload leaves it zero-filled.
if export_size != 0
&& let Some(end) = export_rva
.checked_sub(va)
.and_then(|delta| delta.checked_add(export_size))
&& end as usize <= section_data.len()
{
meaningful = meaningful.max(end);
}
let mut raw_size = if meaningful != 0 { let mut raw_size = if meaningful != 0 {
align_up_u32(meaningful, FILE_ALIGNMENT) align_up_u32(meaningful, FILE_ALIGNMENT)
} else { } else {
@@ -433,6 +450,45 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option<Vec<u8>
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn compaction_keeps_export_space_without_changing_the_next_section() {
for (rva, size, expected_raw_size) in [
(0, 0, 0x200),
(0x1280, 0x181, 0x600),
(0x1f00, 0x200, 0x200),
(0xffff_fff0, 0x40, 0x200),
] {
let mut data = vec![0; 0x3000];
data[..2].copy_from_slice(b"MZ");
data[0x80..0x84].copy_from_slice(b"PE\0\0");
write_u16(&mut data, 0x86, 2);
write_u16(&mut data, 0x94, 0xe0);
write_u16(&mut data, 0x98, 0x10b);
for (offset, value) in [
(0x3c, 0x80),
(0xf8, rva),
(0xfc, size),
(0x180, 0x1000),
(0x184, 0x1000),
(0x1a8, 0x1000),
(0x1ac, 0x2000),
] {
write_u32(&mut data, offset, value);
}
data[0x1000] = 0xaa;
data[0x2000..0x200c].copy_from_slice(b"next section");
let out = compact_memory_image_to_pe(&data, 0x80).unwrap();
assert_eq!(get_u32(&out, 0x188), expected_raw_size);
let headers = senbei_pe::parse(&out).unwrap();
let next = senbei_pe::rva_range(&out, headers, 0x2000, 12).unwrap();
assert_eq!(&out[next], b"next section");
if rva == 0x1280 {
let exports = senbei_pe::rva_range(&out, headers, rva, size).unwrap();
assert!(out[exports].iter().all(|&byte| byte == 0));
}
}
}
/// Review regression: a zero last-section VA (corrupt section table) must /// Review regression: a zero last-section VA (corrupt section table) must
/// bail instead of building .kmiat at RVA 0 — the old code zeroed /// bail instead of building .kmiat at RVA 0 — the old code zeroed
/// `[0, 0x7000)`, wiping the DOS/PE headers, and returned the broken image /// `[0, 0x7000)`, wiping the DOS/PE headers, and returned the broken image
+39 -10
View File
@@ -128,18 +128,19 @@ pub(crate) fn overlay_exports_from_stub(out: &mut [u8], stub: &[u8]) {
Some(v) if v.1 != 0 => v, Some(v) if v.1 != 0 => v,
_ => return, _ => return,
}; };
let dst = match rva_to_file_off(out, export_rva) { let dst = match senbei_pe::parse(out)
Some(o) => o, .and_then(|headers| senbei_pe::rva_range(out, headers, export_rva, export_size))
None => return, {
Ok(range) => range,
Err(_) => return,
}; };
let src = match rva_to_file_off(stub, export_rva) { let src = match senbei_pe::parse(stub)
Some(o) => o, .and_then(|headers| senbei_pe::rva_range(stub, headers, export_rva, export_size))
None => return, {
Ok(range) => range,
Err(_) => return,
}; };
let n = export_size as usize; out[dst].copy_from_slice(&stub[src]);
if dst + n <= out.len() && src + n <= stub.len() {
out[dst..dst + n].copy_from_slice(&stub[src..src + n]);
}
} }
/// Restore the CLR regions retained by an external-companion loader stub. /// Restore the CLR regions retained by an external-companion loader stub.
@@ -633,6 +634,34 @@ mod tests {
data data
} }
#[test]
fn export_overlay_requires_the_entire_range_to_be_file_backed() {
for is_pe32_plus in [false, true] {
let dirs = 0x98 + if is_pe32_plus { 112 } else { 96 };
let section = 0x98 + if is_pe32_plus { 0xf0 } else { 0xe0 };
let mut stub = managed_fixture(is_pe32_plus, 0x600);
stub[0x780..0x880].fill(0x5a);
for (stub_raw_size, out_raw_size) in
[(0x600u32, 0x600u32), (0x200, 0x600), (0x600, 0x200)]
{
let mut out = managed_fixture(is_pe32_plus, 0x400);
out[dirs..dirs + 4].copy_from_slice(&0x2180u32.to_le_bytes());
out[dirs + 4..dirs + 8].copy_from_slice(&0x100u32.to_le_bytes());
out[section + 16..section + 20].copy_from_slice(&out_raw_size.to_le_bytes());
stub[section + 16..section + 20].copy_from_slice(&stub_raw_size.to_le_bytes());
let before = out.clone();
overlay_exports_from_stub(&mut out, &stub);
if stub_raw_size == 0x600 && out_raw_size == 0x600 {
assert_eq!(&out[0x580..0x680], &[0x5a; 0x100]);
assert_eq!(&out[..0x580], &before[..0x580]);
assert_eq!(&out[0x680..], &before[0x680..]);
} else {
assert_eq!(out, before);
}
}
}
}
#[test] #[test]
fn managed_companion_restores_rva_mapped_regions_without_overwriting_il() { fn managed_companion_restores_rva_mapped_regions_without_overwriting_il() {
for is_pe32_plus in [false, true] { for is_pe32_plus in [false, true] {
+7 -7
View File
@@ -412,7 +412,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-crypto" name = "senbei-crypto"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"aes", "aes",
"thiserror", "thiserror",
@@ -420,7 +420,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-elf" name = "senbei-elf"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"goblin", "goblin",
"thiserror", "thiserror",
@@ -428,7 +428,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-engine" name = "senbei-engine"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"memmap2", "memmap2",
"senbei-crypto", "senbei-crypto",
@@ -443,7 +443,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-io" name = "senbei-io"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"indicatif", "indicatif",
@@ -464,7 +464,7 @@ dependencies = [
[[package]] [[package]]
name = "senbei-metadata" name = "senbei-metadata"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@@ -472,14 +472,14 @@ dependencies = [
[[package]] [[package]]
name = "senbei-pe" name = "senbei-pe"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"thiserror", "thiserror",
] ]
[[package]] [[package]]
name = "senbei-wasm" name = "senbei-wasm"
version = "1.3.1" version = "1.3.3"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"senbei-engine", "senbei-engine",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "senbei-wasm" name = "senbei-wasm"
version = "1.3.1" version = "1.3.3"
edition = "2024" edition = "2024"
rust-version = "1.98.1" rust-version = "1.98.1"
description = "WebAssembly bindings for senbei (browser frontend assets live in web/)" description = "WebAssembly bindings for senbei (browser frontend assets live in web/)"