threatflux_binary_analysis/formats/
pe.rs1use crate::{
4 BinaryError, BinaryFormatParser, BinaryFormatTrait, Result,
5 types::{
6 Architecture, BinaryFormat as Format, BinaryMetadata, Endianness, Export, Import, Section,
7 SectionPermissions, SectionType, SecurityFeatures, Symbol, SymbolBinding, SymbolType,
8 SymbolVisibility,
9 },
10};
11use goblin::pe::{PE, dll_characteristic::*};
12
13pub struct PeParser;
15
16impl BinaryFormatParser for PeParser {
17 fn parse(data: &[u8]) -> Result<Box<dyn BinaryFormatTrait>> {
18 let pe = PE::parse(data)?;
19 Ok(Box::new(PeBinary::new(pe, data)?))
20 }
21
22 fn can_parse(data: &[u8]) -> bool {
23 if data.len() < 2 || &data[0..2] != b"MZ" {
25 return false;
26 }
27
28 if data.len() >= 0x3c + 4 {
30 let pe_offset =
31 u32::from_le_bytes([data[0x3c], data[0x3d], data[0x3e], data[0x3f]]) as usize;
32
33 return pe_offset
34 .checked_add(4)
35 .and_then(|end| data.get(pe_offset..end))
36 .is_some_and(|signature| signature == b"PE\0\0");
37 }
38
39 false
40 }
41}
42
43pub struct PeBinary {
45 metadata: BinaryMetadata,
46 sections: Vec<Section>,
47 symbols: Vec<Symbol>,
48 imports: Vec<Import>,
49 exports: Vec<Export>,
50}
51
52impl PeBinary {
53 fn new(pe: PE<'_>, data: &[u8]) -> Result<Self> {
54 let mut output_budget = super::ParseOutputBudget::default();
55
56 let architecture = match pe.header.coff_header.machine {
58 goblin::pe::header::COFF_MACHINE_X86 => Architecture::X86,
59 goblin::pe::header::COFF_MACHINE_X86_64 => Architecture::X86_64,
60 goblin::pe::header::COFF_MACHINE_ARM
61 | goblin::pe::header::COFF_MACHINE_ARMNT
62 | goblin::pe::header::COFF_MACHINE_THUMB => Architecture::Arm,
63 goblin::pe::header::COFF_MACHINE_ARM64 => Architecture::Arm64,
64 goblin::pe::header::COFF_MACHINE_MIPS16
65 | goblin::pe::header::COFF_MACHINE_MIPSFPU
66 | goblin::pe::header::COFF_MACHINE_MIPSFPU16
67 | goblin::pe::header::COFF_MACHINE_R4000
68 | goblin::pe::header::COFF_MACHINE_WCEMIPSV2 => Architecture::Mips,
69 goblin::pe::header::COFF_MACHINE_POWERPC
70 | goblin::pe::header::COFF_MACHINE_POWERPCFP => Architecture::PowerPC,
71 goblin::pe::header::COFF_MACHINE_RISCV32 => Architecture::RiscV,
72 goblin::pe::header::COFF_MACHINE_RISCV64 => Architecture::RiscV64,
73 _ => Architecture::Unknown,
74 };
75
76 let endian = Endianness::Little;
78
79 let security_features = analyze_security_features(&pe);
81
82 let (base_address, entry_point) = if let Some(optional_header) = &pe.header.optional_header
84 {
85 let image_base = optional_header.windows_fields.image_base;
86 let entry_rva = optional_header.standard_fields.address_of_entry_point;
87 let entry_point = if entry_rva == 0 {
88 None
89 } else {
90 Some(
91 image_base
92 .checked_add(u64::from(entry_rva))
93 .ok_or_else(|| {
94 BinaryError::invalid_data("PE entry point address overflows u64")
95 })?,
96 )
97 };
98 (Some(image_base), entry_point)
99 } else {
100 (None, None)
101 };
102
103 let metadata = BinaryMetadata {
104 size: data.len(),
105 format: Format::Pe,
106 architecture,
107 entry_point,
108 base_address,
109 timestamp: (pe.header.coff_header.time_date_stamp != 0)
110 .then_some(u64::from(pe.header.coff_header.time_date_stamp)),
111 compiler_info: None,
112 endian,
113 security_features,
114 };
115
116 let sections = parse_sections(&pe, data, &mut output_budget)?;
118
119 let symbols = parse_symbols(&pe, data, &mut output_budget)?;
121
122 let (imports, exports) = parse_imports_exports(&pe, &mut output_budget)?;
124
125 Ok(Self {
126 metadata,
127 sections,
128 symbols,
129 imports,
130 exports,
131 })
132 }
133}
134
135impl BinaryFormatTrait for PeBinary {
136 fn format_type(&self) -> Format {
137 Format::Pe
138 }
139
140 fn architecture(&self) -> Architecture {
141 self.metadata.architecture
142 }
143
144 fn entry_point(&self) -> Option<u64> {
145 self.metadata.entry_point
146 }
147
148 fn sections(&self) -> &[Section] {
149 &self.sections
150 }
151
152 fn symbols(&self) -> &[Symbol] {
153 &self.symbols
154 }
155
156 fn imports(&self) -> &[Import] {
157 &self.imports
158 }
159
160 fn exports(&self) -> &[Export] {
161 &self.exports
162 }
163
164 fn metadata(&self) -> &BinaryMetadata {
165 &self.metadata
166 }
167}
168
169fn parse_sections(
170 pe: &PE,
171 data: &[u8],
172 output_budget: &mut super::ParseOutputBudget,
173) -> Result<Vec<Section>> {
174 let mut sections = Vec::new();
175
176 for section in &pe.sections {
177 output_budget.reserve_record(&mut sections, "PE sections")?;
178 let decoded_name = String::from_utf8_lossy(§ion.name);
179 let name =
180 output_budget.copy_name(decoded_name.trim_end_matches('\0'), "PE section name")?;
181
182 let section_type =
184 if section.characteristics & goblin::pe::section_table::IMAGE_SCN_CNT_CODE != 0 {
185 SectionType::Code
186 } else if section.characteristics
187 & goblin::pe::section_table::IMAGE_SCN_CNT_INITIALIZED_DATA
188 != 0
189 {
190 if section.characteristics & goblin::pe::section_table::IMAGE_SCN_MEM_WRITE != 0 {
191 SectionType::Data
192 } else {
193 SectionType::ReadOnlyData
194 }
195 } else if section.characteristics
196 & goblin::pe::section_table::IMAGE_SCN_CNT_UNINITIALIZED_DATA
197 != 0
198 {
199 SectionType::Bss
200 } else {
201 SectionType::Other("PE_SECTION".to_string())
202 };
203
204 let permissions = SectionPermissions {
205 read: section.characteristics & goblin::pe::section_table::IMAGE_SCN_MEM_READ != 0,
206 write: section.characteristics & goblin::pe::section_table::IMAGE_SCN_MEM_WRITE != 0,
207 execute: section.characteristics & goblin::pe::section_table::IMAGE_SCN_MEM_EXECUTE
208 != 0,
209 };
210
211 let has_file_data = section.size_of_raw_data != 0;
212 if has_file_data
213 && super::checked_file_range(
214 data,
215 u64::from(section.pointer_to_raw_data),
216 u64::from(section.size_of_raw_data),
217 )
218 .is_none()
219 {
220 return Err(BinaryError::invalid_data(format!(
221 "PE section '{name}' range {}..+{} exceeds the file",
222 section.pointer_to_raw_data, section.size_of_raw_data
223 )));
224 }
225
226 let section_data = if has_file_data {
228 super::inline_section_data(
229 data,
230 u64::from(section.pointer_to_raw_data),
231 u64::from(section.size_of_raw_data),
232 )
233 } else {
234 None
235 };
236
237 sections.push(Section {
238 name,
239 address: pe
240 .image_base
241 .checked_add(u64::from(section.virtual_address))
242 .ok_or_else(|| BinaryError::invalid_data("PE section address overflows u64"))?,
243 size: u64::from(if section.virtual_size == 0 {
244 section.size_of_raw_data
245 } else {
246 section.virtual_size
247 }),
248 offset: section.pointer_to_raw_data as u64,
249 file_size: u64::from(section.size_of_raw_data),
250 permissions,
251 section_type,
252 data: section_data,
253 });
254 }
255
256 Ok(sections)
257}
258
259fn parse_symbols(
260 pe: &PE,
261 data: &[u8],
262 output_budget: &mut super::ParseOutputBudget,
263) -> Result<Vec<Symbol>> {
264 use goblin::pe::symbol::{
265 COFF_SYMBOL_SIZE, IMAGE_SYM_CLASS_EXTERNAL, IMAGE_SYM_CLASS_EXTERNAL_DEF,
266 IMAGE_SYM_CLASS_FILE, IMAGE_SYM_CLASS_SECTION, IMAGE_SYM_CLASS_STATIC,
267 IMAGE_SYM_DTYPE_FUNCTION, IMAGE_SYM_UNDEFINED,
268 };
269
270 let coff = &pe.header.coff_header;
271 let table_offset = usize::try_from(coff.pointer_to_symbol_table)
272 .map_err(|_| BinaryError::invalid_data("COFF symbol table offset exceeds usize"))?;
273 let table_size = usize::try_from(coff.number_of_symbol_table)
274 .ok()
275 .and_then(|count| count.checked_mul(COFF_SYMBOL_SIZE))
276 .ok_or_else(|| BinaryError::invalid_data("COFF symbol table size overflows usize"))?;
277
278 if table_offset == 0 {
279 return Ok(Vec::new());
280 }
281 if table_offset
282 .checked_add(table_size)
283 .is_none_or(|end| end > data.len())
284 {
285 return Err(BinaryError::invalid_data(
286 "COFF symbol table extends beyond the file",
287 ));
288 }
289
290 let Some(table) = coff.symbols(data)? else {
291 return Ok(Vec::new());
292 };
293 let strings = coff.strings(data)?;
294 let mut symbols = Vec::new();
295
296 for (index, inline_name, native) in table.iter() {
297 let parsed_name = match inline_name {
298 Some(name) => name,
299 None => native.name(strings.as_ref().ok_or_else(|| {
300 BinaryError::invalid_data("COFF symbol requires a missing string table")
301 })?)?,
302 };
303 if parsed_name.is_empty() {
304 continue;
305 }
306 output_budget.reserve_record(&mut symbols, "PE symbols")?;
307 let name = output_budget.copy_name(parsed_name, "PE symbol name")?;
308
309 let section_index = usize::try_from(native.section_number)
310 .ok()
311 .and_then(|ordinal| ordinal.checked_sub(1));
312 let address = if let Some(section_index) = section_index {
313 let section = pe.sections.get(section_index).ok_or_else(|| {
314 BinaryError::invalid_data(format!(
315 "COFF symbol '{name}' references missing section {}",
316 native.section_number
317 ))
318 })?;
319 pe.image_base
320 .checked_add(u64::from(section.virtual_address))
321 .and_then(|base| base.checked_add(u64::from(native.value)))
322 .ok_or_else(|| {
323 BinaryError::invalid_data(format!(
324 "COFF symbol '{name}' virtual address overflows u64"
325 ))
326 })?
327 } else {
328 u64::from(native.value)
329 };
330
331 let symbol_type = if native.is_function_definition()
332 || native.derived_type() == IMAGE_SYM_DTYPE_FUNCTION
333 {
334 SymbolType::Function
335 } else if native.is_file() || native.storage_class == IMAGE_SYM_CLASS_FILE {
336 SymbolType::File
337 } else if native.is_section_definition() || native.storage_class == IMAGE_SYM_CLASS_SECTION
338 {
339 SymbolType::Section
340 } else if native.section_number == IMAGE_SYM_UNDEFINED && native.value != 0 {
341 SymbolType::Common
342 } else {
343 SymbolType::Object
344 };
345
346 let binding = if native.is_weak_external() {
347 SymbolBinding::Weak
348 } else if matches!(
349 native.storage_class,
350 IMAGE_SYM_CLASS_EXTERNAL | IMAGE_SYM_CLASS_EXTERNAL_DEF
351 ) {
352 SymbolBinding::Global
353 } else if native.storage_class == IMAGE_SYM_CLASS_STATIC {
354 SymbolBinding::Local
355 } else {
356 SymbolBinding::Other(format!("COFF_STORAGE_CLASS_{}", native.storage_class))
357 };
358
359 let size = if native.is_function_definition() && native.number_of_aux_symbols != 0 {
360 table
361 .aux_function_definition(index + 1)
362 .map_or(0, |aux| u64::from(aux.total_size))
363 } else {
364 0
365 };
366
367 symbols.push(Symbol {
368 name,
369 demangled_name: None,
370 address,
371 size,
372 symbol_type,
373 binding,
374 visibility: SymbolVisibility::Default,
375 section_index,
376 });
377 }
378
379 Ok(symbols)
380}
381
382fn parse_imports_exports(
383 pe: &PE,
384 output_budget: &mut super::ParseOutputBudget,
385) -> crate::types::ImportExportResult {
386 let mut imports = Vec::new();
387 let mut exports = Vec::new();
388
389 for import in &pe.imports {
391 output_budget.reserve_record(&mut imports, "PE imports")?;
392 let rva = u64::try_from(import.rva)
393 .map_err(|_| BinaryError::invalid_data("PE import RVA exceeds u64"))?;
394 imports.push(Import {
395 name: output_budget.copy_name(&import.name, "PE import name")?,
396 library: Some(output_budget.copy_name(import.dll, "PE import library")?),
397 address: Some(
398 pe.image_base
399 .checked_add(rva)
400 .ok_or_else(|| BinaryError::invalid_data("PE import address overflows u64"))?,
401 ),
402 ordinal: Some(import.ordinal),
403 });
404 }
405
406 for export in &pe.exports {
408 if let Some(name) = &export.name {
409 output_budget.reserve_record(&mut exports, "PE exports")?;
410 let rva = u64::try_from(export.rva)
411 .map_err(|_| BinaryError::invalid_data("PE export RVA exceeds u64"))?;
412 let forwarded_name =
413 match export.reexport.as_ref() {
414 Some(goblin::pe::export::Reexport::DLLName { export, lib }) => Some(
415 output_budget
416 .copy_name_parts(&[lib, ".", export], "PE forwarded export name")?,
417 ),
418 Some(goblin::pe::export::Reexport::DLLOrdinal { ordinal, lib }) => {
419 let ordinal = ordinal.to_string();
420 Some(output_budget.copy_name_parts(
421 &[lib, ".#", &ordinal],
422 "PE forwarded export ordinal",
423 )?)
424 }
425 None => None,
426 };
427 exports.push(Export {
428 name: output_budget.copy_name(name, "PE export name")?,
429 address: pe
430 .image_base
431 .checked_add(rva)
432 .ok_or_else(|| BinaryError::invalid_data("PE export address overflows u64"))?,
433 ordinal: None, forwarded_name,
435 });
436 }
437 }
438
439 Ok((imports, exports))
440}
441
442fn analyze_security_features(pe: &PE) -> SecurityFeatures {
443 let mut features = SecurityFeatures::default();
444
445 if let Some(optional_header) = &pe.header.optional_header {
446 let characteristics = optional_header.windows_fields.dll_characteristics;
447
448 features.nx_bit = characteristics & IMAGE_DLLCHARACTERISTICS_NX_COMPAT != 0;
450
451 features.aslr = characteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE != 0;
453
454 let _high_entropy = characteristics & IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA != 0;
456
457 features.cfi = characteristics & IMAGE_DLLCHARACTERISTICS_GUARD_CF != 0;
459
460 features.pie = features.aslr; }
463
464 features.stack_canary = false;
467
468 features.signed = !pe.certificates.is_empty();
470
471 features
472}