Skip to content
Open
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
29 changes: 22 additions & 7 deletions src/smda/synthesis/PeSynthesizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lief

from smda.common.ExceptionHandling import reraise_non_operational_exception
from smda.common.labelprovider.OrdinalHelper import OrdinalHelper
from smda.synthesis.BinarySynthesizer import BinarySynthesizer, align_down, align_up
from smda.utility.lief_helper import safe_lief_parse

Expand Down Expand Up @@ -97,10 +98,23 @@ def _plantStrings(self, regions, base):
break

@staticmethod
def _parseOrdinal(name):
if name.startswith("#") and name[1:].isdigit():
return int(name[1:])
return None
def _parseOrdinal(dll_name, name):
"""The ordinal a name stands for, or None when the name is the import's own.

A PE imports by name or by ordinal, and the report keeps only the string, so
"#5" is ambiguous on its own. Two things settle it. An ordinal is a WORD, so a
larger number never came from an import table. And the import parsers write
"#N" only for an ordinal no table resolves, so a name that does resolve cannot
have come from them either. Both are real imports spelled that way, and writing
one as an ordinal would rename it on the way back - or, past a WORD, truncate it
into a different import entirely.
"""
if not (name.startswith("#") and name[1:].isdigit()):
return None
ordinal = int(name[1:])
if ordinal > 0xFFFF or OrdinalHelper.resolveOrdinal(dll_name, ordinal):
return None
return ordinal

def _buildImportTables(self, import_rvas, ptr_size):
"""Groups imports by DLL and builds descriptor/hint/dll-name blobs.
Expand All @@ -126,7 +140,7 @@ def _buildImportTables(self, import_rvas, ptr_size):
dll_str_offsets[dll_name] = len(dll_strs)
dll_strs += dll_name.encode("ascii", errors="replace") + b"\x00"
for _rva, func in funcs:
if self._parseOrdinal(func) is not None:
if self._parseOrdinal(dll_name, func) is not None:
continue
if (dll_name, func) in hint_offsets:
continue
Expand All @@ -149,11 +163,12 @@ def _writeThunkAt(self, regions, rva, value, ptr_size, data_only=False):
def _writeThunks(self, regions, import_rvas, dll_funcs, sec_vaddr, hint_off, hint_offsets, ptr_size):
ordinal_flag = 1 << (ptr_size * 8 - 1)
for rva, (dll, func) in import_rvas.items():
ordinal = self._parseOrdinal(func)
dll_key = dll or "unknown.dll"
ordinal = self._parseOrdinal(dll_key, func)
if ordinal is not None:
thunk_value = ordinal_flag | ordinal
else:
thunk_value = sec_vaddr + hint_off + hint_offsets[(dll or "unknown.dll", func)]
thunk_value = sec_vaddr + hint_off + hint_offsets[(dll_key, func)]
if not self._writeThunkAt(regions, rva, thunk_value, ptr_size):
self._warn("import thunk 0x%x (%s.%s) outside all sections", rva, dll, func)

Expand Down
27 changes: 25 additions & 2 deletions tests/testSynthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,29 @@ def testPeSynthesisNonContiguousImports(self):
synthesized_names = {entry.name for imported in parsed.imports for entry in imported.entries if entry.name}
assert {"AAASynthFuncA", "AAASynthFuncB"} <= synthesized_names

def testAnImportNamedLikeAnOrdinalIsNotRewrittenIntoOne(self):
"""A PE imports by name or by ordinal; the report keeps only the string."""
report = SmdaReport.fromDict(self.pe_report.toDict())
imports = report.xmetadata["imported_functions"]
base_slot = next(int(k) for k in imports)
# "#1" cannot have come from the import parsers for this DLL: they write "#N"
# only when no table resolves it, and ws2_32.dll ordinal 1 resolves to accept.
# "#99999" is past a WORD, so it never came from an import table either. Only
# "#4000" is a real ordinal - in range, and resolving nowhere.
report.xmetadata["imported_functions"] = {
str(base_slot): ("ws2_32.dll", "#1"),
str(base_slot + 0x10): ("ws2_32.dll", "#4000"),
str(base_slot + 0x20): ("ws2_32.dll", "#99999"),
}
parsed = lief.parse(report.synthesizeBinary(output_format=FORMAT_PE))
entries = [entry for imported in parsed.imports for entry in imported.entries]
by_name = {entry.name for entry in entries if entry.name}
by_ordinal = {entry.ordinal for entry in entries if not entry.name}
self.assertIn("#1", by_name)
self.assertIn("#99999", by_name)
self.assertNotIn(1, by_ordinal)
self.assertIn(4000, by_ordinal)

def testElfSynthesisFromSections(self):
report = self.elf_report
synthesized = report.synthesizeBinary()
Expand Down Expand Up @@ -387,7 +410,7 @@ def _record(regions, rva, thunk_value, ptr_size, data_only=False):
return True

synthesizer._writeThunkAt = _record
synthesizer._parseOrdinal = lambda func: 1
synthesizer._parseOrdinal = lambda dll, func: 1
# two slots of one DLL a megabyte apart: only the real slots may be written
far = 0x1000 + 0x100000
synthesizer._writeThunks(
Expand Down Expand Up @@ -415,7 +438,7 @@ def _record(regions, rva, thunk_value, ptr_size, data_only=False):
return True

synthesizer._writeThunkAt = _record
synthesizer._parseOrdinal = lambda func: 1
synthesizer._parseOrdinal = lambda dll, func: 1
synthesizer._writeThunks(
[],
{0x1000: ("kernel32.dll", "a"), 0x100C: ("kernel32.dll", "b")},
Expand Down