smda 2.3.1__tar.gz → 2.4.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.
Files changed (76) hide show
  1. {smda-2.3.1/smda.egg-info → smda-2.4.2}/PKG-INFO +19 -2
  2. {smda-2.3.1 → smda-2.4.2}/README.md +3 -0
  3. {smda-2.3.1 → smda-2.4.2}/setup.py +1 -1
  4. {smda-2.3.1 → smda-2.4.2}/smda/SmdaConfig.py +1 -1
  5. {smda-2.3.1 → smda-2.4.2}/smda/common/BinaryInfo.py +1 -1
  6. {smda-2.3.1 → smda-2.4.2}/smda/common/SmdaReport.py +1 -1
  7. smda-2.4.2/smda/common/labelprovider/DelphiReSymProvider.py +440 -0
  8. {smda-2.3.1 → smda-2.4.2}/smda/intel/FunctionCandidateManager.py +8 -1
  9. {smda-2.3.1 → smda-2.4.2}/smda/intel/IntelDisassembler.py +2 -0
  10. {smda-2.3.1 → smda-2.4.2}/smda/intel/LanguageAnalyzer.py +10 -2
  11. {smda-2.3.1 → smda-2.4.2}/smda/utility/FileLoader.py +1 -1
  12. {smda-2.3.1 → smda-2.4.2}/smda/utility/PeFileLoader.py +11 -13
  13. {smda-2.3.1 → smda-2.4.2/smda.egg-info}/PKG-INFO +19 -2
  14. {smda-2.3.1 → smda-2.4.2}/smda.egg-info/SOURCES.txt +2 -5
  15. smda-2.4.2/tests/testPeFileLoader.py +24 -0
  16. {smda-2.3.1 → smda-2.4.2}/LICENSE +0 -0
  17. {smda-2.3.1 → smda-2.4.2}/pyproject.toml +0 -0
  18. {smda-2.3.1 → smda-2.4.2}/setup.cfg +0 -0
  19. {smda-2.3.1 → smda-2.4.2}/smda/Disassembler.py +0 -0
  20. {smda-2.3.1 → smda-2.4.2}/smda/DisassemblyResult.py +0 -0
  21. {smda-2.3.1 → smda-2.4.2}/smda/DisassemblyStatistics.py +0 -0
  22. {smda-2.3.1 → smda-2.4.2}/smda/__init__.py +0 -0
  23. {smda-2.3.1 → smda-2.4.2}/smda/cil/CilDisassembler.py +0 -0
  24. {smda-2.3.1 → smda-2.4.2}/smda/cil/CilInstructionEscaper.py +0 -0
  25. {smda-2.3.1 → smda-2.4.2}/smda/cil/FunctionAnalysisState.py +0 -0
  26. {smda-2.3.1 → smda-2.4.2}/smda/cil/__init__.py +0 -0
  27. {smda-2.3.1 → smda-2.4.2}/smda/common/BasicBlock.py +0 -0
  28. {smda-2.3.1 → smda-2.4.2}/smda/common/BlockLocator.py +0 -0
  29. {smda-2.3.1 → smda-2.4.2}/smda/common/CodeXref.py +0 -0
  30. {smda-2.3.1 → smda-2.4.2}/smda/common/DominatorTree.py +0 -0
  31. {smda-2.3.1 → smda-2.4.2}/smda/common/SmdaBasicBlock.py +0 -0
  32. {smda-2.3.1 → smda-2.4.2}/smda/common/SmdaFunction.py +0 -0
  33. {smda-2.3.1 → smda-2.4.2}/smda/common/SmdaInstruction.py +0 -0
  34. {smda-2.3.1 → smda-2.4.2}/smda/common/TailcallAnalyzer.py +0 -0
  35. {smda-2.3.1 → smda-2.4.2}/smda/common/Tarjan.py +0 -0
  36. {smda-2.3.1 → smda-2.4.2}/smda/common/__init__.py +0 -0
  37. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/AbstractLabelProvider.py +0 -0
  38. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/CilSymbolProvider.py +0 -0
  39. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/DelphiKbSymbolProvider.py +0 -0
  40. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/ElfApiResolver.py +0 -0
  41. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/ElfSymbolProvider.py +0 -0
  42. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/GoLabelProvider.py +0 -0
  43. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/OrdinalHelper.py +0 -0
  44. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/PdbSymbolProvider.py +0 -0
  45. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/PeSymbolProvider.py +0 -0
  46. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/WinApiResolver.py +0 -0
  47. {smda-2.3.1 → smda-2.4.2}/smda/common/labelprovider/__init__.py +0 -0
  48. {smda-2.3.1 → smda-2.4.2}/smda/ida/BackendInterface.py +0 -0
  49. {smda-2.3.1 → smda-2.4.2}/smda/ida/IdaExporter.py +0 -0
  50. {smda-2.3.1 → smda-2.4.2}/smda/ida/IdaInterface.py +0 -0
  51. {smda-2.3.1 → smda-2.4.2}/smda/ida/__init__.py +0 -0
  52. {smda-2.3.1 → smda-2.4.2}/smda/intel/BitnessAnalyzer.py +0 -0
  53. {smda-2.3.1 → smda-2.4.2}/smda/intel/FunctionAnalysisState.py +0 -0
  54. {smda-2.3.1 → smda-2.4.2}/smda/intel/FunctionCandidate.py +0 -0
  55. {smda-2.3.1 → smda-2.4.2}/smda/intel/IndirectCallAnalyzer.py +0 -0
  56. {smda-2.3.1 → smda-2.4.2}/smda/intel/IntelInstructionEscaper.py +0 -0
  57. {smda-2.3.1 → smda-2.4.2}/smda/intel/JumpTableAnalyzer.py +0 -0
  58. {smda-2.3.1 → smda-2.4.2}/smda/intel/MnemonicTfIdf.py +0 -0
  59. {smda-2.3.1 → smda-2.4.2}/smda/intel/__init__.py +0 -0
  60. {smda-2.3.1 → smda-2.4.2}/smda/intel/definitions.py +0 -0
  61. {smda-2.3.1 → smda-2.4.2}/smda/utility/BracketQueue.py +0 -0
  62. {smda-2.3.1 → smda-2.4.2}/smda/utility/DelphiKbFileLoader.py +0 -0
  63. {smda-2.3.1 → smda-2.4.2}/smda/utility/ElfFileLoader.py +0 -0
  64. {smda-2.3.1 → smda-2.4.2}/smda/utility/MachoFileLoader.py +0 -0
  65. {smda-2.3.1 → smda-2.4.2}/smda/utility/MemoryFileLoader.py +0 -0
  66. {smda-2.3.1 → smda-2.4.2}/smda/utility/PriorityQueue.py +0 -0
  67. {smda-2.3.1 → smda-2.4.2}/smda/utility/StringExtractor.py +0 -0
  68. {smda-2.3.1 → smda-2.4.2}/smda/utility/__init__.py +0 -0
  69. {smda-2.3.1 → smda-2.4.2}/smda.egg-info/dependency_links.txt +0 -0
  70. {smda-2.3.1 → smda-2.4.2}/smda.egg-info/requires.txt +0 -0
  71. {smda-2.3.1 → smda-2.4.2}/smda.egg-info/top_level.txt +0 -0
  72. {smda-2.3.1 → smda-2.4.2}/tests/testBracketQueue.py +0 -0
  73. {smda-2.3.1 → smda-2.4.2}/tests/testEscaper.py +0 -0
  74. {smda-2.3.1 → smda-2.4.2}/tests/testFileFormatParsers.py +0 -0
  75. {smda-2.3.1 → smda-2.4.2}/tests/testIntegration.py +0 -0
  76. {smda-2.3.1 → smda-2.4.2}/tests/testTarjan.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: smda
