Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions cle/backends/pe/pe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -1178,24 +1187,29 @@ 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("<II", name)
if name_as_dwords[0] == 0:
name = self._read_from_string_table(name_as_dwords[1])
else:
name = name.rstrip(b"\x00").decode("latin-1")
if section > 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)
83 changes: 83 additions & 0 deletions tests/test_pe_coff_symbols.py
Original file line number Diff line number Diff line change
@@ -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"]
Loading