1use crate::Arch;
2use goblin::pe::{
3 PE,
4 header::{
5 COFF_MACHINE_ARM, COFF_MACHINE_ARM64, COFF_MACHINE_ARMNT, COFF_MACHINE_X86,
6 COFF_MACHINE_X86_64,
7 },
8 symbol::Symbol,
9};
10
11use crate::BinaryFormat;
12
13#[derive(Debug, Clone)]
15pub struct PeFunctionSymbol {
16 pub address: u64,
18 pub name: Option<String>,
20 pub is_external: bool,
22 pub library: Option<String>,
24}
25
26#[derive(Debug, Clone)]
28pub struct PeImportSymbol {
29 pub iat_address: u64,
31 pub name: String,
33 pub dll: String,
35 pub ordinal: Option<u16>,
37}
38
39#[derive(Debug, Clone)]
41pub struct PeAnalysis {
42 pub image_base: u64,
44 pub entrypoint: u64,
46 pub known_functions: Vec<PeFunctionSymbol>,
48 pub imports: Vec<PeImportSymbol>,
50}
51
52#[derive(Debug, Clone)]
53pub struct PeSection {
54 pub start: u64,
55 pub mem_size: u64,
56 pub data: Vec<u8>,
57 pub writable: bool,
62}
63
64const IMAGE_SCN_MEM_WRITE: u32 = 0x8000_0000;
66
67impl PeSection {
68 fn end(&self) -> u64 {
69 self.start + self.mem_size
70 }
71
72 fn contains(&self, addr: u64) -> bool {
73 addr >= self.start && addr < self.end()
74 }
75
76 fn byte_at(&self, addr: u64) -> Option<u8> {
77 if !self.contains(addr) {
78 return None;
79 }
80 let offset = (addr - self.start) as usize;
81 Some(self.data.get(offset).copied().unwrap_or(0))
82 }
83
84 fn bytes_at(&self, addr: u64) -> Option<&[u8]> {
85 if !self.contains(addr) {
86 return None;
87 }
88 let offset = (addr - self.start) as usize;
89 self.data.get(offset..)
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct PeBinary {
96 pub load_address: u64,
97 pub sections: Vec<PeSection>,
98 pub analysis: PeAnalysis,
99 pub architecture: Arch,
100 pub is_64: bool,
101}
102
103#[derive(Debug)]
105pub enum PeError {
106 Parse(goblin::error::Error),
107 NoMappedSection,
108 UnsupportedMachine(u16),
109}
110
111impl std::fmt::Display for PeError {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 PeError::Parse(e) => write!(f, "PE parse error: {e}"),
115 PeError::NoMappedSection => write!(f, "PE contains no mapped sections"),
116 PeError::UnsupportedMachine(machine) => {
117 write!(f, "PE has unsupported machine type 0x{machine:x}")
118 }
119 }
120 }
121}
122
123impl std::error::Error for PeError {}
124
125impl From<goblin::error::Error> for PeError {
126 fn from(e: goblin::error::Error) -> Self {
127 PeError::Parse(e)
128 }
129}
130
131impl PeBinary {
132 pub fn parse(file_bytes: &[u8]) -> Result<Self, PeError> {
134 let pe = PE::parse(file_bytes)?;
135 let architecture = from_pe_machine(pe.header.coff_header.machine)
136 .ok_or(PeError::UnsupportedMachine(pe.header.coff_header.machine))?;
137
138 let mut sections = pe
139 .sections
140 .iter()
141 .filter(|s| s.virtual_size > 0 || s.size_of_raw_data > 0)
142 .map(|section| {
143 let start = pe.image_base + u64::from(section.virtual_address);
144 let mem_size = u64::from(section.virtual_size.max(section.size_of_raw_data));
145 let offset = section.pointer_to_raw_data as usize;
146 let filesz = section.size_of_raw_data as usize;
147 let data = file_bytes
148 .get(offset..offset.saturating_add(filesz))
149 .unwrap_or(&[])
150 .to_vec();
151 PeSection {
152 start,
153 mem_size,
154 data,
155 writable: section.characteristics & IMAGE_SCN_MEM_WRITE != 0,
156 }
157 })
158 .filter(|s| s.mem_size > 0)
159 .collect::<Vec<_>>();
160
161 if sections.is_empty() {
162 return Err(PeError::NoMappedSection);
163 }
164 sections.sort_by_key(|s| s.start);
165 let load_address = sections[0].start;
166
167 let entrypoint = pe.image_base + u64::from(pe.entry);
168 let mut known_functions = Vec::new();
169 collect_coff_function_symbols(&pe, file_bytes, &mut known_functions);
170 let imports: Vec<_> = pe
171 .imports
172 .iter()
173 .map(|import| {
174 let name = import.name.to_string();
175 PeImportSymbol {
176 iat_address: pe.image_base + import.offset as u64,
177 name,
178 dll: import.dll.to_string(),
179 ordinal: (import.ordinal != 0).then_some(import.ordinal),
180 }
181 })
182 .collect();
183 mark_import_thunks(&mut known_functions, §ions, &imports, pe.is_64);
184 collect_exception_function_symbols(&pe, §ions, &mut known_functions);
185 uniquify_function_names(&mut known_functions);
186
187 known_functions.sort_by_key(|f| (f.address, !f.is_external, f.name.is_none()));
188 known_functions.dedup_by_key(|f| f.address);
189
190 Ok(Self {
191 load_address,
192 sections,
193 analysis: PeAnalysis {
194 image_base: pe.image_base,
195 entrypoint,
196 known_functions,
197 imports,
198 },
199 architecture,
200 is_64: pe.is_64,
201 })
202 }
203
204 fn import_at_iat(&self, addr: u64) -> Option<&PeImportSymbol> {
205 self.analysis
206 .imports
207 .iter()
208 .find(|import| import.iat_address == addr)
209 }
210}
211
212fn collect_exception_function_symbols(
213 pe: &PE<'_>,
214 sections: &[PeSection],
215 out: &mut Vec<PeFunctionSymbol>,
216) {
217 let Some(exception_data) = pe.exception_data.as_ref() else {
218 return;
219 };
220
221 for function in exception_data.functions().flatten() {
222 if function.begin_address == 0 || function.end_address <= function.begin_address {
223 continue;
224 }
225
226 let address = pe.image_base + u64::from(function.begin_address);
227 if !sections.iter().any(|section| section.contains(address)) {
228 continue;
229 }
230
231 out.push(PeFunctionSymbol {
232 address,
233 name: None,
234 is_external: false,
235 library: None,
236 });
237 }
238}
239
240fn collect_coff_function_symbols(pe: &PE<'_>, file_bytes: &[u8], out: &mut Vec<PeFunctionSymbol>) {
241 let Ok(Some(symbols)) = pe.header.coff_header.symbols(file_bytes) else {
242 return;
243 };
244 let strings = pe.header.coff_header.strings(file_bytes).ok().flatten();
245
246 for (_index, inline_name, symbol) in symbols.iter() {
247 if !symbol.is_function_definition() {
248 continue;
249 }
250 let section_index = (symbol.section_number - 1) as usize;
251 let Some(section) = pe.sections.get(section_index) else {
252 continue;
253 };
254 let Some(name) = symbol_name(&symbol, inline_name, strings.as_ref()) else {
255 continue;
256 };
257 let address = pe.image_base + u64::from(section.virtual_address) + u64::from(symbol.value);
258 out.push(PeFunctionSymbol {
259 address,
260 name: Some(name),
261 is_external: false,
262 library: None,
263 });
264 }
265}
266
267fn symbol_name(
268 symbol: &Symbol,
269 inline_name: Option<&str>,
270 strings: Option<&goblin::strtab::Strtab<'_>>,
271) -> Option<String> {
272 if let Some(name) = inline_name
273 && !name.is_empty()
274 {
275 return Some(normalize_coff_name(name));
276 }
277
278 if let Some(strings) = strings
279 && let Ok(name) = symbol.name(strings)
280 && !name.is_empty()
281 {
282 return Some(normalize_coff_name(name));
283 }
284
285 let end = symbol
286 .name
287 .iter()
288 .position(|&b| b == 0)
289 .unwrap_or(symbol.name.len());
290 let name = std::str::from_utf8(&symbol.name[..end]).ok()?;
291 (!name.is_empty()).then(|| normalize_coff_name(name))
292}
293
294fn normalize_coff_name(name: &str) -> String {
295 name.strip_prefix('_').unwrap_or(name).to_string()
296}
297
298fn mark_import_thunks(
299 functions: &mut [PeFunctionSymbol],
300 sections: &[PeSection],
301 imports: &[PeImportSymbol],
302 is_64: bool,
303) {
304 for function in functions {
305 let Some(iat_address) = import_thunk_iat(sections, function.address, is_64) else {
306 continue;
307 };
308 let Some(import) = imports
309 .iter()
310 .find(|import| import.iat_address == iat_address)
311 else {
312 continue;
313 };
314
315 function.name = Some(import.name.clone());
316 function.is_external = true;
317 function.library = Some(import.dll.clone());
318 }
319}
320
321fn import_thunk_iat(sections: &[PeSection], address: u64, is_64: bool) -> Option<u64> {
322 let bytes = sections
323 .iter()
324 .find(|section| section.contains(address))
325 .and_then(|section| section.bytes_at(address))?;
326
327 if bytes.len() < 6 || bytes[0] != 0xff || bytes[1] != 0x25 {
328 return None;
329 }
330
331 let operand = [bytes[2], bytes[3], bytes[4], bytes[5]];
332 if is_64 {
333 let disp = i32::from_le_bytes(operand) as i64;
334 Some((address as i64 + 6 + disp) as u64)
335 } else {
336 Some(u32::from_le_bytes(operand) as u64)
337 }
338}
339
340fn uniquify_function_names(functions: &mut [PeFunctionSymbol]) {
341 functions.sort_by_key(|function| {
342 (
343 function.name.clone(),
344 !function.is_external,
345 function.address,
346 )
347 });
348
349 let mut names = std::collections::HashSet::new();
350 for function in functions {
351 let Some(name) = function.name.as_mut() else {
352 continue;
353 };
354 if names.insert(name.clone()) {
355 continue;
356 }
357
358 let base = name.clone();
359 let mut candidate = format!("{base}_{:x}", function.address);
360 let mut suffix = 1;
361 while !names.insert(candidate.clone()) {
362 candidate = format!("{base}_{:x}_{suffix}", function.address);
363 suffix += 1;
364 }
365 *name = candidate;
366 }
367}
368
369impl BinaryFormat for PeBinary {
370 fn load_address(&self) -> u64 {
371 self.load_address
372 }
373
374 fn architecture(&self) -> Arch {
375 self.architecture
376 }
377
378 fn os(&self) -> crate::TargetOs {
379 crate::TargetOs::Windows
380 }
381
382 fn linked_libraries(&self) -> Vec<String> {
385 let mut seen: Vec<String> = Vec::new();
386 for import in &self.analysis.imports {
387 if !seen.iter().any(|dll| dll.eq_ignore_ascii_case(&import.dll)) {
388 seen.push(import.dll.clone());
389 }
390 }
391 seen
392 }
393
394 fn byte_at(&self, addr: u64) -> Option<u8> {
395 self.sections
396 .iter()
397 .find(|section| section.contains(addr))
398 .and_then(|section| section.byte_at(addr))
399 }
400
401 fn bytes_at(&self, addr: u64) -> Option<&[u8]> {
402 self.sections
403 .iter()
404 .find(|section| section.contains(addr))
405 .and_then(|section| section.bytes_at(addr))
406 }
407
408 fn mapped_regions(&self) -> Vec<(u64, Vec<u8>, bool, bool)> {
409 self.sections
410 .iter()
411 .map(|sec| {
412 let mut bytes = sec.data.clone();
413 bytes.resize(sec.mem_size as usize, 0);
414 (sec.start, bytes, true, sec.writable)
420 })
421 .collect()
422 }
423
424 fn is_known_read_only(&self, addr: u64) -> bool {
429 self.sections
430 .iter()
431 .any(|section| !section.writable && section.contains(addr))
432 }
433
434 fn segment_bounds(&self, addr: u64) -> Option<(u64, u64)> {
435 self.sections
436 .iter()
437 .find(|section| section.contains(addr))
438 .map(|section| (section.start, section.end()))
439 }
440
441 fn symbol_name(&self, addr: u64) -> Option<&str> {
442 self.analysis
443 .known_functions
444 .iter()
445 .find(|f| f.address == addr)
446 .and_then(|f| f.name.as_deref())
447 .filter(|name| !name.is_empty())
448 .or_else(|| (addr == self.analysis.entrypoint).then_some("_start"))
449 }
450
451 fn is_external_symbol(&self, addr: u64) -> bool {
452 self.analysis
453 .known_functions
454 .iter()
455 .find(|f| f.address == addr)
456 .map(|f| f.is_external)
457 .unwrap_or(false)
458 }
459
460 fn entry_points(&self) -> Vec<u64> {
461 let mut entries = vec![self.analysis.entrypoint];
462 let has_named_code_symbols = self
463 .analysis
464 .known_functions
465 .iter()
466 .any(|f| !f.is_external && f.name.is_some());
467
468 for f in &self.analysis.known_functions {
469 if f.is_external {
470 continue;
471 }
472
473 if (!has_named_code_symbols && f.name.is_none())
474 || f.name.as_deref().is_some_and(is_primary_pe_function_name)
475 {
476 entries.push(f.address);
477 }
478 }
479 entries.sort();
480 entries.dedup();
481 entries
482 }
483
484 fn entrypoint(&self) -> Option<u64> {
485 Some(self.analysis.entrypoint)
486 }
487
488 fn import_symbol_name(&self, addr: u64) -> Option<&str> {
489 self.import_at_iat(addr).map(|import| import.name.as_str())
490 }
491
492 fn import_library(&self, addr: u64) -> Option<&str> {
493 self.import_at_iat(addr)
496 .map(|import| import.dll.as_str())
497 .or_else(|| {
498 self.analysis
499 .known_functions
500 .iter()
501 .find(|f| f.address == addr)
502 .and_then(|f| f.library.as_deref())
503 })
504 }
505}
506
507fn is_primary_pe_function_name(name: &str) -> bool {
508 matches!(name, "main" | "WinMain" | "wmain" | "wWinMain")
509}
510
511pub fn from_pe_machine(value: u16) -> Option<Arch> {
512 match value {
513 COFF_MACHINE_X86 => Some(Arch::I386),
514 COFF_MACHINE_X86_64 => Some(Arch::X86_64),
515 COFF_MACHINE_ARM | COFF_MACHINE_ARMNT => Some(Arch::Arm),
516 COFF_MACHINE_ARM64 => Some(Arch::AArch64),
517 _ => None,
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn import_symbol_name_returns_iat_import_name() {
527 let pe = PeBinary {
528 load_address: 0x400000,
529 sections: vec![],
530 analysis: PeAnalysis {
531 image_base: 0x400000,
532 entrypoint: 0x401000,
533 known_functions: vec![],
534 imports: vec![PeImportSymbol {
535 iat_address: 0x404000,
536 name: "ExitProcess".to_string(),
537 dll: "KERNEL32.DLL".to_string(),
538 ordinal: None,
539 }],
540 },
541 architecture: Arch::I386,
542 is_64: false,
543 };
544
545 assert_eq!(pe.import_symbol_name(0x404000), Some("ExitProcess"));
546 assert_eq!(pe.import_symbol_name(0x404004), None);
547 }
548
549 #[test]
550 fn entrypoint_returns_primary_pe_entrypoint() {
551 let pe = PeBinary {
552 load_address: 0x400000,
553 sections: vec![],
554 analysis: PeAnalysis {
555 image_base: 0x400000,
556 entrypoint: 0x401000,
557 known_functions: vec![PeFunctionSymbol {
558 address: 0x402000,
559 name: Some("main".to_string()),
560 is_external: false,
561 library: None,
562 }],
563 imports: vec![],
564 },
565 architecture: Arch::I386,
566 is_64: false,
567 };
568
569 assert_eq!(pe.entrypoint(), Some(0x401000));
570 assert!(pe.entry_points().contains(&0x401000));
571 }
572}