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] {