From a565cf98109913643e1b927ec08c788ca861c794 Mon Sep 17 00:00:00 2001 From: Yan Date: Fri, 24 Jul 2026 03:53:38 +0000 Subject: [PATCH] Use COFF types for PE exports --- cle/backends/pe/pe.py | 34 +++++++++----- tests/test_pe_coff_symbols.py | 83 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 tests/test_pe_coff_symbols.py diff --git a/cle/backends/pe/pe.py b/cle/backends/pe/pe.py index ccbd4961..0a81cb78 100644 --- a/cle/backends/pe/pe.py +++ b/cle/backends/pe/pe.py @@ -17,6 +17,7 @@ from cle.address_translator import AT from cle.backends.backend import Backend, FunctionHint, FunctionHintSource, register_backend +from cle.backends.coff import IMAGE_SYM_CLASS from cle.backends.symbol import SymbolType from cle.structs import DataDirectory, MemRegion, MemRegionSort, PointerArray, StringBlob, StructArray from cle.utils import extract_null_terminated_bytestr @@ -134,7 +135,8 @@ def __init__( self._symbol_cache = self._exports # same thing self._handle_imports() self._handle_delay_imports() - self._handle_exports() + coff_symbol_types = self._load_symbols_from_coff_header() + self._handle_exports(coff_symbol_types) self._handle_seh() self._parse_meta_regions() if self.loader._perform_relocations: @@ -162,8 +164,6 @@ def __init__( if pdb_path: self.load_symbols_from_pdb(pdb_path) - self._load_symbols_from_coff_header() - self.is_dotnet = ( self._pe.OPTIONAL_HEADER.DATA_DIRECTORY[ pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR"] @@ -420,13 +420,19 @@ def _handle_delay_imports(self): self.imports[imp_name] = reloc self.relocs.append(reloc) - def _handle_exports(self): + def _handle_exports(self, coff_symbol_types: dict[int, set[SymbolType]]): if hasattr(self._pe, "DIRECTORY_ENTRY_EXPORT"): symbols = self._pe.DIRECTORY_ENTRY_EXPORT.symbols for exp in symbols: name = exp.name.decode() if exp.name is not None else None forwarder = exp.forwarder.decode() if exp.forwarder is not None else None - symb = WinSymbol(self, name, exp.address, False, True, exp.ordinal, forwarder) + matching_types = coff_symbol_types.get(exp.address, set()) + symbol_type = ( + next(iter(matching_types)) + if forwarder is None and len(matching_types) == 1 + else SymbolType.TYPE_FUNCTION + ) + symb = WinSymbol(self, name, exp.address, False, True, exp.ordinal, forwarder, symbol_type) self.symbols.add(symb) self._exports[name] = symb self._ordinal_exports[exp.ordinal] = symb @@ -1159,9 +1165,12 @@ def _find_pdb_path(self): log.warning("Unable to find PDB file for this PE. Tried: %s", str(attempts)) return None - def _load_symbols_from_coff_header(self): + def _load_symbols_from_coff_header(self) -> dict[int, set[SymbolType]]: """ - COFF debug info is deprecated, but may still be provided (e.g. by mingw). + Load symbols from the deprecated COFF debug information that some toolchains (e.g. MinGW) still provide. + + Return the concrete types of external definitions grouped by RVA so that the export table can reuse them. + Local symbols are still loaded, but cannot describe an export. """ type_to_symbol_type = { 0: SymbolType.TYPE_OBJECT, @@ -1178,13 +1187,14 @@ def _load_symbols_from_coff_header(self): ) if end_of_table_offset >= len(self._raw_data): log.warning("PE symbol table out of bounds") - return + return {} + symbol_types: dict[int, set[SymbolType]] = {} idx = 0 while idx < self._pe.FILE_HEADER.NumberOfSymbols: offset = self._pe.FILE_HEADER.PointerToSymbolTable + idx * sizeof_symbol_desc sym_desc = self._raw_data[offset : offset + sizeof_symbol_desc] - name, value, section, type_, _, num_aux_syms = struct.unpack("<8sIhHBB", sym_desc) + name, value, section, type_, storage_class, num_aux_syms = struct.unpack("<8sIhHBB", sym_desc) name_as_dwords = struct.unpack(" 0 and type_ in type_to_symbol_type and VALID_SYMBOL_NAME_RE.fullmatch(name): rva = self._pe.sections[section - 1].VirtualAddress + value - symbol = WinSymbol(self, name, rva, False, False, None, None, type_to_symbol_type[type_]) + symbol_type = type_to_symbol_type[type_] + symbol = WinSymbol(self, name, rva, False, False, None, None, symbol_type) log.debug("Adding symbol %s", symbol) self.symbols.add(symbol) + if storage_class == IMAGE_SYM_CLASS.EXTERNAL: + symbol_types.setdefault(rva, set()).add(symbol_type) idx += 1 + num_aux_syms + return symbol_types register_backend("pe", PE) diff --git a/tests/test_pe_coff_symbols.py b/tests/test_pe_coff_symbols.py new file mode 100644 index 00000000..7b68624f --- /dev/null +++ b/tests/test_pe_coff_symbols.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import struct +from types import SimpleNamespace +from typing import Any + +import archinfo + +from cle.backends.coff import IMAGE_SYM_CLASS +from cle.backends.pe.pe import PE +from cle.backends.symbol import SymbolType + + +class _SymbolList(list): + """Minimal symbol container used by the fixture-free PE tests.""" + + def add(self, symbol) -> None: + self.append(symbol) + + +def _make_pe(raw_data: bytes = b"", exports=()) -> Any: + pe: Any = object.__new__(PE) + pe._arch = archinfo.ArchAMD64() + pe._raw_data = raw_data + pe._pe = SimpleNamespace( + FILE_HEADER=SimpleNamespace(PointerToSymbolTable=0, NumberOfSymbols=len(raw_data) // 18), + sections=[SimpleNamespace(VirtualAddress=0x1000)], + DIRECTORY_ENTRY_EXPORT=SimpleNamespace(symbols=exports), + ) + pe.symbols = _SymbolList() + pe._exports = {} + pe._ordinal_exports = {} + pe.deps = [] + return pe + + +def _coff_symbol(name: bytes, value: int, section: int, type_: int, storage_class: int) -> bytes: + return struct.pack("<8sIhHBB", name, value, section, type_, storage_class, 0) + + +def test_coff_symbol_type_hints_only_include_external_definitions(): + raw_data = b"".join( + [ + _coff_symbol(b"function", 0x10, 1, 0x20, IMAGE_SYM_CLASS.EXTERNAL), + _coff_symbol(b"object", 0x20, 1, 0, IMAGE_SYM_CLASS.EXTERNAL), + _coff_symbol(b"static", 0x30, 1, 0, IMAGE_SYM_CLASS.STATIC), + _coff_symbol(b"undefined", 0, 0, 0x20, IMAGE_SYM_CLASS.EXTERNAL), + _coff_symbol(b"other", 0x40, 1, 0x10, IMAGE_SYM_CLASS.EXTERNAL), + ] + ) + pe = _make_pe(raw_data + b"\0\0\0\0") + + symbol_types = pe._load_symbols_from_coff_header() + + assert symbol_types == { + 0x1010: {SymbolType.TYPE_FUNCTION}, + 0x1020: {SymbolType.TYPE_OBJECT}, + } + assert {symbol.name for symbol in pe.symbols} == {"function", "object", "static"} + + +def test_exports_inherit_only_unambiguous_coff_symbol_types(): + exports = [ + SimpleNamespace(name=b"data", address=0x1010, forwarder=None, ordinal=1), + SimpleNamespace(name=b"ambiguous", address=0x1020, forwarder=None, ordinal=2), + SimpleNamespace(name=b"missing", address=0x1030, forwarder=None, ordinal=3), + SimpleNamespace(name=b"forwarded", address=0x1040, forwarder=b"other.target", ordinal=4), + ] + pe = _make_pe(exports=exports) + + pe._handle_exports( + { + 0x1010: {SymbolType.TYPE_OBJECT}, + 0x1020: {SymbolType.TYPE_FUNCTION, SymbolType.TYPE_OBJECT}, + 0x1040: {SymbolType.TYPE_OBJECT}, + } + ) + + assert pe._exports["data"].type is SymbolType.TYPE_OBJECT + assert pe._exports["ambiguous"].type is SymbolType.TYPE_FUNCTION + assert pe._exports["missing"].type is SymbolType.TYPE_FUNCTION + assert pe._exports["forwarded"].type is SymbolType.TYPE_FUNCTION + assert pe.deps == ["other.dll"]