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,304 @@
|
|
|
1
|
+
"""Memory reading and writing operations for IDA Pro MCP.
|
|
2
|
+
|
|
3
|
+
This module provides batch operations for reading and writing memory at various
|
|
4
|
+
granularities (bytes, integers, strings) and patching binary data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
import ida_bytes
|
|
11
|
+
import idaapi
|
|
12
|
+
|
|
13
|
+
from .rpc import tool
|
|
14
|
+
from .sync import idasync
|
|
15
|
+
from .utils import (
|
|
16
|
+
IntRead,
|
|
17
|
+
IntWrite,
|
|
18
|
+
MemoryPatch,
|
|
19
|
+
MemoryRead,
|
|
20
|
+
normalize_list_input,
|
|
21
|
+
parse_address,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Memory Reading Operations
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@tool
|
|
31
|
+
@idasync
|
|
32
|
+
def get_bytes(regions: list[MemoryRead] | MemoryRead) -> list[dict]:
|
|
33
|
+
"""Read bytes from memory addresses"""
|
|
34
|
+
if isinstance(regions, dict):
|
|
35
|
+
regions = [regions]
|
|
36
|
+
|
|
37
|
+
results = []
|
|
38
|
+
for item in regions:
|
|
39
|
+
addr = item.get("addr", "")
|
|
40
|
+
size = item.get("size", 0)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
ea = parse_address(addr)
|
|
44
|
+
data = " ".join(f"{x:#02x}" for x in ida_bytes.get_bytes(ea, size))
|
|
45
|
+
results.append({"addr": addr, "data": data})
|
|
46
|
+
except Exception as e:
|
|
47
|
+
results.append({"addr": addr, "data": None, "error": str(e)})
|
|
48
|
+
|
|
49
|
+
return results
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_INT_CLASS_RE = re.compile(r"^(?P<sign>[iu])(?P<bits>8|16|32|64)(?P<endian>le|be)?$")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_int_class(text: str) -> tuple[int, bool, str, str]:
|
|
56
|
+
if not text:
|
|
57
|
+
raise ValueError("Missing integer class")
|
|
58
|
+
|
|
59
|
+
cleaned = text.strip().lower()
|
|
60
|
+
match = _INT_CLASS_RE.match(cleaned)
|
|
61
|
+
if not match:
|
|
62
|
+
raise ValueError(f"Invalid integer class: {text}")
|
|
63
|
+
|
|
64
|
+
bits = int(match.group("bits"))
|
|
65
|
+
signed = match.group("sign") == "i"
|
|
66
|
+
endian = match.group("endian") or "le"
|
|
67
|
+
byte_order = "little" if endian == "le" else "big"
|
|
68
|
+
normalized = f"{'i' if signed else 'u'}{bits}{endian}"
|
|
69
|
+
return bits, signed, byte_order, normalized
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse_int_value(text: str, signed: bool, bits: int) -> int:
|
|
73
|
+
if text is None:
|
|
74
|
+
raise ValueError("Missing integer value")
|
|
75
|
+
|
|
76
|
+
value_text = str(text).strip()
|
|
77
|
+
try:
|
|
78
|
+
value = int(value_text, 0)
|
|
79
|
+
except ValueError:
|
|
80
|
+
raise ValueError(f"Invalid integer value: {text}")
|
|
81
|
+
|
|
82
|
+
if not signed and value < 0:
|
|
83
|
+
raise ValueError(f"Negative value not allowed for u{bits}")
|
|
84
|
+
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@tool
|
|
89
|
+
@idasync
|
|
90
|
+
def get_int(
|
|
91
|
+
queries: Annotated[
|
|
92
|
+
list[IntRead] | IntRead,
|
|
93
|
+
"Integer read requests (ty, addr). ty: i8/u64/i16le/i16be/etc",
|
|
94
|
+
],
|
|
95
|
+
) -> list[dict]:
|
|
96
|
+
"""Read integer values from memory addresses"""
|
|
97
|
+
if isinstance(queries, dict):
|
|
98
|
+
queries = [queries]
|
|
99
|
+
|
|
100
|
+
results = []
|
|
101
|
+
for item in queries:
|
|
102
|
+
addr = item.get("addr", "")
|
|
103
|
+
ty = item.get("ty", "")
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
bits, signed, byte_order, normalized = _parse_int_class(ty)
|
|
107
|
+
ea = parse_address(addr)
|
|
108
|
+
size = bits // 8
|
|
109
|
+
data = ida_bytes.get_bytes(ea, size)
|
|
110
|
+
if not data or len(data) != size:
|
|
111
|
+
raise ValueError(f"Failed to read {size} bytes at {addr}")
|
|
112
|
+
|
|
113
|
+
value = int.from_bytes(data, byte_order, signed=signed)
|
|
114
|
+
results.append(
|
|
115
|
+
{"addr": addr, "ty": normalized, "value": value, "error": None}
|
|
116
|
+
)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
results.append({"addr": addr, "ty": ty, "value": None, "error": str(e)})
|
|
119
|
+
|
|
120
|
+
return results
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@tool
|
|
124
|
+
@idasync
|
|
125
|
+
def get_string(
|
|
126
|
+
addrs: Annotated[list[str] | str, "Addresses to read strings from"],
|
|
127
|
+
) -> list[dict]:
|
|
128
|
+
"""Read strings from memory addresses"""
|
|
129
|
+
addrs = normalize_list_input(addrs)
|
|
130
|
+
results = []
|
|
131
|
+
|
|
132
|
+
for addr in addrs:
|
|
133
|
+
try:
|
|
134
|
+
ea = parse_address(addr)
|
|
135
|
+
raw = idaapi.get_strlit_contents(ea, -1, 0)
|
|
136
|
+
if not raw:
|
|
137
|
+
results.append(
|
|
138
|
+
{"addr": addr, "value": None, "error": "No string at address"}
|
|
139
|
+
)
|
|
140
|
+
continue
|
|
141
|
+
value = raw.decode("utf-8", errors="replace")
|
|
142
|
+
results.append({"addr": addr, "value": value})
|
|
143
|
+
except Exception as e:
|
|
144
|
+
results.append({"addr": addr, "value": None, "error": str(e)})
|
|
145
|
+
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_global_variable_value_internal(ea: int) -> str:
|
|
150
|
+
import ida_typeinf
|
|
151
|
+
import ida_nalt
|
|
152
|
+
import ida_bytes
|
|
153
|
+
from .sync import IDAError
|
|
154
|
+
|
|
155
|
+
tif = ida_typeinf.tinfo_t()
|
|
156
|
+
if not ida_nalt.get_tinfo(tif, ea):
|
|
157
|
+
if not ida_bytes.has_any_name(ea):
|
|
158
|
+
raise IDAError(f"Failed to get type information for variable at {ea:#x}")
|
|
159
|
+
|
|
160
|
+
size = ida_bytes.get_item_size(ea)
|
|
161
|
+
if size == 0:
|
|
162
|
+
raise IDAError(f"Failed to get type information for variable at {ea:#x}")
|
|
163
|
+
else:
|
|
164
|
+
size = tif.get_size()
|
|
165
|
+
|
|
166
|
+
if size == 0 and tif.is_array() and tif.get_array_element().is_decl_char():
|
|
167
|
+
raw = idaapi.get_strlit_contents(ea, -1, 0)
|
|
168
|
+
if not raw:
|
|
169
|
+
return '""'
|
|
170
|
+
return_string = raw.decode("utf-8", errors="replace").strip()
|
|
171
|
+
return f'"{return_string}"'
|
|
172
|
+
elif size == 1:
|
|
173
|
+
return hex(ida_bytes.get_byte(ea))
|
|
174
|
+
elif size == 2:
|
|
175
|
+
return hex(ida_bytes.get_word(ea))
|
|
176
|
+
elif size == 4:
|
|
177
|
+
return hex(ida_bytes.get_dword(ea))
|
|
178
|
+
elif size == 8:
|
|
179
|
+
return hex(ida_bytes.get_qword(ea))
|
|
180
|
+
else:
|
|
181
|
+
return " ".join(hex(x) for x in ida_bytes.get_bytes(ea, size))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@tool
|
|
185
|
+
@idasync
|
|
186
|
+
def get_global_value(
|
|
187
|
+
queries: Annotated[
|
|
188
|
+
list[str] | str, "Global variable addresses or names to read values from"
|
|
189
|
+
],
|
|
190
|
+
) -> list[dict]:
|
|
191
|
+
"""Read global variable values by address or name
|
|
192
|
+
(auto-detects hex addresses vs names)"""
|
|
193
|
+
from .utils import looks_like_address
|
|
194
|
+
|
|
195
|
+
queries = normalize_list_input(queries)
|
|
196
|
+
results = []
|
|
197
|
+
|
|
198
|
+
for query in queries:
|
|
199
|
+
try:
|
|
200
|
+
ea = idaapi.BADADDR
|
|
201
|
+
|
|
202
|
+
# Try as address first if it looks like one
|
|
203
|
+
if looks_like_address(query):
|
|
204
|
+
try:
|
|
205
|
+
ea = parse_address(query)
|
|
206
|
+
except Exception:
|
|
207
|
+
ea = idaapi.BADADDR
|
|
208
|
+
|
|
209
|
+
# Fall back to name lookup
|
|
210
|
+
if ea == idaapi.BADADDR:
|
|
211
|
+
ea = idaapi.get_name_ea(idaapi.BADADDR, query)
|
|
212
|
+
|
|
213
|
+
if ea == idaapi.BADADDR:
|
|
214
|
+
results.append({"query": query, "value": None, "error": "Not found"})
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
value = get_global_variable_value_internal(ea)
|
|
218
|
+
results.append({"query": query, "value": value, "error": None})
|
|
219
|
+
except Exception as e:
|
|
220
|
+
results.append({"query": query, "value": None, "error": str(e)})
|
|
221
|
+
|
|
222
|
+
return results
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ============================================================================
|
|
226
|
+
# Batch Data Operations
|
|
227
|
+
# ============================================================================
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@tool
|
|
231
|
+
@idasync
|
|
232
|
+
def patch(patches: list[MemoryPatch] | MemoryPatch) -> list[dict]:
|
|
233
|
+
"""Patch bytes at memory addresses with hex data"""
|
|
234
|
+
if isinstance(patches, dict):
|
|
235
|
+
patches = [patches]
|
|
236
|
+
|
|
237
|
+
results = []
|
|
238
|
+
|
|
239
|
+
for patch in patches:
|
|
240
|
+
try:
|
|
241
|
+
ea = parse_address(patch["addr"])
|
|
242
|
+
data = bytes.fromhex(patch["data"])
|
|
243
|
+
|
|
244
|
+
ida_bytes.patch_bytes(ea, data)
|
|
245
|
+
results.append(
|
|
246
|
+
{"addr": patch["addr"], "size": len(data), "ok": True, "error": None}
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
results.append({"addr": patch.get("addr"), "size": 0, "error": str(e)})
|
|
251
|
+
|
|
252
|
+
return results
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@tool
|
|
256
|
+
@idasync
|
|
257
|
+
def put_int(
|
|
258
|
+
items: Annotated[
|
|
259
|
+
list[IntWrite] | IntWrite,
|
|
260
|
+
"Integer write requests (ty, addr, value). value is a string; supports 0x.. and negatives",
|
|
261
|
+
],
|
|
262
|
+
) -> list[dict]:
|
|
263
|
+
"""Write integer values to memory addresses"""
|
|
264
|
+
if isinstance(items, dict):
|
|
265
|
+
items = [items]
|
|
266
|
+
|
|
267
|
+
results = []
|
|
268
|
+
for item in items:
|
|
269
|
+
addr = item.get("addr", "")
|
|
270
|
+
ty = item.get("ty", "")
|
|
271
|
+
value_text = item.get("value")
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
bits, signed, byte_order, normalized = _parse_int_class(ty)
|
|
275
|
+
value = _parse_int_value(value_text, signed, bits)
|
|
276
|
+
size = bits // 8
|
|
277
|
+
try:
|
|
278
|
+
data = value.to_bytes(size, byte_order, signed=signed)
|
|
279
|
+
except OverflowError:
|
|
280
|
+
raise ValueError(f"Value {value_text} does not fit in {normalized}")
|
|
281
|
+
|
|
282
|
+
ea = parse_address(addr)
|
|
283
|
+
ida_bytes.patch_bytes(ea, data)
|
|
284
|
+
results.append(
|
|
285
|
+
{
|
|
286
|
+
"addr": addr,
|
|
287
|
+
"ty": normalized,
|
|
288
|
+
"value": str(value_text),
|
|
289
|
+
"ok": True,
|
|
290
|
+
"error": None,
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
except Exception as e:
|
|
294
|
+
results.append(
|
|
295
|
+
{
|
|
296
|
+
"addr": addr,
|
|
297
|
+
"ty": ty,
|
|
298
|
+
"value": str(value_text) if value_text is not None else None,
|
|
299
|
+
"ok": False,
|
|
300
|
+
"error": str(e),
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return results
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import idaapi
|
|
2
|
+
import idautils
|
|
3
|
+
import idc
|
|
4
|
+
import ida_hexrays
|
|
5
|
+
import ida_bytes
|
|
6
|
+
import ida_typeinf
|
|
7
|
+
import ida_frame
|
|
8
|
+
import ida_dirtree
|
|
9
|
+
|
|
10
|
+
from .rpc import tool
|
|
11
|
+
from .sync import idasync, IDAError
|
|
12
|
+
from .cache import invalidate_function_caches, decompile_cache
|
|
13
|
+
from .utils import (
|
|
14
|
+
parse_address,
|
|
15
|
+
decompile_checked,
|
|
16
|
+
refresh_decompiler_ctext,
|
|
17
|
+
CommentOp,
|
|
18
|
+
AsmPatchOp,
|
|
19
|
+
FunctionRename,
|
|
20
|
+
GlobalRename,
|
|
21
|
+
LocalRename,
|
|
22
|
+
StackRename,
|
|
23
|
+
RenameBatch,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ============================================================================
|
|
28
|
+
# Modification Operations
|
|
29
|
+
# ============================================================================
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@tool
|
|
33
|
+
@idasync
|
|
34
|
+
def set_comments(items: list[CommentOp] | CommentOp):
|
|
35
|
+
"""Set comments at addresses (both disassembly and decompiler views)"""
|
|
36
|
+
if isinstance(items, dict):
|
|
37
|
+
items = [items]
|
|
38
|
+
|
|
39
|
+
results = []
|
|
40
|
+
for item in items:
|
|
41
|
+
addr_str = item.get("addr", "")
|
|
42
|
+
comment = item.get("comment", "")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
ea = parse_address(addr_str)
|
|
46
|
+
|
|
47
|
+
# Invalidate decompile cache for this function
|
|
48
|
+
func = idaapi.get_func(ea)
|
|
49
|
+
if func:
|
|
50
|
+
decompile_cache.invalidate(hex(func.start_ea))
|
|
51
|
+
|
|
52
|
+
if not idaapi.set_cmt(ea, comment, False):
|
|
53
|
+
results.append(
|
|
54
|
+
{
|
|
55
|
+
"addr": addr_str,
|
|
56
|
+
"error": f"Failed to set disassembly comment at {hex(ea)}",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if not ida_hexrays.init_hexrays_plugin():
|
|
62
|
+
results.append({"addr": addr_str, "ok": True})
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
cfunc = decompile_checked(ea)
|
|
67
|
+
except IDAError:
|
|
68
|
+
results.append({"addr": addr_str, "ok": True})
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if ea == cfunc.entry_ea:
|
|
72
|
+
idc.set_func_cmt(ea, comment, True)
|
|
73
|
+
cfunc.refresh_func_ctext()
|
|
74
|
+
results.append({"addr": addr_str, "ok": True})
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
eamap = cfunc.get_eamap()
|
|
78
|
+
if ea not in eamap:
|
|
79
|
+
results.append(
|
|
80
|
+
{
|
|
81
|
+
"addr": addr_str,
|
|
82
|
+
"ok": True,
|
|
83
|
+
"error": f"Failed to set decompiler comment at {hex(ea)}",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
continue
|
|
87
|
+
nearest_ea = eamap[ea][0].ea
|
|
88
|
+
|
|
89
|
+
if cfunc.has_orphan_cmts():
|
|
90
|
+
cfunc.del_orphan_cmts()
|
|
91
|
+
cfunc.save_user_cmts()
|
|
92
|
+
|
|
93
|
+
tl = idaapi.treeloc_t()
|
|
94
|
+
tl.ea = nearest_ea
|
|
95
|
+
for itp in range(idaapi.ITP_SEMI, idaapi.ITP_COLON):
|
|
96
|
+
tl.itp = itp
|
|
97
|
+
cfunc.set_user_cmt(tl, comment)
|
|
98
|
+
cfunc.save_user_cmts()
|
|
99
|
+
cfunc.refresh_func_ctext()
|
|
100
|
+
if not cfunc.has_orphan_cmts():
|
|
101
|
+
results.append({"addr": addr_str, "ok": True})
|
|
102
|
+
break
|
|
103
|
+
cfunc.del_orphan_cmts()
|
|
104
|
+
cfunc.save_user_cmts()
|
|
105
|
+
else:
|
|
106
|
+
results.append(
|
|
107
|
+
{
|
|
108
|
+
"addr": addr_str,
|
|
109
|
+
"ok": True,
|
|
110
|
+
"error": f"Failed to set decompiler comment at {hex(ea)}",
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
results.append({"addr": addr_str, "error": str(e)})
|
|
115
|
+
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@tool
|
|
120
|
+
@idasync
|
|
121
|
+
def patch_asm(items: list[AsmPatchOp] | AsmPatchOp) -> list[dict]:
|
|
122
|
+
"""Patch assembly instructions at addresses"""
|
|
123
|
+
if isinstance(items, dict):
|
|
124
|
+
items = [items]
|
|
125
|
+
|
|
126
|
+
results = []
|
|
127
|
+
for item in items:
|
|
128
|
+
addr_str = item.get("addr", "")
|
|
129
|
+
instructions = item.get("asm", "")
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
ea = parse_address(addr_str)
|
|
133
|
+
assembles = instructions.split(";")
|
|
134
|
+
for assemble in assembles:
|
|
135
|
+
assemble = assemble.strip()
|
|
136
|
+
try:
|
|
137
|
+
(check_assemble, bytes_to_patch) = idautils.Assemble(ea, assemble)
|
|
138
|
+
if not check_assemble:
|
|
139
|
+
results.append(
|
|
140
|
+
{
|
|
141
|
+
"addr": addr_str,
|
|
142
|
+
"error": f"Failed to assemble: {assemble}",
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
break
|
|
146
|
+
ida_bytes.patch_bytes(ea, bytes_to_patch)
|
|
147
|
+
ea += len(bytes_to_patch)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
results.append(
|
|
150
|
+
{"addr": addr_str, "error": f"Failed at {hex(ea)}: {e}"}
|
|
151
|
+
)
|
|
152
|
+
break
|
|
153
|
+
else:
|
|
154
|
+
results.append({"addr": addr_str, "ok": True})
|
|
155
|
+
except Exception as e:
|
|
156
|
+
results.append({"addr": addr_str, "error": str(e)})
|
|
157
|
+
|
|
158
|
+
return results
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@tool
|
|
162
|
+
@idasync
|
|
163
|
+
def rename(batch: RenameBatch) -> dict:
|
|
164
|
+
"""Unified rename operation for functions, globals, locals, and stack variables"""
|
|
165
|
+
|
|
166
|
+
def _normalize_items(items):
|
|
167
|
+
"""Convert single item or None to list"""
|
|
168
|
+
if items is None:
|
|
169
|
+
return []
|
|
170
|
+
return [items] if isinstance(items, dict) else items
|
|
171
|
+
|
|
172
|
+
def _has_user_name(ea: int) -> bool:
|
|
173
|
+
flags = idaapi.get_flags(ea)
|
|
174
|
+
checker = getattr(idaapi, "has_user_name", None)
|
|
175
|
+
if checker is not None:
|
|
176
|
+
return checker(flags)
|
|
177
|
+
try:
|
|
178
|
+
import ida_name
|
|
179
|
+
|
|
180
|
+
checker = getattr(ida_name, "has_user_name", None)
|
|
181
|
+
if checker is not None:
|
|
182
|
+
return checker(flags)
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def _place_func_in_vibe_dir(ea: int) -> tuple[bool, str | None]:
|
|
188
|
+
tree = ida_dirtree.get_std_dirtree(ida_dirtree.DIRTREE_FUNCS)
|
|
189
|
+
if tree is None:
|
|
190
|
+
return False, "Function dirtree not available"
|
|
191
|
+
|
|
192
|
+
if not tree.load():
|
|
193
|
+
return False, "Failed to load function dirtree"
|
|
194
|
+
|
|
195
|
+
vibe_path = "/vibe/"
|
|
196
|
+
if not tree.isdir(vibe_path):
|
|
197
|
+
err = tree.mkdir(vibe_path)
|
|
198
|
+
if err not in (ida_dirtree.DTE_OK, ida_dirtree.DTE_ALREADY_EXISTS):
|
|
199
|
+
return False, f"mkdir failed: {err}"
|
|
200
|
+
|
|
201
|
+
old_cwd = tree.getcwd()
|
|
202
|
+
try:
|
|
203
|
+
if tree.chdir(vibe_path) != ida_dirtree.DTE_OK:
|
|
204
|
+
return False, "Failed to chdir to vibe"
|
|
205
|
+
err = tree.link(ea)
|
|
206
|
+
if err not in (ida_dirtree.DTE_OK, ida_dirtree.DTE_ALREADY_EXISTS):
|
|
207
|
+
return False, f"link failed: {err}"
|
|
208
|
+
if not tree.save():
|
|
209
|
+
return False, "Failed to save function dirtree"
|
|
210
|
+
finally:
|
|
211
|
+
if old_cwd:
|
|
212
|
+
tree.chdir(old_cwd)
|
|
213
|
+
|
|
214
|
+
return True, None
|
|
215
|
+
|
|
216
|
+
def _rename_funcs(items: list[FunctionRename]) -> list[dict]:
|
|
217
|
+
results = []
|
|
218
|
+
for item in items:
|
|
219
|
+
try:
|
|
220
|
+
ea = parse_address(item["addr"])
|
|
221
|
+
had_user_name = _has_user_name(ea)
|
|
222
|
+
success = idaapi.set_name(ea, item["name"], idaapi.SN_CHECK)
|
|
223
|
+
if success:
|
|
224
|
+
# Invalidate caches for this function
|
|
225
|
+
invalidate_function_caches(ea)
|
|
226
|
+
func = idaapi.get_func(ea)
|
|
227
|
+
if func:
|
|
228
|
+
refresh_decompiler_ctext(func.start_ea)
|
|
229
|
+
if not had_user_name and func:
|
|
230
|
+
placed, place_error = _place_func_in_vibe_dir(func.start_ea)
|
|
231
|
+
else:
|
|
232
|
+
placed, place_error = None, None
|
|
233
|
+
results.append(
|
|
234
|
+
{
|
|
235
|
+
"addr": item["addr"],
|
|
236
|
+
"name": item["name"],
|
|
237
|
+
"ok": success,
|
|
238
|
+
"error": None if success else "Rename failed",
|
|
239
|
+
"dir": "vibe" if success and placed else None,
|
|
240
|
+
"dir_error": place_error if success else None,
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
results.append({"addr": item.get("addr"), "error": str(e)})
|
|
245
|
+
return results
|
|
246
|
+
|
|
247
|
+
def _rename_globals(items: list[GlobalRename]) -> list[dict]:
|
|
248
|
+
results = []
|
|
249
|
+
for item in items:
|
|
250
|
+
try:
|
|
251
|
+
ea = idaapi.get_name_ea(idaapi.BADADDR, item["old"])
|
|
252
|
+
if ea == idaapi.BADADDR:
|
|
253
|
+
results.append(
|
|
254
|
+
{
|
|
255
|
+
"old": item["old"],
|
|
256
|
+
"new": item["new"],
|
|
257
|
+
"ok": False,
|
|
258
|
+
"error": f"Global '{item['old']}' not found",
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
continue
|
|
262
|
+
success = idaapi.set_name(ea, item["new"], idaapi.SN_CHECK)
|
|
263
|
+
results.append(
|
|
264
|
+
{
|
|
265
|
+
"old": item["old"],
|
|
266
|
+
"new": item["new"],
|
|
267
|
+
"ok": success,
|
|
268
|
+
"error": None if success else "Rename failed",
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
results.append({"old": item.get("old"), "error": str(e)})
|
|
273
|
+
return results
|
|
274
|
+
|
|
275
|
+
def _rename_locals(items: list[LocalRename]) -> list[dict]:
|
|
276
|
+
results = []
|
|
277
|
+
for item in items:
|
|
278
|
+
try:
|
|
279
|
+
func = idaapi.get_func(parse_address(item["func_addr"]))
|
|
280
|
+
if not func:
|
|
281
|
+
results.append(
|
|
282
|
+
{
|
|
283
|
+
"func_addr": item["func_addr"],
|
|
284
|
+
"old": item["old"],
|
|
285
|
+
"new": item["new"],
|
|
286
|
+
"ok": False,
|
|
287
|
+
"error": "No function found",
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
continue
|
|
291
|
+
success = ida_hexrays.rename_lvar(
|
|
292
|
+
func.start_ea, item["old"], item["new"]
|
|
293
|
+
)
|
|
294
|
+
if success:
|
|
295
|
+
refresh_decompiler_ctext(func.start_ea)
|
|
296
|
+
results.append(
|
|
297
|
+
{
|
|
298
|
+
"func_addr": item["func_addr"],
|
|
299
|
+
"old": item["old"],
|
|
300
|
+
"new": item["new"],
|
|
301
|
+
"ok": success,
|
|
302
|
+
"error": None if success else "Rename failed",
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
results.append({"func_addr": item.get("func_addr"), "error": str(e)})
|
|
307
|
+
return results
|
|
308
|
+
|
|
309
|
+
def _rename_stack(items: list[StackRename]) -> list[dict]:
|
|
310
|
+
results = []
|
|
311
|
+
for item in items:
|
|
312
|
+
try:
|
|
313
|
+
func = idaapi.get_func(parse_address(item["func_addr"]))
|
|
314
|
+
if not func:
|
|
315
|
+
results.append(
|
|
316
|
+
{
|
|
317
|
+
"func_addr": item["func_addr"],
|
|
318
|
+
"old": item["old"],
|
|
319
|
+
"new": item["new"],
|
|
320
|
+
"ok": False,
|
|
321
|
+
"error": "No function found",
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
frame_tif = ida_typeinf.tinfo_t()
|
|
327
|
+
if not ida_frame.get_func_frame(frame_tif, func):
|
|
328
|
+
results.append(
|
|
329
|
+
{
|
|
330
|
+
"func_addr": item["func_addr"],
|
|
331
|
+
"old": item["old"],
|
|
332
|
+
"new": item["new"],
|
|
333
|
+
"ok": False,
|
|
334
|
+
"error": "No frame",
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
idx, udm = frame_tif.get_udm(item["old"])
|
|
340
|
+
if not udm:
|
|
341
|
+
results.append(
|
|
342
|
+
{
|
|
343
|
+
"func_addr": item["func_addr"],
|
|
344
|
+
"old": item["old"],
|
|
345
|
+
"new": item["new"],
|
|
346
|
+
"ok": False,
|
|
347
|
+
"error": f"'{item['old']}' not found",
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
tid = frame_tif.get_udm_tid(idx)
|
|
353
|
+
if ida_frame.is_special_frame_member(tid):
|
|
354
|
+
results.append(
|
|
355
|
+
{
|
|
356
|
+
"func_addr": item["func_addr"],
|
|
357
|
+
"old": item["old"],
|
|
358
|
+
"new": item["new"],
|
|
359
|
+
"ok": False,
|
|
360
|
+
"error": "Special frame member",
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
udm = ida_typeinf.udm_t()
|
|
366
|
+
frame_tif.get_udm_by_tid(udm, tid)
|
|
367
|
+
offset = udm.offset // 8
|
|
368
|
+
if ida_frame.is_funcarg_off(func, offset):
|
|
369
|
+
results.append(
|
|
370
|
+
{
|
|
371
|
+
"func_addr": item["func_addr"],
|
|
372
|
+
"old": item["old"],
|
|
373
|
+
"new": item["new"],
|
|
374
|
+
"ok": False,
|
|
375
|
+
"error": "Argument member",
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
sval = ida_frame.soff_to_fpoff(func, offset)
|
|
381
|
+
success = ida_frame.define_stkvar(func, item["new"], sval, udm.type)
|
|
382
|
+
results.append(
|
|
383
|
+
{
|
|
384
|
+
"func_addr": item["func_addr"],
|
|
385
|
+
"old": item["old"],
|
|
386
|
+
"new": item["new"],
|
|
387
|
+
"ok": success,
|
|
388
|
+
"error": None if success else "Rename failed",
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
except Exception as e:
|
|
392
|
+
results.append({"func_addr": item.get("func_addr"), "error": str(e)})
|
|
393
|
+
return results
|
|
394
|
+
|
|
395
|
+
# Process each category
|
|
396
|
+
result = {}
|
|
397
|
+
if "func" in batch:
|
|
398
|
+
result["func"] = _rename_funcs(_normalize_items(batch["func"]))
|
|
399
|
+
if "data" in batch:
|
|
400
|
+
result["data"] = _rename_globals(_normalize_items(batch["data"]))
|
|
401
|
+
if "local" in batch:
|
|
402
|
+
result["local"] = _rename_locals(_normalize_items(batch["local"]))
|
|
403
|
+
if "stack" in batch:
|
|
404
|
+
result["stack"] = _rename_stack(_normalize_items(batch["stack"]))
|
|
405
|
+
|
|
406
|
+
return result
|