From b0c3d19cee12b7a8af992711daea2c6aa5c27a7d Mon Sep 17 00:00:00 2001 From: r0ny123 <49360849+r0ny123@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:08:29 +0530 Subject: [PATCH] fix(core): keep an import named like an ordinal from becoming one A PE imports by name or by ordinal, and the report keeps only the string, so "#5" in a report is ambiguous: it is what the import parsers write for an ordinal nothing resolves, and it is also a legal import name. The synthesizer read every "#N" as an ordinal, so an import really named that way was written as an ordinal thunk and came back under a different name. Two facts settle it without the report having to carry more. An ordinal is a WORD, so a larger number never came from an import table at all - and writing it as one truncated it into a different import. And the parsers write "#N" only for an ordinal no table resolves, so a name that does resolve cannot have come from them. On ws2_32.dll, "#1" is a real name, because an ordinal 1 there would have been written as accept. Both ends now key the decision on the same DLL name rather than deriving it twice, so the hint table and the thunks cannot disagree about which entries are ordinals. --- src/smda/synthesis/PeSynthesizer.py | 29 ++++++++++++++++++++++------- tests/testSynthesis.py | 27 +++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/smda/synthesis/PeSynthesizer.py b/src/smda/synthesis/PeSynthesizer.py index 73b40c2b..599a4063 100644 --- a/src/smda/synthesis/PeSynthesizer.py +++ b/src/smda/synthesis/PeSynthesizer.py @@ -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 @@ -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. @@ -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 @@ -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) diff --git a/tests/testSynthesis.py b/tests/testSynthesis.py index 53da927d..ee66d1e0 100644 --- a/tests/testSynthesis.py +++ b/tests/testSynthesis.py @@ -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() @@ -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( @@ -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")},