3
- Version: 2.3.1
3
+ Version: 2.4.2
4
4
  Summary: A recursive disassmbler optimized for CFG recovery from memory dumps. Based on capstone.
5
5
  Home-page: https://github.com/danielplohmann/smda
6
6
  Author: Daniel Plohmann
@@ -15,6 +15,20 @@ Classifier: Topic :: Security
15
15
  Classifier: Topic :: Software Development :: Disassemblers
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
+ Requires-Dist: capstone
19
+ Requires-Dist: dncil
20
+ Requires-Dist: dnfile
21
+ Requires-Dist: lief>=0.16.0
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: license
29
+ Dynamic: license-file
30
+ Dynamic: requires-dist
31
+ Dynamic: summary
18
32
 
19
33
 
20
34
  # SMDA
@@ -119,6 +133,9 @@ make test
119
133
  ```
120
134
 
121
135
  ## Version History
136
+ * 2025-11-28: v2.4.2 - Fix for a bug when extracting and merging code areas from section tables. (THX: @r0ny123)
137
+ * 2025-11-28: v2.4.1 - Modernized packaging by also building a wheel. (THX: @dimbleby)
138
+ * 2025-11-21: v2.4.0 - Integration of DelphiReSym by @WenzWenzWenz for Delphi VMT parsing, thanks to @r0ny123 for adapting it!!
122
139
  * 2025-10-21: v2.3.1 - Fixed lief error for section/segment flags in ELF files crashing file loading. Now properly parsing and providing symbol info for PEs in their own xmetadata section.
123
140
  * 2025-10-21: v2.3.0 - Major code refactor and cleanup, with many thanks to the contribution @r0ny123!!
124
141
  * 2025-07-25: v2.2.3 - Minor bugfixes.
@@ -101,6 +101,9 @@ make test
101
101
  ```
102
102
 
103
103
  ## Version History
104
+ * 2025-11-28: v2.4.2 - Fix for a bug when extracting and merging code areas from section tables. (THX: @r0ny123)
105
+ * 2025-11-28: v2.4.1 - Modernized packaging by also building a wheel. (THX: @dimbleby)
106
+ * 2025-11-21: v2.4.0 - Integration of DelphiReSym by @WenzWenzWenz for Delphi VMT parsing, thanks to @r0ny123 for adapting it!!
104
107
  * 2025-10-21: v2.3.1 - Fixed lief error for section/segment flags in ELF files crashing file loading. Now properly parsing and providing symbol info for PEs in their own xmetadata section.
105
108
  * 2025-10-21: v2.3.0 - Major code refactor and cleanup, with many thanks to the contribution @r0ny123!!
106
109
  * 2025-07-25: v2.2.3 - Minor bugfixes.
