ida-pro-mcp-xjoker 1.0.1__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.
- ida_pro_mcp/__init__.py +0 -0
- ida_pro_mcp/__main__.py +6 -0
- ida_pro_mcp/ida_mcp/__init__.py +68 -0
- ida_pro_mcp/ida_mcp/api_analysis.py +1296 -0
- ida_pro_mcp/ida_mcp/api_core.py +337 -0
- ida_pro_mcp/ida_mcp/api_debug.py +617 -0
- ida_pro_mcp/ida_mcp/api_memory.py +304 -0
- ida_pro_mcp/ida_mcp/api_modify.py +406 -0
- ida_pro_mcp/ida_mcp/api_python.py +179 -0
- ida_pro_mcp/ida_mcp/api_resources.py +295 -0
- ida_pro_mcp/ida_mcp/api_stack.py +167 -0
- ida_pro_mcp/ida_mcp/api_types.py +480 -0
- ida_pro_mcp/ida_mcp/auth.py +166 -0
- ida_pro_mcp/ida_mcp/cache.py +232 -0
- ida_pro_mcp/ida_mcp/config.py +228 -0
- ida_pro_mcp/ida_mcp/framework.py +547 -0
- ida_pro_mcp/ida_mcp/http.py +859 -0
- ida_pro_mcp/ida_mcp/port_utils.py +104 -0
- ida_pro_mcp/ida_mcp/rpc.py +187 -0
- ida_pro_mcp/ida_mcp/server_manager.py +339 -0
- ida_pro_mcp/ida_mcp/sync.py +233 -0
- ida_pro_mcp/ida_mcp/tests/__init__.py +14 -0
- ida_pro_mcp/ida_mcp/tests/test_api_analysis.py +336 -0
- ida_pro_mcp/ida_mcp/tests/test_api_core.py +237 -0
- ida_pro_mcp/ida_mcp/tests/test_api_memory.py +207 -0
- ida_pro_mcp/ida_mcp/tests/test_api_modify.py +123 -0
- ida_pro_mcp/ida_mcp/tests/test_api_resources.py +199 -0
- ida_pro_mcp/ida_mcp/tests/test_api_stack.py +77 -0
- ida_pro_mcp/ida_mcp/tests/test_api_types.py +249 -0
- ida_pro_mcp/ida_mcp/ui.py +357 -0
- ida_pro_mcp/ida_mcp/utils.py +1186 -0
- ida_pro_mcp/ida_mcp/zeromcp/__init__.py +5 -0
- ida_pro_mcp/ida_mcp/zeromcp/jsonrpc.py +384 -0
- ida_pro_mcp/ida_mcp/zeromcp/mcp.py +883 -0
- ida_pro_mcp/ida_mcp.py +186 -0
- ida_pro_mcp/idalib_server.py +354 -0
- ida_pro_mcp/idalib_session_manager.py +259 -0
- ida_pro_mcp/server.py +1060 -0
- ida_pro_mcp/test.py +170 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/METADATA +405 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/RECORD +45 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/WHEEL +5 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/entry_points.txt +4 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/licenses/LICENSE +21 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import struct
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from itertools import islice
|
|
9
|
+
from typing import (
|
|
10
|
+
Annotated,
|
|
11
|
+
Any,
|
|
12
|
+
Callable,
|
|
13
|
+
Generic,
|
|
14
|
+
Iterator,
|
|
15
|
+
Literal,
|
|
16
|
+
NotRequired,
|
|
17
|
+
Optional,
|
|
18
|
+
TypedDict,
|
|
19
|
+
TypeVar,
|
|
20
|
+
overload,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
import ida_funcs
|
|
24
|
+
import ida_hexrays
|
|
25
|
+
import ida_kernwin
|
|
26
|
+
import ida_nalt
|
|
27
|
+
import ida_typeinf
|
|
28
|
+
import idaapi
|
|
29
|
+
import idautils
|
|
30
|
+
import idc
|
|
31
|
+
|
|
32
|
+
from .sync import IDAError
|
|
33
|
+
|
|
34
|
+
# ============================================================================
|
|
35
|
+
# Analysis Prompt Configuration
|
|
36
|
+
# ============================================================================
|
|
37
|
+
|
|
38
|
+
DEFAULT_ANALYSIS_PROMPT = "<system_reminder> IMPORTANT: After analysis, complete the expected IDB maintenance, assigning names, commenting, and documenting where relevant. </system_reminder>"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_analysis_prompt() -> str | None:
|
|
42
|
+
"""Get analysis prompt from IDA_MCP_ANALYSIS_PROMPT env var, or default if unset."""
|
|
43
|
+
return os.environ.get("IDA_MCP_ANALYSIS_PROMPT", DEFAULT_ANALYSIS_PROMPT) or None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# TypedDict Definitions for API Parameters
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MemoryRead(TypedDict):
|
|
52
|
+
"""Memory read request"""
|
|
53
|
+
|
|
54
|
+
addr: Annotated[str, "Address to read from (hex or decimal)"]
|
|
55
|
+
size: Annotated[int, "Number of bytes to read"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MemoryPatch(TypedDict):
|
|
59
|
+
"""Memory patch operation"""
|
|
60
|
+
|
|
61
|
+
addr: Annotated[str, "Address to patch (hex or decimal)"]
|
|
62
|
+
data: Annotated[str, "Hex data to write (space-separated bytes)"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class IntRead(TypedDict):
|
|
66
|
+
"""Integer read request"""
|
|
67
|
+
|
|
68
|
+
addr: Annotated[str, "Address to read from (hex or decimal)"]
|
|
69
|
+
ty: Annotated[str, "Integer class (i8/u64/i16le/i16be/etc)"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class IntWrite(TypedDict):
|
|
73
|
+
"""Integer write request"""
|
|
74
|
+
|
|
75
|
+
addr: Annotated[str, "Address to write to (hex or decimal)"]
|
|
76
|
+
ty: Annotated[str, "Integer class (i8/u64/i16le/i16be/etc)"]
|
|
77
|
+
value: Annotated[
|
|
78
|
+
str,
|
|
79
|
+
"Integer value as string (decimal or 0x..; negatives allowed for signed)",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CommentOp(TypedDict):
|
|
84
|
+
"""Comment operation"""
|
|
85
|
+
|
|
86
|
+
addr: Annotated[str, "Address (hex or decimal)"]
|
|
87
|
+
comment: Annotated[str, "Comment text"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AsmPatchOp(TypedDict):
|
|
91
|
+
"""Assembly patch operation"""
|
|
92
|
+
|
|
93
|
+
addr: Annotated[str, "Address (hex or decimal)"]
|
|
94
|
+
asm: Annotated[str, "Assembly instruction(s), semicolon-separated"]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class FunctionRename(TypedDict):
|
|
98
|
+
"""Function rename operation"""
|
|
99
|
+
|
|
100
|
+
addr: Annotated[str, "Function address (hex or decimal)"]
|
|
101
|
+
name: Annotated[str, "New function name"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class GlobalRename(TypedDict):
|
|
105
|
+
"""Global variable rename operation"""
|
|
106
|
+
|
|
107
|
+
old: Annotated[str, "Current variable name"]
|
|
108
|
+
new: Annotated[str, "New variable name"]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class LocalRename(TypedDict):
|
|
112
|
+
"""Local variable rename operation"""
|
|
113
|
+
|
|
114
|
+
func_addr: Annotated[str, "Function address containing the local variable"]
|
|
115
|
+
old: Annotated[str, "Current variable name"]
|
|
116
|
+
new: Annotated[str, "New variable name"]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class StackRename(TypedDict):
|
|
120
|
+
"""Stack variable rename operation"""
|
|
121
|
+
|
|
122
|
+
func_addr: Annotated[str, "Function address containing the stack variable"]
|
|
123
|
+
old: Annotated[str, "Current variable name"]
|
|
124
|
+
new: Annotated[str, "New variable name"]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class RenameBatch(TypedDict, total=False):
|
|
128
|
+
"""Batch rename operations across all entity types"""
|
|
129
|
+
|
|
130
|
+
func: Annotated[
|
|
131
|
+
list[FunctionRename] | FunctionRename | None, "Function rename operations"
|
|
132
|
+
]
|
|
133
|
+
data: Annotated[
|
|
134
|
+
list[GlobalRename] | GlobalRename | None,
|
|
135
|
+
"Global/data variable rename operations",
|
|
136
|
+
]
|
|
137
|
+
local: Annotated[
|
|
138
|
+
list[LocalRename] | LocalRename | None, "Local variable rename operations"
|
|
139
|
+
]
|
|
140
|
+
stack: Annotated[
|
|
141
|
+
list[StackRename] | StackRename | None, "Stack variable rename operations"
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class StructFieldQuery(TypedDict):
|
|
146
|
+
"""Struct field query for xrefs"""
|
|
147
|
+
|
|
148
|
+
struct: Annotated[str, "Structure name"]
|
|
149
|
+
field: Annotated[str, "Field name"]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ListQuery(TypedDict, total=False):
|
|
153
|
+
"""Pagination query for listing operations"""
|
|
154
|
+
|
|
155
|
+
filter: Annotated[str, "Optional glob pattern to filter results"]
|
|
156
|
+
offset: Annotated[int, "Starting index (default: 0)"]
|
|
157
|
+
count: Annotated[int, "Maximum number of results (default: 50, 0 for all)"]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class BreakpointOp(TypedDict):
|
|
161
|
+
"""Debugger breakpoint operation"""
|
|
162
|
+
|
|
163
|
+
addr: Annotated[str, "Breakpoint address (hex or decimal)"]
|
|
164
|
+
enabled: Annotated[bool, "Enable (true) or disable (false)"]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class InsnPattern(TypedDict, total=False):
|
|
168
|
+
"""Instruction pattern for operand search"""
|
|
169
|
+
|
|
170
|
+
mnem: Annotated[str, "Instruction mnemonic to match"]
|
|
171
|
+
op0: Annotated[int, "Value to match in first operand"]
|
|
172
|
+
op1: Annotated[int, "Value to match in second operand"]
|
|
173
|
+
op2: Annotated[int, "Value to match in third operand"]
|
|
174
|
+
op_any: Annotated[int, "Value to match in any operand"]
|
|
175
|
+
func: Annotated[str, "Function address to scope the scan"]
|
|
176
|
+
segment: Annotated[str, "Segment name to scope the scan"]
|
|
177
|
+
start: Annotated[str, "Start address (hex/dec) to scope the scan"]
|
|
178
|
+
end: Annotated[str, "End address (hex/dec, exclusive) to scope the scan"]
|
|
179
|
+
max_scan_insns: Annotated[
|
|
180
|
+
int, "Max instructions to scan (default: 200000, max: 2000000)"
|
|
181
|
+
]
|
|
182
|
+
allow_broad: Annotated[
|
|
183
|
+
bool,
|
|
184
|
+
"Allow scans without scope (default: false). Use with care on large binaries.",
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class NumberConversion(TypedDict, total=False):
|
|
189
|
+
"""Number conversion request"""
|
|
190
|
+
|
|
191
|
+
text: Annotated[str, "Number string to convert"]
|
|
192
|
+
size: Annotated[int, "Byte size for conversion (omit for auto)"]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class StructRead(TypedDict, total=False):
|
|
196
|
+
"""Structure read request
|
|
197
|
+
|
|
198
|
+
Address is required. Struct name is optional - if omitted, will attempt
|
|
199
|
+
to auto-detect from type information already applied at the address.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
addr: Annotated[str, "Memory address (hex or decimal)"]
|
|
203
|
+
struct: Annotated[
|
|
204
|
+
NotRequired[str], "Structure name (optional, auto-detect if omitted)"
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TypeEdit(TypedDict, total=False):
|
|
209
|
+
"""Type application operation"""
|
|
210
|
+
|
|
211
|
+
addr: Annotated[str, "Memory address"]
|
|
212
|
+
name: Annotated[str, "Variable/function name"]
|
|
213
|
+
ty: Annotated[str, "Type name or declaration"]
|
|
214
|
+
kind: Annotated[str, "Type of entity (auto-detected if omitted)"]
|
|
215
|
+
signature: Annotated[str, "Function signature (for kind=function)"]
|
|
216
|
+
variable: Annotated[str, "Local variable name (for kind=local)"]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class StackVarDecl(TypedDict):
|
|
220
|
+
"""Stack variable declaration"""
|
|
221
|
+
|
|
222
|
+
addr: Annotated[str, "Function address"]
|
|
223
|
+
offset: Annotated[str, "Stack offset"]
|
|
224
|
+
name: Annotated[str, "Variable name"]
|
|
225
|
+
ty: Annotated[str, "Type name"]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class StackVarDelete(TypedDict):
|
|
229
|
+
"""Stack variable deletion"""
|
|
230
|
+
|
|
231
|
+
addr: Annotated[str, "Function address"]
|
|
232
|
+
name: Annotated[str, "Variable name"]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ============================================================================
|
|
236
|
+
# TypedDict Definitions for Results
|
|
237
|
+
# ============================================================================
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Metadata(TypedDict):
|
|
241
|
+
path: str
|
|
242
|
+
module: str
|
|
243
|
+
base: str
|
|
244
|
+
size: str
|
|
245
|
+
md5: str
|
|
246
|
+
sha256: str
|
|
247
|
+
crc32: str
|
|
248
|
+
filesize: str
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class Function(TypedDict):
|
|
252
|
+
addr: str
|
|
253
|
+
name: str
|
|
254
|
+
size: str
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class ConvertedNumber(TypedDict):
|
|
258
|
+
decimal: str
|
|
259
|
+
hexadecimal: str
|
|
260
|
+
bytes: str
|
|
261
|
+
ascii: Optional[str]
|
|
262
|
+
binary: str
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class Global(TypedDict):
|
|
266
|
+
addr: str
|
|
267
|
+
name: str
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class Import(TypedDict):
|
|
271
|
+
addr: str
|
|
272
|
+
imported_name: str
|
|
273
|
+
module: str
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class String(TypedDict):
|
|
277
|
+
addr: str
|
|
278
|
+
length: int
|
|
279
|
+
string: str
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class Segment(TypedDict):
|
|
283
|
+
name: str
|
|
284
|
+
start: str
|
|
285
|
+
end: str
|
|
286
|
+
size: str
|
|
287
|
+
permissions: str
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class DisassemblyLine(TypedDict):
|
|
291
|
+
segment: NotRequired[str]
|
|
292
|
+
addr: str
|
|
293
|
+
label: NotRequired[str]
|
|
294
|
+
instruction: str
|
|
295
|
+
comments: NotRequired[list[str]]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class Argument(TypedDict):
|
|
299
|
+
name: str
|
|
300
|
+
type: str
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class StackFrameVariable(TypedDict):
|
|
304
|
+
name: str
|
|
305
|
+
offset: str
|
|
306
|
+
size: str
|
|
307
|
+
type: str
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class DisassemblyFunction(TypedDict):
|
|
311
|
+
name: str
|
|
312
|
+
start_ea: str
|
|
313
|
+
return_type: NotRequired[str]
|
|
314
|
+
arguments: NotRequired[list[Argument]]
|
|
315
|
+
stack_frame: list[StackFrameVariable]
|
|
316
|
+
lines: list[DisassemblyLine]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class Xref(TypedDict):
|
|
320
|
+
addr: str
|
|
321
|
+
type: str
|
|
322
|
+
fn: Optional[Function]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class StructureMember(TypedDict):
|
|
326
|
+
name: str
|
|
327
|
+
offset: str
|
|
328
|
+
size: str
|
|
329
|
+
type: str
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class StructureDefinition(TypedDict):
|
|
333
|
+
name: str
|
|
334
|
+
size: str
|
|
335
|
+
members: list[StructureMember]
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class RegisterValue(TypedDict):
|
|
339
|
+
name: str
|
|
340
|
+
value: str
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ThreadRegisters(TypedDict):
|
|
344
|
+
thread_id: int
|
|
345
|
+
registers: list[RegisterValue]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class Breakpoint(TypedDict):
|
|
349
|
+
addr: str
|
|
350
|
+
enabled: bool
|
|
351
|
+
condition: Optional[str]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class FunctionAnalysis(TypedDict):
|
|
355
|
+
addr: str
|
|
356
|
+
name: Optional[str]
|
|
357
|
+
code: Optional[str]
|
|
358
|
+
asm: Optional[str]
|
|
359
|
+
xto: list[Xref]
|
|
360
|
+
xfrom: list[Xref]
|
|
361
|
+
callees: list[dict]
|
|
362
|
+
callers: list[Function]
|
|
363
|
+
strings: list[String]
|
|
364
|
+
constants: list[dict]
|
|
365
|
+
blocks: list[dict]
|
|
366
|
+
error: Optional[str]
|
|
367
|
+
prompt: Optional[str]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class PatternMatch(TypedDict):
|
|
371
|
+
pattern: str
|
|
372
|
+
matches: list[str]
|
|
373
|
+
count: int
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class CodePattern(TypedDict):
|
|
377
|
+
mnemonic: str
|
|
378
|
+
operands: NotRequired[list[str]]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class BasicBlock(TypedDict):
|
|
382
|
+
start: str
|
|
383
|
+
end: str
|
|
384
|
+
size: int
|
|
385
|
+
type: int
|
|
386
|
+
successors: list[str]
|
|
387
|
+
predecessors: list[str]
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
T = TypeVar("T")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class Page(TypedDict, Generic[T]):
|
|
394
|
+
data: list[T]
|
|
395
|
+
next_offset: Optional[int]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ============================================================================
|
|
399
|
+
# Helper Functions
|
|
400
|
+
# ============================================================================
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def get_image_size() -> int:
|
|
404
|
+
try:
|
|
405
|
+
info = idaapi.get_inf_structure()
|
|
406
|
+
omin_ea = info.omin_ea
|
|
407
|
+
omax_ea = info.omax_ea
|
|
408
|
+
except AttributeError:
|
|
409
|
+
import ida_ida
|
|
410
|
+
|
|
411
|
+
omin_ea = ida_ida.inf_get_omin_ea()
|
|
412
|
+
omax_ea = ida_ida.inf_get_omax_ea()
|
|
413
|
+
image_size = omax_ea - omin_ea
|
|
414
|
+
header = idautils.peutils_t().header()
|
|
415
|
+
if header and header[:4] == b"PE\0\0":
|
|
416
|
+
image_size = struct.unpack("<I", header[0x50:0x54])[0]
|
|
417
|
+
return image_size
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def parse_address(addr: str | int) -> int:
|
|
421
|
+
if isinstance(addr, int):
|
|
422
|
+
return addr
|
|
423
|
+
try:
|
|
424
|
+
return int(addr, 0)
|
|
425
|
+
except ValueError:
|
|
426
|
+
for ch in addr:
|
|
427
|
+
if ch not in "0123456789abcdefABCDEF":
|
|
428
|
+
raise IDAError(f"Failed to parse address: {addr}")
|
|
429
|
+
raise IDAError(f"Failed to parse address (missing 0x prefix): {addr}")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def normalize_list_input(value: list | str) -> list:
|
|
433
|
+
"""Normalize input to list - accepts list or comma-separated string"""
|
|
434
|
+
if isinstance(value, list):
|
|
435
|
+
return value
|
|
436
|
+
if isinstance(value, str):
|
|
437
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
438
|
+
return [value]
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def normalize_dict_list(
|
|
442
|
+
value: list[dict] | dict | str | list[str] | Any,
|
|
443
|
+
string_parser: Optional[Callable[[str], dict]] = None,
|
|
444
|
+
) -> list[dict]:
|
|
445
|
+
"""Normalize input to list[dict] with optional string parsing
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
value: Input value (dict, list[dict], str, list[str], or any)
|
|
449
|
+
string_parser: Optional function to convert string → dict
|
|
450
|
+
If None, strings → empty dict
|
|
451
|
+
|
|
452
|
+
Flow:
|
|
453
|
+
dict → [dict]
|
|
454
|
+
str → split by ',' → list[str] → map(string_parser) → list[dict]
|
|
455
|
+
list[str] → map(string_parser) → list[dict]
|
|
456
|
+
list[dict] → list[dict]
|
|
457
|
+
Any → [{}]
|
|
458
|
+
"""
|
|
459
|
+
if isinstance(value, dict):
|
|
460
|
+
return [value]
|
|
461
|
+
elif isinstance(value, list):
|
|
462
|
+
if not value:
|
|
463
|
+
return [{}]
|
|
464
|
+
# Check if list[str] or list[dict]
|
|
465
|
+
if all(isinstance(item, dict) for item in value):
|
|
466
|
+
return value
|
|
467
|
+
elif all(isinstance(item, str) for item in value):
|
|
468
|
+
# list[str] → map with parser
|
|
469
|
+
if string_parser:
|
|
470
|
+
return [string_parser(s.strip()) for s in value if s.strip()]
|
|
471
|
+
return [{}]
|
|
472
|
+
else:
|
|
473
|
+
# Mixed types - filter dicts only
|
|
474
|
+
return [item for item in value if isinstance(item, dict)] or [{}]
|
|
475
|
+
elif isinstance(value, str):
|
|
476
|
+
# Try JSON parse first
|
|
477
|
+
try:
|
|
478
|
+
parsed = json.loads(value)
|
|
479
|
+
if isinstance(parsed, dict):
|
|
480
|
+
return [parsed]
|
|
481
|
+
elif isinstance(parsed, list):
|
|
482
|
+
return parsed
|
|
483
|
+
except (json.JSONDecodeError, ValueError):
|
|
484
|
+
pass
|
|
485
|
+
|
|
486
|
+
# Not JSON - split by comma and parse
|
|
487
|
+
parts = [s.strip() for s in value.split(",") if s.strip()]
|
|
488
|
+
if not parts:
|
|
489
|
+
return [{}]
|
|
490
|
+
|
|
491
|
+
if string_parser:
|
|
492
|
+
return [string_parser(part) for part in parts]
|
|
493
|
+
return [{}]
|
|
494
|
+
else:
|
|
495
|
+
# Any other type → empty dict
|
|
496
|
+
return [{}]
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def looks_like_address(s: str) -> bool:
|
|
500
|
+
"""Check if string looks like an address (0x prefix or all hex chars)"""
|
|
501
|
+
if s.startswith("0x") or s.startswith("0X"):
|
|
502
|
+
return True
|
|
503
|
+
# All hex chars and at least 4 chars → likely address
|
|
504
|
+
if len(s) >= 4 and all(c in "0123456789abcdefABCDEF" for c in s):
|
|
505
|
+
return True
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@overload
|
|
510
|
+
def get_function(addr: int, *, raise_error: Literal[True]) -> Function: ...
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@overload
|
|
514
|
+
def get_function(addr: int) -> Function: ...
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@overload
|
|
518
|
+
def get_function(addr: int, *, raise_error: Literal[False]) -> Optional[Function]: ...
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def get_function(addr, *, raise_error=True):
|
|
522
|
+
fn = idaapi.get_func(addr)
|
|
523
|
+
if fn is None:
|
|
524
|
+
if raise_error:
|
|
525
|
+
raise IDAError(f"No function found at address {hex(addr)}")
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
name = fn.get_name()
|
|
530
|
+
except AttributeError:
|
|
531
|
+
name = ida_funcs.get_func_name(fn.start_ea)
|
|
532
|
+
|
|
533
|
+
return Function(addr=hex(addr), name=name, size=hex(fn.end_ea - fn.start_ea))
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def get_prototype(fn: ida_funcs.func_t) -> Optional[str]:
|
|
537
|
+
try:
|
|
538
|
+
prototype: ida_typeinf.tinfo_t = fn.get_prototype()
|
|
539
|
+
if prototype is not None:
|
|
540
|
+
return str(prototype)
|
|
541
|
+
else:
|
|
542
|
+
return None
|
|
543
|
+
except AttributeError:
|
|
544
|
+
try:
|
|
545
|
+
return idc.get_type(fn.start_ea)
|
|
546
|
+
except Exception:
|
|
547
|
+
tif = ida_typeinf.tinfo_t()
|
|
548
|
+
if ida_nalt.get_tinfo(tif, fn.start_ea):
|
|
549
|
+
return str(tif)
|
|
550
|
+
return None
|
|
551
|
+
except Exception as e:
|
|
552
|
+
print(f"Error getting function prototype: {e}")
|
|
553
|
+
return None
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
DEMANGLED_TO_EA = {}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def create_demangled_to_ea_map():
|
|
560
|
+
for ea in idautils.Functions():
|
|
561
|
+
demangled = idaapi.demangle_name(idc.get_name(ea, 0), idaapi.MNG_NODEFINIT)
|
|
562
|
+
if demangled:
|
|
563
|
+
DEMANGLED_TO_EA[demangled] = ea
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def get_type_by_name(type_name: str) -> ida_typeinf.tinfo_t:
|
|
567
|
+
# 8-bit integers
|
|
568
|
+
if type_name in ("int8", "__int8", "int8_t", "char", "signed char"):
|
|
569
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT8)
|
|
570
|
+
elif type_name in ("uint8", "__uint8", "uint8_t", "unsigned char", "byte", "BYTE"):
|
|
571
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT8)
|
|
572
|
+
# 16-bit integers
|
|
573
|
+
elif type_name in (
|
|
574
|
+
"int16",
|
|
575
|
+
"__int16",
|
|
576
|
+
"int16_t",
|
|
577
|
+
"short",
|
|
578
|
+
"short int",
|
|
579
|
+
"signed short",
|
|
580
|
+
"signed short int",
|
|
581
|
+
):
|
|
582
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT16)
|
|
583
|
+
elif type_name in (
|
|
584
|
+
"uint16",
|
|
585
|
+
"__uint16",
|
|
586
|
+
"uint16_t",
|
|
587
|
+
"unsigned short",
|
|
588
|
+
"unsigned short int",
|
|
589
|
+
"word",
|
|
590
|
+
"WORD",
|
|
591
|
+
):
|
|
592
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT16)
|
|
593
|
+
# 32-bit integers
|
|
594
|
+
elif type_name in (
|
|
595
|
+
"int32",
|
|
596
|
+
"__int32",
|
|
597
|
+
"int32_t",
|
|
598
|
+
"int",
|
|
599
|
+
"signed int",
|
|
600
|
+
"long",
|
|
601
|
+
"long int",
|
|
602
|
+
"signed long",
|
|
603
|
+
"signed long int",
|
|
604
|
+
):
|
|
605
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT32)
|
|
606
|
+
elif type_name in (
|
|
607
|
+
"uint32",
|
|
608
|
+
"__uint32",
|
|
609
|
+
"uint32_t",
|
|
610
|
+
"unsigned int",
|
|
611
|
+
"unsigned long",
|
|
612
|
+
"unsigned long int",
|
|
613
|
+
"dword",
|
|
614
|
+
"DWORD",
|
|
615
|
+
):
|
|
616
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT32)
|
|
617
|
+
# 64-bit integers
|
|
618
|
+
elif type_name in (
|
|
619
|
+
"int64",
|
|
620
|
+
"__int64",
|
|
621
|
+
"int64_t",
|
|
622
|
+
"long long",
|
|
623
|
+
"long long int",
|
|
624
|
+
"signed long long",
|
|
625
|
+
"signed long long int",
|
|
626
|
+
):
|
|
627
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT64)
|
|
628
|
+
elif type_name in (
|
|
629
|
+
"uint64",
|
|
630
|
+
"__uint64",
|
|
631
|
+
"uint64_t",
|
|
632
|
+
"unsigned int64",
|
|
633
|
+
"unsigned long long",
|
|
634
|
+
"unsigned long long int",
|
|
635
|
+
"qword",
|
|
636
|
+
"QWORD",
|
|
637
|
+
):
|
|
638
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT64)
|
|
639
|
+
# 128-bit integers
|
|
640
|
+
elif type_name in ("int128", "__int128", "int128_t", "__int128_t"):
|
|
641
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_INT128)
|
|
642
|
+
elif type_name in (
|
|
643
|
+
"uint128",
|
|
644
|
+
"__uint128",
|
|
645
|
+
"uint128_t",
|
|
646
|
+
"__uint128_t",
|
|
647
|
+
"unsigned int128",
|
|
648
|
+
):
|
|
649
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_UINT128)
|
|
650
|
+
# Floating point types
|
|
651
|
+
elif type_name in ("float",):
|
|
652
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_FLOAT)
|
|
653
|
+
elif type_name in ("double",):
|
|
654
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_DOUBLE)
|
|
655
|
+
elif type_name in ("long double", "ldouble"):
|
|
656
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_LDOUBLE)
|
|
657
|
+
# Boolean type
|
|
658
|
+
elif type_name in ("bool", "_Bool", "boolean"):
|
|
659
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_BOOL)
|
|
660
|
+
# Void type
|
|
661
|
+
elif type_name in ("void",):
|
|
662
|
+
return ida_typeinf.tinfo_t(ida_typeinf.BTF_VOID)
|
|
663
|
+
# Named types
|
|
664
|
+
tif = ida_typeinf.tinfo_t()
|
|
665
|
+
if tif.get_named_type(None, type_name, ida_typeinf.BTF_STRUCT):
|
|
666
|
+
return tif
|
|
667
|
+
if tif.get_named_type(None, type_name, ida_typeinf.BTF_TYPEDEF):
|
|
668
|
+
return tif
|
|
669
|
+
if tif.get_named_type(None, type_name, ida_typeinf.BTF_ENUM):
|
|
670
|
+
return tif
|
|
671
|
+
if tif.get_named_type(None, type_name, ida_typeinf.BTF_UNION):
|
|
672
|
+
return tif
|
|
673
|
+
|
|
674
|
+
# Try parsing as a type string (e.g., "int *")
|
|
675
|
+
tif = ida_typeinf.tinfo_t()
|
|
676
|
+
if ida_typeinf.parse_decl(tif, None, f"{type_name} x;", ida_typeinf.PT_SIL):
|
|
677
|
+
return tif
|
|
678
|
+
|
|
679
|
+
raise IDAError(f"Unable to retrieve {type_name} type info object")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def paginate(data: list[T], offset: int, count: int) -> Page[T]:
|
|
683
|
+
if count == 0:
|
|
684
|
+
count = len(data)
|
|
685
|
+
next_offset = offset + count
|
|
686
|
+
if next_offset >= len(data):
|
|
687
|
+
next_offset = None
|
|
688
|
+
return {
|
|
689
|
+
"data": data[offset : offset + count],
|
|
690
|
+
"next_offset": next_offset,
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def lazy_paginate(
|
|
695
|
+
iterator: Iterator[T],
|
|
696
|
+
offset: int,
|
|
697
|
+
count: int,
|
|
698
|
+
filter_fn: Optional[Callable[[T], bool]] = None,
|
|
699
|
+
) -> Page[T]:
|
|
700
|
+
"""Lazy pagination that only iterates over needed elements.
|
|
701
|
+
|
|
702
|
+
Much more efficient for large datasets when only fetching first pages.
|
|
703
|
+
Complexity: O(offset + count) instead of O(N) for full dataset.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
iterator: An iterator over the data source
|
|
707
|
+
offset: Number of items to skip
|
|
708
|
+
count: Maximum number of items to return (0 for all remaining)
|
|
709
|
+
filter_fn: Optional filter function to apply before pagination
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
Page with data and next_offset (None if no more items)
|
|
713
|
+
"""
|
|
714
|
+
# Apply filter if provided
|
|
715
|
+
if filter_fn is not None:
|
|
716
|
+
iterator = filter(filter_fn, iterator)
|
|
717
|
+
|
|
718
|
+
# Skip offset items
|
|
719
|
+
if offset > 0:
|
|
720
|
+
iterator = islice(iterator, offset, None)
|
|
721
|
+
|
|
722
|
+
# Handle count=0 (all remaining items)
|
|
723
|
+
if count == 0:
|
|
724
|
+
items = list(iterator)
|
|
725
|
+
return {
|
|
726
|
+
"data": items,
|
|
727
|
+
"next_offset": None,
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
# Fetch count + 1 items to check if there are more
|
|
731
|
+
items = list(islice(iterator, count + 1))
|
|
732
|
+
|
|
733
|
+
has_more = len(items) > count
|
|
734
|
+
if has_more:
|
|
735
|
+
items = items[:count]
|
|
736
|
+
|
|
737
|
+
return {
|
|
738
|
+
"data": items,
|
|
739
|
+
"next_offset": offset + count if has_more else None,
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def pattern_filter(data: list[T], pattern: str, key: str) -> list[T]:
|
|
744
|
+
if not pattern:
|
|
745
|
+
return data
|
|
746
|
+
|
|
747
|
+
regex = None
|
|
748
|
+
use_glob = False
|
|
749
|
+
|
|
750
|
+
# Regex pattern: /pattern/flags
|
|
751
|
+
if pattern.startswith("/") and pattern.count("/") >= 2:
|
|
752
|
+
last_slash = pattern.rfind("/")
|
|
753
|
+
body = pattern[1:last_slash]
|
|
754
|
+
flag_str = pattern[last_slash + 1 :]
|
|
755
|
+
|
|
756
|
+
flags = 0
|
|
757
|
+
for ch in flag_str:
|
|
758
|
+
if ch == "i":
|
|
759
|
+
flags |= re.IGNORECASE
|
|
760
|
+
elif ch == "m":
|
|
761
|
+
flags |= re.MULTILINE
|
|
762
|
+
elif ch == "s":
|
|
763
|
+
flags |= re.DOTALL
|
|
764
|
+
|
|
765
|
+
try:
|
|
766
|
+
regex = re.compile(body, flags or re.IGNORECASE)
|
|
767
|
+
except re.error:
|
|
768
|
+
regex = None
|
|
769
|
+
# Glob pattern: contains * or ?
|
|
770
|
+
elif "*" in pattern or "?" in pattern:
|
|
771
|
+
use_glob = True
|
|
772
|
+
|
|
773
|
+
def get_value(item) -> str:
|
|
774
|
+
try:
|
|
775
|
+
v = item[key]
|
|
776
|
+
except Exception:
|
|
777
|
+
v = getattr(item, key, "")
|
|
778
|
+
return "" if v is None else str(v)
|
|
779
|
+
|
|
780
|
+
def matches(item) -> bool:
|
|
781
|
+
text = get_value(item)
|
|
782
|
+
if regex is not None:
|
|
783
|
+
return bool(regex.search(text))
|
|
784
|
+
if use_glob:
|
|
785
|
+
return fnmatch.fnmatch(text.lower(), pattern.lower())
|
|
786
|
+
return pattern.lower() in text.lower()
|
|
787
|
+
|
|
788
|
+
return [item for item in data if matches(item)]
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def refresh_decompiler_widget():
|
|
792
|
+
widget = ida_kernwin.get_current_widget()
|
|
793
|
+
if widget is not None:
|
|
794
|
+
vu = ida_hexrays.get_widget_vdui(widget)
|
|
795
|
+
if vu is not None:
|
|
796
|
+
vu.refresh_ctext()
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def refresh_decompiler_ctext(fn_addr: int):
|
|
800
|
+
error = ida_hexrays.hexrays_failure_t()
|
|
801
|
+
cfunc: ida_hexrays.cfunc_t = ida_hexrays.decompile_func(
|
|
802
|
+
fn_addr, error, ida_hexrays.DECOMP_WARNINGS
|
|
803
|
+
)
|
|
804
|
+
if cfunc:
|
|
805
|
+
cfunc.refresh_func_ctext()
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
class my_modifier_t(ida_hexrays.user_lvar_modifier_t):
|
|
809
|
+
def __init__(self, var_name: str, new_type: ida_typeinf.tinfo_t):
|
|
810
|
+
ida_hexrays.user_lvar_modifier_t.__init__(self)
|
|
811
|
+
self.var_name = var_name
|
|
812
|
+
self.new_type = new_type
|
|
813
|
+
|
|
814
|
+
def modify_lvars(self, lvinf):
|
|
815
|
+
for lvar_saved in lvinf.lvvec:
|
|
816
|
+
lvar_saved: ida_hexrays.lvar_saved_info_t
|
|
817
|
+
if lvar_saved.name == self.var_name:
|
|
818
|
+
lvar_saved.type = self.new_type
|
|
819
|
+
return True
|
|
820
|
+
return False
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def parse_decls_ctypes(decls: str, hti_flags: int) -> tuple[int, list[str]]:
|
|
824
|
+
if sys.platform == "win32":
|
|
825
|
+
import ctypes
|
|
826
|
+
|
|
827
|
+
assert isinstance(decls, str), "decls must be a string"
|
|
828
|
+
assert isinstance(hti_flags, int), "hti_flags must be an int"
|
|
829
|
+
c_decls = decls.encode("utf-8")
|
|
830
|
+
c_til = None
|
|
831
|
+
ida_dll = ctypes.CDLL("ida")
|
|
832
|
+
ida_dll.parse_decls.argtypes = [
|
|
833
|
+
ctypes.c_void_p,
|
|
834
|
+
ctypes.c_char_p,
|
|
835
|
+
ctypes.c_void_p,
|
|
836
|
+
ctypes.c_int,
|
|
837
|
+
]
|
|
838
|
+
ida_dll.parse_decls.restype = ctypes.c_int
|
|
839
|
+
|
|
840
|
+
messages: list[str] = []
|
|
841
|
+
|
|
842
|
+
@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p)
|
|
843
|
+
def magic_printer(fmt: bytes, arg1: bytes):
|
|
844
|
+
if fmt.count(b"%") == 1 and b"%s" in fmt:
|
|
845
|
+
formatted = fmt.replace(b"%s", arg1)
|
|
846
|
+
messages.append(formatted.decode("utf-8"))
|
|
847
|
+
return len(formatted) + 1
|
|
848
|
+
else:
|
|
849
|
+
messages.append(f"unsupported magic_printer fmt: {repr(fmt)}")
|
|
850
|
+
return 0
|
|
851
|
+
|
|
852
|
+
errors = ida_dll.parse_decls(c_til, c_decls, magic_printer, hti_flags)
|
|
853
|
+
else:
|
|
854
|
+
errors = ida_typeinf.parse_decls(None, decls, False, hti_flags)
|
|
855
|
+
messages = []
|
|
856
|
+
return errors, messages
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def get_stack_frame_variables_internal(
|
|
860
|
+
fn_addr: int, raise_error: bool
|
|
861
|
+
) -> list[StackFrameVariable]:
|
|
862
|
+
from .sync import ida_major
|
|
863
|
+
|
|
864
|
+
if ida_major < 9:
|
|
865
|
+
return []
|
|
866
|
+
|
|
867
|
+
func = idaapi.get_func(fn_addr)
|
|
868
|
+
if not func:
|
|
869
|
+
if raise_error:
|
|
870
|
+
raise IDAError(f"No function found at address {fn_addr}")
|
|
871
|
+
return []
|
|
872
|
+
|
|
873
|
+
tif = ida_typeinf.tinfo_t()
|
|
874
|
+
if not tif.get_type_by_tid(func.frame) or not tif.is_udt():
|
|
875
|
+
return []
|
|
876
|
+
|
|
877
|
+
members: list[StackFrameVariable] = []
|
|
878
|
+
udt = ida_typeinf.udt_type_data_t()
|
|
879
|
+
tif.get_udt_details(udt)
|
|
880
|
+
for udm in udt:
|
|
881
|
+
if not udm.is_gap():
|
|
882
|
+
name = udm.name
|
|
883
|
+
offset = udm.offset // 8
|
|
884
|
+
size = udm.size // 8
|
|
885
|
+
type = str(udm.type)
|
|
886
|
+
members.append(
|
|
887
|
+
StackFrameVariable(
|
|
888
|
+
name=name, offset=hex(offset), size=hex(size), type=type
|
|
889
|
+
)
|
|
890
|
+
)
|
|
891
|
+
return members
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def decompile_checked(addr: int):
|
|
895
|
+
"""Decompile a function and raise IDAError on failure (uses cache)"""
|
|
896
|
+
if not ida_hexrays.init_hexrays_plugin():
|
|
897
|
+
raise IDAError("Hex-Rays decompiler is not available")
|
|
898
|
+
hf = ida_hexrays.hexrays_failure_t()
|
|
899
|
+
cfunc = ida_hexrays.decompile(addr, hf)
|
|
900
|
+
if not cfunc:
|
|
901
|
+
if hf.code == ida_hexrays.MERR_LICENSE:
|
|
902
|
+
raise IDAError(
|
|
903
|
+
"Decompiler license is not available. Use `disassemble_function` to get the assembly code instead."
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
message = f"Decompilation failed at {hex(addr)}"
|
|
907
|
+
if hf.str:
|
|
908
|
+
message += f": {hf.str}"
|
|
909
|
+
if hf.errea != idaapi.BADADDR:
|
|
910
|
+
message += f" (address: {hex(hf.errea)})"
|
|
911
|
+
raise IDAError(message)
|
|
912
|
+
return cfunc
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def decompile_function_safe(ea: int) -> Optional[str]:
|
|
916
|
+
"""Safely decompile a function, returning None on failure (uses cache)"""
|
|
917
|
+
import ida_lines
|
|
918
|
+
import ida_kernwin
|
|
919
|
+
|
|
920
|
+
try:
|
|
921
|
+
if not ida_hexrays.init_hexrays_plugin():
|
|
922
|
+
return None
|
|
923
|
+
cfunc = ida_hexrays.decompile(ea)
|
|
924
|
+
if not cfunc:
|
|
925
|
+
return None
|
|
926
|
+
sv = cfunc.get_pseudocode()
|
|
927
|
+
lines = []
|
|
928
|
+
for sl in sv:
|
|
929
|
+
sl: ida_kernwin.simpleline_t
|
|
930
|
+
item = ida_hexrays.ctree_item_t()
|
|
931
|
+
line_ea = None
|
|
932
|
+
if cfunc.get_line_item(sl.line, 0, False, None, item, None):
|
|
933
|
+
dstr: str | None = item.dstr()
|
|
934
|
+
if dstr:
|
|
935
|
+
ds = dstr.split(": ")
|
|
936
|
+
if len(ds) == 2:
|
|
937
|
+
try:
|
|
938
|
+
line_ea = int(ds[0], 16)
|
|
939
|
+
except ValueError:
|
|
940
|
+
pass
|
|
941
|
+
text = ida_lines.tag_remove(sl.line)
|
|
942
|
+
if line_ea is not None:
|
|
943
|
+
lines.append(f"{text} /*{line_ea:#x}*/")
|
|
944
|
+
else:
|
|
945
|
+
lines.append(text)
|
|
946
|
+
return "\n".join(lines)
|
|
947
|
+
except Exception:
|
|
948
|
+
return None
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def get_assembly_lines(ea: int) -> str:
|
|
952
|
+
"""Get assembly lines for a function in compact string format"""
|
|
953
|
+
func = idaapi.get_func(ea)
|
|
954
|
+
if not func:
|
|
955
|
+
return ""
|
|
956
|
+
|
|
957
|
+
func_name: str = ida_funcs.get_func_name(func.start_ea) or "<unnamed>"
|
|
958
|
+
|
|
959
|
+
# Get segment from first instruction
|
|
960
|
+
first_seg = idaapi.getseg(func.start_ea)
|
|
961
|
+
segment_name = idaapi.get_segm_name(first_seg) if first_seg else "UNKNOWN"
|
|
962
|
+
|
|
963
|
+
# Build compact string format
|
|
964
|
+
lines_str = f"{func_name} ({segment_name} @ {hex(func.start_ea)}):"
|
|
965
|
+
|
|
966
|
+
for item_ea in idautils.FuncItems(func.start_ea):
|
|
967
|
+
mnem = idc.print_insn_mnem(item_ea) or ""
|
|
968
|
+
ops = []
|
|
969
|
+
for n in range(8):
|
|
970
|
+
if idc.get_operand_type(item_ea, n) == idaapi.o_void:
|
|
971
|
+
break
|
|
972
|
+
ops.append(idc.print_operand(item_ea, n) or "")
|
|
973
|
+
instruction = f"{mnem} {', '.join(ops)}".rstrip()
|
|
974
|
+
lines_str += f"\n{item_ea:x} {instruction}"
|
|
975
|
+
|
|
976
|
+
return lines_str
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def get_all_xrefs(ea: int) -> dict:
|
|
980
|
+
"""Get all xrefs to and from an address"""
|
|
981
|
+
return {
|
|
982
|
+
"to": [
|
|
983
|
+
{"addr": hex(x.frm), "type": "code" if x.iscode else "data"}
|
|
984
|
+
for x in idautils.XrefsTo(ea, 0)
|
|
985
|
+
],
|
|
986
|
+
"from": [
|
|
987
|
+
{"addr": hex(x.to), "type": "code" if x.iscode else "data"}
|
|
988
|
+
for x in idautils.XrefsFrom(ea, 0)
|
|
989
|
+
],
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def get_all_comments(ea: int) -> dict:
|
|
994
|
+
"""Get all comments for an address"""
|
|
995
|
+
func = idaapi.get_func(ea)
|
|
996
|
+
if not func:
|
|
997
|
+
return {}
|
|
998
|
+
|
|
999
|
+
comments = {}
|
|
1000
|
+
for item_ea in idautils.FuncItems(func.start_ea):
|
|
1001
|
+
cmt = idaapi.get_cmt(item_ea, False)
|
|
1002
|
+
if cmt:
|
|
1003
|
+
comments[hex(item_ea)] = {"regular": cmt}
|
|
1004
|
+
cmt = idaapi.get_cmt(item_ea, True)
|
|
1005
|
+
if cmt:
|
|
1006
|
+
if hex(item_ea) not in comments:
|
|
1007
|
+
comments[hex(item_ea)] = {}
|
|
1008
|
+
comments[hex(item_ea)]["repeatable"] = cmt
|
|
1009
|
+
return comments
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def get_callees(addr: str) -> list[dict]:
|
|
1013
|
+
"""Get callees for a single function address"""
|
|
1014
|
+
try:
|
|
1015
|
+
func_start = parse_address(addr)
|
|
1016
|
+
func = idaapi.get_func(func_start)
|
|
1017
|
+
if not func:
|
|
1018
|
+
return []
|
|
1019
|
+
func_end = idc.find_func_end(func_start)
|
|
1020
|
+
callees: list[dict[str, str]] = []
|
|
1021
|
+
current_ea = func_start
|
|
1022
|
+
while current_ea < func_end:
|
|
1023
|
+
insn = idaapi.insn_t()
|
|
1024
|
+
idaapi.decode_insn(insn, current_ea)
|
|
1025
|
+
if insn.itype in [idaapi.NN_call, idaapi.NN_callfi, idaapi.NN_callni]:
|
|
1026
|
+
target = idc.get_operand_value(current_ea, 0)
|
|
1027
|
+
target_type = idc.get_operand_type(current_ea, 0)
|
|
1028
|
+
if target_type in [idaapi.o_mem, idaapi.o_near, idaapi.o_far]:
|
|
1029
|
+
func_type = (
|
|
1030
|
+
"internal"
|
|
1031
|
+
if idaapi.get_func(target) is not None
|
|
1032
|
+
else "external"
|
|
1033
|
+
)
|
|
1034
|
+
func_name = idc.get_name(target)
|
|
1035
|
+
if func_name is not None:
|
|
1036
|
+
callees.append(
|
|
1037
|
+
{
|
|
1038
|
+
"addr": hex(target),
|
|
1039
|
+
"name": func_name,
|
|
1040
|
+
"type": func_type,
|
|
1041
|
+
}
|
|
1042
|
+
)
|
|
1043
|
+
current_ea = idc.next_head(current_ea, func_end)
|
|
1044
|
+
|
|
1045
|
+
unique_callee_tuples = {tuple(callee.items()) for callee in callees}
|
|
1046
|
+
unique_callees = [dict(callee) for callee in unique_callee_tuples]
|
|
1047
|
+
return unique_callees
|
|
1048
|
+
except Exception:
|
|
1049
|
+
return []
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def get_callers(addr: str, limit: int = 50) -> list[Function]:
|
|
1053
|
+
"""Get callers for a single function address"""
|
|
1054
|
+
try:
|
|
1055
|
+
callers = {}
|
|
1056
|
+
iterations = 0
|
|
1057
|
+
max_iterations = limit * 100
|
|
1058
|
+
for caller_addr in idautils.CodeRefsTo(parse_address(addr), 0):
|
|
1059
|
+
iterations += 1
|
|
1060
|
+
if len(callers) >= limit or iterations >= max_iterations:
|
|
1061
|
+
break
|
|
1062
|
+
func = get_function(caller_addr, raise_error=False)
|
|
1063
|
+
if not func:
|
|
1064
|
+
continue
|
|
1065
|
+
insn = idaapi.insn_t()
|
|
1066
|
+
idaapi.decode_insn(insn, caller_addr)
|
|
1067
|
+
if insn.itype not in [
|
|
1068
|
+
idaapi.NN_call,
|
|
1069
|
+
idaapi.NN_callfi,
|
|
1070
|
+
idaapi.NN_callni,
|
|
1071
|
+
]:
|
|
1072
|
+
continue
|
|
1073
|
+
callers[func["addr"]] = func
|
|
1074
|
+
|
|
1075
|
+
return list(callers.values())
|
|
1076
|
+
except Exception:
|
|
1077
|
+
return []
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def get_xrefs_from_internal(ea: int) -> list[Xref]:
|
|
1081
|
+
"""Get all xrefs from an address"""
|
|
1082
|
+
xrefs = []
|
|
1083
|
+
for xref in idautils.XrefsFrom(ea, 0):
|
|
1084
|
+
xrefs.append(
|
|
1085
|
+
Xref(
|
|
1086
|
+
addr=hex(xref.to),
|
|
1087
|
+
type="code" if xref.iscode else "data",
|
|
1088
|
+
fn=get_function(xref.to, raise_error=False),
|
|
1089
|
+
)
|
|
1090
|
+
)
|
|
1091
|
+
return xrefs
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def extract_function_strings(ea: int) -> list[String]:
|
|
1095
|
+
"""Extract string references from a function"""
|
|
1096
|
+
func = idaapi.get_func(ea)
|
|
1097
|
+
if not func:
|
|
1098
|
+
return []
|
|
1099
|
+
|
|
1100
|
+
strings = []
|
|
1101
|
+
for item_ea in idautils.FuncItems(func.start_ea):
|
|
1102
|
+
for xref in idautils.XrefsFrom(item_ea, 0):
|
|
1103
|
+
if not xref.iscode:
|
|
1104
|
+
# Check if target is a string
|
|
1105
|
+
str_type = ida_nalt.get_str_type(xref.to)
|
|
1106
|
+
if str_type != ida_nalt.STRTYPE_C:
|
|
1107
|
+
continue
|
|
1108
|
+
try:
|
|
1109
|
+
str_content = idc.get_strlit_contents(xref.to)
|
|
1110
|
+
if str_content:
|
|
1111
|
+
strings.append(
|
|
1112
|
+
String(
|
|
1113
|
+
addr=hex(xref.to),
|
|
1114
|
+
length=len(str_content),
|
|
1115
|
+
string=str_content.decode("utf-8", errors="replace"),
|
|
1116
|
+
)
|
|
1117
|
+
)
|
|
1118
|
+
except Exception:
|
|
1119
|
+
pass
|
|
1120
|
+
return strings
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def extract_function_constants(ea: int) -> list[dict]:
|
|
1124
|
+
"""Extract immediate constants from a function"""
|
|
1125
|
+
func = idaapi.get_func(ea)
|
|
1126
|
+
if not func:
|
|
1127
|
+
return []
|
|
1128
|
+
|
|
1129
|
+
constants = []
|
|
1130
|
+
for item_ea in idautils.FuncItems(func.start_ea):
|
|
1131
|
+
insn = idaapi.insn_t()
|
|
1132
|
+
if idaapi.decode_insn(insn, item_ea) > 0:
|
|
1133
|
+
for op in insn.ops:
|
|
1134
|
+
if op.type == idaapi.o_imm:
|
|
1135
|
+
constants.append(
|
|
1136
|
+
{
|
|
1137
|
+
"addr": hex(item_ea),
|
|
1138
|
+
"value": hex(op.value),
|
|
1139
|
+
"decimal": op.value,
|
|
1140
|
+
}
|
|
1141
|
+
)
|
|
1142
|
+
return constants
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
# ============================================================================
|
|
1146
|
+
# Large Output Handling
|
|
1147
|
+
# ============================================================================
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def handle_large_output(result: Any, line_threshold: int = 3000) -> Any:
|
|
1151
|
+
"""
|
|
1152
|
+
Handle potentially large outputs by writing to temp file if needed.
|
|
1153
|
+
|
|
1154
|
+
Args:
|
|
1155
|
+
result: The result object to check
|
|
1156
|
+
line_threshold: Number of lines above which to write to file (default: 3000)
|
|
1157
|
+
|
|
1158
|
+
Returns:
|
|
1159
|
+
Either the original result or a dict with file path if written to file
|
|
1160
|
+
"""
|
|
1161
|
+
try:
|
|
1162
|
+
serialized = json.dumps(result, indent=2)
|
|
1163
|
+
line_count = serialized.count("\n") + 1
|
|
1164
|
+
|
|
1165
|
+
if line_count > line_threshold:
|
|
1166
|
+
fd, temp_path = tempfile.mkstemp(
|
|
1167
|
+
suffix=".json", prefix="ida_mcp_", text=True
|
|
1168
|
+
)
|
|
1169
|
+
try:
|
|
1170
|
+
with os.fdopen(fd, "w") as f:
|
|
1171
|
+
f.write(serialized)
|
|
1172
|
+
|
|
1173
|
+
return {
|
|
1174
|
+
"type": "file_reference",
|
|
1175
|
+
"path": temp_path,
|
|
1176
|
+
"line_count": line_count,
|
|
1177
|
+
"message": f"Output too large ({line_count} lines), written to file",
|
|
1178
|
+
}
|
|
1179
|
+
except Exception:
|
|
1180
|
+
os.close(fd)
|
|
1181
|
+
raise
|
|
1182
|
+
|
|
1183
|
+
return result
|
|
1184
|
+
|
|
1185
|
+
except Exception:
|
|
1186
|
+
return result
|