sigmaker 1.4.0__py3-none-any.whl
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.
sigmaker/__init__.py
ADDED
|
@@ -0,0 +1,1612 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sigmaker.py - IDA Python Signature Maker
|
|
3
|
+
https://github.com/mahmoudimus/ida-sigmaker
|
|
4
|
+
|
|
5
|
+
by @mahmoudimus (Mahmoud Abdelkader)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import contextvars
|
|
12
|
+
import dataclasses
|
|
13
|
+
import enum
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import string
|
|
17
|
+
import traceback
|
|
18
|
+
import typing
|
|
19
|
+
|
|
20
|
+
import idaapi
|
|
21
|
+
import idc
|
|
22
|
+
|
|
23
|
+
__author__ = "mahmoudimus"
|
|
24
|
+
__version__ = "1.4.0"
|
|
25
|
+
|
|
26
|
+
PLUGIN_NAME: str = "Signature Maker (py)"
|
|
27
|
+
PLUGIN_VERSION: str = __version__
|
|
28
|
+
PLUGIN_AUTHOR: str = __author__
|
|
29
|
+
|
|
30
|
+
WILDCARD_POLICY_CTX: contextvars.ContextVar["WildcardPolicy"] = contextvars.ContextVar(
|
|
31
|
+
"wildcard_policy"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Unexpected(Exception):
|
|
36
|
+
"""Exception type used throughout the module to indicate unexpected errors."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_address_marked_as_code(ea: int) -> bool:
|
|
40
|
+
"""Returns True if the specified address (ea) is marked as code in the disassembled binary."""
|
|
41
|
+
return idaapi.is_code(idaapi.get_flags(ea))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclasses.dataclass
|
|
45
|
+
class SigMakerConfig:
|
|
46
|
+
"""Configuration for SigMaker operations.
|
|
47
|
+
|
|
48
|
+
This class holds all the configuration parameters needed for
|
|
49
|
+
SigMaker operations.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
output_format: SignatureType
|
|
53
|
+
wildcard_operands: bool
|
|
54
|
+
continue_outside_of_function: bool
|
|
55
|
+
wildcard_optimized: bool
|
|
56
|
+
ask_longer_signature: bool = True
|
|
57
|
+
print_top_x: int = 5
|
|
58
|
+
max_single_signature_length: int = 100
|
|
59
|
+
max_xref_signature_length: int = 250
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclasses.dataclass(slots=True, frozen=True, repr=False)
|
|
63
|
+
class Match:
|
|
64
|
+
"""Container for a single match.
|
|
65
|
+
|
|
66
|
+
Acts like an int, but provides a more readable representation.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
address: int
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return f"Match(address={hex(self.address)})"
|
|
73
|
+
|
|
74
|
+
def __str__(self) -> str:
|
|
75
|
+
return hex(self.address)
|
|
76
|
+
|
|
77
|
+
def __int__(self) -> int:
|
|
78
|
+
return self.address
|
|
79
|
+
|
|
80
|
+
__index__ = __int__
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SignatureType(enum.Enum):
|
|
84
|
+
"""Enumeration representing the various supported signature output formats."""
|
|
85
|
+
|
|
86
|
+
IDA = "ida"
|
|
87
|
+
x64Dbg = "x64dbg"
|
|
88
|
+
Mask = "mask"
|
|
89
|
+
BitMask = "bitmask"
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def at(cls, index: int) -> "SignatureType":
|
|
93
|
+
"""Return the enum member at a given index (definition order)."""
|
|
94
|
+
return list(cls.__members__.values())[index]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SignatureByte(typing.NamedTuple):
|
|
98
|
+
"""Container representing a single byte in a signature.
|
|
99
|
+
|
|
100
|
+
The ``value`` attribute holds the byte value and ``is_wildcard`` indicates
|
|
101
|
+
whether this byte should be treated as a wildcard in comparisons and output.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
value: int
|
|
105
|
+
is_wildcard: bool
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Signature(list[SignatureByte]):
|
|
109
|
+
"""
|
|
110
|
+
A data container for a sequence of signature bytes.
|
|
111
|
+
|
|
112
|
+
This class is responsible for storing and manipulating the raw data of a
|
|
113
|
+
signature. It does not handle formatting into string representations.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def add_byte_to_signature(self, address: int, is_wildcard: bool) -> None:
|
|
117
|
+
"""Appends a single byte from the IDA database to the signature."""
|
|
118
|
+
byte_value = idaapi.get_byte(address)
|
|
119
|
+
self.append(SignatureByte(byte_value, is_wildcard))
|
|
120
|
+
|
|
121
|
+
def add_bytes_to_signature(
|
|
122
|
+
self, address: int, count: int, is_wildcard: bool
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Appends multiple bytes from the IDA database to the signature."""
|
|
125
|
+
# Using get_bytes is more efficient than a loop of get_byte
|
|
126
|
+
bytes_data = idaapi.get_bytes(address, count)
|
|
127
|
+
if bytes_data:
|
|
128
|
+
self.extend(SignatureByte(b, is_wildcard) for b in bytes_data)
|
|
129
|
+
|
|
130
|
+
def trim_signature(self) -> None:
|
|
131
|
+
"""Removes trailing wildcard bytes from the signature in-place."""
|
|
132
|
+
n = len(self)
|
|
133
|
+
while n > 0 and self[n - 1].is_wildcard:
|
|
134
|
+
n -= 1
|
|
135
|
+
# Efficiently truncate the list
|
|
136
|
+
del self[n:]
|
|
137
|
+
|
|
138
|
+
def __str__(self) -> str:
|
|
139
|
+
"""
|
|
140
|
+
Provides the default string representation.
|
|
141
|
+
This is equivalent to format(self, '').
|
|
142
|
+
"""
|
|
143
|
+
return self.__format__("")
|
|
144
|
+
|
|
145
|
+
def __format__(self, format_spec: str) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Formats the signature according to the provided format specifier.
|
|
148
|
+
|
|
149
|
+
This method allows the Signature object to be used with f-strings
|
|
150
|
+
and the format() built-in function.
|
|
151
|
+
|
|
152
|
+
Supported format_spec values:
|
|
153
|
+
- '' (default) or 'ida': "55 8B ? EC"
|
|
154
|
+
- 'x64dbg': "55 8B ?? EC"
|
|
155
|
+
- 'mask': "\\x55\\x8B\\x00\\xEC xx?x"
|
|
156
|
+
- 'bitmask': "0x55, 0x8B, 0x00, 0xEC 0b1101"
|
|
157
|
+
"""
|
|
158
|
+
# Use .lower() to make specifiers case-insensitive
|
|
159
|
+
spec = format_spec.lower()
|
|
160
|
+
try:
|
|
161
|
+
formatter = FORMATTER_MAP[SignatureType(spec)]
|
|
162
|
+
except KeyError:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Unknown format code '{format_spec}' for object of type 'Signature'"
|
|
165
|
+
)
|
|
166
|
+
return formatter.format(self)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class SignatureFormatter(typing.Protocol):
|
|
170
|
+
"""
|
|
171
|
+
A protocol for objects that can format a Signature into a string.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def format(self, signature: "Signature") -> str:
|
|
175
|
+
"""Formats the given Signature object into a string."""
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
180
|
+
class IdaFormatter:
|
|
181
|
+
"""
|
|
182
|
+
Formats a signature into the IDA style ('DE AD ? EF').
|
|
183
|
+
The wildcard character can be configured.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
wildcard_byte: str = "?"
|
|
187
|
+
|
|
188
|
+
def format(self, signature: "Signature") -> str:
|
|
189
|
+
parts = []
|
|
190
|
+
for byte in signature:
|
|
191
|
+
if byte.is_wildcard:
|
|
192
|
+
parts.append(self.wildcard_byte)
|
|
193
|
+
else:
|
|
194
|
+
parts.append(f"{byte.value:02X}")
|
|
195
|
+
return " ".join(parts)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
199
|
+
class X64DbgFormatter(IdaFormatter):
|
|
200
|
+
"""
|
|
201
|
+
Formats a signature for x64Dbg by specializing IdaFormatter
|
|
202
|
+
to use '??' as the wildcard.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
wildcard_byte: str = "??"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
209
|
+
class MaskedBytesFormatter:
|
|
210
|
+
"""Formats into a C-style byte array and a mask string ('\\xDE\\xAD', 'xx?')."""
|
|
211
|
+
|
|
212
|
+
wildcard_byte: str = "\\x00"
|
|
213
|
+
mask: str = "x"
|
|
214
|
+
wildcard_mask: str = "?"
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def build_signature_parts(
|
|
218
|
+
signature: "Signature",
|
|
219
|
+
byte_format: str,
|
|
220
|
+
wildcard_byte: str,
|
|
221
|
+
mask_char: str,
|
|
222
|
+
wildcard_mask_char: str,
|
|
223
|
+
) -> tuple[list[str], list[str]]:
|
|
224
|
+
"""
|
|
225
|
+
Iterates over a signature and builds lists of its pattern and mask parts.
|
|
226
|
+
This is the common logic shared by multiple masked byte formatters.
|
|
227
|
+
"""
|
|
228
|
+
pattern_parts = []
|
|
229
|
+
mask_parts = []
|
|
230
|
+
for byte in signature:
|
|
231
|
+
if byte.is_wildcard:
|
|
232
|
+
pattern_parts.append(wildcard_byte)
|
|
233
|
+
mask_parts.append(wildcard_mask_char)
|
|
234
|
+
else:
|
|
235
|
+
pattern_parts.append(byte_format.format(byte.value))
|
|
236
|
+
mask_parts.append(mask_char)
|
|
237
|
+
return pattern_parts, mask_parts
|
|
238
|
+
|
|
239
|
+
def format(self, signature: "Signature") -> str:
|
|
240
|
+
pattern_parts, mask_parts = self.build_signature_parts(
|
|
241
|
+
signature,
|
|
242
|
+
"\\x{:02X}",
|
|
243
|
+
self.wildcard_byte,
|
|
244
|
+
self.mask,
|
|
245
|
+
self.wildcard_mask,
|
|
246
|
+
)
|
|
247
|
+
return "".join(pattern_parts) + " " + "".join(mask_parts)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
251
|
+
class ByteArrayBitmaskFormatter:
|
|
252
|
+
"""Formats into a C-style byte array and a bitmask ('0xDE,', '0b1101')."""
|
|
253
|
+
|
|
254
|
+
wildcard_byte: str = "0x00"
|
|
255
|
+
mask: str = "1"
|
|
256
|
+
wildcard_mask: str = "0"
|
|
257
|
+
|
|
258
|
+
def format(self, signature: "Signature") -> str:
|
|
259
|
+
pattern_parts, mask_parts = MaskedBytesFormatter.build_signature_parts(
|
|
260
|
+
signature,
|
|
261
|
+
"0x{:02X}",
|
|
262
|
+
self.wildcard_byte,
|
|
263
|
+
self.mask,
|
|
264
|
+
self.wildcard_mask,
|
|
265
|
+
)
|
|
266
|
+
pattern_str = ", ".join(pattern_parts)
|
|
267
|
+
mask_str = "".join(mask_parts)[::-1]
|
|
268
|
+
return f"{pattern_str} 0b{mask_str}"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
FORMATTER_MAP: typing.Dict[SignatureType, SignatureFormatter] = {
|
|
272
|
+
SignatureType.IDA: IdaFormatter(),
|
|
273
|
+
SignatureType.x64Dbg: X64DbgFormatter(),
|
|
274
|
+
SignatureType.Mask: MaskedBytesFormatter(),
|
|
275
|
+
SignatureType.BitMask: ByteArrayBitmaskFormatter(),
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclasses.dataclass(slots=True, frozen=True)
|
|
280
|
+
class WildcardPolicy:
|
|
281
|
+
"""
|
|
282
|
+
Policy for which operand types are wildcardable.
|
|
283
|
+
Stores allowed IDA operand type codes (ints).
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
allowed_types: frozenset[int]
|
|
287
|
+
_ctx = WILDCARD_POLICY_CTX
|
|
288
|
+
|
|
289
|
+
class RarelyWildcardable(enum.IntEnum):
|
|
290
|
+
VOID = idaapi.o_void
|
|
291
|
+
REG = idaapi.o_reg
|
|
292
|
+
|
|
293
|
+
# Base operand types common to all architectures
|
|
294
|
+
class BaseKind(enum.IntEnum):
|
|
295
|
+
MEM = idaapi.o_mem
|
|
296
|
+
PHRASE = idaapi.o_phrase
|
|
297
|
+
DISPL = idaapi.o_displ
|
|
298
|
+
IMM = idaapi.o_imm
|
|
299
|
+
FAR = idaapi.o_far
|
|
300
|
+
NEAR = idaapi.o_near
|
|
301
|
+
|
|
302
|
+
# Architecture-specific operand types
|
|
303
|
+
class X86Kind(enum.IntEnum):
|
|
304
|
+
TRREG = idaapi.o_idpspec0 # Trace register
|
|
305
|
+
DBREG = idaapi.o_idpspec1 # Debug register
|
|
306
|
+
CRREG = idaapi.o_idpspec2 # Control register
|
|
307
|
+
FPREG = idaapi.o_idpspec3 # Floating point register
|
|
308
|
+
MMX = idaapi.o_idpspec4 # MMX register
|
|
309
|
+
XMM = idaapi.o_idpspec5 # XMM register
|
|
310
|
+
YMM = idaapi.o_idpspec5 + 1 # YMM register
|
|
311
|
+
ZMM = idaapi.o_idpspec5 + 2 # ZMM register
|
|
312
|
+
KREG = idaapi.o_idpspec5 + 3 # K register (mask)
|
|
313
|
+
|
|
314
|
+
class ARMKind(enum.IntEnum):
|
|
315
|
+
REGLIST = idaapi.o_idpspec1 # Register list (LDM/STM)
|
|
316
|
+
CREGLIST = idaapi.o_idpspec2 # Coprocessor register list (CDP)
|
|
317
|
+
CREG = idaapi.o_idpspec3 # Coprocessor register (LDC/STC)
|
|
318
|
+
FPREGLIST = idaapi.o_idpspec4 # Floating point register list
|
|
319
|
+
TEXT = idaapi.o_idpspec5 # Arbitrary text
|
|
320
|
+
COND = idaapi.o_idpspec5 + 1 # ARM condition
|
|
321
|
+
|
|
322
|
+
class MIPSKind(enum.IntEnum):
|
|
323
|
+
# MIPS doesn't have specific operand types in the example
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
class PPCKind(enum.IntEnum):
|
|
327
|
+
SPR = idaapi.o_idpspec0 # Special purpose register
|
|
328
|
+
TWOFPR = idaapi.o_idpspec1 # Two FPRs
|
|
329
|
+
SHMBME = idaapi.o_idpspec2 # SH & MB & ME
|
|
330
|
+
CRF = idaapi.o_idpspec3 # CR field
|
|
331
|
+
CRB = idaapi.o_idpspec4 # CR bit
|
|
332
|
+
DCR = idaapi.o_idpspec5 # Device control register
|
|
333
|
+
|
|
334
|
+
# Hoisted context manager class
|
|
335
|
+
@dataclasses.dataclass(slots=True)
|
|
336
|
+
class _Use:
|
|
337
|
+
"""Context manager to temporarily override current policy."""
|
|
338
|
+
|
|
339
|
+
policy: "WildcardPolicy"
|
|
340
|
+
policy_class: type["WildcardPolicy"]
|
|
341
|
+
token: contextvars.Token | None = None
|
|
342
|
+
|
|
343
|
+
def __enter__(self):
|
|
344
|
+
self.token = self.policy_class.set_current(self.policy)
|
|
345
|
+
return self.policy
|
|
346
|
+
|
|
347
|
+
def __exit__(self, exc_type, exc, tb):
|
|
348
|
+
if self.token is not None:
|
|
349
|
+
self.policy_class.reset_current(self.token)
|
|
350
|
+
|
|
351
|
+
# ---- construction helpers ----
|
|
352
|
+
@classmethod
|
|
353
|
+
def for_x86(cls) -> "WildcardPolicy":
|
|
354
|
+
return cls(frozenset(cls.BaseKind) | frozenset(cls.X86Kind))
|
|
355
|
+
|
|
356
|
+
@classmethod
|
|
357
|
+
def for_arm(cls) -> "WildcardPolicy":
|
|
358
|
+
return cls(frozenset(cls.BaseKind) | frozenset(cls.ARMKind))
|
|
359
|
+
|
|
360
|
+
@classmethod
|
|
361
|
+
def for_mips(cls) -> "WildcardPolicy":
|
|
362
|
+
return cls(frozenset({cls.BaseKind.MEM, cls.BaseKind.FAR, cls.BaseKind.NEAR}))
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def for_ppc(cls) -> "WildcardPolicy":
|
|
366
|
+
return cls(frozenset(cls.BaseKind) | frozenset(cls.PPCKind))
|
|
367
|
+
|
|
368
|
+
@classmethod
|
|
369
|
+
def default_generic(cls) -> "WildcardPolicy":
|
|
370
|
+
return cls(frozenset(cls.BaseKind))
|
|
371
|
+
|
|
372
|
+
@classmethod
|
|
373
|
+
def detect_from_processor(cls) -> "WildcardPolicy":
|
|
374
|
+
arch = idaapi.ph_get_id()
|
|
375
|
+
if arch == idaapi.PLFM_386:
|
|
376
|
+
return cls.for_x86()
|
|
377
|
+
if arch == idaapi.PLFM_ARM:
|
|
378
|
+
return cls.for_arm()
|
|
379
|
+
if arch == idaapi.PLFM_MIPS:
|
|
380
|
+
return cls.for_mips()
|
|
381
|
+
if arch == idaapi.PLFM_PPC:
|
|
382
|
+
return cls.for_ppc()
|
|
383
|
+
return cls.default_generic()
|
|
384
|
+
|
|
385
|
+
# ---- queries / adapters ----
|
|
386
|
+
def allows_type(self, op_type: int) -> bool:
|
|
387
|
+
return op_type in self.allowed_types
|
|
388
|
+
|
|
389
|
+
def to_mask(self) -> int:
|
|
390
|
+
"""Compatibility bitmask: 1 << op.type for each allowed type."""
|
|
391
|
+
return sum(1 << int(t) for t in self.allowed_types)
|
|
392
|
+
|
|
393
|
+
@classmethod
|
|
394
|
+
def from_mask(cls, mask: int) -> "WildcardPolicy":
|
|
395
|
+
types = {t for t in range(0, 64) if (mask >> t) & 1}
|
|
396
|
+
return cls(frozenset(types))
|
|
397
|
+
|
|
398
|
+
@classmethod
|
|
399
|
+
def current(cls) -> "WildcardPolicy":
|
|
400
|
+
"""Get current policy (falling back to arch-detected default)."""
|
|
401
|
+
policy = cls._ctx.get(cls.detect_from_processor())
|
|
402
|
+
cls._ctx.set(policy)
|
|
403
|
+
return policy
|
|
404
|
+
|
|
405
|
+
@classmethod
|
|
406
|
+
def set_current(cls, policy: "WildcardPolicy") -> contextvars.Token:
|
|
407
|
+
"""Override current policy (returns token for reset)."""
|
|
408
|
+
return cls._ctx.set(policy)
|
|
409
|
+
|
|
410
|
+
@classmethod
|
|
411
|
+
def reset_current(cls, token: contextvars.Token) -> None:
|
|
412
|
+
cls._ctx.reset(token)
|
|
413
|
+
|
|
414
|
+
@classmethod
|
|
415
|
+
def use(cls, policy: "WildcardPolicy") -> "WildcardPolicy._Use":
|
|
416
|
+
"""Context manager to temporarily override current policy.
|
|
417
|
+
|
|
418
|
+
Example:
|
|
419
|
+
```
|
|
420
|
+
with WildcardPolicy.use(WildcardPolicy.for_x86()):
|
|
421
|
+
sig = SignatureMaker().make_signature(anchor_ea, ctx)
|
|
422
|
+
assert any(b.is_wildcard for b in sig.signature)
|
|
423
|
+
```
|
|
424
|
+
"""
|
|
425
|
+
return cls._Use(policy, cls)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@dataclasses.dataclass(slots=True, frozen=True)
|
|
429
|
+
class GeneratedSignature:
|
|
430
|
+
"""Result container for signature generation operations."""
|
|
431
|
+
|
|
432
|
+
signature: Signature
|
|
433
|
+
address: Match | None = None
|
|
434
|
+
|
|
435
|
+
def display(self, cfg: SigMakerConfig) -> None:
|
|
436
|
+
"""Display the signature result to the user."""
|
|
437
|
+
if not self.signature:
|
|
438
|
+
idaapi.msg("Error: Empty signature\n")
|
|
439
|
+
return
|
|
440
|
+
t = cfg.output_format.value
|
|
441
|
+
fmted = format(self.signature, t)
|
|
442
|
+
if self.address is not None:
|
|
443
|
+
idaapi.msg(f"Signature for {self.address}: {fmted}\n")
|
|
444
|
+
else:
|
|
445
|
+
idaapi.msg(f"Signature: {fmted}\n")
|
|
446
|
+
|
|
447
|
+
if not Clipboard.set_text(fmted):
|
|
448
|
+
idaapi.msg("Failed to copy to clipboard!")
|
|
449
|
+
|
|
450
|
+
def __lt__(self, other) -> bool:
|
|
451
|
+
if not isinstance(other, GeneratedSignature):
|
|
452
|
+
return NotImplemented
|
|
453
|
+
return len(self.signature) < len(other.signature)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@dataclasses.dataclass(slots=True)
|
|
457
|
+
class XrefGeneratedSignature:
|
|
458
|
+
"""Result container for XREF signature finding operations."""
|
|
459
|
+
|
|
460
|
+
signatures: list[GeneratedSignature]
|
|
461
|
+
|
|
462
|
+
def display(self, cfg: SigMakerConfig) -> None:
|
|
463
|
+
"""Display the XREF signatures to the user."""
|
|
464
|
+
if not self.signatures:
|
|
465
|
+
idaapi.msg("No XREFs have been found for your address\n")
|
|
466
|
+
return
|
|
467
|
+
t = cfg.output_format.value
|
|
468
|
+
top_length = min(cfg.print_top_x, len(self.signatures))
|
|
469
|
+
idaapi.msg(
|
|
470
|
+
f"Top {top_length} Signatures out of {len(self.signatures)} xrefs:\n"
|
|
471
|
+
)
|
|
472
|
+
for i, generated_signature in enumerate(self.signatures[:top_length], start=1):
|
|
473
|
+
address = generated_signature.address
|
|
474
|
+
signature = generated_signature.signature
|
|
475
|
+
fmted = format(signature, t)
|
|
476
|
+
idaapi.msg(f"XREF Signature #{i} @ {address}: {fmted}\n")
|
|
477
|
+
if i == 0:
|
|
478
|
+
Clipboard.set_text(fmted)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class SigText:
|
|
482
|
+
"""Signature normalizer with wildcard support ('?' per nibble)."""
|
|
483
|
+
|
|
484
|
+
_HEX_SET = frozenset(string.hexdigits)
|
|
485
|
+
_TRANS = str.maketrans(
|
|
486
|
+
{
|
|
487
|
+
",": " ",
|
|
488
|
+
";": " ",
|
|
489
|
+
":": " ",
|
|
490
|
+
"|": " ",
|
|
491
|
+
"_": " ",
|
|
492
|
+
"-": " ",
|
|
493
|
+
"\t": " ",
|
|
494
|
+
"\n": " ",
|
|
495
|
+
"\r": " ",
|
|
496
|
+
".": "?", # '.' → '?' (optional)
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
@staticmethod
|
|
501
|
+
def _tok_is_hex(s: str) -> bool:
|
|
502
|
+
return len(s) > 0 and all(c in SigText._HEX_SET for c in s)
|
|
503
|
+
|
|
504
|
+
@staticmethod
|
|
505
|
+
def _split_hex_pairs(s: str) -> list[str]:
|
|
506
|
+
# Split an even-length pure-hex string into HH pairs
|
|
507
|
+
return [s[i : i + 2].upper() for i in range(0, len(s), 2)]
|
|
508
|
+
|
|
509
|
+
@staticmethod
|
|
510
|
+
def normalize(sig_str: str) -> tuple[str, list[tuple[int, bool]]]:
|
|
511
|
+
if not sig_str:
|
|
512
|
+
return "", []
|
|
513
|
+
# 1) normalize separators -> spaces; remove 0x prefixes token-wise
|
|
514
|
+
s = sig_str.translate(SigText._TRANS)
|
|
515
|
+
raw = [t for t in s.split() if t]
|
|
516
|
+
toks: list[str] = []
|
|
517
|
+
for t in raw:
|
|
518
|
+
t = t.strip()
|
|
519
|
+
if t.startswith(("0x", "0X")):
|
|
520
|
+
t = t[2:]
|
|
521
|
+
if not t:
|
|
522
|
+
continue
|
|
523
|
+
toks.append(t)
|
|
524
|
+
|
|
525
|
+
out: list[str] = []
|
|
526
|
+
i = 0
|
|
527
|
+
while i < len(toks):
|
|
528
|
+
t = toks[i]
|
|
529
|
+
|
|
530
|
+
# Fast path: canonical tokens we already accept
|
|
531
|
+
if t == "??":
|
|
532
|
+
out.append("??")
|
|
533
|
+
i += 1
|
|
534
|
+
continue
|
|
535
|
+
|
|
536
|
+
if len(t) == 2 and SigText._tok_is_hex(t):
|
|
537
|
+
out.append(t.upper())
|
|
538
|
+
i += 1
|
|
539
|
+
continue
|
|
540
|
+
|
|
541
|
+
# Single hex nibble -> 'H?'
|
|
542
|
+
if len(t) == 1 and t in SigText._HEX_SET:
|
|
543
|
+
out.append((t + "?").upper())
|
|
544
|
+
i += 1
|
|
545
|
+
continue
|
|
546
|
+
|
|
547
|
+
# Single '?'
|
|
548
|
+
if t == "?":
|
|
549
|
+
out.append("??")
|
|
550
|
+
i += 1
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
# Long pure-hex strings (must be even length)
|
|
554
|
+
if SigText._tok_is_hex(t):
|
|
555
|
+
if (len(t) & 1) != 0:
|
|
556
|
+
# odd-length => split into pairs and pad last nibble with '?' as high nibble
|
|
557
|
+
pairs = SigText._split_hex_pairs(t)
|
|
558
|
+
pairs_len = len(pairs)
|
|
559
|
+
# Last pair will be single character, make it '?X'
|
|
560
|
+
if pairs and len(pairs[pairs_len - 1]) == 1:
|
|
561
|
+
pairs[pairs_len - 1] = "?" + pairs[pairs_len - 1]
|
|
562
|
+
out.extend(pairs)
|
|
563
|
+
i += 1
|
|
564
|
+
continue
|
|
565
|
+
else:
|
|
566
|
+
out.extend(SigText._split_hex_pairs(t))
|
|
567
|
+
i += 1
|
|
568
|
+
continue
|
|
569
|
+
|
|
570
|
+
# Mixed 2-char forms with nibble wildcards: '?F', 'F?', '??'
|
|
571
|
+
if len(t) == 2:
|
|
572
|
+
hi, lo = t[0], t[1]
|
|
573
|
+
if (hi in SigText._HEX_SET or hi == "?") and (
|
|
574
|
+
lo in SigText._HEX_SET or lo == "?"
|
|
575
|
+
):
|
|
576
|
+
out.append((hi + lo).upper())
|
|
577
|
+
i += 1
|
|
578
|
+
continue
|
|
579
|
+
|
|
580
|
+
# Unrecognized token format
|
|
581
|
+
raise ValueError(f"invalid signature token: {t!r}")
|
|
582
|
+
|
|
583
|
+
# Build (value, wildcard) list
|
|
584
|
+
pattern: list[tuple[int, bool]] = []
|
|
585
|
+
for tok in out:
|
|
586
|
+
hi, lo = tok[0], tok[1]
|
|
587
|
+
wild = (hi == "?") or (lo == "?")
|
|
588
|
+
hv = 0 if hi == "?" else int(hi, 16)
|
|
589
|
+
lv = 0 if lo == "?" else int(lo, 16)
|
|
590
|
+
pattern.append(((hv << 4) | lv, wild))
|
|
591
|
+
|
|
592
|
+
return " ".join(out), pattern
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class OperandProcessor:
|
|
596
|
+
"""Handles operand processing for signature generation (policy-driven).
|
|
597
|
+
# TODO: refactor this to support more architectures, not just ARM/X64.
|
|
598
|
+
"""
|
|
599
|
+
|
|
600
|
+
def __init__(self):
|
|
601
|
+
self._is_arm = self._check_is_arm()
|
|
602
|
+
|
|
603
|
+
@staticmethod
|
|
604
|
+
def _check_is_arm() -> bool:
|
|
605
|
+
return idaapi.ph_get_id() == idaapi.PLFM_ARM
|
|
606
|
+
|
|
607
|
+
def _get_operand_offset_arm(
|
|
608
|
+
self, ins: idaapi.insn_t, off: typing.List[int], length: typing.List[int]
|
|
609
|
+
) -> bool:
|
|
610
|
+
policy = WildcardPolicy.current()
|
|
611
|
+
for op in ins:
|
|
612
|
+
if op.type in policy.allowed_types:
|
|
613
|
+
off[0] = op.offb
|
|
614
|
+
length[0] = 3 if ins.size == 4 else (7 if ins.size == 8 else 0)
|
|
615
|
+
return True
|
|
616
|
+
return False
|
|
617
|
+
|
|
618
|
+
def get_operand(
|
|
619
|
+
self,
|
|
620
|
+
ins: idaapi.insn_t,
|
|
621
|
+
off: typing.List[int],
|
|
622
|
+
length: typing.List[int],
|
|
623
|
+
wildcard_optimized: bool,
|
|
624
|
+
) -> bool:
|
|
625
|
+
policy = WildcardPolicy.current()
|
|
626
|
+
if self._is_arm:
|
|
627
|
+
return self._get_operand_offset_arm(ins, off, length)
|
|
628
|
+
for op in ins:
|
|
629
|
+
if op.type == idaapi.o_void:
|
|
630
|
+
continue
|
|
631
|
+
if not policy.allows_type(op.type):
|
|
632
|
+
continue
|
|
633
|
+
if op.offb == 0 and not wildcard_optimized:
|
|
634
|
+
continue
|
|
635
|
+
off[0] = op.offb
|
|
636
|
+
length[0] = ins.size - op.offb
|
|
637
|
+
return True
|
|
638
|
+
return False
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class InstructionProcessor:
|
|
642
|
+
"""Processes a single instruction to append its bytes to a signature."""
|
|
643
|
+
|
|
644
|
+
def __init__(self, operand_processor: OperandProcessor):
|
|
645
|
+
self.operand_processor = operand_processor
|
|
646
|
+
|
|
647
|
+
def append_instruction_to_sig(
|
|
648
|
+
self,
|
|
649
|
+
sig: Signature,
|
|
650
|
+
ea: int,
|
|
651
|
+
ins: idaapi.insn_t,
|
|
652
|
+
wildcard_operands: bool,
|
|
653
|
+
wildcard_optimized: bool,
|
|
654
|
+
) -> None:
|
|
655
|
+
"""
|
|
656
|
+
Appends instruction bytes to the signature, optionally wildcarding operands.
|
|
657
|
+
"""
|
|
658
|
+
if not wildcard_operands:
|
|
659
|
+
# Default case: add the whole instruction as-is
|
|
660
|
+
sig.add_bytes_to_signature(ea, ins.size, is_wildcard=False)
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
off, length = [0], [0]
|
|
664
|
+
has_operand = self.operand_processor.get_operand(
|
|
665
|
+
ins, off, length, wildcard_optimized
|
|
666
|
+
)
|
|
667
|
+
if not has_operand or length[0] <= 0:
|
|
668
|
+
sig.add_bytes_to_signature(ea, ins.size, is_wildcard=False)
|
|
669
|
+
return
|
|
670
|
+
|
|
671
|
+
# Add bytes before the operand
|
|
672
|
+
if off[0] > 0:
|
|
673
|
+
sig.add_bytes_to_signature(ea, off[0], is_wildcard=False)
|
|
674
|
+
|
|
675
|
+
# Add the operand as a wildcard
|
|
676
|
+
sig.add_bytes_to_signature(ea + off[0], length[0], is_wildcard=True)
|
|
677
|
+
|
|
678
|
+
# Add bytes after the operand
|
|
679
|
+
remaining_len = ins.size - (off[0] + length[0])
|
|
680
|
+
if remaining_len > 0:
|
|
681
|
+
sig.add_bytes_to_signature(
|
|
682
|
+
ea + off[0] + length[0], remaining_len, is_wildcard=False
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@dataclasses.dataclass(slots=True)
|
|
687
|
+
class InstructionWalker:
|
|
688
|
+
"""
|
|
689
|
+
A stateful iterator for walking instructions within a given address range.
|
|
690
|
+
|
|
691
|
+
This class encapsulates the logic of decoding instructions and tracks the
|
|
692
|
+
current address (cursor), which remains available for inspection after
|
|
693
|
+
the iteration is complete.
|
|
694
|
+
"""
|
|
695
|
+
|
|
696
|
+
start_ea: int
|
|
697
|
+
end_ea: int = idaapi.BADADDR
|
|
698
|
+
|
|
699
|
+
# Internal state fields
|
|
700
|
+
cursor: int = dataclasses.field(init=False)
|
|
701
|
+
_instruction: idaapi.insn_t = dataclasses.field(
|
|
702
|
+
init=False, repr=False, default_factory=idaapi.insn_t
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
def __post_init__(self):
|
|
706
|
+
if self.start_ea == idaapi.BADADDR:
|
|
707
|
+
raise ValueError("Invalid start address for InstructionWalker")
|
|
708
|
+
# Initialize the cursor to the starting address
|
|
709
|
+
self.cursor = self.start_ea
|
|
710
|
+
|
|
711
|
+
def __iter__(self):
|
|
712
|
+
# Reset cursor to allow for re-iteration if needed
|
|
713
|
+
self.cursor = self.start_ea
|
|
714
|
+
return self
|
|
715
|
+
|
|
716
|
+
def __next__(self) -> tuple[int, idaapi.insn_t, int]:
|
|
717
|
+
"""Decodes and returns the next instruction, advancing the cursor."""
|
|
718
|
+
if self.end_ea != idaapi.BADADDR and self.cursor >= self.end_ea:
|
|
719
|
+
raise StopIteration
|
|
720
|
+
|
|
721
|
+
if idaapi.user_cancelled():
|
|
722
|
+
raise StopIteration("Aborted by user")
|
|
723
|
+
|
|
724
|
+
current_instruction_ea = self.cursor
|
|
725
|
+
ins_len = idaapi.decode_insn(self._instruction, current_instruction_ea)
|
|
726
|
+
|
|
727
|
+
if ins_len <= 0:
|
|
728
|
+
raise StopIteration
|
|
729
|
+
|
|
730
|
+
self.cursor += ins_len
|
|
731
|
+
|
|
732
|
+
return current_instruction_ea, self._instruction, ins_len
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
class UniqueSignatureGenerator:
|
|
736
|
+
"""Strategy for generating a signature that is guaranteed to be unique."""
|
|
737
|
+
|
|
738
|
+
def __init__(self, processor: InstructionProcessor):
|
|
739
|
+
self.processor = processor
|
|
740
|
+
|
|
741
|
+
def generate(self, ea: int, cfg: SigMakerConfig) -> Signature:
|
|
742
|
+
if not is_address_marked_as_code(ea):
|
|
743
|
+
raise Unexpected("Cannot create code signature for data")
|
|
744
|
+
|
|
745
|
+
sig = Signature()
|
|
746
|
+
start_fn = idaapi.get_func(ea)
|
|
747
|
+
bytes_since_last_check = 0
|
|
748
|
+
|
|
749
|
+
for cur_ea, ins, ins_len in InstructionWalker(ea):
|
|
750
|
+
# Check length constraint
|
|
751
|
+
if bytes_since_last_check > cfg.max_single_signature_length:
|
|
752
|
+
if (
|
|
753
|
+
not cfg.ask_longer_signature
|
|
754
|
+
or idaapi.ask_yn(
|
|
755
|
+
idaapi.ASKBTN_NO,
|
|
756
|
+
f"Signature is already {len(sig)} bytes. Continue?",
|
|
757
|
+
)
|
|
758
|
+
!= idaapi.ASKBTN_YES
|
|
759
|
+
):
|
|
760
|
+
raise Unexpected("Signature not unique within length constraints")
|
|
761
|
+
bytes_since_last_check = 0 # Reset counter after user confirmation
|
|
762
|
+
|
|
763
|
+
# Check function boundary constraint
|
|
764
|
+
if (
|
|
765
|
+
not cfg.continue_outside_of_function
|
|
766
|
+
and start_fn
|
|
767
|
+
and cur_ea >= start_fn.end_ea
|
|
768
|
+
):
|
|
769
|
+
raise Unexpected("Signature left function scope without being unique")
|
|
770
|
+
|
|
771
|
+
self.processor.append_instruction_to_sig(
|
|
772
|
+
sig, cur_ea, ins, cfg.wildcard_operands, cfg.wildcard_optimized
|
|
773
|
+
)
|
|
774
|
+
bytes_since_last_check += ins_len
|
|
775
|
+
|
|
776
|
+
if SignatureSearcher.is_unique(f"{sig:ida}"):
|
|
777
|
+
sig.trim_signature()
|
|
778
|
+
return sig
|
|
779
|
+
|
|
780
|
+
raise Unexpected("Signature not unique (reached end of analysis)")
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
class RangeSignatureGenerator:
|
|
784
|
+
"""Strategy for generating a signature for a fixed address range."""
|
|
785
|
+
|
|
786
|
+
def __init__(self, processor: InstructionProcessor):
|
|
787
|
+
self.processor = processor
|
|
788
|
+
|
|
789
|
+
def generate(self, start_ea: int, end_ea: int, cfg: SigMakerConfig) -> Signature:
|
|
790
|
+
sig = Signature()
|
|
791
|
+
|
|
792
|
+
# Handle pure data ranges
|
|
793
|
+
if not is_address_marked_as_code(start_ea):
|
|
794
|
+
sig.add_bytes_to_signature(start_ea, end_ea - start_ea, is_wildcard=False)
|
|
795
|
+
return sig
|
|
796
|
+
|
|
797
|
+
# Iterate through instructions within the range
|
|
798
|
+
walker = InstructionWalker(start_ea, end_ea)
|
|
799
|
+
for cur_ea, ins, _ in walker:
|
|
800
|
+
self.processor.append_instruction_to_sig(
|
|
801
|
+
sig, cur_ea, ins, cfg.wildcard_operands, cfg.wildcard_optimized
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
# Add any remaining bytes if the last instruction was partially in range
|
|
805
|
+
# or if the range ended in a data block.
|
|
806
|
+
if walker.cursor < end_ea:
|
|
807
|
+
remaining_bytes = end_ea - walker.cursor
|
|
808
|
+
sig.add_bytes_to_signature(
|
|
809
|
+
walker.cursor, remaining_bytes, is_wildcard=False
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
sig.trim_signature()
|
|
813
|
+
return sig
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@dataclasses.dataclass(slots=True)
|
|
817
|
+
class SignatureMaker:
|
|
818
|
+
"""
|
|
819
|
+
Generates unique or range-based signatures.
|
|
820
|
+
"""
|
|
821
|
+
|
|
822
|
+
_operand_processor: OperandProcessor = dataclasses.field(
|
|
823
|
+
default_factory=OperandProcessor
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
# Internal components built from dependencies
|
|
827
|
+
_instruction_processor: InstructionProcessor = dataclasses.field(init=False)
|
|
828
|
+
_unique_generator: UniqueSignatureGenerator = dataclasses.field(init=False)
|
|
829
|
+
_range_generator: RangeSignatureGenerator = dataclasses.field(init=False)
|
|
830
|
+
|
|
831
|
+
def __post_init__(self):
|
|
832
|
+
"""Initialize internal components after the main object is created."""
|
|
833
|
+
self._instruction_processor = InstructionProcessor(self._operand_processor)
|
|
834
|
+
self._unique_generator = UniqueSignatureGenerator(self._instruction_processor)
|
|
835
|
+
self._range_generator = RangeSignatureGenerator(self._instruction_processor)
|
|
836
|
+
|
|
837
|
+
def make_signature(
|
|
838
|
+
self, ea: int | Match, cfg: SigMakerConfig, end: int | None = None
|
|
839
|
+
) -> GeneratedSignature:
|
|
840
|
+
"""
|
|
841
|
+
Creates a signature for a single address (unique) or an address range.
|
|
842
|
+
"""
|
|
843
|
+
start_ea = int(ea)
|
|
844
|
+
if start_ea == idaapi.BADADDR:
|
|
845
|
+
raise Unexpected("Invalid start address")
|
|
846
|
+
|
|
847
|
+
if end is None:
|
|
848
|
+
# Delegate to the unique signature generation strategy
|
|
849
|
+
sig = self._unique_generator.generate(start_ea, cfg)
|
|
850
|
+
return GeneratedSignature(sig, Match(start_ea))
|
|
851
|
+
|
|
852
|
+
if end <= start_ea:
|
|
853
|
+
raise Unexpected("End address must be after start address")
|
|
854
|
+
|
|
855
|
+
# Delegate to the range signature generation strategy
|
|
856
|
+
sig = self._range_generator.generate(start_ea, end, cfg)
|
|
857
|
+
return GeneratedSignature(sig)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
class XrefFinder:
|
|
861
|
+
"""Handles finding and generating signatures for XREF addresses."""
|
|
862
|
+
|
|
863
|
+
def __init__(self):
|
|
864
|
+
self.progress_dialog = ProgressDialog()
|
|
865
|
+
self.signature_maker = SignatureMaker()
|
|
866
|
+
|
|
867
|
+
@classmethod
|
|
868
|
+
def iter_code_xrefs_to(cls, ea: int) -> typing.Iterable[int]:
|
|
869
|
+
"""Yield code xref sources (xb.frm) that point *to* 'ea'."""
|
|
870
|
+
xb = idaapi.xrefblk_t()
|
|
871
|
+
if not xb.first_to(ea, idaapi.XREF_ALL):
|
|
872
|
+
return
|
|
873
|
+
|
|
874
|
+
while True:
|
|
875
|
+
if is_address_marked_as_code(xb.frm):
|
|
876
|
+
yield xb.frm
|
|
877
|
+
if not xb.next_to():
|
|
878
|
+
break
|
|
879
|
+
|
|
880
|
+
@classmethod
|
|
881
|
+
def count_code_xrefs_to(cls, ea: int) -> int:
|
|
882
|
+
"""Count code xrefs to 'ea' without duplicating traversal logic."""
|
|
883
|
+
return sum(1 for _ in cls.iter_code_xrefs_to(ea))
|
|
884
|
+
|
|
885
|
+
def find_xrefs(self, ea: int, cfg: SigMakerConfig) -> XrefGeneratedSignature:
|
|
886
|
+
"""Find XREF signatures to a given address."""
|
|
887
|
+
xref_signatures: list[GeneratedSignature] = []
|
|
888
|
+
|
|
889
|
+
total = self.count_code_xrefs_to(ea)
|
|
890
|
+
if total == 0:
|
|
891
|
+
return XrefGeneratedSignature([])
|
|
892
|
+
|
|
893
|
+
# Non-interactive during xref search
|
|
894
|
+
cfg_no_prompt = dataclasses.replace(cfg, ask_longer_signature=False)
|
|
895
|
+
|
|
896
|
+
shortest_len = cfg.max_xref_signature_length + 1
|
|
897
|
+
|
|
898
|
+
for i, frm_ea in enumerate(self.iter_code_xrefs_to(ea), start=1):
|
|
899
|
+
if self.progress_dialog.user_canceled():
|
|
900
|
+
break
|
|
901
|
+
|
|
902
|
+
self.progress_dialog.replace_message(
|
|
903
|
+
f"Processing xref {i} of {total} ({(i / total) * 100.0:.1f}%)...\n\n"
|
|
904
|
+
f"Suitable Signatures: {len(xref_signatures)}\n"
|
|
905
|
+
f"Shortest Signature: {shortest_len if shortest_len <= cfg.max_xref_signature_length else 0} Bytes"
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
try:
|
|
909
|
+
# Public API: returns SignatureResult
|
|
910
|
+
result = self.signature_maker.make_signature(frm_ea, cfg_no_prompt)
|
|
911
|
+
sig: typing.Optional[Signature] = result.signature
|
|
912
|
+
except Exception:
|
|
913
|
+
sig = None
|
|
914
|
+
|
|
915
|
+
if sig is None:
|
|
916
|
+
continue
|
|
917
|
+
|
|
918
|
+
if len(sig) < shortest_len:
|
|
919
|
+
shortest_len = len(sig)
|
|
920
|
+
xref_signatures.append(GeneratedSignature(sig, Match(frm_ea)))
|
|
921
|
+
|
|
922
|
+
xref_signatures.sort()
|
|
923
|
+
return XrefGeneratedSignature(xref_signatures)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
@dataclasses.dataclass(slots=True)
|
|
927
|
+
class SearchResults:
|
|
928
|
+
"""Result container for signature search operations."""
|
|
929
|
+
|
|
930
|
+
matches: list[Match]
|
|
931
|
+
signature_str: str
|
|
932
|
+
|
|
933
|
+
def display(self) -> None:
|
|
934
|
+
"""Display the search results to the user."""
|
|
935
|
+
idaapi.msg(f"Signature: {self.signature_str}\n")
|
|
936
|
+
|
|
937
|
+
if not self.matches:
|
|
938
|
+
idaapi.msg("Signature does not match!\n")
|
|
939
|
+
return
|
|
940
|
+
|
|
941
|
+
for ea in self.matches:
|
|
942
|
+
fn_name = None
|
|
943
|
+
with contextlib.suppress(BaseException):
|
|
944
|
+
fn_name = idaapi.get_func_name(int(ea))
|
|
945
|
+
if fn_name:
|
|
946
|
+
idaapi.msg(f"Match @ {ea} in {fn_name}\n")
|
|
947
|
+
else:
|
|
948
|
+
idaapi.msg(f"Match @ {ea}\n")
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
class SignatureParser:
|
|
952
|
+
"""Centralized, readable parsing for various signature input styles.
|
|
953
|
+
|
|
954
|
+
Supported inputs (examples):
|
|
955
|
+
- Mask notation: bytes + mask string like "xxxx?x" or binary mask "0b10101"
|
|
956
|
+
- Hex escapes: "\x48\x8b\x05 ..."
|
|
957
|
+
- 0x-prefixed run: "0x48 0x8B 0x05 ..." or "0x488B05..."
|
|
958
|
+
- Loose hex: "48 8B 05 ? ? 00"
|
|
959
|
+
|
|
960
|
+
Output is an IDA-style signature string (space-separated; '?' for wildcards),
|
|
961
|
+
or an empty string on failure.
|
|
962
|
+
"""
|
|
963
|
+
|
|
964
|
+
_HEX_PAIR = re.compile(r"^[0-9A-Fa-f]{2}$")
|
|
965
|
+
_ESCAPED_HEX = re.compile(r"\\x[0-9A-Fa-f]{2}")
|
|
966
|
+
_RUN_0X = re.compile(r"(?:0x[0-9A-Fa-f]{2})+")
|
|
967
|
+
|
|
968
|
+
# Regex to match a mask string consisting of 'x' and '?' characters, starting with 'x'
|
|
969
|
+
_MASK_REGEX = re.compile(r"x(?:x|\?)+")
|
|
970
|
+
# Regex to match a binary mask string, e.g., '0b10101'
|
|
971
|
+
_BINARY_MASK_REGEX = re.compile(r"0b[01]+")
|
|
972
|
+
|
|
973
|
+
@classmethod
|
|
974
|
+
def parse(cls, input_str: str) -> str:
|
|
975
|
+
mask = cls._extract_mask(input_str)
|
|
976
|
+
parsed = ""
|
|
977
|
+
if mask:
|
|
978
|
+
# Try to pair mask with bytes from either escaped form or 0x run
|
|
979
|
+
bytestr: list[str] = []
|
|
980
|
+
if (bytestr := cls._ESCAPED_HEX.findall(input_str)) and len(bytestr) == len(
|
|
981
|
+
mask
|
|
982
|
+
):
|
|
983
|
+
parsed = cls._masked_bytes_to_ida(bytestr, mask, slice_from=2)
|
|
984
|
+
|
|
985
|
+
elif (bytestr := cls._RUN_0X.findall(input_str)) and len(bytestr) == len(
|
|
986
|
+
mask
|
|
987
|
+
):
|
|
988
|
+
parsed = cls._masked_bytes_to_ida(bytestr, mask, slice_from=2)
|
|
989
|
+
else:
|
|
990
|
+
idaapi.msg(
|
|
991
|
+
f'Detected mask "{mask}" but failed to match corresponding bytes\n'
|
|
992
|
+
)
|
|
993
|
+
else:
|
|
994
|
+
# Fallback: normalize a loose byte string into IDA format
|
|
995
|
+
parsed = cls._normalize_loose_hex(input_str)
|
|
996
|
+
return parsed.strip()
|
|
997
|
+
|
|
998
|
+
# ---- internals ----
|
|
999
|
+
|
|
1000
|
+
@classmethod
|
|
1001
|
+
def _extract_mask(cls, s: str) -> str:
|
|
1002
|
+
"""Extract mask from patterns like 'xxx?x' or binary '0b10101'."""
|
|
1003
|
+
|
|
1004
|
+
m = cls._MASK_REGEX.search(s)
|
|
1005
|
+
if m:
|
|
1006
|
+
return m.group(0)
|
|
1007
|
+
|
|
1008
|
+
m = cls._BINARY_MASK_REGEX.search(s)
|
|
1009
|
+
if not m:
|
|
1010
|
+
return ""
|
|
1011
|
+
bits = m.group(0)[2:]
|
|
1012
|
+
# Binary mask is LSB-first in original code; reverse to align with bytes
|
|
1013
|
+
return "".join("x" if b == "1" else "?" for b in bits[::-1])
|
|
1014
|
+
|
|
1015
|
+
@staticmethod
|
|
1016
|
+
def _masked_bytes_to_ida(
|
|
1017
|
+
byte_tokens: list[str], mask: str, *, slice_from: int
|
|
1018
|
+
) -> str:
|
|
1019
|
+
sig = Signature(
|
|
1020
|
+
[
|
|
1021
|
+
SignatureByte(int(tok[slice_from:], 16), mask[i] == "?")
|
|
1022
|
+
for i, tok in enumerate(byte_tokens)
|
|
1023
|
+
]
|
|
1024
|
+
)
|
|
1025
|
+
return f"{sig:ida}"
|
|
1026
|
+
|
|
1027
|
+
@classmethod
|
|
1028
|
+
def _normalize_loose_hex(cls, input_str: str) -> str:
|
|
1029
|
+
"""Best-effort cleanup into 'AA BB CC ? DD ' format expected by downstream."""
|
|
1030
|
+
s = input_str
|
|
1031
|
+
s = re.sub(r"[\)\(\[\]]+", "", s) # strip brackets
|
|
1032
|
+
s = re.sub(r"^\s+", "", s) # lstrip
|
|
1033
|
+
s = re.sub(r"[? ]+$", "", s) + " " # ensure trailing space
|
|
1034
|
+
s = re.sub(r"\\?\\x", "", s) # drop any stray \x or escaped \x
|
|
1035
|
+
s = re.sub(r"\s+", " ", s) # collapse whitespace
|
|
1036
|
+
|
|
1037
|
+
# Also coerce any '??' or '?' tokens into a single '?' and ensure hex pairs are normalized
|
|
1038
|
+
tokens = [t.strip() for t in s.split() if t.strip()]
|
|
1039
|
+
out: list[str] = []
|
|
1040
|
+
for t in tokens:
|
|
1041
|
+
if t == "?" or t == "??":
|
|
1042
|
+
out.append("?")
|
|
1043
|
+
continue
|
|
1044
|
+
# accept '0xAA' or 'AA'; normalize to two hex chars upper
|
|
1045
|
+
if t.lower().startswith("0x"):
|
|
1046
|
+
t = t[2:]
|
|
1047
|
+
if not cls._HEX_PAIR.match(t):
|
|
1048
|
+
# If it's not a hex pair, treat as wildcard to be safe
|
|
1049
|
+
out.append("?")
|
|
1050
|
+
continue
|
|
1051
|
+
out.append(t.upper())
|
|
1052
|
+
|
|
1053
|
+
return (" ".join(out) + " ") if out else ""
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
@dataclasses.dataclass(slots=True)
|
|
1057
|
+
class SignatureSearcher:
|
|
1058
|
+
"""Parses a signature string and searches the DB for matches."""
|
|
1059
|
+
|
|
1060
|
+
input_signature: str = ""
|
|
1061
|
+
|
|
1062
|
+
@classmethod
|
|
1063
|
+
def from_signature(cls, input_signature: str) -> "SignatureSearcher":
|
|
1064
|
+
return cls(input_signature=input_signature)
|
|
1065
|
+
|
|
1066
|
+
def search(self) -> SearchResults:
|
|
1067
|
+
sig_str = SignatureParser.parse(self.input_signature)
|
|
1068
|
+
if not sig_str:
|
|
1069
|
+
idaapi.msg("Unrecognized signature type\n")
|
|
1070
|
+
return SearchResults([], "")
|
|
1071
|
+
matches = self.find_all(sig_str)
|
|
1072
|
+
return SearchResults(matches, sig_str)
|
|
1073
|
+
|
|
1074
|
+
@staticmethod
|
|
1075
|
+
def find_all(ida_signature: str) -> list[Match]:
|
|
1076
|
+
binary = idaapi.compiled_binpat_vec_t()
|
|
1077
|
+
idaapi.parse_binpat_str(binary, idaapi.inf_get_min_ea(), ida_signature, 16)
|
|
1078
|
+
out: list[Match] = []
|
|
1079
|
+
ea = idaapi.inf_get_min_ea()
|
|
1080
|
+
_bin_search = getattr(idaapi, "bin_search", None) or getattr(
|
|
1081
|
+
idaapi, "bin_search3"
|
|
1082
|
+
)
|
|
1083
|
+
while True:
|
|
1084
|
+
hit, _ = _bin_search(
|
|
1085
|
+
ea,
|
|
1086
|
+
idaapi.inf_get_max_ea(),
|
|
1087
|
+
binary,
|
|
1088
|
+
idaapi.BIN_SEARCH_NOCASE | idaapi.BIN_SEARCH_FORWARD,
|
|
1089
|
+
)
|
|
1090
|
+
if hit == idaapi.BADADDR:
|
|
1091
|
+
break
|
|
1092
|
+
out.append(Match(hit))
|
|
1093
|
+
ea = hit + 1
|
|
1094
|
+
return out
|
|
1095
|
+
|
|
1096
|
+
@classmethod
|
|
1097
|
+
def is_unique(cls, ida_signature: str) -> bool:
|
|
1098
|
+
return len(cls.find_all(ida_signature)) == 1
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
# no cover: start
|
|
1102
|
+
# we do not cover the below because this is mainly executing IDA GUI functionality.
|
|
1103
|
+
# any logic here should be pulled out into a separate class and tested separately.
|
|
1104
|
+
class ProgressDialog:
|
|
1105
|
+
"""Context manager wrapping IDA wait boxes.
|
|
1106
|
+
|
|
1107
|
+
When used as a context manager the progress dialog will display a wait box
|
|
1108
|
+
on entry and hide it on exit.
|
|
1109
|
+
|
|
1110
|
+
The message may be updated via `replace_message()` and cancelation can be tested with `user_canceled()` from this class or `idaapi.user_cancelled()` from IDA API.
|
|
1111
|
+
"""
|
|
1112
|
+
|
|
1113
|
+
def __init__(self, message: str = "Please wait...", hide_cancel: bool = False):
|
|
1114
|
+
self._default_msg: str = message
|
|
1115
|
+
self.hide_cancel: bool = hide_cancel
|
|
1116
|
+
|
|
1117
|
+
def _message(
|
|
1118
|
+
self,
|
|
1119
|
+
message: typing.Optional[str] = None,
|
|
1120
|
+
hide_cancel: typing.Optional[bool] = None,
|
|
1121
|
+
) -> str:
|
|
1122
|
+
"""Internal helper to assemble the full wait box message string."""
|
|
1123
|
+
display_msg = self._default_msg if message is None else message
|
|
1124
|
+
hide = self.hide_cancel if hide_cancel is None else hide_cancel
|
|
1125
|
+
prefix = "HIDECANCEL\n" if hide else ""
|
|
1126
|
+
return prefix + display_msg
|
|
1127
|
+
|
|
1128
|
+
def configure(
|
|
1129
|
+
self, message: str = "Please wait...", hide_cancel: bool = False
|
|
1130
|
+
) -> "ProgressDialog":
|
|
1131
|
+
"""Configure the default message and cancel button visibility."""
|
|
1132
|
+
self._default_msg = message
|
|
1133
|
+
self.hide_cancel = hide_cancel
|
|
1134
|
+
return self
|
|
1135
|
+
|
|
1136
|
+
__call__ = configure # Allow calling instance to reconfigure.
|
|
1137
|
+
|
|
1138
|
+
def __enter__(self) -> "ProgressDialog":
|
|
1139
|
+
idaapi.show_wait_box(self._message())
|
|
1140
|
+
return self
|
|
1141
|
+
|
|
1142
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
1143
|
+
idaapi.hide_wait_box()
|
|
1144
|
+
|
|
1145
|
+
def replace_message(self, new_message: str, hide_cancel: bool = False) -> None:
|
|
1146
|
+
"""Replace the currently displayed message."""
|
|
1147
|
+
msg = self._message(message=new_message, hide_cancel=hide_cancel)
|
|
1148
|
+
idaapi.replace_wait_box(msg)
|
|
1149
|
+
|
|
1150
|
+
def user_canceled(self) -> bool:
|
|
1151
|
+
"""Return True if the user has canceled the wait box."""
|
|
1152
|
+
return idaapi.user_cancelled()
|
|
1153
|
+
|
|
1154
|
+
# Provide alias with alternative spelling for backwards compatibility.
|
|
1155
|
+
user_cancelled = user_canceled
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
class Clipboard:
|
|
1159
|
+
"""Cross platform utilities for setting text on the system clipboard."""
|
|
1160
|
+
|
|
1161
|
+
@staticmethod
|
|
1162
|
+
def _set_text_pyqt5(text: str) -> bool:
|
|
1163
|
+
"""Set clipboard text via PyQt5 if available."""
|
|
1164
|
+
try:
|
|
1165
|
+
from PyQt5.QtWidgets import QApplication # type: ignore
|
|
1166
|
+
|
|
1167
|
+
QApplication.clipboard().setText(text)
|
|
1168
|
+
return True
|
|
1169
|
+
except (ImportError, Exception) as e:
|
|
1170
|
+
idaapi.msg(f"Error setting clipboard text: {e}")
|
|
1171
|
+
return False
|
|
1172
|
+
|
|
1173
|
+
@classmethod
|
|
1174
|
+
def set_text(cls, text: str) -> bool:
|
|
1175
|
+
"""Set the clipboard text on the current operating system.
|
|
1176
|
+
|
|
1177
|
+
This method first attempts to use PyQt5 for cross-platform clipboard
|
|
1178
|
+
support and falls back to platform specific implementations.
|
|
1179
|
+
|
|
1180
|
+
Parameters
|
|
1181
|
+
----------
|
|
1182
|
+
text : str
|
|
1183
|
+
The text to place on the clipboard.
|
|
1184
|
+
|
|
1185
|
+
Returns
|
|
1186
|
+
-------
|
|
1187
|
+
bool
|
|
1188
|
+
True on success, False on failure.
|
|
1189
|
+
"""
|
|
1190
|
+
return cls._set_text_pyqt5(text)
|
|
1191
|
+
|
|
1192
|
+
def __call__(self, text: str) -> bool:
|
|
1193
|
+
"""Allow instances to be invoked directly as a function."""
|
|
1194
|
+
return self.set_text(text)
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
class ConfigureOperandWildcardBitmaskForm(idaapi.Form):
|
|
1198
|
+
"""Interactive form to configure wildcardable operands using checkboxes."""
|
|
1199
|
+
|
|
1200
|
+
def __init__(self) -> None:
|
|
1201
|
+
F = idaapi.Form
|
|
1202
|
+
# Define the form layout
|
|
1203
|
+
form_text = """BUTTON YES* OK
|
|
1204
|
+
BUTTON CANCEL Cancel
|
|
1205
|
+
Wildcardable Operands
|
|
1206
|
+
{FormChangeCb}
|
|
1207
|
+
Select operand types that should be wildcarded:
|
|
1208
|
+
|
|
1209
|
+
<General Register (al, ax, es, ds...):{opt1}>
|
|
1210
|
+
<Direct Memory Reference (DATA) :{opt2}>
|
|
1211
|
+
<Memory Ref [Base Reg + Index Reg] :{opt3}>
|
|
1212
|
+
<Memory Ref [Base Reg + Index Reg + Displacement] :{opt4}>
|
|
1213
|
+
<Immediate Value :{opt5}>
|
|
1214
|
+
<Immediate Far Address (CODE) :{opt6}>
|
|
1215
|
+
<Immediate Near Address (CODE) :{opt7}>"""
|
|
1216
|
+
registers: typing.List[str] = [
|
|
1217
|
+
"opt1",
|
|
1218
|
+
"opt2",
|
|
1219
|
+
"opt3",
|
|
1220
|
+
"opt4",
|
|
1221
|
+
"opt5",
|
|
1222
|
+
"opt6",
|
|
1223
|
+
"opt7",
|
|
1224
|
+
]
|
|
1225
|
+
|
|
1226
|
+
# Processor-specific operand types
|
|
1227
|
+
proc_arch = idaapi.ph_get_id()
|
|
1228
|
+
if proc_arch == idaapi.PLFM_386:
|
|
1229
|
+
form_text += """
|
|
1230
|
+
<Trace Register :{opt8}>
|
|
1231
|
+
<Debug Register :{opt9}>
|
|
1232
|
+
<Control Register :{opt10}>
|
|
1233
|
+
<Floating Point Register :{opt11}>
|
|
1234
|
+
<MMX Register :{opt12}>
|
|
1235
|
+
<XMM Register :{opt13}>
|
|
1236
|
+
<YMM Register :{opt14}>
|
|
1237
|
+
<ZMM Register :{opt15}>
|
|
1238
|
+
<Opmask Register :{opt16}>{cWildcardableOperands}>"""
|
|
1239
|
+
registers.extend(
|
|
1240
|
+
[
|
|
1241
|
+
"opt8",
|
|
1242
|
+
"opt9",
|
|
1243
|
+
"opt10",
|
|
1244
|
+
"opt11",
|
|
1245
|
+
"opt12",
|
|
1246
|
+
"opt13",
|
|
1247
|
+
"opt14",
|
|
1248
|
+
"opt15",
|
|
1249
|
+
"opt16",
|
|
1250
|
+
]
|
|
1251
|
+
)
|
|
1252
|
+
elif proc_arch == idaapi.PLFM_ARM:
|
|
1253
|
+
form_text += """
|
|
1254
|
+
<(Unused) :{opt8}>
|
|
1255
|
+
<Register list (for LDM/STM) :{opt9}>
|
|
1256
|
+
<Coprocessor register list (for CDP) :{opt10}>
|
|
1257
|
+
<Coprocessor register (for LDC/STC) :{opt11}>
|
|
1258
|
+
<Floating point register list :{opt12}>
|
|
1259
|
+
<Arbitrary text stored in the operand :{opt13}>
|
|
1260
|
+
<ARM condition as an operand :{opt14}>{cWildcardableOperands}>"""
|
|
1261
|
+
registers.extend(
|
|
1262
|
+
["opt8", "opt9", "opt10", "opt11", "opt12", "opt13", "opt14"]
|
|
1263
|
+
)
|
|
1264
|
+
elif proc_arch == idaapi.PLFM_PPC:
|
|
1265
|
+
form_text += """
|
|
1266
|
+
<Special purpose register :{opt8}>
|
|
1267
|
+
<Two FPRs :{opt9}>
|
|
1268
|
+
<SH & MB & ME :{opt10}>
|
|
1269
|
+
<crfield :{opt11}>
|
|
1270
|
+
<crbit :{opt12}>
|
|
1271
|
+
<Device control register :{opt13}>{cWildcardableOperands}>"""
|
|
1272
|
+
registers.extend(["opt8", "opt9", "opt10", "opt11", "opt12", "opt13"])
|
|
1273
|
+
else:
|
|
1274
|
+
form_text += """{cWildcardableOperands}>
|
|
1275
|
+
"""
|
|
1276
|
+
# Skip o_void visually (>>1) by shifting the bitmask
|
|
1277
|
+
options = WildcardPolicy.current().to_mask() >> 1
|
|
1278
|
+
|
|
1279
|
+
controls = {
|
|
1280
|
+
"FormChangeCb": F.FormChangeCb(self.OnFormChange),
|
|
1281
|
+
"cWildcardableOperands": F.ChkGroupControl(
|
|
1282
|
+
tuple(registers),
|
|
1283
|
+
value=options,
|
|
1284
|
+
),
|
|
1285
|
+
}
|
|
1286
|
+
super().__init__(form_text, controls)
|
|
1287
|
+
|
|
1288
|
+
def OnFormChange(self, fid: int) -> int:
|
|
1289
|
+
"""Callback invoked when the form state changes."""
|
|
1290
|
+
if fid == self.cWildcardableOperands.id: # type: ignore
|
|
1291
|
+
# re-shift b/c we skipped o_void
|
|
1292
|
+
mask = self.GetControlValue(self.cWildcardableOperands) << 1 # type: ignore
|
|
1293
|
+
WildcardPolicy.set_current(WildcardPolicy.from_mask(mask))
|
|
1294
|
+
return 1
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
class ConfigureOptionsForm(idaapi.Form):
|
|
1298
|
+
"""Interactive form to configure XREF and signature generation options."""
|
|
1299
|
+
|
|
1300
|
+
def __init__(self) -> None:
|
|
1301
|
+
F = idaapi.Form
|
|
1302
|
+
|
|
1303
|
+
# Define the form layout
|
|
1304
|
+
form_text = """BUTTON YES* OK
|
|
1305
|
+
BUTTON CANCEL Cancel
|
|
1306
|
+
Options
|
|
1307
|
+
|
|
1308
|
+
<#Print top X shortest signatures when generating xref signatures#Print top X XREF signatures :{opt1}>
|
|
1309
|
+
<#Stop after reaching X bytes when generating a single signature#Maximum single signature length :{opt2}>
|
|
1310
|
+
<#Stop after reaching X bytes when generating xref signatures#Maximum xref signature length :{opt3}>
|
|
1311
|
+
"""
|
|
1312
|
+
|
|
1313
|
+
self.controls = {
|
|
1314
|
+
"opt1": F.NumericInput(tp=F.FT_DEC),
|
|
1315
|
+
"opt2": F.NumericInput(tp=F.FT_DEC),
|
|
1316
|
+
"opt3": F.NumericInput(tp=F.FT_DEC),
|
|
1317
|
+
}
|
|
1318
|
+
super().__init__(form_text, self.controls)
|
|
1319
|
+
|
|
1320
|
+
def ExecuteForm(self) -> int:
|
|
1321
|
+
"""Execute the form and apply changes to global variables."""
|
|
1322
|
+
|
|
1323
|
+
# Pre-fill form values
|
|
1324
|
+
self.controls["opt1"].value = SigMakerConfig.print_top_x
|
|
1325
|
+
self.controls["opt2"].value = SigMakerConfig.max_single_signature_length
|
|
1326
|
+
self.controls["opt3"].value = SigMakerConfig.max_xref_signature_length
|
|
1327
|
+
|
|
1328
|
+
result = self.Execute()
|
|
1329
|
+
if result != 1:
|
|
1330
|
+
self.Free()
|
|
1331
|
+
return result
|
|
1332
|
+
|
|
1333
|
+
SigMakerConfig.print_top_x = self.controls["opt1"].value
|
|
1334
|
+
SigMakerConfig.max_single_signature_length = self.controls["opt2"].value
|
|
1335
|
+
SigMakerConfig.max_xref_signature_length = self.controls["opt3"].value
|
|
1336
|
+
self.Free()
|
|
1337
|
+
return result
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
class SignatureMakerForm(idaapi.Form):
|
|
1341
|
+
"""Main form presented when the user invokes the SigMaker plugin."""
|
|
1342
|
+
|
|
1343
|
+
def __init__(self) -> None:
|
|
1344
|
+
F = idaapi.Form
|
|
1345
|
+
form_text = (
|
|
1346
|
+
f"""STARTITEM 0
|
|
1347
|
+
BUTTON YES* OK
|
|
1348
|
+
BUTTON CANCEL Cancel
|
|
1349
|
+
Signature Maker v{PLUGIN_VERSION}"""
|
|
1350
|
+
+ r"""
|
|
1351
|
+
{FormChangeCb}
|
|
1352
|
+
Select action:
|
|
1353
|
+
<#Select an address, and create a code signature for it#Create unique signature for current code address:{rCreateUniqueSig}>
|
|
1354
|
+
<#Select an address or variable, and create code signatures for its references. Will output the shortest 5 signatures#Find shortest XREF signature for current data or code address:{rFindXRefSig}>
|
|
1355
|
+
<#Select 1+ instructions, and copy the bytes using the specified output format#Copy selected code:{rCopyCode}>
|
|
1356
|
+
<#Paste any string containing your signature/mask and find matches#Search for a signature:{rSearchSignature}>{rAction}>
|
|
1357
|
+
|
|
1358
|
+
Output format:
|
|
1359
|
+
<#Example - E8 ? ? ? ? 45 33 F6 66 44 89 34 33#IDA Signature:{rIDASig}>
|
|
1360
|
+
<#Example - E8 ?? ?? ?? ?? 45 33 F6 66 44 89 34 33#x64Dbg Signature:{rx64DbgSig}>
|
|
1361
|
+
<#Example - \\\xE8\\\x00\\\x00\\\x00\\\x00\\\x45\\\x33\\\xF6\\\x66\\\x44\\\x89\\\x34\\\x33 x????xxxxxxxx#C Byte Array String Signature + String mask:{rByteArrayMaskSig}>
|
|
1362
|
+
<#Example - 0xE8, 0x00, 0x00, 0x00, 0x00, 0x45, 0x33, 0xF6, 0x66, 0x44, 0x89, 0x34, 0x33 0b1111111100001#C Bytes Signature + Bitmask:{rRawBytesBitmaskSig}>{rOutputFormat}>
|
|
1363
|
+
|
|
1364
|
+
Quick Options:
|
|
1365
|
+
<#Enable wildcarding for operands, to improve stability of created signatures#Wildcards for operands:{cWildcardOperands}>
|
|
1366
|
+
<#Don't stop signature generation when reaching end of function#Continue when leaving function scope:{cContinueOutside}>
|
|
1367
|
+
<#Wildcard the whole instruction when the operand (usually a register) is encoded into the operator#Wildcard optimized / combined instructions:{cWildcardOptimized}>{cGroupOptions}>
|
|
1368
|
+
|
|
1369
|
+
<Operand types...:{bOperandTypes}><Other options...:{bOtherOptions}>
|
|
1370
|
+
"""
|
|
1371
|
+
)
|
|
1372
|
+
controls = {
|
|
1373
|
+
"cVersion": F.StringLabel(PLUGIN_VERSION),
|
|
1374
|
+
"FormChangeCb": F.FormChangeCb(self.OnFormChange),
|
|
1375
|
+
"rAction": F.RadGroupControl(
|
|
1376
|
+
("rCreateUniqueSig", "rFindXRefSig", "rCopyCode", "rSearchSignature")
|
|
1377
|
+
),
|
|
1378
|
+
"rOutputFormat": F.RadGroupControl(
|
|
1379
|
+
("rIDASig", "rx64DbgSig", "rByteArrayMaskSig", "rRawBytesBitmaskSig")
|
|
1380
|
+
),
|
|
1381
|
+
"cGroupOptions": idaapi.Form.ChkGroupControl(
|
|
1382
|
+
("cWildcardOperands", "cContinueOutside", "cWildcardOptimized"),
|
|
1383
|
+
value=5,
|
|
1384
|
+
),
|
|
1385
|
+
"bOperandTypes": F.ButtonInput(self.ConfigureOperandWildcardBitmask),
|
|
1386
|
+
"bOtherOptions": F.ButtonInput(self.ConfigureOptions),
|
|
1387
|
+
}
|
|
1388
|
+
super().__init__(form_text, controls)
|
|
1389
|
+
|
|
1390
|
+
def OnFormChange(self, fid: int) -> int:
|
|
1391
|
+
"""Optional form change handler; currently unused."""
|
|
1392
|
+
return 1
|
|
1393
|
+
|
|
1394
|
+
def ConfigureOperandWildcardBitmask(self, code: int = 0) -> int:
|
|
1395
|
+
form = ConfigureOperandWildcardBitmaskForm()
|
|
1396
|
+
form.Compile()
|
|
1397
|
+
ok = form.Execute()
|
|
1398
|
+
if not ok:
|
|
1399
|
+
return 0
|
|
1400
|
+
return 1
|
|
1401
|
+
|
|
1402
|
+
def ConfigureOptions(self, code: int = 0) -> int:
|
|
1403
|
+
"""Launch the options configuration form."""
|
|
1404
|
+
form = ConfigureOptionsForm()
|
|
1405
|
+
form.Compile()
|
|
1406
|
+
return form.ExecuteForm()
|
|
1407
|
+
|
|
1408
|
+
def __enter__(self) -> "SignatureMakerForm":
|
|
1409
|
+
return self
|
|
1410
|
+
|
|
1411
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
1412
|
+
self.Free()
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
class _ActionHandler(idaapi.action_handler_t):
|
|
1416
|
+
"""Internal helper bridging IDA UI actions to plugin methods."""
|
|
1417
|
+
|
|
1418
|
+
def __init__(self, action_function):
|
|
1419
|
+
super().__init__()
|
|
1420
|
+
self.action_function = action_function
|
|
1421
|
+
|
|
1422
|
+
def activate(self, ctx: idaapi.action_ctx_base_t) -> int:
|
|
1423
|
+
self.action_function(ctx=ctx)
|
|
1424
|
+
return 1
|
|
1425
|
+
|
|
1426
|
+
def update(self, ctx: idaapi.action_ctx_base_t) -> int:
|
|
1427
|
+
if ctx.widget_type == idaapi.BWN_DISASM:
|
|
1428
|
+
return idaapi.AST_ENABLE_FOR_WIDGET
|
|
1429
|
+
return idaapi.AST_DISABLE_FOR_WIDGET
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
class _PopupHook(idaapi.UI_Hooks):
|
|
1433
|
+
"""Hook used to attach actions to IDA pop-ups."""
|
|
1434
|
+
|
|
1435
|
+
def __init__(
|
|
1436
|
+
self,
|
|
1437
|
+
action_name: str,
|
|
1438
|
+
predicate=None,
|
|
1439
|
+
widget_populator=None,
|
|
1440
|
+
category: typing.Optional[str] = None,
|
|
1441
|
+
) -> None:
|
|
1442
|
+
super().__init__()
|
|
1443
|
+
self.action_name = action_name
|
|
1444
|
+
self.predicate = predicate or self.is_disassembly_widget
|
|
1445
|
+
self.widget_populator = widget_populator or self._default_populator
|
|
1446
|
+
self.category = category
|
|
1447
|
+
|
|
1448
|
+
@classmethod
|
|
1449
|
+
def is_disassembly_widget(cls, widget, popup, ctx) -> bool:
|
|
1450
|
+
"""Return True if the given widget is a disassembly view."""
|
|
1451
|
+
return idaapi.get_widget_type(widget) == idaapi.BWN_DISASM
|
|
1452
|
+
|
|
1453
|
+
def term(self) -> None:
|
|
1454
|
+
idaapi.unregister_action(self.action_name)
|
|
1455
|
+
|
|
1456
|
+
@staticmethod
|
|
1457
|
+
def _default_populator(instance, widget, popup_handle, ctx) -> None:
|
|
1458
|
+
if instance.predicate(widget, popup_handle, ctx):
|
|
1459
|
+
args = [widget, popup_handle, instance.action_name]
|
|
1460
|
+
if instance.category:
|
|
1461
|
+
args.append(f"{instance.category}/")
|
|
1462
|
+
idaapi.attach_action_to_popup(*args)
|
|
1463
|
+
|
|
1464
|
+
def finish_populating_widget_popup(self, widget, popup_handle, ctx=None) -> None:
|
|
1465
|
+
return self.widget_populator(self, widget, popup_handle, ctx)
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
class SigMakerPlugin(idaapi.plugin_t):
|
|
1469
|
+
"""IDA Pro plugin class implementing signature generation and search."""
|
|
1470
|
+
|
|
1471
|
+
flags = idaapi.PLUGIN_KEEP
|
|
1472
|
+
comment = f"{PLUGIN_NAME} v{PLUGIN_VERSION} for IDA Pro by {PLUGIN_AUTHOR}"
|
|
1473
|
+
help = "Select location in disassembly and press CTRL+ALT+S to open menu"
|
|
1474
|
+
wanted_name = PLUGIN_NAME
|
|
1475
|
+
wanted_hotkey = "Ctrl-Alt-S"
|
|
1476
|
+
|
|
1477
|
+
ACTION_SHOW_SIGMAKER: str = "pysigmaker:show"
|
|
1478
|
+
|
|
1479
|
+
def init(self) -> int:
|
|
1480
|
+
self._hooks = self._init_hooks(_PopupHook(self.ACTION_SHOW_SIGMAKER))
|
|
1481
|
+
self._register_actions()
|
|
1482
|
+
return idaapi.PLUGIN_KEEP
|
|
1483
|
+
|
|
1484
|
+
def _init_hooks(self, *hooks) -> typing.Tuple[idaapi.UI_Hooks, ...]:
|
|
1485
|
+
for hook in hooks:
|
|
1486
|
+
hook.hook()
|
|
1487
|
+
return hooks
|
|
1488
|
+
|
|
1489
|
+
def _deinit_hooks(self, *hooks) -> None:
|
|
1490
|
+
for hook in hooks:
|
|
1491
|
+
hook.unhook()
|
|
1492
|
+
|
|
1493
|
+
def _register_actions(self) -> None:
|
|
1494
|
+
self._deregister_actions()
|
|
1495
|
+
idaapi.register_action(
|
|
1496
|
+
idaapi.action_desc_t(
|
|
1497
|
+
self.ACTION_SHOW_SIGMAKER,
|
|
1498
|
+
"SigMaker",
|
|
1499
|
+
_ActionHandler(self.run),
|
|
1500
|
+
self.wanted_hotkey,
|
|
1501
|
+
"Show the signature maker dialog.",
|
|
1502
|
+
154,
|
|
1503
|
+
)
|
|
1504
|
+
)
|
|
1505
|
+
|
|
1506
|
+
def _deregister_actions(self) -> None:
|
|
1507
|
+
idaapi.unregister_action(self.ACTION_SHOW_SIGMAKER)
|
|
1508
|
+
|
|
1509
|
+
def run(self, ctx) -> None:
|
|
1510
|
+
"""Entry point called when the user activates the plugin."""
|
|
1511
|
+
with SignatureMakerForm() as form:
|
|
1512
|
+
form.Compile()
|
|
1513
|
+
ok = form.Execute()
|
|
1514
|
+
if not ok:
|
|
1515
|
+
return
|
|
1516
|
+
|
|
1517
|
+
action = form.rAction.value # type: ignore
|
|
1518
|
+
output_format = form.rOutputFormat.value # type: ignore
|
|
1519
|
+
wildcard_operands = bool(form.cGroupOptions.value & 1) # type: ignore
|
|
1520
|
+
continue_outside_of_function = bool(form.cGroupOptions.value & 2) # type: ignore
|
|
1521
|
+
wildcard_optimized = bool(form.cGroupOptions.value & 4) # type: ignore
|
|
1522
|
+
|
|
1523
|
+
# Create SigMakerConfig
|
|
1524
|
+
config = SigMakerConfig(
|
|
1525
|
+
output_format=SignatureType.at(int(output_format)),
|
|
1526
|
+
wildcard_operands=wildcard_operands,
|
|
1527
|
+
continue_outside_of_function=continue_outside_of_function,
|
|
1528
|
+
wildcard_optimized=wildcard_optimized,
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
try:
|
|
1532
|
+
if action == 0:
|
|
1533
|
+
ea = idaapi.get_screen_ea()
|
|
1534
|
+
signature = SignatureMaker().make_signature(ea, config)
|
|
1535
|
+
signature.display(config)
|
|
1536
|
+
elif action == 1:
|
|
1537
|
+
ea = idaapi.get_screen_ea()
|
|
1538
|
+
signatures = XrefFinder().find_xrefs(ea, config)
|
|
1539
|
+
signatures.display(cfg=config)
|
|
1540
|
+
elif action == 2:
|
|
1541
|
+
start, end = self.get_selected_addresses(idaapi.get_current_viewer())
|
|
1542
|
+
if start and end:
|
|
1543
|
+
signature = SignatureMaker().make_signature(start, config, end=end)
|
|
1544
|
+
signature.display(config)
|
|
1545
|
+
else:
|
|
1546
|
+
idaapi.msg("Select a range to copy the code!\n")
|
|
1547
|
+
elif action == 3:
|
|
1548
|
+
input_signature = idaapi.ask_str(
|
|
1549
|
+
"", idaapi.HIST_SRCH, "Enter a signature"
|
|
1550
|
+
)
|
|
1551
|
+
if input_signature:
|
|
1552
|
+
searcher = SignatureSearcher.from_signature(input_signature)
|
|
1553
|
+
results = searcher.search()
|
|
1554
|
+
results.display()
|
|
1555
|
+
else:
|
|
1556
|
+
idaapi.msg("No signature entered!\n")
|
|
1557
|
+
else:
|
|
1558
|
+
idaapi.msg("Invalid action!\n")
|
|
1559
|
+
except Unexpected as e:
|
|
1560
|
+
idaapi.msg(f"Error: {str(e)}\n")
|
|
1561
|
+
except Exception as e:
|
|
1562
|
+
print(e, os.linesep, traceback.format_exc())
|
|
1563
|
+
return
|
|
1564
|
+
|
|
1565
|
+
def term(self) -> None:
|
|
1566
|
+
self._deregister_actions()
|
|
1567
|
+
self._deinit_hooks(*self._hooks)
|
|
1568
|
+
|
|
1569
|
+
@staticmethod
|
|
1570
|
+
def get_selected_addresses(
|
|
1571
|
+
ctx,
|
|
1572
|
+
) -> typing.Tuple[typing.Optional[int], typing.Optional[int]]:
|
|
1573
|
+
"""Return the start and end of the selection or current line."""
|
|
1574
|
+
is_selected, start_ea, end_ea = idaapi.read_range_selection(ctx)
|
|
1575
|
+
if is_selected:
|
|
1576
|
+
return start_ea, end_ea
|
|
1577
|
+
p0, p1 = idaapi.twinpos_t(), idaapi.twinpos_t()
|
|
1578
|
+
idaapi.read_selection(ctx, p0, p1)
|
|
1579
|
+
p0.place(ctx)
|
|
1580
|
+
p1.place(ctx)
|
|
1581
|
+
if p0.at and p1.at:
|
|
1582
|
+
start_ea = p0.at.toea()
|
|
1583
|
+
end_ea = p1.at.toea()
|
|
1584
|
+
if start_ea == end_ea:
|
|
1585
|
+
start_ea = idc.get_item_head(start_ea)
|
|
1586
|
+
end_ea = idc.get_item_end(start_ea)
|
|
1587
|
+
return start_ea, end_ea
|
|
1588
|
+
|
|
1589
|
+
start_ea = idaapi.get_screen_ea()
|
|
1590
|
+
try:
|
|
1591
|
+
end_ea = idaapi.ask_addr(start_ea, "Enter end address for selection:")
|
|
1592
|
+
finally:
|
|
1593
|
+
idaapi.jumpto(start_ea)
|
|
1594
|
+
|
|
1595
|
+
if end_ea and end_ea <= start_ea:
|
|
1596
|
+
print(
|
|
1597
|
+
f"Error: End address 0x{end_ea:X} must be greater than start address 0x{start_ea:X}."
|
|
1598
|
+
)
|
|
1599
|
+
end_ea = None
|
|
1600
|
+
if end_ea is None:
|
|
1601
|
+
end_ea = idc.get_item_end(start_ea)
|
|
1602
|
+
print(f"No end address selected, using line end: 0x{end_ea:X}")
|
|
1603
|
+
|
|
1604
|
+
return start_ea, end_ea
|
|
1605
|
+
|
|
1606
|
+
|
|
1607
|
+
def PLUGIN_ENTRY() -> SigMakerPlugin:
|
|
1608
|
+
"""Entry point function required by IDA Pro to instantiate the plugin."""
|
|
1609
|
+
return SigMakerPlugin()
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
# no cover: stop
|