From 6e233bbf183efdd3f677a4eb06e7d4c9746fee9e Mon Sep 17 00:00:00 2001 From: bfloat16 Date: Thu, 8 Oct 2026 23:49:36 +0800 Subject: [PATCH] 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. --- .../src/windows/exe/pipeline/pe32.rs | 10 +++- senbei-engine/src/windows/layout/image.rs | 58 ++++++++++++++++++- senbei-io/src/windows/mod.rs | 49 ++++++++++++---- 3 files changed, 105 insertions(+), 12 deletions(-) diff --git a/senbei-engine/src/windows/exe/pipeline/pe32.rs b/senbei-engine/src/windows/exe/pipeline/pe32.rs index 66827e1..3c2c485 100644 --- a/senbei-engine/src/windows/exe/pipeline/pe32.rs +++ b/senbei-engine/src/windows/exe/pipeline/pe32.rs @@ -823,7 +823,15 @@ impl<'a> Unpacker<'a> { let d = export_va as usize; let s = export_file_off 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) ---- diff --git a/senbei-engine/src/windows/layout/image.rs b/senbei-engine/src/windows/layout/image.rs index 04065cd..ca4f362 100644 --- a/senbei-engine/src/windows/layout/image.rs +++ b/senbei-engine/src/windows/layout/image.rs @@ -331,6 +331,13 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option let opt_hdr = pe_header.wrapping_add(24); let sec_table = opt_hdr.wrapping_add(opt_hdr_size); 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 { sec_off: u32, @@ -365,11 +372,21 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option break; } } - let meaningful = if last_nonzero >= 0 { + let mut meaningful = if last_nonzero >= 0 { (last_nonzero + 1) as u32 } else { 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 { align_up_u32(meaningful, FILE_ALIGNMENT) } else { @@ -433,6 +450,45 @@ pub fn compact_memory_image_to_pe(data: &[u8], pe_header: u32) -> Option mod tests { 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 /// bail instead of building .kmiat at RVA 0 — the old code zeroed /// `[0, 0x7000)`, wiping the DOS/PE headers, and returned the broken image diff --git a/senbei-io/src/windows/mod.rs b/senbei-io/src/windows/mod.rs index c7ba901..1854e4e 100644 --- a/senbei-io/src/windows/mod.rs +++ b/senbei-io/src/windows/mod.rs @@ -128,18 +128,19 @@ pub(crate) fn overlay_exports_from_stub(out: &mut [u8], stub: &[u8]) { Some(v) if v.1 != 0 => v, _ => return, }; - let dst = match rva_to_file_off(out, export_rva) { - Some(o) => o, - None => return, + let dst = match senbei_pe::parse(out) + .and_then(|headers| senbei_pe::rva_range(out, headers, export_rva, export_size)) + { + Ok(range) => range, + Err(_) => return, }; - let src = match rva_to_file_off(stub, export_rva) { - Some(o) => o, - None => return, + let src = match senbei_pe::parse(stub) + .and_then(|headers| senbei_pe::rva_range(stub, headers, export_rva, export_size)) + { + Ok(range) => range, + Err(_) => return, }; - let n = export_size as usize; - if dst + n <= out.len() && src + n <= stub.len() { - out[dst..dst + n].copy_from_slice(&stub[src..src + n]); - } + out[dst].copy_from_slice(&stub[src]); } /// Restore the CLR regions retained by an external-companion loader stub. @@ -633,6 +634,34 @@ mod tests { 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] fn managed_companion_restores_rva_mapped_regions_without_overwriting_il() { for is_pe32_plus in [false, true] {