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| u64::from(s.pointer_to_raw_data) + u64::from(s.size_of_raw_data))
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; if is_dll {
275 let chars: u16 = 0x2022; pe[0x56..0x58].copy_from_slice(&chars.to_le_bytes());
277 } else {
278 let chars: u16 = 0x0022;
279 pe[0x56..0x58].copy_from_slice(&chars.to_le_bytes());
280 }
281
282 pe[0x58] = 0x0B;
284 pe[0x59] = 0x02; pe[0x70] = 0x00;
287 pe[0x71] = 0x00;
288 pe[0x72] = 0x40; pe[0x78] = 0x00;
291 pe[0x79] = 0x10; pe[0x7C] = 0x00;
294 pe[0x7D] = 0x02; pe[0x88] = 0x06;
297 pe[0x90] = 0x00;
299 pe[0x91] = 0x10; pe[0x94] = 0x00;
302 pe[0x95] = 0x02; pe[0x9C] = 0x02;
305 pe[0xA0] = 0x00;
307 pe[0xA1] = 0x00;
308 pe[0xA2] = 0x10; pe[0xA8] = 0x00;
311 pe[0xA9] = 0x10; pe[0xB0] = 0x00;
314 pe[0xB1] = 0x00;
315 pe[0xB2] = 0x10; pe[0xB8] = 0x00;
318 pe[0xB9] = 0x10; pe[0xC4] = 0x10; pe
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use test_helpers::make_minimal_pe_x64;
330
331 #[test]
334 fn rejects_empty_slice() {
335 assert!(matches!(parse_pe(&[]), Err(PeError::NotPe)));
336 }
337
338 #[test]
339 fn rejects_single_byte() {
340 assert!(matches!(parse_pe(&[0x4D]), Err(PeError::NotPe)));
341 }
342
343 #[test]
344 fn rejects_random_bytes() {
345 assert!(parse_pe(b"this is not a PE file at all").is_err());
346 }
347
348 #[test]
349 fn rejects_elf_magic() {
350 let elf = [0x7F, b'E', b'L', b'F', 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
351 assert!(parse_pe(&elf).is_err());
352 }
353
354 #[test]
355 fn rejects_truncated_mz() {
356 assert!(parse_pe(b"MZ").is_err());
357 }
358
359 #[test]
360 fn rejects_mz_with_no_pe_sig() {
361 let mut buf = vec![0u8; 64];
362 buf[0] = b'M';
363 buf[1] = b'Z';
364 buf[0x3C] = 0x40; assert!(parse_pe(&buf).is_err());
366 }
367
368 #[test]
371 fn accepts_minimal_x64() {
372 let bytes = make_minimal_pe_x64(0, false);
373 assert!(
374 parse_pe(&bytes).is_ok(),
375 "minimal PE32+ must parse successfully"
376 );
377 }
378
379 #[test]
380 fn extracts_machine_amd64() {
381 let bytes = make_minimal_pe_x64(0, false);
382 let pe = parse_pe(&bytes).expect("minimal PE");
383 assert_eq!(pe.machine, 0x8664);
384 }
385
386 #[test]
387 fn extracts_compile_timestamp() {
388 let ts = 0x5F00_ABCD_u32;
389 let bytes = make_minimal_pe_x64(ts, false);
390 let pe = parse_pe(&bytes).expect("minimal PE");
391 assert_eq!(pe.compile_timestamp, ts);
392 }
393
394 #[test]
395 fn exe_is_not_dll() {
396 let bytes = make_minimal_pe_x64(0, false);
397 let pe = parse_pe(&bytes).expect("minimal PE");
398 assert!(!pe.is_dll);
399 assert!(pe.is_exe);
400 }
401
402 #[test]
403 fn dll_flag_detected() {
404 let bytes = make_minimal_pe_x64(0, true);
405 let pe = parse_pe(&bytes).expect("minimal DLL PE");
406 assert!(pe.is_dll);
407 }
408
409 #[test]
410 fn minimal_pe_has_no_imports() {
411 let bytes = make_minimal_pe_x64(0, false);
412 let pe = parse_pe(&bytes).expect("minimal PE");
413 assert!(pe.imports.is_empty());
414 }
415
416 #[test]
417 fn minimal_pe_has_no_sections() {
418 let bytes = make_minimal_pe_x64(0, false);
419 let pe = parse_pe(&bytes).expect("minimal PE");
420 assert!(pe.sections.is_empty());
421 }
422
423 #[test]
424 fn populates_sha256() {
425 let bytes = make_minimal_pe_x64(0, false);
426 let pe = parse_pe(&bytes).expect("minimal PE");
427 assert_eq!(pe.sha256.len(), 64, "SHA-256 hex string is 64 chars");
428 assert!(pe.sha256.chars().all(|c| c.is_ascii_hexdigit()));
429 }
430
431 #[test]
432 fn populates_size() {
433 let bytes = make_minimal_pe_x64(0, false);
434 let expected_size = bytes.len();
435 let pe = parse_pe(&bytes).expect("minimal PE");
436 assert_eq!(pe.size, expected_size);
437 }
438
439 #[test]
442 fn parse_pe_path_nonexistent_returns_io_error() {
443 let result = parse_pe_path(Path::new("/nonexistent/rbcw.exe"));
444 assert!(result.is_err());
445 }
446
447 #[test]
448 fn parse_pe_path_non_pe_file_returns_not_pe() {
449 use std::io::Write;
450 let mut tmp = tempfile::NamedTempFile::new().expect("tmp file");
451 tmp.write_all(b"this is plain text, not a PE")
452 .expect("write");
453 let result = parse_pe_path(tmp.path());
454 assert!(result.is_err());
455 }
456
457 #[test]
460 fn image_base_extracted_from_optional_header() {
461 let bytes = make_minimal_pe_x64(0, false);
462 let pe = parse_pe(&bytes).expect("minimal PE");
463 assert_eq!(
465 pe.image_base, 0x0040_0000,
466 "image_base must be extracted from optional header"
467 );
468 }
469
470 #[test]
471 fn entry_point_rva_is_zero_for_minimal_pe() {
472 let bytes = make_minimal_pe_x64(0, false);
473 let pe = parse_pe(&bytes).expect("minimal PE");
474 assert_eq!(pe.entry_point_rva, 0, "minimal PE has no entry point");
475 }
476
477 #[test]
478 fn minimal_pe_has_no_dotnet() {
479 let bytes = make_minimal_pe_x64(0, false);
480 let pe = parse_pe(&bytes).expect("minimal PE");
481 assert!(!pe.is_dotnet);
482 }
483
484 #[test]
485 fn minimal_pe_has_zero_tls_callbacks() {
486 let bytes = make_minimal_pe_x64(0, false);
487 let pe = parse_pe(&bytes).expect("minimal PE");
488 assert_eq!(pe.tls_callback_count, 0);
489 }
490
491 #[test]
492 fn minimal_pe_has_no_reloc() {
493 let bytes = make_minimal_pe_x64(0, false);
494 let pe = parse_pe(&bytes).expect("minimal PE");
495 assert!(!pe.has_reloc);
496 }
497
498 #[test]
499 fn minimal_pe_is_unsigned() {
500 let bytes = make_minimal_pe_x64(0, false);
501 let pe = parse_pe(&bytes).expect("minimal PE");
502 assert!(!pe.is_signed);
503 }
504
505 #[test]
506 fn minimal_pe_has_no_pdb_path() {
507 let bytes = make_minimal_pe_x64(0, false);
508 let pe = parse_pe(&bytes).expect("minimal PE");
509 assert!(pe.pdb_path.is_none());
510 }
511
512 #[test]
513 fn minimal_pe_has_no_overlay() {
514 let bytes = make_minimal_pe_x64(0, false);
515 let pe = parse_pe(&bytes).expect("minimal PE");
516 assert!(pe.overlay_offset.is_none());
517 assert!(pe.overlay_size.is_none());
518 }
519
520 #[test]
521 fn minimal_pe_has_no_rich_header() {
522 let bytes = make_minimal_pe_x64(0, false);
524 let pe = parse_pe(&bytes).expect("minimal PE");
525 assert!(pe.rich_header.is_none());
526 }
527
528 fn write_section(
533 pe: &mut [u8],
534 name: &[u8],
535 virtual_size: u32,
536 virtual_address: u32,
537 raw_size: u32,
538 raw_ptr: u32,
539 characteristics: u32,
540 ) {
541 let sh = 0x148;
542 pe[sh..sh + name.len()].copy_from_slice(name);
543 pe[sh + 0x08..sh + 0x0C].copy_from_slice(&virtual_size.to_le_bytes());
544 pe[sh + 0x0C..sh + 0x10].copy_from_slice(&virtual_address.to_le_bytes());
545 pe[sh + 0x10..sh + 0x14].copy_from_slice(&raw_size.to_le_bytes());
546 pe[sh + 0x14..sh + 0x18].copy_from_slice(&raw_ptr.to_le_bytes());
547 pe[sh + 0x24..sh + 0x28].copy_from_slice(&characteristics.to_le_bytes());
548 }
549
550 #[test]
551 fn section_fields_and_overlay_extracted() {
552 let mut pe = make_minimal_pe_x64(0, false);
554 pe[0x46] = 1; pe[0x90] = 0x00;
556 pe[0x91] = 0x20; write_section(&mut pe, b".text", 0x100, 0x1000, 0x100, 0x200, 0x6000_0020);
558 pe.resize(0x300, 0); pe.extend_from_slice(&[0xAA; 0x10]); let parsed = parse_pe(&pe).expect("section-bearing PE");
562 assert_eq!(parsed.sections.len(), 1);
563 let sec = &parsed.sections[0];
564 assert_eq!(sec.name, ".text");
565 assert_eq!(sec.virtual_address, 0x1000);
566 assert_eq!(sec.raw_size, 0x100);
567 assert!(sec.is_executable);
568 assert!(sec.is_readable);
569 assert!(!sec.is_writable);
570 assert_eq!(parsed.overlay_offset, Some(0x300));
572 assert_eq!(parsed.overlay_size, Some(0x10));
573 }
574
575 #[test]
576 fn pe_without_optional_header_defaults_entry_base_checksum_to_zero() {
577 let mut pe = make_minimal_pe_x64(0, false);
580 pe[0x54] = 0;
581 pe[0x55] = 0; for b in &mut pe[0x58..0x148] {
583 *b = 0;
584 }
585 let parsed = parse_pe(&pe).expect("PE with no optional header");
586 assert_eq!(parsed.entry_point_rva, 0);
587 assert_eq!(parsed.image_base, 0);
588 assert_eq!(parsed.checksum, 0);
589 }
590
591 #[test]
592 fn pdb_path_extracted_from_codeview_debug_directory() {
593 let mut pe = make_minimal_pe_x64(0, false);
596 pe[0x46] = 1;
597 pe[0x90] = 0x00;
598 pe[0x91] = 0x20; write_section(&mut pe, b".rdata", 0x200, 0x1000, 0x200, 0x200, 0x4000_0040);
600 let dd = 0xC8 + 6 * 8;
602 pe[dd..dd + 4].copy_from_slice(&0x1000u32.to_le_bytes());
603 pe[dd + 4..dd + 8].copy_from_slice(&28u32.to_le_bytes());
604 pe.resize(0x400, 0);
605
606 let d = 0x200;
608 pe[d + 12..d + 16].copy_from_slice(&2u32.to_le_bytes()); let cv_rva = 0x1000u32 + 28;
610 let cv_ptr = 0x200u32 + 28;
611 pe[d + 16..d + 20].copy_from_slice(&64u32.to_le_bytes()); pe[d + 20..d + 24].copy_from_slice(&cv_rva.to_le_bytes()); pe[d + 24..d + 28].copy_from_slice(&cv_ptr.to_le_bytes()); let c = 0x21C;
617 pe[c..c + 4].copy_from_slice(&0x5344_5352u32.to_le_bytes()); for (i, b) in pe[c + 4..c + 20].iter_mut().enumerate() {
619 *b = i as u8; }
621 pe[c + 20..c + 24].copy_from_slice(&1u32.to_le_bytes()); let name = b"C:\\build\\payload.pdb\0";
623 pe[c + 24..c + 24 + name.len()].copy_from_slice(name);
624
625 let parsed = parse_pe(&pe).expect("PE with CodeView debug dir");
626 assert_eq!(parsed.pdb_path.as_deref(), Some("C:\\build\\payload.pdb"));
627 }
628}