Skip to main content

threatflux_binary_analysis/formats/
pe.rs

1//! PE (Portable Executable) format parser for Windows binaries
2
3use 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
13/// PE format parser
14pub 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        // Check for DOS header signature "MZ"
24        if data.len() < 2 || &data[0..2] != b"MZ" {
25            return false;
26        }
27
28        // Check for PE signature
29        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
43/// Parsed PE binary
44pub 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        // Convert architecture
57        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        // PE is always little endian
77        let endian = Endianness::Little;
78
79        // Analyze security features
80        let security_features = analyze_security_features(&pe);
81
82        // Get base address and entry point from optional header
83        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        // Parse sections
117        let sections = parse_sections(&pe, data, &mut output_budget)?;
118
119        // Parse symbols
120        let symbols = parse_symbols(&pe, data, &mut output_budget)?;
121
122        // Parse imports and exports
123        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(&section.name);
179        let name =
180            output_budget.copy_name(decoded_name.trim_end_matches('\0'), "PE section name")?;
181
182        // Determine section type based on characteristics
183        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        // Extract small section data
227        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    // Parse imports
390    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    // Parse exports
407    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, // PE exports don't have ordinals in goblin 0.10
434                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        // DEP/NX bit
449        features.nx_bit = characteristics & IMAGE_DLLCHARACTERISTICS_NX_COMPAT != 0;
450
451        // ASLR
452        features.aslr = characteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE != 0;
453
454        // High entropy ASLR
455        let _high_entropy = characteristics & IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA != 0;
456
457        // CFI (Control Flow Guard)
458        features.cfi = characteristics & IMAGE_DLLCHARACTERISTICS_GUARD_CF != 0;
459
460        // Position Independent Executable (requires relocation table removal)
461        features.pie = features.aslr; // Simplified check
462    }
463
464    // Check for stack canaries (would need more complex analysis)
465    // This would require analyzing the binary for __security_cookie references
466    features.stack_canary = false;
467
468    // Check if binary is signed (would need to parse certificate table)
469    features.signed = !pe.certificates.is_empty();
470
471    features
472}