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")},