1use std::path::Path;
4
5use crate::error::PeError;
6use crate::rich_header::RichHeader;
7
8#[derive(Debug, Clone, serde::Serialize)]
10pub struct PeFile {
11 pub machine: u16,
14 pub compile_timestamp: u32,
16 pub is_dll: bool,
18 pub is_exe: bool,
20
21 pub entry_point_rva: u32,
25 pub image_base: u64,
27 pub checksum: u32,
29
30 pub is_dotnet: bool,
33 pub tls_callback_count: usize,
35 pub has_reloc: bool,
37 pub is_signed: bool,
39
40 pub pdb_path: Option<String>,
44
45 pub overlay_offset: Option<u64>,
48 pub overlay_size: Option<u64>,
50
51 pub rich_header: Option<RichHeader>,
55
56 pub imports: Vec<String>,
59 pub exports: Vec<String>,
61 pub sections: Vec<PeSection>,
63
64 pub ascii_strings: Vec<String>,
67 pub utf16_strings: Vec<String>,
69
70 pub sha256: String,
73 pub size: usize,
75}
76
77impl PeFile {
78 pub fn all_strings(&self) -> impl Iterator<Item = &str> {
80 self.ascii_strings
81 .iter()
82 .chain(self.utf16_strings.iter())
83 .map(String::as_str)
84 }
85}
86
87#[derive(Debug, Clone, serde::Serialize)]
89pub struct PeSection {
90 pub name: String,
92 pub virtual_size: u32,
94 pub raw_size: u32,
96 pub virtual_address: u32,
98 pub entropy: f32,
100 pub is_executable: bool,
102 pub is_writable: bool,
104 pub is_readable: bool,
106}
107
108pub fn parse_pe(bytes: &[u8]) -> Result<PeFile, PeError> {
113 use forensicnomicon::heuristics::pe::MZ_MAGIC;
114 use goblin::pe::PE;
115 use sha2::{Digest, Sha256};
116
117 if bytes.len() < 2 || bytes[0..2] != MZ_MAGIC {
118 return Err(PeError::NotPe);
119 }
120
121 let pe = PE::parse(bytes).map_err(|e| PeError::Structure(e.to_string()))?;
122
123 let machine = pe.header.coff_header.machine;
124 let compile_timestamp = pe.header.coff_header.time_date_stamp;
125 let characteristics = pe.header.coff_header.characteristics;
126 let is_dll = characteristics & 0x2000 != 0;
127 let is_exe = characteristics & 0x0002 != 0;
128
129 let imports: Vec<String> = pe.imports.iter().map(|i| i.name.to_string()).collect();
130 let exports: Vec<String> = pe
131 .exports
132 .iter()
133 .filter_map(|e| e.name.map(str::to_string))
134 .collect();
135
136 let sections = pe
137 .sections
138 .iter()
139 .map(|sec| {
140 let name = String::from_utf8_lossy(&sec.name)
141 .trim_end_matches('\0')
142 .to_string();
143 let offset = sec.pointer_to_raw_data as usize;
144 let raw_size = sec.size_of_raw_data;
145 let data = bytes
146 .get(offset..offset.saturating_add(raw_size as usize))
147 .unwrap_or(&[]);
148 let entropy = crate::strings::compute_entropy(data);
149 PeSection {
150 name,
151 virtual_size: sec.virtual_size,
152 raw_size,
153 virtual_address: sec.virtual_address,
154 entropy,
155 is_executable: sec.characteristics & 0x2000_0000 != 0,
156 is_writable: sec.characteristics & 0x8000_0000 != 0,
157 is_readable: sec.characteristics & 0x4000_0000 != 0,
158 }
159 })
160 .collect();
161
162 let ascii_strings = crate::strings::extract_ascii(bytes, crate::strings::MIN_STRING_LEN);
163 let utf16_strings = crate::strings::extract_utf16le(bytes, crate::strings::MIN_STRING_LEN);
164
165 let sha256 = {
166 let mut hasher = Sha256::new();
167 hasher.update(bytes);
168 hex::encode(hasher.finalize())
169 };
170
171 let (entry_point_rva, image_base, checksum) = if let Some(oh) = pe.header.optional_header {
173 (
174 oh.standard_fields.address_of_entry_point,
175 oh.windows_fields.image_base,
176 oh.windows_fields.check_sum,
177 )
178 } else {
179 (0, 0, 0)
180 };
181
182 let is_dotnet = pe.clr_data.is_some();
184 let tls_callback_count = pe.tls_data.as_ref().map_or(0, |t| t.callbacks.len());
185 let has_reloc = pe.relocation_data.is_some();
186 let is_signed = !pe.certificates.is_empty();
187
188 let pdb_path = pe.debug_data.as_ref().and_then(|d| {
190 d.codeview_pdb70_debug_info.map(|cv| {
191 String::from_utf8_lossy(cv.filename)
192 .trim_end_matches('\0')
193 .to_string()
194 })
195 });
196
197 let last_section_end: u64 = pe
199 .sections
200 .iter()
201 .filter(|s| s.size_of_raw_data > 0)
202 .map(|s| s.pointer_to_raw_data as u64 + s.size_of_raw_data as u64)
203 .max()
204 .unwrap_or(0);
205 let file_size = bytes.len() as u64;
206 let (overlay_offset, overlay_size) = if last_section_end > 0 && file_size > last_section_end {
207 (Some(last_section_end), Some(file_size - last_section_end))
208 } else {
209 (None, None)
210 };
211
212 let rich_header = crate::rich_header::parse_rich_header(bytes);
214
215 Ok(PeFile {
216 machine,
217 compile_timestamp,
218 is_dll,
219 is_exe,
220 entry_point_rva,
221 image_base,
222 checksum,
223 is_dotnet,
224 tls_callback_count,
225 has_reloc,
226 is_signed,
227 pdb_path,
228 overlay_offset,
229 overlay_size,
230 rich_header,
231 imports,
232 exports,
233 sections,
234 ascii_strings,
235 utf16_strings,
236 sha256,
237 size: bytes.len(),
238 })
239}
240
241pub fn parse_pe_path(path: &Path) -> Result<PeFile, PeError> {
245 let bytes = std::fs::read(path)?;
246 parse_pe(&bytes)
247}
248
249#[cfg(test)]
250pub(crate) mod test_helpers {
251 pub fn make_minimal_pe_x64(timestamp: u32, is_dll: bool) -> Vec<u8> {
256 let mut pe = vec![0u8; 512];
257
258 pe[0] = b'M';
260 pe[1] = b'Z';
261 pe[0x3C] = 0x40; pe[0x40] = b'P';
265 pe[0x41] = b'E';
266
267 pe[0x44] = 0x64;
269 pe[0x45] = 0x86; pe[0x48..0x4C].copy_from_slice(×tamp.to_le_bytes()); pe[0x54] = 0xF0; pe[0x56] = if is_dll { 0x22 | 0x20 } else { 0x22 }; if is_dll {
277 let chars: u16 = 0x2022; pe[0x56..0x58].copy_from_slice(&chars.to_le_bytes());
279 } else {
280 let chars: u16 = 0x0022;
281 pe[0x56..0x58].copy_from_slice(&chars.to_le_bytes());
282 }
283
284 pe[0x58] = 0x0B;
286 pe[0x59] = 0x02; pe[0x70] = 0x00;
289 pe[0x71] = 0x00;
290 pe[0x72] = 0x40; pe[0x78] = 0x00;
293 pe[0x79] = 0x10; pe[0x7C] = 0x00;
296 pe[0x7D] = 0x02; pe[0x88] = 0x06;
299 pe[0x90] = 0x00;
301 pe[0x91] = 0x10; pe[0x94] = 0x00;
304 pe[0x95] = 0x02; pe[0x9C] = 0x02;
307 pe[0xA0] = 0x00;
309 pe[0xA1] = 0x00;
310 pe[0xA2] = 0x10; pe[0xA8] = 0x00;
313 pe[0xA9] = 0x10; pe[0xB0] = 0x00;
316 pe[0xB1] = 0x00;
317 pe[0xB2] = 0x10; pe[0xB8] = 0x00;
320 pe[0xB9] = 0x10; pe[0xC4] = 0x10; pe
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use test_helpers::make_minimal_pe_x64;
332
333 #[test]
336 fn rejects_empty_slice() {
337 assert!(matches!(parse_pe(&[]), Err(PeError::NotPe)));
338 }
339
340 #[test]
341 fn rejects_single_byte() {
342 assert!(matches!(parse_pe(&[0x4D]), Err(PeError::NotPe)));
343 }
344
345 #[test]
346 fn rejects_random_bytes() {
347 assert!(parse_pe(b"this is not a PE file at all").is_err());
348 }
349
350 #[test]
351 fn rejects_elf_magic() {
352 let elf = [0x7F, b'E', b'L', b'F', 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
353 assert!(parse_pe(&elf).is_err());
354 }
355
356 #[test]
357 fn rejects_truncated_mz() {
358 assert!(parse_pe(b"MZ").is_err());
359 }
360
361 #[test]
362 fn rejects_mz_with_no_pe_sig() {
363 let mut buf = vec![0u8; 64];
364 buf[0] = b'M';
365 buf[1] = b'Z';
366 buf[0x3C] = 0x40; assert!(parse_pe(&buf).is_err());
368 }
369
370 #[test]
373 fn accepts_minimal_x64() {
374 let bytes = make_minimal_pe_x64(0, false);
375 assert!(
376 parse_pe(&bytes).is_ok(),
377 "minimal PE32+ must parse successfully"
378 );
379 }
380
381 #[test]
382 fn extracts_machine_amd64() {
383 let bytes = make_minimal_pe_x64(0, false);
384 let pe = parse_pe(&bytes).expect("minimal PE");
385 assert_eq!(pe.machine, 0x8664);
386 }
387
388 #[test]
389 fn extracts_compile_timestamp() {
390 let ts = 0x5F00_ABCD_u32;
391 let bytes = make_minimal_pe_x64(ts, false);
392 let pe = parse_pe(&bytes).expect("minimal PE");
393 assert_eq!(pe.compile_timestamp, ts);
394 }
395
396 #[test]
397 fn exe_is_not_dll() {
398 let bytes = make_minimal_pe_x64(0, false);
399 let pe = parse_pe(&bytes).expect("minimal PE");
400 assert!(!pe.is_dll);
401 assert!(pe.is_exe);
402 }
403
404 #[test]
405 fn dll_flag_detected() {
406 let bytes = make_minimal_pe_x64(0, true);
407 let pe = parse_pe(&bytes).expect("minimal DLL PE");
408 assert!(pe.is_dll);
409 }
410
411 #[test]
412 fn minimal_pe_has_no_imports() {
413 let bytes = make_minimal_pe_x64(0, false);
414 let pe = parse_pe(&bytes).expect("minimal PE");
415 assert!(pe.imports.is_empty());
416 }
417
418 #[test]
419 fn minimal_pe_has_no_sections() {
420 let bytes = make_minimal_pe_x64(0, false);
421 let pe = parse_pe(&bytes).expect("minimal PE");
422 assert!(pe.sections.is_empty());
423 }
424
425 #[test]
426 fn populates_sha256() {
427 let bytes = make_minimal_pe_x64(0, false);
428 let pe = parse_pe(&bytes).expect("minimal PE");
429 assert_eq!(pe.sha256.len(), 64, "SHA-256 hex string is 64 chars");
430 assert!(pe.sha256.chars().all(|c| c.is_ascii_hexdigit()));
431 }
432
433 #[test]
434 fn populates_size() {
435 let bytes = make_minimal_pe_x64(0, false);
436 let expected_size = bytes.len();
437 let pe = parse_pe(&bytes).expect("minimal PE");
438 assert_eq!(pe.size, expected_size);
439 }
440
441 #[test]
444 fn parse_pe_path_nonexistent_returns_io_error() {
445 let result = parse_pe_path(Path::new("/nonexistent/rbcw.exe"));
446 assert!(result.is_err());
447 }
448
449 #[test]
450 fn parse_pe_path_non_pe_file_returns_not_pe() {
451 use std::io::Write;
452 let mut tmp = tempfile::NamedTempFile::new().expect("tmp file");
453 tmp.write_all(b"this is plain text, not a PE")
454 .expect("write");
455 let result = parse_pe_path(tmp.path());
456 assert!(result.is_err());
457 }
458
459 #[test]
462 fn image_base_extracted_from_optional_header() {
463 let bytes = make_minimal_pe_x64(0, false);
464 let pe = parse_pe(&bytes).expect("minimal PE");
465 assert_eq!(
467 pe.image_base, 0x0040_0000,
468 "image_base must be extracted from optional header"
469 );
470 }
471
472 #[test]
473 fn entry_point_rva_is_zero_for_minimal_pe() {
474 let bytes = make_minimal_pe_x64(0, false);
475 let pe = parse_pe(&bytes).expect("minimal PE");
476 assert_eq!(pe.entry_point_rva, 0, "minimal PE has no entry point");
477 }
478
479 #[test]
480 fn minimal_pe_has_no_dotnet() {
481 let bytes = make_minimal_pe_x64(0, false);
482 let pe = parse_pe(&bytes).expect("minimal PE");
483 assert!(!pe.is_dotnet);
484 }
485
486 #[test]
487 fn minimal_pe_has_zero_tls_callbacks() {
488 let bytes = make_minimal_pe_x64(0, false);
489 let pe = parse_pe(&bytes).expect("minimal PE");
490 assert_eq!(pe.tls_callback_count, 0);
491 }
492
493 #[test]
494 fn minimal_pe_has_no_reloc() {
495 let bytes = make_minimal_pe_x64(0, false);
496 let pe = parse_pe(&bytes).expect("minimal PE");
497 assert!(!pe.has_reloc);
498 }
499
500 #[test]
501 fn minimal_pe_is_unsigned() {
502 let bytes = make_minimal_pe_x64(0, false);
503 let pe = parse_pe(&bytes).expect("minimal PE");
504 assert!(!pe.is_signed);
505 }
506
507 #[test]
508 fn minimal_pe_has_no_pdb_path() {
509 let bytes = make_minimal_pe_x64(0, false);
510 let pe = parse_pe(&bytes).expect("minimal PE");
511 assert!(pe.pdb_path.is_none());
512 }
513
514 #[test]
515 fn minimal_pe_has_no_overlay() {
516 let bytes = make_minimal_pe_x64(0, false);
517 let pe = parse_pe(&bytes).expect("minimal PE");
518 assert!(pe.overlay_offset.is_none());
519 assert!(pe.overlay_size.is_none());
520 }
521
522 #[test]
523 fn minimal_pe_has_no_rich_header() {
524 let bytes = make_minimal_pe_x64(0, false);
526 let pe = parse_pe(&bytes).expect("minimal PE");
527 assert!(pe.rich_header.is_none());
528 }
529}