Skip to main content

wazabin_binary/
pe.rs

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/// A known function start address extracted from PE metadata.
14#[derive(Debug, Clone)]
15pub struct PeFunctionSymbol {
16    /// Virtual address of the function.
17    pub address: u64,
18    /// Symbol name, if present.
19    pub name: Option<String>,
20    /// Whether this is an external imported function.
21    pub is_external: bool,
22    /// For an import thunk, the DLL the import comes from.
23    pub library: Option<String>,
24}
25
26/// A PE import resolved from the import address table.
27#[derive(Debug, Clone)]
28pub struct PeImportSymbol {
29    /// Virtual address of the import address table slot.
30    pub iat_address: u64,
31    /// Bare imported function name used for display.
32    pub name: String,
33    /// Source DLL name.
34    pub dll: String,
35    /// Import ordinal, if present.
36    pub ordinal: Option<u16>,
37}
38
39/// Results of PE analysis.
40#[derive(Debug, Clone)]
41pub struct PeAnalysis {
42    /// Preferred image-base virtual address.
43    pub image_base: u64,
44    /// PE optional-header entrypoint VA.
45    pub entrypoint: u64,
46    /// Function start addresses discovered from COFF symbols and imports.
47    pub known_functions: Vec<PeFunctionSymbol>,
48    /// Imported functions keyed by their IAT slots.
49    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    /// `IMAGE_SCN_MEM_WRITE` from the section characteristics. Unlike
58    /// executability (which the loader deliberately leaves permissive, see
59    /// [`BinaryFormat::mapped_regions`]), writability is read straight from the
60    /// section table, so a section without the bit is proven read-only.
61    pub writable: bool,
62}
63
64/// `IMAGE_SCN_MEM_WRITE`: the section is writable at run time.
65const 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/// A parsed and loaded PE32/PE32+ executable image.
94#[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/// Errors that can occur while parsing a PE binary.
104#[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    /// Parse a PE32/PE32+ executable from raw file bytes.
133    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, &sections, &imports, pe.is_64);
184        collect_exception_function_symbols(&pe, &sections, &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    /// Import-directory DLL names in first-seen order, deduplicated
383    /// case-insensitively but reported in their original case.
384    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                // Mark every mapped section executable so resolved jump targets
415                // are not filtered out (the flag is only a permissive sanity
416                // check). Writability is the real `IMAGE_SCN_MEM_WRITE` bit, so
417                // a snapshot built from these regions can tell `.rdata` from
418                // `.data` exactly as the live binary does.
419                (sec.start, bytes, true, sec.writable)
420            })
421            .collect()
422    }
423
424    /// A mapped section without `IMAGE_SCN_MEM_WRITE` is proven read-only.
425    /// (There is deliberately no `is_known_writable` override: flipping that
426    /// predicate would change which constants the folding passes trust, which is
427    /// a separate question from proving a region immutable.)
428    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        // Externals are minted either at an import thunk's address or (for
494        // direct `call [iat]` sites with no thunk) at the IAT slot itself.
495        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}