@@ -12,7 +12,7 @@ requirements.append("lief>=0.16.0")
12
12
  setup(
13
13
  name="smda",
14
14
  # note to self: always change this in config as well.
15
- version="2.3.1",
15
+ version="2.4.2",
16
16
  description="A recursive disassmbler optimized for CFG recovery from memory dumps. Based on capstone.",
17
17
  long_description_content_type="text/markdown",
18
18
  long_description=long_description,
@@ -4,7 +4,7 @@ import os
4
4
 
5
5
  class SmdaConfig:
6
6
  # note to self: always change this in setup.py as well!
7
- VERSION = "2.3.1"
7
+ VERSION = "2.4.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, "..", ".."])))
@@ -94,7 +94,7 @@ class BinaryInfo:
94
94
  def isInCodeAreas(self, address):
95
95
  is_inside = False
96
96
  # if no code areas found, assume the whole image is code and calculate according to base address and size
97
- if len(self.code_areas) == 0:
97
+ if self.code_areas is None or len(self.code_areas) == 0:
98
98
  if self.base_addr <= address <= self.base_addr + self.binary_size:
99
99
  is_inside = True
100
100
  else:
@@ -226,7 +226,7 @@ class SmdaReport:
226
226
  smda_report.base_addr = report_dict["base_addr"]
227
227
  smda_report.binary_size = report_dict["binary_size"]
228
228
  smda_report.bitness = report_dict["bitness"]
229
- smda_report.code_areas = report_dict["code_areas"]
229
+ smda_report.code_areas = report_dict["code_areas"] if report_dict["code_areas"] is not None else []
230
230
  smda_report.code_sections = (
231
231
  [("", section[1], section[2]) for section in report_dict["code_sections"]]
232
232
  if "code_sections" in report_dict
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/python
2
+ """
3
+ DelphiReSymProvider - Delphi Symbol Recovery for SMDA
4
+ Adapted from DelphiReSym by Lukas Wenz (https://github.com/WenzWenzWenz/DelphiReSym)
5
+ Parses Delphi metadata structures (VMT, MDT, RTTI) to extract function symbols from Delphi binaries.
6
+ Supports Delphi versions 2010 through 12 Athens.
7
+ """
8
+
9
+ import logging
10
+ import struct
11
+ from dataclasses import dataclass, field
12
+ from typing import List, Optional
13
+
14
+ from .AbstractLabelProvider import AbstractLabelProvider
15
+
16
+ LOGGER = logging.getLogger(__name__)
17
+
18
+ # VMT (Virtual Method Table) structure constants
19
+ VMT_MDT_FIELD_INDEX = 6 # Index of MDT pointer field in VMT
20
+ VMT_RTTI_FIELD_INDEX = 4 # Index of RTTI_Class pointer field in VMT
21
+ VMT_VMETHOD_START_INDEX = 11 # First virtual method field index
22
+ VMT_VMETHOD_END_INDEX = 22 # Last virtual method field index (exclusive)
23
+ VMT_SAFECALL_EXCEPTION_INDEX = 14 # SafeCallException field (optional, skip in validation)
24
+
25
+ # Architecture-specific VMT jump distances (distance from VMT start to first method)
26
+ VMT_JUMP_DIST_32BIT = 88
27
+ VMT_JUMP_DIST_64BIT = 200
28
+
29
+ # RTTI (Runtime Type Information) constants
30
+ RTTI_MAX_MAGIC_BYTE = 0x15 # Maximum valid magic byte value for RTTI objects
31
+ RTTI_CLASS_MAGIC_BYTE = 0x07 # Magic byte indicating RTTI_Class type
32
+
33
+ # Method entry structure magic value for parameter offset calculation
34
+ # This value is from the original DelphiReSym and works for Delphi 11
35
+ PARAM_ENTRY_PADDING = 3
36
+
37
+
38
+ @dataclass
39
+ class ArchitectureSettings:
40
+ """Architecture-specific settings for VMT/MDT parsing."""
41
+
42
+ ptr_size: int
43
+ jump_dist: int # Expected distance from VMT to first method
44
+
45
+ @property
46
+ def mdt_offset(self) -> int:
47
+ """Offset to MDT pointer within VMT structure."""
48
+ return self.ptr_size * VMT_MDT_FIELD_INDEX
49
+
50
+ @property
51
+ def rtti_offset(self) -> int:
52
+ """Offset to RTTI_Class pointer within VMT structure."""
53
+ return self.ptr_size * VMT_RTTI_FIELD_INDEX
54
+
55
+
56
+ @dataclass
57
+ class ParameterInfo:
58
+ """Information about a function parameter."""
59
+
60
+ type_name: Optional[str] = None
61
+ parameter_name: str = ""
62
+
63
+
64
+ @dataclass
65
+ class MethodInfo:
66
+ """Information about a method extracted from MDT."""
67
+
68
+ function_offset: int
69
+ function_name: str = ""
70
+ return_type: str = "void"
71
+ parameters: List[ParameterInfo] = field(default_factory=list)
72
+
73
+
74
+ class DelphiReSymProvider(AbstractLabelProvider):
75
+ """
76
+ Label provider for extracting Delphi symbols from modern Delphi executables.
77
+ Parses Virtual Method Tables (VMT), Method Definition Tables (MDT), and RTTI metadata.
78
+ """
79
+
80
+ def __init__(self, config):
81
+ self._config = config
82
+ self._func_symbols = {}
83
+ self._binary = None
84
+ self._base_addr = 0
85
+ self._bitness = 32
86
+ self._code_start = 0
87
+ self._code_end = 0
88
+ self._settings = None
89
+
90
+ def update(self, binary_info):
91
+ """Parse Delphi metadata from the given binary."""
92
+ self._binary = binary_info.binary
93
+ self._base_addr = binary_info.base_addr
94
+ self._bitness = binary_info.bitness
95
+
96
+ # Only process PE files with .text sections
97
+ if not self._is_compatible():
98
+ return
99
+
100
+ # Determine code areas
101
+ code_areas = binary_info.code_areas
102
+ if not code_areas:
103
+ LOGGER.debug("No code areas found, skipping DelphiReSym parsing")
104
+ return
105
+
106
+ # Code areas are virtual addresses - convert to file offsets for scanning
107
+ # Use the first code area (typically .text section)
108
+ code_area_start_va = code_areas[0][0]
109
+ code_area_end_va = code_areas[0][1]
110
+
111
+ # Convert virtual addresses to file offsets
112
+ self._code_start = code_area_start_va - self._base_addr
113
+ self._code_end = code_area_end_va - self._base_addr
114
+
115
+ # Validate the offsets are within binary bounds
116
+ if self._code_start < 0 or self._code_end > len(self._binary):
117
+ LOGGER.debug(
118
+ f"Code area offsets out of bounds: {self._code_start}-{self._code_end}, binary size: {len(self._binary)}"
119
+ )
120
+ return
121
+
122
+ # Set up architecture-specific settings
123
+ if self._bitness == 32:
124
+ self._settings = ArchitectureSettings(ptr_size=4, jump_dist=VMT_JUMP_DIST_32BIT)
125
+ elif self._bitness == 64:
126
+ self._settings = ArchitectureSettings(ptr_size=8, jump_dist=VMT_JUMP_DIST_64BIT)
127
+ else:
128
+ LOGGER.warning(f"Unsupported bitness: {self._bitness}")
129
+ return
130
+
131
+ try:
132
+ LOGGER.debug("Starting DelphiReSym symbol extraction...")
133
+ self._parse_delphi_symbols()
134
+ if self._func_symbols:
135
+ LOGGER.info(f"Extracted {len(self._func_symbols)} Delphi symbols")
136
+ else:
137
+ LOGGER.debug("No Delphi symbols extracted")
138
+ except Exception as e:
139
+ LOGGER.warning(f"Error during DelphiReSym parsing: {e}")
140
+
141
+ def _is_compatible(self):
142
+ """Check if binary is a PE file."""
143
+ if len(self._binary) < 0x40:
144
+ return False
145
+ return self._binary[:2] == b"MZ"
146
+
147
+ def _read_ptr(self, offset: int) -> Optional[int]:
148
+ """Read a pointer at the given offset."""
149
+ try:
150
+ if self._settings.ptr_size == 4:
151
+ return struct.unpack("<I", self._binary[offset : offset + 4])[0]
152
+ else:
153
+ return struct.unpack("<Q", self._binary[offset : offset + 8])[0]
154
+ except (struct.error, IndexError):
155
+ return None
156
+
157
+ def _read_byte(self, offset: int) -> Optional[int]:
158
+ """Read a byte at the given offset."""
159
+ if 0 <= offset < len(self._binary):
160
+ return self._binary[offset]
161
+ return None
162
+
163
+ def _read_short(self, offset: int) -> Optional[int]:
164
+ """Read a 16-bit value at the given offset."""
165
+ try:
166
+ return struct.unpack("<H", self._binary[offset : offset + 2])[0]
167
+ except (struct.error, IndexError):
168
+ return None
169
+
170
+ def _read_pascal_string(self, offset: int) -> str:
171
+ """
172
+ Read a Pascal-style string (length byte followed by characters).
173
+ Returns empty string on error.
174
+ """
175
+ try:
176
+ length = self._read_byte(offset)
177
+ if length is None or length == 0:
178
+ return ""
179
+
180
+ string_data = self._binary[offset + 1 : offset + 1 + length]
181
+ if len(string_data) < length:
182
+ return ""
183
+
184
+ return string_data.decode("latin-1", errors="ignore")
185
+ except (IndexError, UnicodeDecodeError):
186
+ return ""
187
+
188
+ def _offset_in_code_area(self, offset: int) -> bool:
189
+ """Check if offset is within code area."""
190
+ return self._code_start <= offset < self._code_end
191
+
192
+ def _addr_to_offset(self, addr: int) -> int:
193
+ """Convert virtual address to file offset."""
194
+ return addr - self._base_addr
195
+
196
+ def _offset_to_addr(self, offset: int) -> int:
197
+ """Convert file offset to virtual address."""
198
+ return offset + self._base_addr
199
+
200
+ def _check_vmt_candidate(self, vmt_offset: int, next_struct_offset: int) -> bool:
201
+ """
202
+ Perform sanity checks on a VMT candidate.
203
+ Returns True if the candidate passes all checks.
204
+ """
205
+ addresses_to_check = [next_struct_offset]
206
+
207
+ # Check MDT pointer
208
+ mdt_offset_field = vmt_offset + self._settings.mdt_offset
209
+ mdt_addr = self._read_ptr(mdt_offset_field)
210
+ if mdt_addr is not None:
211
+ mdt_offset = self._addr_to_offset(mdt_addr)
212
+ addresses_to_check.append(mdt_offset)
213
+ # MDTs should be at higher addresses than VMTs
214
+ if mdt_offset <= vmt_offset:
215
+ return False
216
+
217
+ # Check virtual method pointers (fields 11-22, excluding SafeCallException which is optional)
218
+ for field_num in range(VMT_VMETHOD_START_INDEX, VMT_VMETHOD_END_INDEX):
219
+ if field_num != VMT_SAFECALL_EXCEPTION_INDEX:
220
+ field_offset = vmt_offset + (self._settings.ptr_size * field_num)
221
+ method_addr = self._read_ptr(field_offset)
222
+ if method_addr is not None:
223
+ method_offset = self._addr_to_offset(method_addr)
224
+ addresses_to_check.append(method_offset)
225
+
226
+ # All addresses should be within code area
227
+ return all(self._offset_in_code_area(off) for off in addresses_to_check)
228
+
229
+ def _find_vmts(self) -> list:
230
+ """
231
+ Scan for Virtual Method Tables in the code section.
232
+ Uses a sliding window approach to find VMT structures.
233
+ """
234
+ vmt_offsets = []
235
+ current_offset = self._code_start
236
+
237
+ LOGGER.debug(f"Scanning for VMTs from 0x{current_offset:x} to 0x{self._code_end:x}")
238
+
239
+ while current_offset < self._code_end - self._settings.ptr_size:
240
+ ptr_value = self._read_ptr(current_offset)
241
+ if ptr_value is None:
242
+ current_offset += 1
243
+ continue
244
+
245
+ target_offset = self._addr_to_offset(ptr_value)
246
+ distance = target_offset - current_offset
247
+
248
+ # Check if this matches the expected VMT jump distance
249
+ if distance == self._settings.jump_dist and self._check_vmt_candidate(current_offset, target_offset):
250
+ vmt_offsets.append(current_offset)
251
+ LOGGER.debug(f"Found VMT at offset 0x{current_offset:x}")
252
+
253
+ current_offset += 1
254
+
255
+ return vmt_offsets
256
+
257
+ def _traverse_rtti_object(self, rtti_offset: int) -> Optional[str]:
258
+ """
259
+ Traverse an RTTI object to extract type information.
260
+ Returns the type name (with namespace for RTTI_Class types).
261
+ """
262
+ magic_byte = self._read_byte(rtti_offset)
263
+ if magic_byte is None or magic_byte > RTTI_MAX_MAGIC_BYTE:
264
+ return None
265
+
266
+ # Read object name (Pascal string at offset +1)
267
+ object_name = self._read_pascal_string(rtti_offset + 1)
268
+ if not object_name:
269
+ return None
270
+
271
+ # RTTI_Class contains namespace information
272
+ # RTTI_Class structure layout:
273
+ # MagicByte(1) + ObjectName(pascal) + Unknown(1) + Pointers(2*ptr_size) + Unknown(2) + Namespace(pascal)
274
+ if magic_byte == RTTI_CLASS_MAGIC_BYTE:
275
+ namespace_offset = rtti_offset + 1 + len(object_name) + 1 + 2 * self._settings.ptr_size + 2
276
+ namespace = self._read_pascal_string(namespace_offset)
277
+ if namespace:
278
+ return f"{namespace}.{object_name}"
279
+
280
+ return object_name
281
+
282
+ def _resolve_type_from_double_ptr(self, ptr_field_offset: int) -> Optional[str]:
283
+ """
284
+ Resolve a type name from a double-dereferenced pointer to an RTTI object.
285
+ Used for both return types and parameter types.
286
+ """
287
+ ptr_addr = self._read_ptr(ptr_field_offset)
288
+ if ptr_addr is None or ptr_addr == 0:
289
+ return None
290
+
291
+ ptr_offset = self._addr_to_offset(ptr_addr)
292
+ rtti_addr = self._read_ptr(ptr_offset)
293
+ if rtti_addr is None or rtti_addr == 0:
294
+ return None
295
+
296
+ rtti_offset = self._addr_to_offset(rtti_addr)
297
+ return self._traverse_rtti_object(rtti_offset)
298
+
299
+ def _extract_method_info(self, method_entry_offset: int) -> Optional[MethodInfo]:
300
+ """Extract detailed information about a method from its MethodEntry structure."""
301
+ try:
302
+ # Function entry point (offset +2)
303
+ func_addr = self._read_ptr(method_entry_offset + 2)
304
+ if func_addr is None:
305
+ return None
306
+ func_offset = self._addr_to_offset(func_addr)
307
+
308
+ # Function name (Pascal string at offset +ptr_size+2)
309
+ func_name_offset = method_entry_offset + self._settings.ptr_size + 2
310
+ func_name = self._read_pascal_string(func_name_offset)
311
+ if not func_name:
312
+ return None
313
+
314
+ method_info = MethodInfo(function_offset=func_offset, function_name=func_name)
315
+
316
+ # Return type (at offset +function_name_len+ptr_size+4)
317
+ return_type_field_offset = method_entry_offset + len(func_name) + 1 + self._settings.ptr_size + 4
318
+ return_type_name = self._resolve_type_from_double_ptr(return_type_field_offset)
319
+ if return_type_name:
320
+ method_info.return_type = return_type_name
321
+
322
+ # Parameter count (at offset +function_name_len+2*ptr_size+6)
323
+ param_count_offset = method_entry_offset + len(func_name) + 1 + 2 * self._settings.ptr_size + 6
324
+ param_count = self._read_byte(param_count_offset)
325
+
326
+ if param_count is not None and param_count > 0:
327
+ # Parse parameters
328
+ param_offset = param_count_offset + 2
329
+ for _ in range(param_count):
330
+ # Parameter RTTI (double-dereferenced pointer)
331
+ param_type_name = self._resolve_type_from_double_ptr(param_offset)
332
+
333
+ # Parameter name (Pascal string at offset +ptr_size+2)
334
+ param_name_offset = param_offset + self._settings.ptr_size + 2
335
+ param_name = self._read_pascal_string(param_name_offset)
336
+
337
+ method_info.parameters.append(ParameterInfo(type_name=param_type_name, parameter_name=param_name))
338
+
339
+ # Move to next parameter entry
340
+ param_offset = param_name_offset + len(param_name) + 1 + PARAM_ENTRY_PADDING
341
+
342
+ return method_info
343
+
344
+ except Exception as e:
345
+ LOGGER.debug(f"Error extracting method info at 0x{method_entry_offset:x}: {e}")
346
+ return None
347
+
348
+ def _parse_mdt(self, mdt_offset: int) -> list:
349
+ """Parse a Method Definition Table to extract method information."""
350
+ methods = []
351
+
352
+ # Read number of method entries (short at offset +2)
353
+ num_entries = self._read_short(mdt_offset + 2)
354
+ if num_entries is None or num_entries == 0:
355
+ return methods
356
+
357
+ # Method entry references start at offset +4
358
+ me_refs_start = mdt_offset + 4
359
+
360
+ for i in range(num_entries):
361
+ me_ref_offset = me_refs_start + i * (self._settings.ptr_size + 4)
362
+ me_addr = self._read_ptr(me_ref_offset)
363
+
364
+ if me_addr is None:
365
+ continue
366
+
367
+ me_offset = self._addr_to_offset(me_addr)
368
+ method_info = self._extract_method_info(me_offset)
369
+
370
+ if method_info:
371
+ methods.append(method_info)
372
+
373
+ return methods
374
+
375
+ def _parse_delphi_symbols(self):
376
+ """Main parsing routine to extract Delphi symbols."""
377
+ # Step 1: Find all VMT structures
378
+ vmt_offsets = self._find_vmts()
379
+ LOGGER.debug(f"Found {len(vmt_offsets)} VMT candidates")
380
+
381
+ if not vmt_offsets:
382
+ return
383
+
384
+ # Step 2: For each VMT, extract MDT and parse methods
385
+ for vmt_offset in vmt_offsets:
386
+ # Get MDT address from VMT
387
+ mdt_field_offset = vmt_offset + self._settings.mdt_offset
388
+ mdt_addr = self._read_ptr(mdt_field_offset)
389
+
390
+ if mdt_addr is None:
391
+ continue
392
+
393
+ mdt_offset = self._addr_to_offset(mdt_addr)
394
+
395
+ # Get RTTI namespace for this VMT
396
+ rtti_field_offset = vmt_offset + self._settings.rtti_offset
397
+ rtti_addr = self._read_ptr(rtti_field_offset)
398
+ namespace = None
399
+ if rtti_addr is not None:
400
+ rtti_offset = self._addr_to_offset(rtti_addr)
401
+ namespace = self._traverse_rtti_object(rtti_offset)
402
+
403
+ # Parse all methods in this MDT
404
+ methods = self._parse_mdt(mdt_offset)
405
+
406
+ # Store function symbols
407
+ for method in methods:
408
+ func_addr = self._offset_to_addr(method.function_offset)
409
+
410
+ # Build function name with namespace if available
411
+ full_name = f"{namespace}.{method.function_name}" if namespace else method.function_name
412
+
413
+ # Add return type and parameters to make it more informative
414
+ if method.return_type != "void" or method.parameters:
415
+ param_str = ", ".join([f"{p.parameter_name}: {p.type_name or '?'}" for p in method.parameters])
416
+ full_name = f"{full_name}({param_str}): {method.return_type}"
417
+
418
+ self._func_symbols[func_addr] = full_name
419
+
420
+ def isSymbolProvider(self):
421
+ return True
422
+
423
+ def isApiProvider(self):
424
+ return False
425
+
426
+ def getApi(self, absolute_addr):
427
+ return None
428
+
429
+ def getSymbol(self, address):
430
+ # sanitize output because the extractor may produce non-printable characters
431
+ symbol = self._func_symbols.get(address, "")
432
+ if not all(c.isprintable() or c.isspace() for c in symbol):
433
+ symbol = ""
434
+ return symbol
435
+
436
+ def getFunctionSymbols(self):
437
+ return self._func_symbols
438
+
439
+ def getRelocations(self):
440
+ return {}
@@ -562,10 +562,17 @@ class FunctionCandidateManager:
562
562
  elif self.lang_analyzer.checkDelphi():
563
563
  LOGGER.debug("Programming language recognized as Delphi, adding function start addresses from VMTs")
564
564
  delphi_objects = self.lang_analyzer.getDelphiObjects()
565
- LOGGER.debug("delphi candidates based on VMT analysis: %d", len(delphi_objects))
565
+ LOGGER.debug("delphi candidates based on legacy VMT analysis: %d", len(delphi_objects))
566
566
  for obj in delphi_objects:
567
567
  self.addLanguageSpecCandidate(obj, "delphi")
568
568
 
569
+ # Also extract symbols using DelphiReSym metadata parsing
570
+ LOGGER.debug("Extracting Delphi symbols using DelphiReSym metadata parsing")
571
+ delphi_resym_objects = self.lang_analyzer.getDelphiReSymObjects()
572
+ LOGGER.debug("delphi candidates based on DelphiReSym analysis: %d", len(delphi_resym_objects))
573
+ for obj in delphi_resym_objects:
574
+ self.addLanguageSpecCandidate(obj, "delphi_resym")
575
+
569
576
  def locateStubChainCandidates(self):
570
577
  # binaries often contain long sequences of stubs, consisting only of jmp dword ptr <offset>, add such chains as candidates
571
578
  for block in re.finditer(b"(?P<block>(\xff\x25[\\S\\s]{4}){2,})", self.disassembly.binary_info.binary):
@@ -8,6 +8,7 @@ from capstone import CS_ARCH_X86, CS_MODE_32, CS_MODE_64, Cs
8
8
 
9
9
  from smda.common.BinaryInfo import BinaryInfo
10
10
  from smda.common.labelprovider.DelphiKbSymbolProvider import DelphiKbSymbolProvider
11
+ from smda.common.labelprovider.DelphiReSymProvider import DelphiReSymProvider
11
12
  from smda.common.labelprovider.ElfApiResolver import ElfApiResolver
12
13
  from smda.common.labelprovider.ElfSymbolProvider import ElfSymbolProvider
13
14
  from smda.common.labelprovider.GoLabelProvider import GoSymbolProvider
@@ -92,6 +93,7 @@ class IntelDisassembler:
92
93
  self.label_providers.append(PdbSymbolProvider(self.config))
93
94
  self.label_providers.append(GoSymbolProvider(self.config))
94
95
  self.label_providers.append(DelphiKbSymbolProvider(self.config))
96
+ self.label_providers.append(DelphiReSymProvider(self.config))
95
97
 
96
98
  def _updateLabelProviders(self, binary_info):
97
99
  for provider in self.label_providers:
@@ -5,6 +5,7 @@ import struct
5
5
  from io import BytesIO
6
6
 
7
7
  from smda.common.labelprovider.DelphiKbSymbolProvider import DelphiKbSymbolProvider
8
+ from smda.common.labelprovider.DelphiReSymProvider import DelphiReSymProvider
8
9
  from smda.common.labelprovider.GoLabelProvider import GoSymbolProvider
9
10
 
10
11
  LOGGER = logging.getLogger(__name__)
@@ -15,6 +16,7 @@ class LanguageAnalyzer:
15
16
  self.disassembly = disassembly
16
17
  self.go_resolver = GoSymbolProvider(None)
17
18
  self.delphi_kb_resolver = DelphiKbSymbolProvider(None)
19
+ self.delphi_resym_resolver = DelphiReSymProvider(None)
18
20
  self.strings = None
19
21
 
20
22
  def validPEHeader(self):
@@ -89,12 +91,13 @@ class LanguageAnalyzer:
89
91
  match.group("string")
90
92
  # Regex: <DWORD_LEN_STRING><STRING><TERMINATOR>
91
93
  for match in re.finditer(
92
- b"(?P<length>[\\S\\s]{4})(?P<string>[ -~]{6,128})\x00",
94
+ b"\x00\x00.(?P<length>.)(?P<string>[ -~]{6,128})\x00",
93
95
  self.disassembly.binary_info.binary,
94
96
  )
95
- if len(match.group("string")) == struct.unpack("<I", match.group("length"))[0]
97
+ if len(match.group("string")) == ord(match.group("length"))
96
98
  ]
97
99
  if len(delphi_strings) > 100:
100
+ LOGGER.info("Detected %d Delphi-like strings.", len(delphi_strings))
98
101
  delphi_score = max(delphi_score, 0.8)
99
102
  return delphi_score
100
103
 
@@ -223,6 +226,11 @@ class LanguageAnalyzer:
223
226
  self.delphi_kb_resolver.update(self.disassembly.binary_info)
224
227
  return self.delphi_kb_resolver.getFunctionSymbols()
225
228
 
229
+ def getDelphiReSymObjects(self):
230
+ """Extract Delphi symbols using DelphiReSym metadata parsing."""
231
+ self.delphi_resym_resolver.update(self.disassembly.binary_info)
232
+ return self.delphi_resym_resolver.getFunctionSymbols()
233
+
226
234
  def identify(self):
227
235
  result = {
228
236
  # programming language : probability
@@ -24,7 +24,7 @@ class FileLoader:
24
24
  self._loadFile()
25
25
 
26
26
  def _loadRawFileContent(self):
27
- binary = ""
27
+ binary = b""
28
28
  if os.path.isfile(self._file_path):
29
29
  with open(self._file_path, "rb") as inf:
30
30
  binary = inf.read()
@@ -160,17 +160,15 @@ class PeFileLoader:
160
160
 
161
161
  @staticmethod
162
162
  def mergeCodeAreas(code_areas):
163
- merged_code_areas = sorted(code_areas)
164
- result = []
165
- index = 0
166
- while index < len(merged_code_areas) - 1:
167
- this_area = merged_code_areas[index]
168
- next_area = merged_code_areas[index + 1]
169
- if this_area[1] != next_area[0]:
170
- result.append(this_area)
171
- index += 1
163
+ if not code_areas:
164
+ return []
165
+ sorted_areas = sorted(code_areas)
166
+ result = [sorted_areas[0]]
167
+ for i in range(1, len(sorted_areas)):
168
+ last_area = result[-1]
169
+ current_area = sorted_areas[i]
170
+ if last_area[1] == current_area[0]:
171
+ result[-1] = [last_area[0], current_area[1]]
172
172
  else:
173
- merged_code_areas = (
174
- merged_code_areas[:index] + [[this_area[0], next_area[1]]] + merged_code_areas[index + 2 :]
175
- )
176
- return merged_code_areas
173
+ result.append(current_area)
174
+ return result
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: smda
3
- Version: 2.3.1
3
+ Version: 2.4.2
4
4
  Summary: A recursive disassmbler optimized for CFG recovery from memory dumps. Based on capstone.
5
5
  Home-page: https://github.com/danielplohmann/smda
6
6
  Author: Daniel Plohmann
@@ -15,6 +15,20 @@ Classifier: Topic :: Security
15
15
  Classifier: Topic :: Software Development :: Disassemblers
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
+ Requires-Dist: capstone
19
+ Requires-Dist: dncil
20
+ Requires-Dist: dnfile
21
+ Requires-Dist: lief>=0.16.0
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: license
29
+ Dynamic: license-file
30
+ Dynamic: requires-dist
31
+ Dynamic: summary
18
32
 
19
33
 
20
34
  # SMDA
@@ -119,6 +133,9 @@ make test
119
133
  ```
120
134
 
121
135
  ## Version History
136
+ * 2025-11-28: v2.4.2 - Fix for a bug when extracting and merging code areas from section tables. (THX: @r0ny123)
137
+ * 2025-11-28: v2.4.1 - Modernized packaging by also building a wheel. (THX: @dimbleby)
138
+ * 2025-11-21: v2.4.0 - Integration of DelphiReSym by @WenzWenzWenz for Delphi VMT parsing, thanks to @r0ny123 for adapting it!!
122
139
  * 2025-10-21: v2.3.1 - Fixed lief error for section/segment flags in ELF files crashing file loading. Now properly parsing and providing symbol info for PEs in their own xmetadata section.
123
140
  * 2025-10-21: v2.3.0 - Major code refactor and cleanup, with many thanks to the contribution @r0ny123!!
124
141
  * 2025-07-25: v2.2.3 - Minor bugfixes.
@@ -2,11 +2,6 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  setup.py
5
- ./smda/Disassembler.py
6
- ./smda/DisassemblyResult.py
7
- ./smda/DisassemblyStatistics.py
8
- ./smda/SmdaConfig.py
9
- ./smda/__init__.py
10
5
  smda/Disassembler.py
11
6
  smda/DisassemblyResult.py
12
7
  smda/DisassemblyStatistics.py
@@ -36,6 +31,7 @@ smda/common/__init__.py
36
31
  smda/common/labelprovider/AbstractLabelProvider.py
37
32
  smda/common/labelprovider/CilSymbolProvider.py
38
33
  smda/common/labelprovider/DelphiKbSymbolProvider.py
34
+ smda/common/labelprovider/DelphiReSymProvider.py
39
35
  smda/common/labelprovider/ElfApiResolver.py
40
36
  smda/common/labelprovider/ElfSymbolProvider.py
41
37
  smda/common/labelprovider/GoLabelProvider.py
@@ -74,4 +70,5 @@ tests/testBracketQueue.py
74
70
  tests/testEscaper.py
75
71
  tests/testFileFormatParsers.py
76
72
  tests/testIntegration.py
73
+ tests/testPeFileLoader.py
77
74
  tests/testTarjan.py
@@ -0,0 +1,24 @@
1
+ import unittest
2
+
3
+ from smda.utility.PeFileLoader import PeFileLoader
4
+
5
+
6
+ class PeFileLoaderTestSuite(unittest.TestCase):
7
+ def test_mergeCodeAreas(self):
8
+ test_cases = [
9
+ ("Overlapping intervals", [[1, 5], [3, 7], [8, 12]], [[1, 5], [3, 7], [8, 12]]),
10
+ ("Contiguous intervals", [[1, 5], [5, 10], [10, 15]], [[1, 15]]),
11
+ ("Unsorted contiguous intervals", [[10, 15], [1, 5], [5, 10]], [[1, 15]]),
12
+ ("Separated intervals", [[1, 5], [6, 10], [11, 15]], [[1, 5], [6, 10], [11, 15]]),
13
+ ("Empty list", [], []),
14
+ ("Single interval", [[1, 5]], [[1, 5]]),
15
+ ("Mixed intervals", [[1, 5], [5, 10], [11, 15], [15, 20]], [[1, 10], [11, 20]]),
16
+ ]
17
+
18
+ for name, intervals, expected in test_cases:
19
+ with self.subTest(msg=name):
20
+ self.assertEqual(PeFileLoader.mergeCodeAreas(intervals), expected)
21
+
22
+
23
+ if __name__ == "__main__":
24
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes