smda 4.2.0__tar.gz → 4.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {smda-4.2.0/src/smda.egg-info → smda-4.2.2}/PKG-INFO +3 -1
- {smda-4.2.0 → smda-4.2.2}/README.md +2 -0
- {smda-4.2.0 → smda-4.2.2}/pyproject.toml +1 -1
- {smda-4.2.0 → smda-4.2.2}/src/smda/SmdaConfig.py +1 -1
- smda-4.2.2/src/smda/__init__.py +1 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/AArch64Backend.py +7 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/RecursiveDisassembler.py +1 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/SmdaFunction.py +8 -3
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/PeSymbolProvider.py +4 -2
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/WinApiResolver.py +2 -1
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/import_parsers.py +22 -0
- smda-4.2.2/src/smda/ida/IdaExporter.py +123 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/ida/IdaInterface.py +13 -1
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/FunctionAnalysisState.py +6 -4
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/FunctionCandidateManager.py +46 -6
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/IntelInstructionEscaper.py +4 -3
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/X86Backend.py +10 -3
- {smda-4.2.0 → smda-4.2.2/src/smda.egg-info}/PKG-INFO +3 -1
- {smda-4.2.0 → smda-4.2.2}/tests/testAArch64Disassembler.py +70 -3
- {smda-4.2.0 → smda-4.2.2}/tests/testCommonModels.py +25 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testEscaper.py +13 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testIntegration.py +4 -2
- {smda-4.2.0 → smda-4.2.2}/tests/testIntelDisassembler.py +129 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testPeSymbolProvider.py +67 -0
- smda-4.2.0/src/smda/__init__.py +0 -1
- smda-4.2.0/src/smda/ida/IdaExporter.py +0 -92
- {smda-4.2.0 → smda-4.2.2}/LICENSE +0 -0
- {smda-4.2.0 → smda-4.2.2}/setup.cfg +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/Disassembler.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/DisassemblyResult.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/DisassemblyStatistics.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/AArch64CapstoneVerification.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/AArch64Disassembler.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/AArch64InstructionEscaper.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/FunctionAnalysisState.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/FunctionCandidateManager.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/analyzers.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/definitions.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/cil/CilDisassembler.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/cil/CilInstructionEscaper.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/cil/FunctionAnalysisState.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/cil/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/BasicBlock.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/BinaryInfo.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/BlockLocator.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/CodeXref.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/DominatorTree.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/ExceptionHandling.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/SmdaBasicBlock.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/SmdaInstruction.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/SmdaReport.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/TailcallAnalyzer.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/Tarjan.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/arch/ArchBackend.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/arch/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/AbstractLabelProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/CilSymbolProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/DelphiKbSymbolProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/DelphiPythiaProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/DelphiReSymProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/ElfApiResolver.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/ElfSymbolProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/GoLabelProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/MachoDemangler.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/MachoSymbolProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/OrdinalHelper.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/PdbSymbolProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/RustSymbolProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/main.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/rust.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/rust_legacy.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/rust_v0.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/utils.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/dalvik/DalvikDisassembler.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/dalvik/DalvikFunctionAnalysisState.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/dalvik/DalvikOpcodeDecoder.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/dalvik/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/ida/BackendInterface.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/ida/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/BitnessAnalyzer.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/FunctionCandidate.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/IndirectCallAnalyzer.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/IntelDisassembler.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/JumpTableAnalyzer.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/LanguageAnalyzer.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/MnemonicTfIdf.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/intel/definitions.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/BracketQueue.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/DelphiKbFileLoader.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/DexFileLoader.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/ElfFileLoader.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/FileLoader.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/MachoBinary.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/MachoFileLoader.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/MemoryFileLoader.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/PeFileLoader.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/PriorityQueue.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/StringExtractor.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/__init__.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda/utility/common.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda.egg-info/SOURCES.txt +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda.egg-info/dependency_links.txt +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda.egg-info/requires.txt +0 -0
- {smda-4.2.0 → smda-4.2.2}/src/smda.egg-info/top_level.txt +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testAArch64EscaperCapstoneVerification.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testAArch64EscaperCoverage.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testAArch64EscaperImproved.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testAArch64MachoCorpus.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testBenchmarkEval.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testBitnessAnalyzer.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testBracketQueue.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testCandidateSafeguards.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testDalvikDisassembler.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testDefinitionsExpansion.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testDelphiPythiaProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testElfSymbolProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testExceptionHandling.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testFileFormatParsers.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testIndirectCallAnalyzer.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testLanguageAnalyzer.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testLateCandidateProduction.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testMachoSymbolProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testPdataExtraction.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testPeFileLoader.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testRustSymbolProvider.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testSmdaInstructionDetail.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testSmdaInstructionDetailAArch64.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testSmdaReportBuffer.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testTarjan.py +0 -0
- {smda-4.2.0 → smda-4.2.2}/tests/testXmetadataNormalization.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smda
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.2
|
|
4
4
|
Summary: A recursive disassmbler optimized for CFG recovery from memory dumps. Based on capstone.
|
|
5
5
|
Author-email: Daniel Plohmann <daniel.plohmann@mailbox.org>
|
|
6
6
|
License: BSD 2-Clause
|
|
@@ -146,6 +146,8 @@ make test
|
|
|
146
146
|
|
|
147
147
|
## Version History
|
|
148
148
|
|
|
149
|
+
* 2026-07-09: v4.2.2 - Now also parsing delay import tables from Windows PEs.
|
|
150
|
+
* 2026-07-09: v4.2.1 - Better detection of CFG instructions with prefixes, improved accuracy of gap search. (THX: @r0ny123) IDA ARM64 export.
|
|
149
151
|
* 2026-06-24: v4.2.0 - Further improvements for inter-procedural operand escaping (closer to x86_x64 and traditional PIC hashing).
|
|
150
152
|
* 2026-06-23: v4.1.0 - Significantly extended Aarch64 mnemonic escapes and improved PIC/OPC hashing. (THX: @r0ny123)
|
|
151
153
|
* 2026-06-23: v4.0.2 - Improvements to Aarch64 function recovery, adressing tailcalls and gap function cornercases.
|
|
@@ -105,6 +105,8 @@ make test
|
|
|
105
105
|
|
|
106
106
|
## Version History
|
|
107
107
|
|
|
108
|
+
* 2026-07-09: v4.2.2 - Now also parsing delay import tables from Windows PEs.
|
|
109
|
+
* 2026-07-09: v4.2.1 - Better detection of CFG instructions with prefixes, improved accuracy of gap search. (THX: @r0ny123) IDA ARM64 export.
|
|
108
110
|
* 2026-06-24: v4.2.0 - Further improvements for inter-procedural operand escaping (closer to x86_x64 and traditional PIC hashing).
|
|
109
111
|
* 2026-06-23: v4.1.0 - Significantly extended Aarch64 mnemonic escapes and improved PIC/OPC hashing. (THX: @r0ny123)
|
|
110
112
|
* 2026-06-23: v4.0.2 - Improvements to Aarch64 function recovery, adressing tailcalls and gap function cornercases.
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
|
|
5
5
|
class SmdaConfig:
|
|
6
6
|
# keep this in sync with smda.__version__
|
|
7
|
-
VERSION = "4.2.
|
|
7
|
+
VERSION = "4.2.2"
|
|
8
8
|
ESCAPER_DOWNWARD_COMPATIBILITY = "1.13.16"
|
|
9
9
|
CONFIG_FILE_PATH = str(os.path.abspath(__file__))
|
|
10
10
|
PROJECT_ROOT = str(os.path.abspath(os.sep.join([CONFIG_FILE_PATH, "..", "..", ".."])))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.2.2"
|
|
@@ -130,6 +130,7 @@ class AArch64Backend(ArchBackend):
|
|
|
130
130
|
@classmethod
|
|
131
131
|
def _callFallthroughFunctionStart(cls, d, addr):
|
|
132
132
|
if addr in d.fc_manager.getFunctionStartCandidates():
|
|
133
|
+
print(f"found candidate at call fallthrough 0x{addr:08x}")
|
|
133
134
|
return addr
|
|
134
135
|
|
|
135
136
|
cursor = addr
|
|
@@ -138,7 +139,13 @@ class AArch64Backend(ArchBackend):
|
|
|
138
139
|
skipped_nop = True
|
|
139
140
|
cursor += INSTRUCTION_SIZE
|
|
140
141
|
if skipped_nop and cursor in d.fc_manager.getFunctionStartCandidates():
|
|
142
|
+
print(f"skipped NOPs, found candidate at 0x{cursor:08x}")
|
|
141
143
|
return cursor
|
|
144
|
+
if skipped_nop and cursor % 16 == 0:
|
|
145
|
+
word = cls._wordAt(d, cursor)
|
|
146
|
+
if word not in (None, 0, NOP):
|
|
147
|
+
print(f"skipped NOPs, found non-NOP at 0x{cursor:08x}: 0x{word:08x}")
|
|
148
|
+
return cursor
|
|
142
149
|
return None
|
|
143
150
|
|
|
144
151
|
def _analyzeCondBranch(self, d, instruction, state):
|
|
@@ -275,6 +275,7 @@ class RecursiveDisassembler:
|
|
|
275
275
|
while True:
|
|
276
276
|
for i in cache:
|
|
277
277
|
i_address, i_size, i_mnemonic, i_op_str = i
|
|
278
|
+
|
|
278
279
|
i_op_str = i_op_str.strip()
|
|
279
280
|
i_relative_address = i_address - self.disassembly.binary_info.base_addr
|
|
280
281
|
i_bytes = self.disassembly.binary_info.binary[i_relative_address : i_relative_address + i_size]
|
|
@@ -199,20 +199,25 @@ class SmdaFunction:
|
|
|
199
199
|
architecture = self.smda_report.architecture if self.smda_report else ""
|
|
200
200
|
if architecture == "dalvik":
|
|
201
201
|
return sum(1 for block in self.blocks.values() for ins in block if ins.mnemonic.startswith("invoke-"))
|
|
202
|
-
|
|
202
|
+
# capstone prepends mandatory prefixes (bnd/rep/lock/...) to the mnemonic string
|
|
203
|
+
return sum(1 for block in self.blocks.values() for ins in block if ins.mnemonic.split(" ")[-1] == "call")
|
|
203
204
|
|
|
204
205
|
@property
|
|
205
206
|
def num_returns(self):
|
|
206
207
|
architecture = self.smda_report.architecture if self.smda_report else ""
|
|
207
208
|
if architecture == "dalvik":
|
|
208
209
|
return sum(1 for block in self.blocks.values() for ins in block if ins.mnemonic.startswith("return"))
|
|
209
|
-
|
|
210
|
+
# capstone prepends mandatory prefixes (bnd/rep/lock/...) to the mnemonic string
|
|
211
|
+
return sum(
|
|
212
|
+
1 for block in self.blocks.values() for ins in block if ins.mnemonic.split(" ")[-1] in ("ret", "retn")
|
|
213
|
+
)
|
|
210
214
|
|
|
211
215
|
def isApiThunk(self):
|
|
212
216
|
if self.num_instructions != 1:
|
|
213
217
|
return False
|
|
214
218
|
first_ins = self.blocks[self.offset][0]
|
|
215
|
-
|
|
219
|
+
# capstone prepends mandatory prefixes (bnd/rep/lock/...) to the mnemonic string
|
|
220
|
+
if first_ins.mnemonic.split(" ")[-1] not in ["jmp", "call"]:
|
|
216
221
|
return False
|
|
217
222
|
return len(self.apirefs) != 0
|
|
218
223
|
|
|
@@ -6,7 +6,7 @@ import logging
|
|
|
6
6
|
import lief
|
|
7
7
|
|
|
8
8
|
from .AbstractLabelProvider import AbstractLabelProvider
|
|
9
|
-
from .import_parsers import parse_pe_imports, resolve_pe_base_addr
|
|
9
|
+
from .import_parsers import parse_pe_delay_imports, parse_pe_imports, resolve_pe_base_addr
|
|
10
10
|
|
|
11
11
|
lief.logging.disable()
|
|
12
12
|
LOGGER = logging.getLogger(__name__)
|
|
@@ -89,7 +89,9 @@ class PeSymbolProvider(AbstractLabelProvider):
|
|
|
89
89
|
return function_symbols
|
|
90
90
|
|
|
91
91
|
def parseImports(self, lief_binary, base_addr=None):
|
|
92
|
-
|
|
92
|
+
import_symbols = parse_pe_imports(lief_binary, base_addr)
|
|
93
|
+
import_symbols.update(parse_pe_delay_imports(lief_binary, base_addr))
|
|
94
|
+
return import_symbols
|
|
93
95
|
|
|
94
96
|
def collectSymbols(self, lief_binary, base_addr=None):
|
|
95
97
|
symbols = {}
|
|
@@ -9,7 +9,7 @@ import lief
|
|
|
9
9
|
lief.logging.disable()
|
|
10
10
|
|
|
11
11
|
from .AbstractLabelProvider import AbstractLabelProvider # noqa: E402
|
|
12
|
-
from .import_parsers import parse_pe_imports # noqa: E402
|
|
12
|
+
from .import_parsers import parse_pe_delay_imports, parse_pe_imports # noqa: E402
|
|
13
13
|
|
|
14
14
|
LOGGER = logging.getLogger(__name__)
|
|
15
15
|
|
|
@@ -41,6 +41,7 @@ class WinApiResolver(AbstractLabelProvider):
|
|
|
41
41
|
if not isinstance(lief_binary, lief.PE.Binary):
|
|
42
42
|
return
|
|
43
43
|
self._api_map["lief"] = parse_pe_imports(lief_binary, binary_info.base_addr)
|
|
44
|
+
self._api_map["lief"].update(parse_pe_delay_imports(lief_binary, binary_info.base_addr))
|
|
44
45
|
|
|
45
46
|
def setOsName(self, os_name):
|
|
46
47
|
self._os_name = os_name
|
|
@@ -34,6 +34,28 @@ def parse_pe_imports(lief_binary, base_addr=None):
|
|
|
34
34
|
return import_symbols
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def parse_pe_delay_imports(lief_binary, base_addr=None):
|
|
38
|
+
if not hasattr(lief_binary, "delay_imports"):
|
|
39
|
+
return {}
|
|
40
|
+
if not lief_binary.has_delay_imports:
|
|
41
|
+
return {}
|
|
42
|
+
active_base = resolve_pe_base_addr(lief_binary, base_addr)
|
|
43
|
+
magic = getattr(lief_binary.optional_header, "magic", None)
|
|
44
|
+
ptr_size = 8 if magic is not None and magic == lief.PE.PE_TYPE.PE32_PLUS else 4
|
|
45
|
+
import_symbols = {}
|
|
46
|
+
for delay_import in lief_binary.delay_imports:
|
|
47
|
+
lib_name_lower = (getattr(delay_import, "name", "") or "").lower()
|
|
48
|
+
for index, func in enumerate(delay_import.entries):
|
|
49
|
+
slot_addr = active_base + delay_import.iat + index * ptr_size
|
|
50
|
+
if func.name:
|
|
51
|
+
import_symbols[slot_addr] = (lib_name_lower, func.name)
|
|
52
|
+
elif func.is_ordinal:
|
|
53
|
+
resolved_ordinal = OrdinalHelper.resolveOrdinal(lib_name_lower, func.ordinal)
|
|
54
|
+
ordinal_name = resolved_ordinal if resolved_ordinal else f"#{func.ordinal}"
|
|
55
|
+
import_symbols[slot_addr] = (lib_name_lower, ordinal_name)
|
|
56
|
+
return import_symbols
|
|
57
|
+
|
|
58
|
+
|
|
37
59
|
def parse_elf_relocation_imports(lief_binary):
|
|
38
60
|
"""Build import map keyed by relocation slot address: addr -> (lib, name)."""
|
|
39
61
|
if not isinstance(lief_binary, lief.ELF.Binary):
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from capstone import CS_ARCH_ARM64, CS_ARCH_X86, CS_MODE_32, CS_MODE_64, CS_MODE_LITTLE_ENDIAN, Cs
|
|
5
|
+
|
|
6
|
+
from smda.DisassemblyResult import DisassemblyResult
|
|
7
|
+
|
|
8
|
+
from .IdaInterface import IdaInterface
|
|
9
|
+
|
|
10
|
+
LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IdaExporter:
|
|
14
|
+
def __init__(self, config, bitness=None):
|
|
15
|
+
self.config = config
|
|
16
|
+
self.ida_interface = IdaInterface()
|
|
17
|
+
self.bitness = bitness if bitness else self.ida_interface.getBitness()
|
|
18
|
+
self.architecture = self.ida_interface.getArchitecture()
|
|
19
|
+
self.capstone = None
|
|
20
|
+
self.disassembly = DisassemblyResult()
|
|
21
|
+
self.disassembly.smda_version = config.VERSION
|
|
22
|
+
self._initCapstone()
|
|
23
|
+
|
|
24
|
+
def _initCapstone(self):
|
|
25
|
+
if self.architecture == "aarch64":
|
|
26
|
+
self.capstone = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
|
|
27
|
+
elif self.bitness == 64:
|
|
28
|
+
self.capstone = Cs(CS_ARCH_X86, CS_MODE_64)
|
|
29
|
+
else:
|
|
30
|
+
self.capstone = Cs(CS_ARCH_X86, CS_MODE_32)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _splitInstructionBytes(capstone, offset, instruction_bytes, architecture, errors=None):
|
|
34
|
+
"""Split *instruction_bytes* (which IDA may report as N×4 bytes for an AArch64
|
|
35
|
+
MOV-macro head) into one ``(addr, size, mnemonic, op_str, bytes)`` tuple per
|
|
36
|
+
decoded 4-byte instruction.
|
|
37
|
+
|
|
38
|
+
Background
|
|
39
|
+
----------
|
|
40
|
+
IDA's MOV-macro feature collapses a ``MOVZ+MOVK`` pair (materialising a 32-bit
|
|
41
|
+
constant in a Wn register across two 4-byte AArch64 instructions) into one
|
|
42
|
+
logical "head" whose reported size is 8 bytes. Without splitting,
|
|
43
|
+
:meth:`analyzeBuffer` would pass the whole 8-byte buffer to
|
|
44
|
+
:meth:`capstone.disasm_lite` and then take only ``cache[0]`` (the MOVZ),
|
|
45
|
+
silently dropping the MOVK and producing an exported report that drifted from
|
|
46
|
+
SMDA's own per-instruction AArch64 output.
|
|
47
|
+
|
|
48
|
+
This static helper iterates every instruction capstone decoded from the buffer
|
|
49
|
+
and emits one tuple per sub-instruction with correctly sliced 4-byte payloads,
|
|
50
|
+
so both the MOVZ (head) and the MOVK (head+4) appear as their own records.
|
|
51
|
+
"""
|
|
52
|
+
cache = list(capstone.disasm_lite(instruction_bytes, offset))
|
|
53
|
+
out = []
|
|
54
|
+
consumed = 0
|
|
55
|
+
for i_address, i_size, i_mnemonic, i_op_str in cache:
|
|
56
|
+
sub_bytes = bytes(instruction_bytes[consumed : consumed + i_size])
|
|
57
|
+
out.append((i_address, i_size, i_mnemonic, i_op_str, sub_bytes))
|
|
58
|
+
consumed += i_size
|
|
59
|
+
while consumed < len(instruction_bytes):
|
|
60
|
+
chunk = 4 if architecture == "aarch64" else (len(instruction_bytes) - consumed)
|
|
61
|
+
chunk = min(chunk, len(instruction_bytes) - consumed)
|
|
62
|
+
sub_bytes = bytes(instruction_bytes[consumed : consumed + chunk])
|
|
63
|
+
bytes_hex = sub_bytes.hex()
|
|
64
|
+
LOGGER.warning("missing capstone disassembly output at 0x%x (%s)", offset + consumed, bytes_hex)
|
|
65
|
+
if errors is not None:
|
|
66
|
+
errors[offset + consumed] = {"type": "capstone disassembly failure", "instruction_bytes": bytes_hex}
|
|
67
|
+
out.append((offset + consumed, chunk, "error", "error", sub_bytes))
|
|
68
|
+
consumed += chunk
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
def _convertIdaInsToSmda(self, offset, instruction_bytes):
|
|
72
|
+
return self._splitInstructionBytes(
|
|
73
|
+
self.capstone, offset, instruction_bytes, self.architecture, self.disassembly.errors
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def analyzeBuffer(self, binary_info, cb_analysis_timeout=None):
|
|
77
|
+
"""instead of performing a full analysis, simply collect all data from IDA and convert it into a report"""
|
|
78
|
+
self.disassembly.analysis_start_ts = datetime.datetime.now(datetime.timezone.utc)
|
|
79
|
+
self.disassembly.binary_info = binary_info
|
|
80
|
+
self.disassembly.binary_info.architecture = self.ida_interface.getArchitecture()
|
|
81
|
+
if not self.disassembly.binary_info.base_addr:
|
|
82
|
+
self.disassembly.binary_info.base_addr = self.ida_interface.getBaseAddr()
|
|
83
|
+
if not self.disassembly.binary_info.binary:
|
|
84
|
+
self.disassembly.binary_info.binary = self.ida_interface.getBinary()
|
|
85
|
+
if not self.disassembly.binary_info.bitness:
|
|
86
|
+
self.disassembly.binary_info.bitness = self.bitness
|
|
87
|
+
self.disassembly.function_symbols = self.ida_interface.getFunctionSymbols()
|
|
88
|
+
api_map = self.ida_interface.getApiMap()
|
|
89
|
+
for function_offset in self.ida_interface.getFunctions():
|
|
90
|
+
if self.ida_interface.isExternalFunction(function_offset):
|
|
91
|
+
continue
|
|
92
|
+
converted_function = []
|
|
93
|
+
for block in self.ida_interface.getBlocks(function_offset):
|
|
94
|
+
converted_block = []
|
|
95
|
+
for instruction_offset in block:
|
|
96
|
+
instruction_bytes = self.ida_interface.getInstructionBytes(instruction_offset)
|
|
97
|
+
smda_instructions = self._convertIdaInsToSmda(instruction_offset, instruction_bytes)
|
|
98
|
+
num_subs = len(smda_instructions)
|
|
99
|
+
for idx, smda_instruction in enumerate(smda_instructions):
|
|
100
|
+
converted_block.append(smda_instruction)
|
|
101
|
+
self.disassembly.instructions[smda_instruction[0]] = (
|
|
102
|
+
smda_instruction[2],
|
|
103
|
+
smda_instruction[1],
|
|
104
|
+
)
|
|
105
|
+
# IDA tracks code refs at the macro-head level only; for split
|
|
106
|
+
# macro heads, attach in-refs to the first sub-instruction (entry)
|
|
107
|
+
# and out-refs to the last sub-instruction (exit).
|
|
108
|
+
if idx == 0:
|
|
109
|
+
for in_ref in self.ida_interface.getCodeInRefs(instruction_offset):
|
|
110
|
+
self.disassembly.addCodeRefs(in_ref[0], in_ref[1])
|
|
111
|
+
if idx == num_subs - 1:
|
|
112
|
+
for out_ref in self.ida_interface.getCodeOutRefs(instruction_offset):
|
|
113
|
+
self.disassembly.addCodeRefs(out_ref[0], out_ref[1])
|
|
114
|
+
if out_ref[1] in api_map:
|
|
115
|
+
self.disassembly.addr_to_api[smda_instruction[0]] = api_map[out_ref[1]]
|
|
116
|
+
converted_function.append(converted_block)
|
|
117
|
+
self.disassembly.functions[function_offset] = converted_function
|
|
118
|
+
if self.disassembly.isRecursiveFunction(function_offset):
|
|
119
|
+
self.disassembly.recursive_functions.add(function_offset)
|
|
120
|
+
if self.disassembly.isLeafFunction(function_offset):
|
|
121
|
+
self.disassembly.leaf_functions.add(function_offset)
|
|
122
|
+
self.disassembly.analysis_end_ts = datetime.datetime.now(datetime.timezone.utc)
|
|
123
|
+
return self.disassembly
|
|
@@ -52,7 +52,7 @@ class IdaInterface:
|
|
|
52
52
|
class Ida74Interface(BackendInterface):
|
|
53
53
|
def __init__(self):
|
|
54
54
|
self.version = "IDA Pro 7.4 - 8.4"
|
|
55
|
-
self._processor_map = {"metapc": "intel"}
|
|
55
|
+
self._processor_map = {"metapc": "intel", "ARM": "aarch64"}
|
|
56
56
|
self._api_map = {}
|
|
57
57
|
self._import_module_name = ""
|
|
58
58
|
|
|
@@ -60,6 +60,10 @@ class Ida74Interface(BackendInterface):
|
|
|
60
60
|
# https://reverseengineering.stackexchange.com/a/11398
|
|
61
61
|
info = ida_idaapi.get_inf_structure()
|
|
62
62
|
procname = info.procname if idaapi.IDA_SDK_VERSION >= 800 else info.procName
|
|
63
|
+
if procname == "ARM":
|
|
64
|
+
if self.getBitness() != 64:
|
|
65
|
+
raise ValueError(f"Unsupported Architecture: {procname} ({self.getBitness()}bit)")
|
|
66
|
+
return "aarch64"
|
|
63
67
|
if procname in self._processor_map:
|
|
64
68
|
return self._processor_map[procname]
|
|
65
69
|
else:
|
|
@@ -177,6 +181,10 @@ class Ida73Interface(BackendInterface):
|
|
|
177
181
|
# https://reverseengineering.stackexchange.com/a/11398
|
|
178
182
|
info = idaapi.get_inf_structure()
|
|
179
183
|
procname = info.procName
|
|
184
|
+
if procname == "ARM":
|
|
185
|
+
if self.getBitness() != 64:
|
|
186
|
+
raise ValueError(f"Unsupported Architecture: {procname} ({self.getBitness()}bit)")
|
|
187
|
+
return "aarch64"
|
|
180
188
|
if procname in self._processor_map:
|
|
181
189
|
return self._processor_map[procname]
|
|
182
190
|
else:
|
|
@@ -290,6 +298,10 @@ class Ida85Interface(BackendInterface):
|
|
|
290
298
|
|
|
291
299
|
def getArchitecture(self):
|
|
292
300
|
procname = idaapi.inf_get_procname()
|
|
301
|
+
if procname == "ARM":
|
|
302
|
+
if self.getBitness() != 64:
|
|
303
|
+
raise ValueError(f"Unsupported Architecture: {procname} ({self.getBitness()}bit)")
|
|
304
|
+
return "aarch64"
|
|
293
305
|
if procname in self._processor_map:
|
|
294
306
|
return self._processor_map[procname]
|
|
295
307
|
else:
|
|
@@ -167,14 +167,15 @@ class FunctionAnalysisState:
|
|
|
167
167
|
# for instruction in sorted(self.instructions):
|
|
168
168
|
# print("0x%08x: %s %s" % (instruction[0], instruction[2], instruction[3]))
|
|
169
169
|
if as_gap and not self.is_sanely_ending:
|
|
170
|
-
|
|
170
|
+
# capstone prepends mandatory prefixes (bnd/rep/lock/...) to the mnemonic string
|
|
171
|
+
if len(self.instructions) == 1 and self.instructions[0][2].split(" ")[-1] == "jmp":
|
|
171
172
|
byte = self.disassembly.getByte(self.instructions[0][0])
|
|
172
173
|
if isinstance(byte, int):
|
|
173
174
|
byte = chr(byte)
|
|
174
175
|
if byte == "\xeb":
|
|
175
176
|
return False
|
|
176
177
|
# sane case, stub found that just jumps to a referenced function
|
|
177
|
-
elif self.num_blocks_analyzed == 1 and self.instructions[-1][2] in [
|
|
178
|
+
elif self.num_blocks_analyzed == 1 and self.instructions[-1][2].split(" ")[-1] in [
|
|
178
179
|
"jmp",
|
|
179
180
|
"call",
|
|
180
181
|
]:
|
|
@@ -226,11 +227,12 @@ class FunctionAnalysisState:
|
|
|
226
227
|
block = []
|
|
227
228
|
for i in range(ins[start], len(self.instructions)):
|
|
228
229
|
current = self.instructions[i]
|
|
230
|
+
current_mnemonic = current[2].split(" ")[-1]
|
|
229
231
|
block.append(current)
|
|
230
232
|
# if one code reference is to another address than the next
|
|
231
233
|
if (
|
|
232
234
|
current[0] in self.code_refs_from
|
|
233
|
-
and
|
|
235
|
+
and current_mnemonic not in self.CALL_MNEMONICS
|
|
234
236
|
and i != len(self.instructions) - 1
|
|
235
237
|
and any(r != self.instructions[i + 1][0] for r in self.code_refs_from[current[0]])
|
|
236
238
|
):
|
|
@@ -252,7 +254,7 @@ class FunctionAnalysisState:
|
|
|
252
254
|
)
|
|
253
255
|
):
|
|
254
256
|
break
|
|
255
|
-
if
|
|
257
|
+
if current_mnemonic in self.END_MNEMONICS:
|
|
256
258
|
break
|
|
257
259
|
if block:
|
|
258
260
|
blocks.append(block)
|
|
@@ -8,12 +8,16 @@ from smda.common.ExceptionHandling import reraise_non_operational_exception
|
|
|
8
8
|
from smda.utility.BracketQueue import BracketQueue
|
|
9
9
|
from smda.utility.PriorityQueue import PriorityQueue
|
|
10
10
|
|
|
11
|
-
from .definitions import DEFAULT_PROLOGUES, GAP_SEQUENCES
|
|
11
|
+
from .definitions import COMMON_PROLOGUES, DEFAULT_PROLOGUES, GAP_SEQUENCES
|
|
12
12
|
from .FunctionCandidate import FunctionCandidate
|
|
13
13
|
from .LanguageAnalyzer import LanguageAnalyzer
|
|
14
14
|
|
|
15
15
|
LOGGER = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
|
+
# bytes.lstrip() can skip long runs of one-byte padding in C instead of
|
|
18
|
+
# crawling them byte-by-byte in the gap scanner.
|
|
19
|
+
_PADDING_STRIP_BYTES = bytes(sorted(seq[0] for seq in GAP_SEQUENCES[1]))
|
|
20
|
+
|
|
17
21
|
|
|
18
22
|
class FunctionCandidateManager:
|
|
19
23
|
def __init__(self, config):
|
|
@@ -147,11 +151,31 @@ class FunctionCandidateManager:
|
|
|
147
151
|
prev_ins = 0
|
|
148
152
|
min_code = min(self.disassembly.code_map) if self.disassembly.code_map else self.getBitMask()
|
|
149
153
|
max_code = max(self.disassembly.code_map) if self.disassembly.code_map else 0
|
|
150
|
-
|
|
154
|
+
# Raw memory dumps are loaded without section info, so self._code_areas is empty; fall back to
|
|
155
|
+
# the full mapped image so the head (before the first instruction) and tail (after the last
|
|
156
|
+
# instruction) still get gap-scanned. Without this, functions that lie entirely before the
|
|
157
|
+
# first or after the last already-discovered instruction (e.g. trailing jmp/thunk tables) are
|
|
158
|
+
# never reached by the gap pass.
|
|
159
|
+
using_synthetic_area = not self._code_areas
|
|
160
|
+
code_areas = self._code_areas or [
|
|
161
|
+
[
|
|
162
|
+
self.disassembly.binary_info.base_addr,
|
|
163
|
+
self.disassembly.binary_info.base_addr + self.disassembly.binary_info.binary_size,
|
|
164
|
+
]
|
|
165
|
+
]
|
|
166
|
+
for code_area in code_areas:
|
|
151
167
|
if code_area[0] < min_code < code_area[1] and min_code != code_area[0]:
|
|
152
168
|
gaps.append([code_area[0], min_code, min_code - code_area[0]])
|
|
153
169
|
if code_area[0] < max_code < code_area[1] and max_code != code_area[1]:
|
|
154
|
-
|
|
170
|
+
# For the synthetic full-image fallback (raw memory dumps with no section info),
|
|
171
|
+
# the tail gap must start AFTER the last instruction's address, mirroring the
|
|
172
|
+
# interior-hole branch's prev_ins + 1 below. max_code is itself in code_map, so a
|
|
173
|
+
# gap starting at max_code leaves the gap-pointer on a code_map address;
|
|
174
|
+
# getNextGap()'s strict "gap[0] > gap_pointer" test then finds no further gap,
|
|
175
|
+
# returns the bitmask sentinel, and the scan terminates -- abandoning the whole tail
|
|
176
|
+
# region unscanned. Section-derived code areas keep the legacy start (behavior-neutral).
|
|
177
|
+
tail_start = max_code + 1 if using_synthetic_area else max_code
|
|
178
|
+
gaps.append([tail_start, code_area[1], code_area[1] - tail_start])
|
|
155
179
|
for ins in sorted(self.disassembly.code_map.keys()):
|
|
156
180
|
if prev_ins != 0 and ins - prev_ins > 1:
|
|
157
181
|
gaps.append([prev_ins + 1, ins, ins - prev_ins])
|
|
@@ -197,6 +221,13 @@ class FunctionCandidateManager:
|
|
|
197
221
|
def isEffectiveNop(self, byte_sequence):
|
|
198
222
|
return byte_sequence in GAP_SEQUENCES[len(byte_sequence)]
|
|
199
223
|
|
|
224
|
+
def isHotpatchPrologue(self, byte_window):
|
|
225
|
+
# An MSVC hotpatch stub (`mov edi, edi; push ebp; mov ebp, esp`) opens with a
|
|
226
|
+
# `mov edi, edi` that is byte-identical to a 2-byte effective NOP, yet it IS the
|
|
227
|
+
# function's true entry. Recognizing the 5-byte prologue lets callers avoid
|
|
228
|
+
# skipping/rounding past that leading NOP and mislocating the start two bytes late.
|
|
229
|
+
return byte_window in COMMON_PROLOGUES["5"].get(self.bitness, {})
|
|
230
|
+
|
|
200
231
|
def isAlignmentSequence(self, instruction_sequence):
|
|
201
232
|
is_alignment_sequence = False
|
|
202
233
|
instructions_analyzed = 0
|
|
@@ -213,7 +244,7 @@ class FunctionCandidateManager:
|
|
|
213
244
|
break
|
|
214
245
|
if len(instruction_sequence) > instructions_analyzed and instruction_sequence[
|
|
215
246
|
instructions_analyzed
|
|
216
|
-
].mnemonic in [
|
|
247
|
+
].mnemonic.split(" ")[-1] in [
|
|
217
248
|
"leave",
|
|
218
249
|
"ret",
|
|
219
250
|
"retn",
|
|
@@ -260,11 +291,15 @@ class FunctionCandidateManager:
|
|
|
260
291
|
LOGGER.warning("could not fetch raw byte for gap pointer.")
|
|
261
292
|
# try to find padding symbols and skip them
|
|
262
293
|
if byte in GAP_SEQUENCES[1]:
|
|
294
|
+
window = get_window_slice(gap_offset, 256)
|
|
295
|
+
run = len(window) - len(window.lstrip(_PADDING_STRIP_BYTES))
|
|
263
296
|
LOGGER.debug(
|
|
264
|
-
"nextGapCandidate() found
|
|
297
|
+
"nextGapCandidate() found %d-byte padding run - gap_ptr += %d: 0x%08x",
|
|
298
|
+
run,
|
|
299
|
+
run,
|
|
265
300
|
self.gap_pointer,
|
|
266
301
|
)
|
|
267
|
-
self.gap_pointer += 1
|
|
302
|
+
self.gap_pointer += run if run else 1
|
|
268
303
|
continue
|
|
269
304
|
# try to find instructions that directly encode as NOP and skip them
|
|
270
305
|
ins_buf = list(self.capstone.disasm_lite(get_window_slice(gap_offset, 15), gap_offset))
|
|
@@ -285,6 +320,11 @@ class FunctionCandidateManager:
|
|
|
285
320
|
found_multi_byte_nop = False
|
|
286
321
|
for gap_length in range(max(GAP_SEQUENCES.keys()), 1, -1):
|
|
287
322
|
if get_window_slice(gap_offset, gap_length) in GAP_SEQUENCES[gap_length]:
|
|
323
|
+
# Do not skip a `mov edi, edi` effective NOP when it is the landing pad of
|
|
324
|
+
# a hotpatch stub: that byte pair is the function's true start, and skipping
|
|
325
|
+
# it would mislocate the function two bytes late (at the `push ebp`).
|
|
326
|
+
if self.isHotpatchPrologue(get_window_slice(gap_offset, 5)):
|
|
327
|
+
break
|
|
288
328
|
LOGGER.debug(
|
|
289
329
|
"nextGapCandidate() found %d byte effective nop - gap_ptr += %d: 0x%08x",
|
|
290
330
|
gap_length,
|
|
@@ -2114,7 +2114,7 @@ class IntelInstructionEscaper:
|
|
|
2114
2114
|
esc_regs = True
|
|
2115
2115
|
esc_consts = True
|
|
2116
2116
|
if offsets_only:
|
|
2117
|
-
if ins.mnemonic in [
|
|
2117
|
+
if ins.mnemonic.split(" ")[-1] in [
|
|
2118
2118
|
"call",
|
|
2119
2119
|
"lcall",
|
|
2120
2120
|
"jmp",
|
|
@@ -2191,11 +2191,12 @@ class IntelInstructionEscaper:
|
|
|
2191
2191
|
@staticmethod
|
|
2192
2192
|
def escapeBinary(ins, escape_intraprocedural_jumps=False, lower_addr=None, upper_addr=None):
|
|
2193
2193
|
escaped_sequence = ins.bytes
|
|
2194
|
+
mnemonic = ins.mnemonic.split(" ")[-1]
|
|
2194
2195
|
# remove segment, operand, address, repeat override prefixes
|
|
2195
|
-
if
|
|
2196
|
+
if mnemonic in ["call", "lcall", "jmp", "ljmp", "loop", "loopne", "loope"]:
|
|
2196
2197
|
escaped_sequence = IntelInstructionEscaper.escapeBinaryJumpCall(ins, escape_intraprocedural_jumps)
|
|
2197
2198
|
return escaped_sequence
|
|
2198
|
-
if
|
|
2199
|
+
if mnemonic in [
|
|
2199
2200
|
"je",
|
|
2200
2201
|
"jne",
|
|
2201
2202
|
"js",
|
|
@@ -367,12 +367,19 @@ class X86Backend(ArchBackend):
|
|
|
367
367
|
state.endBlock()
|
|
368
368
|
state.setSanelyEnding(True)
|
|
369
369
|
if d.fc_manager.isAlignmentSequence(instruction_sequence):
|
|
370
|
-
|
|
370
|
+
# A hotpatch stub right after a call can look like alignment padding
|
|
371
|
+
# (its leading `mov edi, edi` is an effective NOP), but that byte pair
|
|
372
|
+
# is the next function's true entry. Seed it directly rather than the
|
|
373
|
+
# 16-byte-rounded address, which would land two bytes late.
|
|
374
|
+
if d.fc_manager.isHotpatchPrologue(d._getDisasmWindowBuffer(i_address)[:5]):
|
|
375
|
+
next_candidate_address = i_address
|
|
376
|
+
else:
|
|
377
|
+
next_candidate_address = previous_address + (16 - previous_address % 16)
|
|
371
378
|
LOGGER.debug(
|
|
372
379
|
" Adding: 0x%x as candidate.",
|
|
373
|
-
|
|
380
|
+
next_candidate_address,
|
|
374
381
|
)
|
|
375
|
-
d.fc_manager.addCandidate(
|
|
382
|
+
d.fc_manager.addCandidate(next_candidate_address, is_gap=True)
|
|
376
383
|
return True
|
|
377
384
|
else:
|
|
378
385
|
LOGGER.debug(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smda
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.2
|
|
4
4
|
Summary: A recursive disassmbler optimized for CFG recovery from memory dumps. Based on capstone.
|
|
5
5
|
Author-email: Daniel Plohmann <daniel.plohmann@mailbox.org>
|
|
6
6
|
License: BSD 2-Clause
|
|
@@ -146,6 +146,8 @@ make test
|
|
|
146
146
|
|
|
147
147
|
## Version History
|
|
148
148
|
|
|
149
|
+
* 2026-07-09: v4.2.2 - Now also parsing delay import tables from Windows PEs.
|
|
150
|
+
* 2026-07-09: v4.2.1 - Better detection of CFG instructions with prefixes, improved accuracy of gap search. (THX: @r0ny123) IDA ARM64 export.
|
|
149
151
|
* 2026-06-24: v4.2.0 - Further improvements for inter-procedural operand escaping (closer to x86_x64 and traditional PIC hashing).
|
|
150
152
|
* 2026-06-23: v4.1.0 - Significantly extended Aarch64 mnemonic escapes and improved PIC/OPC hashing. (THX: @r0ny123)
|
|
151
153
|
* 2026-06-23: v4.0.2 - Improvements to Aarch64 function recovery, adressing tailcalls and gap function cornercases.
|
|
@@ -927,8 +927,8 @@ class TestAArch64StaticFixture(unittest.TestCase):
|
|
|
927
927
|
self.assertEqual(self.report.bitness, 64)
|
|
928
928
|
self.assertEqual(self.report.base_addr, 0x400000)
|
|
929
929
|
self.assertEqual(self.report.oep, 0x534)
|
|
930
|
-
self.assertEqual(len(self.report.xcfg),
|
|
931
|
-
self.assertEqual(sum(1 for f in self.report.getFunctions() for _ in f.getInstructions()),
|
|
930
|
+
self.assertEqual(len(self.report.xcfg), 278)
|
|
931
|
+
self.assertEqual(sum(1 for f in self.report.getFunctions() for _ in f.getInstructions()), 19881)
|
|
932
932
|
self.assertEqual(sum(1 for f in self.report.getFunctions() for _ in f.getBlocks()), 3525)
|
|
933
933
|
self.assertIsNotNone(self.report.getFunction(0x400534))
|
|
934
934
|
|
|
@@ -979,7 +979,7 @@ class TestAArch64StaticFixture(unittest.TestCase):
|
|
|
979
979
|
self.assertEqual(roundtrip.architecture, "aarch64")
|
|
980
980
|
self.assertEqual(roundtrip.bitness, 64)
|
|
981
981
|
self.assertEqual(roundtrip.oep, 0x534)
|
|
982
|
-
self.assertEqual(len(roundtrip.xcfg),
|
|
982
|
+
self.assertEqual(len(roundtrip.xcfg), 278)
|
|
983
983
|
|
|
984
984
|
|
|
985
985
|
class TestAArch64Analyzers(unittest.TestCase):
|
|
@@ -1273,5 +1273,72 @@ class TestAArch64Analyzers(unittest.TestCase):
|
|
|
1273
1273
|
self.assertEqual(targets[:3], [0x411120, 0x411140, 0x4110E0])
|
|
1274
1274
|
|
|
1275
1275
|
|
|
1276
|
+
class TestAArch64MovMacroSplit(unittest.TestCase):
|
|
1277
|
+
"""IDA's MOV-macro feature collapses MOVZ+MOVK pairs (materialising a 32-bit
|
|
1278
|
+
constant in a Wn register across two 4-byte AArch64 instructions) into one
|
|
1279
|
+
logical "head" whose reported size is 8 bytes. Without splitting,
|
|
1280
|
+
IdaExporter._convertIdaInsToSmda would feed all 8 bytes to capstone and
|
|
1281
|
+
take only ``cache[0]`` (the MOVZ), silently dropping the MOVK and producing
|
|
1282
|
+
an exported report that drifted from SMDA's own per-instruction AArch64 output.
|
|
1283
|
+
|
|
1284
|
+
These tests exercise IdaExporter._splitInstructionBytes directly (no IDA
|
|
1285
|
+
mocking required), verifying that an 8-byte macro head from IDA is correctly
|
|
1286
|
+
unpacked into two 4-byte sub-instructions.
|
|
1287
|
+
"""
|
|
1288
|
+
|
|
1289
|
+
def _split(self, offset, instruction_bytes):
|
|
1290
|
+
from capstone import CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN, Cs
|
|
1291
|
+
|
|
1292
|
+
from smda.ida.IdaExporter import IdaExporter
|
|
1293
|
+
|
|
1294
|
+
capstone = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
|
|
1295
|
+
errors = {}
|
|
1296
|
+
return (
|
|
1297
|
+
IdaExporter._splitInstructionBytes(capstone, offset, instruction_bytes, "aarch64", errors),
|
|
1298
|
+
errors,
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
def test_movz_movk_macro_splits_into_two_4_byte_instructions(self):
|
|
1302
|
+
# The IDA export example:
|
|
1303
|
+
# 03 20 80 52 MOVZ W3, #0x100 -> capstone: 'mov' w3, #0x100
|
|
1304
|
+
# 03 00 A1 72 MOVK W3, #0x800, LSL #16 -> capstone: 'movk' w3, #0x800, lsl #16
|
|
1305
|
+
# originally shown by IDA as one 8-byte "MOV W3, #0x8000100" macro at 0x100004838.
|
|
1306
|
+
macro_bytes = bytes.fromhex("032080520300A172")
|
|
1307
|
+
insns, errors = self._split(0x100004838, macro_bytes)
|
|
1308
|
+
|
|
1309
|
+
self.assertEqual(errors, {})
|
|
1310
|
+
self.assertEqual(len(insns), 2)
|
|
1311
|
+
|
|
1312
|
+
addr, size, mnem, ops, raw = insns[0]
|
|
1313
|
+
self.assertEqual(addr, 0x100004838)
|
|
1314
|
+
self.assertEqual(size, 4)
|
|
1315
|
+
self.assertEqual(mnem, "mov")
|
|
1316
|
+
self.assertEqual(ops, "w3, #0x100")
|
|
1317
|
+
self.assertEqual(raw, bytes.fromhex("03208052"))
|
|
1318
|
+
|
|
1319
|
+
addr, size, mnem, ops, raw = insns[1]
|
|
1320
|
+
self.assertEqual(addr, 0x10000483C)
|
|
1321
|
+
self.assertEqual(size, 4)
|
|
1322
|
+
self.assertEqual(mnem, "movk")
|
|
1323
|
+
self.assertEqual(ops, "w3, #0x800, lsl #16")
|
|
1324
|
+
self.assertEqual(raw, bytes.fromhex("0300A172"))
|
|
1325
|
+
|
|
1326
|
+
def test_single_4_byte_instruction_still_returns_one_tuple(self):
|
|
1327
|
+
# Non-macro AArch64 instructions return exactly 4 bytes from
|
|
1328
|
+
# IdaInterface.getInstructionBytes; the split helper must preserve
|
|
1329
|
+
# the single-instruction output shape (a one-element list).
|
|
1330
|
+
single = bytes.fromhex("200080D2") # mov x0, #1
|
|
1331
|
+
insns, errors = self._split(0x401000, single)
|
|
1332
|
+
|
|
1333
|
+
self.assertEqual(errors, {})
|
|
1334
|
+
self.assertEqual(len(insns), 1)
|
|
1335
|
+
addr, size, mnem, ops, raw = insns[0]
|
|
1336
|
+
self.assertEqual(addr, 0x401000)
|
|
1337
|
+
self.assertEqual(size, 4)
|
|
1338
|
+
self.assertEqual(mnem, "mov")
|
|
1339
|
+
self.assertEqual(ops, "x0, #1")
|
|
1340
|
+
self.assertEqual(raw, single)
|
|
1341
|
+
|
|
1342
|
+
|
|
1276
1343
|
if __name__ == "__main__":
|
|
1277
1344
|
unittest.main()
|