ida-code 0.2.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_code/__init__.py +2 -0
- ida_code/_search_utils.py +33 -0
- ida_code/comments.py +191 -0
- ida_code/config.py +9 -0
- ida_code/doc_search.py +255 -0
- ida_code/example_search.py +570 -0
- ida_code/executor.py +145 -0
- ida_code/guidelines.py +370 -0
- ida_code/macho.py +67 -0
- ida_code/prompts.py +176 -0
- ida_code/server.py +1011 -0
- ida_code/session.py +293 -0
- ida_code/snapshots.py +110 -0
- ida_code/structures.py +227 -0
- ida_code/undo.py +102 -0
- ida_code/variables.py +206 -0
- ida_code-0.2.1.dist-info/METADATA +167 -0
- ida_code-0.2.1.dist-info/RECORD +21 -0
- ida_code-0.2.1.dist-info/WHEEL +4 -0
- ida_code-0.2.1.dist-info/entry_points.txt +2 -0
- ida_code-0.2.1.dist-info/licenses/LICENSE +21 -0
ida_code/session.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import enum
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from ida_code.config import IDA_INSTALL_DIR
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Add idalib's Python package to sys.path so `idapro` can be imported
|
|
12
|
+
# without requiring manual `pip install`.
|
|
13
|
+
_idalib_python = IDA_INSTALL_DIR / "idalib" / "python"
|
|
14
|
+
if _idalib_python.is_dir() and str(_idalib_python) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_idalib_python))
|
|
16
|
+
|
|
17
|
+
# Set IDADIR so idapro finds the IDA install directory
|
|
18
|
+
# without requiring `py-activate-idalib.py`.
|
|
19
|
+
os.environ.setdefault("IDADIR", str(IDA_INSTALL_DIR))
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import idapro
|
|
23
|
+
except ImportError as e:
|
|
24
|
+
raise ImportError(
|
|
25
|
+
f"Could not import idapro from {_idalib_python}. "
|
|
26
|
+
f"Set IDA_INSTALL_DIR to your IDA Pro 9.2+ installation directory "
|
|
27
|
+
f"(currently {IDA_INSTALL_DIR}). "
|
|
28
|
+
f"Original error: {e}"
|
|
29
|
+
) from e
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class State(enum.Enum):
|
|
33
|
+
NO_DATABASE = "no_database"
|
|
34
|
+
DATABASE_OPEN = "database_open"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_state = State.NO_DATABASE
|
|
38
|
+
_db_path: str | None = None
|
|
39
|
+
_db_file_path: str | None = None # actual .i64/.idb path on disk
|
|
40
|
+
_orphaned: bool = False # True when database file vanished from disk
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_state() -> State:
|
|
44
|
+
return _state
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _find_database_file(path: str) -> str | None:
|
|
48
|
+
"""Find the .i64/.idb database file for *path*.
|
|
49
|
+
|
|
50
|
+
Checks both naming conventions IDA uses: replace-suffix and append.
|
|
51
|
+
Returns the path if found, else None.
|
|
52
|
+
"""
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
p = Path(path)
|
|
55
|
+
for ext in (".i64", ".idb"):
|
|
56
|
+
for candidate in (p.with_suffix(ext), Path(str(p) + ext)):
|
|
57
|
+
if candidate.is_file():
|
|
58
|
+
return str(candidate)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def require_open() -> None:
|
|
63
|
+
"""Raise ``ToolError`` if no database is usable.
|
|
64
|
+
|
|
65
|
+
Checks both the state enum *and* that the database file still exists
|
|
66
|
+
on disk. If the file has been moved or deleted, resets state to
|
|
67
|
+
``NO_DATABASE`` and raises a descriptive error instead of letting
|
|
68
|
+
idalib segfault.
|
|
69
|
+
"""
|
|
70
|
+
from fastmcp.exceptions import ToolError
|
|
71
|
+
|
|
72
|
+
global _state, _db_file_path, _orphaned
|
|
73
|
+
|
|
74
|
+
if _state == State.NO_DATABASE:
|
|
75
|
+
raise ToolError("No database is open. Call open_database first.")
|
|
76
|
+
|
|
77
|
+
if _db_file_path is not None and not os.path.isfile(_db_file_path):
|
|
78
|
+
log.warning(
|
|
79
|
+
"Database file missing: %s — resetting state", _db_file_path
|
|
80
|
+
)
|
|
81
|
+
_state = State.NO_DATABASE
|
|
82
|
+
_orphaned = True
|
|
83
|
+
_db_file_path = None
|
|
84
|
+
from ida_code.executor import reset
|
|
85
|
+
reset()
|
|
86
|
+
raise ToolError(
|
|
87
|
+
"The database file has been moved or deleted. "
|
|
88
|
+
"Call open_database with the new path."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def info() -> dict:
|
|
93
|
+
"""Return a summary dict of the current database.
|
|
94
|
+
|
|
95
|
+
Raises ``ToolError`` if no database is open.
|
|
96
|
+
"""
|
|
97
|
+
require_open()
|
|
98
|
+
return _collect_summary(_db_path or "<unknown>")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def open(
|
|
102
|
+
path: str,
|
|
103
|
+
auto_analysis: bool = True,
|
|
104
|
+
overwrite: bool = False,
|
|
105
|
+
timeout: int = 0,
|
|
106
|
+
arch: str | None = None,
|
|
107
|
+
) -> dict:
|
|
108
|
+
"""Open a binary/database via idalib. Returns a summary dict.
|
|
109
|
+
|
|
110
|
+
*timeout* limits auto-analysis wait time in seconds (0 = unlimited).
|
|
111
|
+
When the timeout expires, the database remains open with partial analysis.
|
|
112
|
+
|
|
113
|
+
*arch* selects a specific architecture slice from a fat (universal) Mach-O
|
|
114
|
+
binary (e.g. "arm64e", "x86_64"). The slice is extracted to a temporary
|
|
115
|
+
thin file before opening. Ignored for non-fat binaries.
|
|
116
|
+
|
|
117
|
+
Raises ``ToolError`` on failure.
|
|
118
|
+
"""
|
|
119
|
+
from fastmcp.exceptions import ToolError
|
|
120
|
+
|
|
121
|
+
from ida_code import macho
|
|
122
|
+
|
|
123
|
+
global _state, _db_path, _db_file_path, _orphaned
|
|
124
|
+
|
|
125
|
+
_orphaned = False
|
|
126
|
+
|
|
127
|
+
# Close any existing database first.
|
|
128
|
+
if _state == State.DATABASE_OPEN:
|
|
129
|
+
close()
|
|
130
|
+
|
|
131
|
+
# If an architecture is requested, extract the slice from a fat Mach-O.
|
|
132
|
+
open_path = path
|
|
133
|
+
if arch:
|
|
134
|
+
try:
|
|
135
|
+
open_path = macho.extract_slice(path, arch)
|
|
136
|
+
except ValueError:
|
|
137
|
+
# Not a fat binary or arch not found.
|
|
138
|
+
available = macho.list_architectures(path)
|
|
139
|
+
if available:
|
|
140
|
+
raise ToolError(
|
|
141
|
+
f"Architecture '{arch}' not found. "
|
|
142
|
+
f"Available: {available}"
|
|
143
|
+
)
|
|
144
|
+
log.warning(
|
|
145
|
+
"arch='%s' requested but %s is not a fat Mach-O; "
|
|
146
|
+
"opening as-is",
|
|
147
|
+
arch, path,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if overwrite:
|
|
151
|
+
_remove_existing_databases(open_path)
|
|
152
|
+
|
|
153
|
+
use_polling = auto_analysis and timeout > 0
|
|
154
|
+
run_auto = auto_analysis and not use_polling
|
|
155
|
+
|
|
156
|
+
log.info(
|
|
157
|
+
"Opening database: %s (auto_analysis=%s, overwrite=%s, timeout=%d, arch=%s)",
|
|
158
|
+
open_path, auto_analysis, overwrite, timeout, arch,
|
|
159
|
+
)
|
|
160
|
+
rc = idapro.open_database(open_path, run_auto)
|
|
161
|
+
if rc != 0:
|
|
162
|
+
log.error("open_database failed with code %d for %s", rc, open_path)
|
|
163
|
+
raise ToolError(f"open_database returned code {rc}")
|
|
164
|
+
|
|
165
|
+
_state = State.DATABASE_OPEN
|
|
166
|
+
_db_path = open_path
|
|
167
|
+
_db_file_path = _find_database_file(open_path)
|
|
168
|
+
|
|
169
|
+
timed_out = False
|
|
170
|
+
if use_polling:
|
|
171
|
+
timed_out = _wait_for_analysis(timeout)
|
|
172
|
+
|
|
173
|
+
log.info("Database opened successfully: %s", open_path)
|
|
174
|
+
|
|
175
|
+
# Reset executor namespace for the new database.
|
|
176
|
+
from ida_code.executor import reset
|
|
177
|
+
reset()
|
|
178
|
+
|
|
179
|
+
summary = _collect_summary(open_path)
|
|
180
|
+
if arch:
|
|
181
|
+
summary["arch"] = arch
|
|
182
|
+
summary["original_path"] = path
|
|
183
|
+
summary["warning"] = (
|
|
184
|
+
"Auto-analysis timed out \u2014 results may be incomplete."
|
|
185
|
+
if timed_out
|
|
186
|
+
else None
|
|
187
|
+
)
|
|
188
|
+
return summary
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _wait_for_analysis(timeout: int) -> bool:
|
|
192
|
+
"""Poll until auto-analysis finishes or *timeout* seconds elapse.
|
|
193
|
+
|
|
194
|
+
Returns True if the timeout expired (analysis incomplete).
|
|
195
|
+
"""
|
|
196
|
+
import time
|
|
197
|
+
import ida_auto
|
|
198
|
+
import ida_funcs
|
|
199
|
+
|
|
200
|
+
deadline = time.monotonic() + timeout
|
|
201
|
+
interval = 0.5 # seconds between polls
|
|
202
|
+
last_count = 0
|
|
203
|
+
|
|
204
|
+
while not ida_auto.auto_is_ok():
|
|
205
|
+
now = time.monotonic()
|
|
206
|
+
if now >= deadline:
|
|
207
|
+
log.warning("Auto-analysis timed out after %ds", timeout)
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
count = ida_funcs.get_func_qty()
|
|
211
|
+
if count != last_count:
|
|
212
|
+
log.info("Auto-analysis in progress: %d functions so far", count)
|
|
213
|
+
last_count = count
|
|
214
|
+
|
|
215
|
+
remaining = deadline - now
|
|
216
|
+
time.sleep(min(interval, remaining))
|
|
217
|
+
|
|
218
|
+
log.info("Auto-analysis completed")
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _collect_summary(path: str) -> dict:
|
|
223
|
+
import ida_ida
|
|
224
|
+
import ida_entry
|
|
225
|
+
import ida_segment
|
|
226
|
+
import idautils
|
|
227
|
+
|
|
228
|
+
processor = ida_ida.inf_get_procname()
|
|
229
|
+
bits = 64 if ida_ida.inf_is_64bit() else (32 if ida_ida.inf_is_32bit() else 16)
|
|
230
|
+
|
|
231
|
+
# Segments
|
|
232
|
+
segments = []
|
|
233
|
+
for seg_ea in idautils.Segments():
|
|
234
|
+
seg = ida_segment.getseg(seg_ea)
|
|
235
|
+
name = ida_segment.get_segm_name(seg)
|
|
236
|
+
segments.append({
|
|
237
|
+
"name": name,
|
|
238
|
+
"start": f"{seg.start_ea:#x}",
|
|
239
|
+
"end": f"{seg.end_ea:#x}",
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
# Entry points (capped to avoid huge responses for symbol-heavy binaries)
|
|
243
|
+
entry_count = ida_entry.get_entry_qty()
|
|
244
|
+
max_entries = 20
|
|
245
|
+
entry_points = []
|
|
246
|
+
for i in range(min(entry_count, max_entries)):
|
|
247
|
+
ordinal = ida_entry.get_entry_ordinal(i)
|
|
248
|
+
ea = ida_entry.get_entry(ordinal)
|
|
249
|
+
name = ida_entry.get_entry_name(ordinal)
|
|
250
|
+
entry_points.append({"name": name, "address": f"{ea:#x}"})
|
|
251
|
+
|
|
252
|
+
# Function count
|
|
253
|
+
func_count = sum(1 for _ in idautils.Functions())
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"path": path,
|
|
257
|
+
"processor": processor,
|
|
258
|
+
"bits": bits,
|
|
259
|
+
"function_count": func_count,
|
|
260
|
+
"segments": segments,
|
|
261
|
+
"entry_point_count": entry_count,
|
|
262
|
+
"entry_points": entry_points,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _remove_existing_databases(path: str) -> None:
|
|
267
|
+
"""Remove existing IDA database files so a fresh analysis starts."""
|
|
268
|
+
from pathlib import Path
|
|
269
|
+
p = Path(path)
|
|
270
|
+
for ext in (".i64", ".idb"):
|
|
271
|
+
for candidate in {p.with_suffix(ext), Path(str(p) + ext)}:
|
|
272
|
+
if candidate.is_file():
|
|
273
|
+
candidate.unlink()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def close() -> None:
|
|
277
|
+
"""Close the current database."""
|
|
278
|
+
global _state, _db_path, _db_file_path, _orphaned
|
|
279
|
+
if _state == State.DATABASE_OPEN:
|
|
280
|
+
log.info("Closing database")
|
|
281
|
+
if not _orphaned:
|
|
282
|
+
idapro.close_database()
|
|
283
|
+
_state = State.NO_DATABASE
|
|
284
|
+
_db_path = None
|
|
285
|
+
_db_file_path = None
|
|
286
|
+
_orphaned = False
|
|
287
|
+
from ida_code.executor import reset
|
|
288
|
+
reset()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@atexit.register
|
|
292
|
+
def _cleanup():
|
|
293
|
+
close()
|
ida_code/snapshots.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from fastmcp.exceptions import ToolError
|
|
5
|
+
|
|
6
|
+
from ida_code import session
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _build_snapshot_list():
|
|
12
|
+
"""Return list of snapshot_t from the snapshot tree."""
|
|
13
|
+
import ida_loader
|
|
14
|
+
|
|
15
|
+
root = ida_loader.snapshot_t()
|
|
16
|
+
ida_loader.build_snapshot_tree(root)
|
|
17
|
+
return list(root.children)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _find_by_id(snapshots, id_str: str):
|
|
21
|
+
"""Find a snapshot by its string ID. Raises ToolError if not found."""
|
|
22
|
+
for ss in snapshots:
|
|
23
|
+
if str(ss.id) == id_str:
|
|
24
|
+
return ss
|
|
25
|
+
raise ToolError(f"Snapshot with id '{id_str}' not found.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _to_dict(ss) -> dict:
|
|
29
|
+
"""Convert a snapshot_t to a plain dict."""
|
|
30
|
+
return {
|
|
31
|
+
"id": str(ss.id),
|
|
32
|
+
"desc": ss.desc,
|
|
33
|
+
"filename": ss.filename,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def list_snapshots() -> dict:
|
|
38
|
+
"""List all snapshots for the current database."""
|
|
39
|
+
session.require_open()
|
|
40
|
+
snapshots = _build_snapshot_list()
|
|
41
|
+
return {
|
|
42
|
+
"snapshots": [_to_dict(ss) for ss in snapshots],
|
|
43
|
+
"count": len(snapshots),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_snapshot(desc: str = "") -> dict:
|
|
48
|
+
"""Create a new database snapshot."""
|
|
49
|
+
session.require_open()
|
|
50
|
+
|
|
51
|
+
import ida_kernwin
|
|
52
|
+
import ida_loader
|
|
53
|
+
|
|
54
|
+
ss = ida_loader.snapshot_t()
|
|
55
|
+
ss.desc = desc[:ida_loader.MAX_DATABASE_DESCRIPTION]
|
|
56
|
+
|
|
57
|
+
ok, err = ida_kernwin.take_database_snapshot(ss)
|
|
58
|
+
if not ok:
|
|
59
|
+
raise ToolError(f"Failed to create snapshot: {err}")
|
|
60
|
+
|
|
61
|
+
log.info("Created snapshot id=%s desc=%r", ss.id, ss.desc)
|
|
62
|
+
return _to_dict(ss)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def restore_snapshot(snapshot_id: str) -> dict:
|
|
66
|
+
"""Restore the database to a previous snapshot."""
|
|
67
|
+
session.require_open()
|
|
68
|
+
|
|
69
|
+
import ida_kernwin
|
|
70
|
+
|
|
71
|
+
snapshots = _build_snapshot_list()
|
|
72
|
+
ss = _find_by_id(snapshots, snapshot_id)
|
|
73
|
+
|
|
74
|
+
# restore_database_snapshot takes (snapshot, callback, userdata).
|
|
75
|
+
# In idalib headless mode the restore is synchronous; the callback
|
|
76
|
+
# receives (error_msg_or_empty, userdata).
|
|
77
|
+
restore_err = []
|
|
78
|
+
|
|
79
|
+
def _cb(err_msg, ud):
|
|
80
|
+
if err_msg:
|
|
81
|
+
restore_err.append(err_msg)
|
|
82
|
+
|
|
83
|
+
ida_kernwin.restore_database_snapshot(ss, _cb, None)
|
|
84
|
+
|
|
85
|
+
if restore_err:
|
|
86
|
+
raise ToolError(f"Failed to restore snapshot: {restore_err[0]}")
|
|
87
|
+
|
|
88
|
+
# Database state changed — reset executor namespace.
|
|
89
|
+
from ida_code.executor import reset
|
|
90
|
+
reset()
|
|
91
|
+
|
|
92
|
+
log.info("Restored snapshot id=%s", snapshot_id)
|
|
93
|
+
return {"status": "restored", "id": snapshot_id}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def remove_snapshot(snapshot_id: str) -> dict:
|
|
97
|
+
"""Remove a snapshot by deleting its file from disk."""
|
|
98
|
+
session.require_open()
|
|
99
|
+
|
|
100
|
+
snapshots = _build_snapshot_list()
|
|
101
|
+
ss = _find_by_id(snapshots, snapshot_id)
|
|
102
|
+
|
|
103
|
+
filename = ss.filename
|
|
104
|
+
try:
|
|
105
|
+
os.remove(filename)
|
|
106
|
+
except OSError as e:
|
|
107
|
+
raise ToolError(f"Failed to remove snapshot file '{filename}': {e}")
|
|
108
|
+
|
|
109
|
+
log.info("Removed snapshot id=%s file=%s", snapshot_id, filename)
|
|
110
|
+
return {"status": "removed", "id": snapshot_id, "filename": filename}
|
ida_code/structures.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Structure (struct/union) management via IDA type info library."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from fastmcp.exceptions import ToolError
|
|
7
|
+
|
|
8
|
+
from ida_code import session
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_NAME_RE = re.compile(r"(?:struct|union)\s+(\w+)\s*\{")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _extract_name(c_code: str) -> str:
|
|
16
|
+
"""Extract the struct/union name from a C definition string.
|
|
17
|
+
|
|
18
|
+
Raises ToolError if no name can be found.
|
|
19
|
+
"""
|
|
20
|
+
m = _NAME_RE.search(c_code)
|
|
21
|
+
if not m:
|
|
22
|
+
raise ToolError(
|
|
23
|
+
"Could not extract struct/union name from definition. "
|
|
24
|
+
"Expected format: 'struct name { ... };' or 'union name { ... };'"
|
|
25
|
+
)
|
|
26
|
+
return m.group(1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_struct_tinfo(name: str):
|
|
30
|
+
"""Look up a struct/union by name, returning (tinfo, ordinal).
|
|
31
|
+
|
|
32
|
+
Raises ToolError if the type is not found.
|
|
33
|
+
"""
|
|
34
|
+
import ida_typeinf
|
|
35
|
+
|
|
36
|
+
til = ida_typeinf.get_idati()
|
|
37
|
+
tif = ida_typeinf.tinfo_t()
|
|
38
|
+
if not tif.get_named_type(til, name):
|
|
39
|
+
raise ToolError(f"Structure '{name}' not found.")
|
|
40
|
+
ordinal = ida_typeinf.get_type_ordinal(til, name)
|
|
41
|
+
return tif, ordinal
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _annotated_definition(tif, ordinal: int) -> str:
|
|
45
|
+
"""Build a C definition with ``/* offset */`` comments on each member."""
|
|
46
|
+
import ida_typeinf
|
|
47
|
+
import idc
|
|
48
|
+
|
|
49
|
+
# Get base definition from IDA.
|
|
50
|
+
if ordinal <= 0:
|
|
51
|
+
return ""
|
|
52
|
+
raw = idc.print_decls(ordinal, 0) or ""
|
|
53
|
+
raw = raw.strip()
|
|
54
|
+
if not raw:
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
# Extract member offsets.
|
|
58
|
+
udt = ida_typeinf.udt_type_data_t()
|
|
59
|
+
if not tif.get_udt_details(udt):
|
|
60
|
+
return raw
|
|
61
|
+
|
|
62
|
+
offsets = {} # member name -> offset in bytes
|
|
63
|
+
for i in range(udt.size()):
|
|
64
|
+
udm = udt.at(i)
|
|
65
|
+
offsets[udm.name] = udm.offset // 8
|
|
66
|
+
|
|
67
|
+
# Annotate each member line inside the braces.
|
|
68
|
+
lines = raw.split("\n")
|
|
69
|
+
out = []
|
|
70
|
+
for line in lines:
|
|
71
|
+
stripped = line.strip()
|
|
72
|
+
# Skip opening/closing braces and empty lines.
|
|
73
|
+
if stripped in ("{", "};", "") or stripped.startswith("struct ") or stripped.startswith("union "):
|
|
74
|
+
out.append(line)
|
|
75
|
+
continue
|
|
76
|
+
# Try to match a member name in this line.
|
|
77
|
+
for mname, off in offsets.items():
|
|
78
|
+
if mname in stripped:
|
|
79
|
+
line = f"{line} /* {off:#x} */"
|
|
80
|
+
break
|
|
81
|
+
out.append(line)
|
|
82
|
+
return "\n".join(out)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _struct_to_dict(name: str) -> dict:
|
|
86
|
+
"""Convert a named struct/union to a result dict with C definition."""
|
|
87
|
+
import ida_typeinf
|
|
88
|
+
|
|
89
|
+
tif, ordinal = _get_struct_tinfo(name)
|
|
90
|
+
size = tif.get_size()
|
|
91
|
+
is_union = tif.is_union()
|
|
92
|
+
member_count = tif.get_udt_nmembers()
|
|
93
|
+
|
|
94
|
+
# Get alignment from UDT details.
|
|
95
|
+
alignment = 0
|
|
96
|
+
udt = ida_typeinf.udt_type_data_t()
|
|
97
|
+
if tif.get_udt_details(udt):
|
|
98
|
+
alignment = udt.effalign
|
|
99
|
+
|
|
100
|
+
definition = _annotated_definition(tif, ordinal)
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"name": name,
|
|
104
|
+
"size": size,
|
|
105
|
+
"is_union": is_union,
|
|
106
|
+
"alignment": alignment,
|
|
107
|
+
"member_count": member_count,
|
|
108
|
+
"definition": definition,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def list_structures(offset: int = 0, limit: int = 50, filter: str = "") -> dict:
|
|
113
|
+
"""List structures in the database with pagination."""
|
|
114
|
+
session.require_open()
|
|
115
|
+
|
|
116
|
+
import ida_typeinf
|
|
117
|
+
import idautils
|
|
118
|
+
|
|
119
|
+
all_structs = list(idautils.Structs())
|
|
120
|
+
til = ida_typeinf.get_idati()
|
|
121
|
+
structures = []
|
|
122
|
+
skipped = 0
|
|
123
|
+
|
|
124
|
+
for ordinal, sid, name in all_structs:
|
|
125
|
+
if filter and filter.lower() not in name.lower():
|
|
126
|
+
continue
|
|
127
|
+
if skipped < offset:
|
|
128
|
+
skipped += 1
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
tif = ida_typeinf.tinfo_t()
|
|
132
|
+
if tif.get_named_type(til, name):
|
|
133
|
+
size = tif.get_size()
|
|
134
|
+
member_count = tif.get_udt_nmembers()
|
|
135
|
+
udt = ida_typeinf.udt_type_data_t()
|
|
136
|
+
alignment = udt.effalign if tif.get_udt_details(udt) else 0
|
|
137
|
+
else:
|
|
138
|
+
size = 0
|
|
139
|
+
member_count = 0
|
|
140
|
+
alignment = 0
|
|
141
|
+
|
|
142
|
+
structures.append({
|
|
143
|
+
"name": name,
|
|
144
|
+
"size": size,
|
|
145
|
+
"alignment": alignment,
|
|
146
|
+
"member_count": member_count,
|
|
147
|
+
})
|
|
148
|
+
if len(structures) >= limit:
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"structures": structures,
|
|
153
|
+
"total": len(all_structs),
|
|
154
|
+
"showing": len(structures),
|
|
155
|
+
"offset": offset,
|
|
156
|
+
"filter": filter,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_structure(name: str) -> dict:
|
|
161
|
+
"""Get detailed info about a structure by name."""
|
|
162
|
+
session.require_open()
|
|
163
|
+
return _struct_to_dict(name)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def create_structure(definition: str) -> dict:
|
|
167
|
+
"""Create a new structure from a C definition string."""
|
|
168
|
+
session.require_open()
|
|
169
|
+
|
|
170
|
+
import ida_typeinf
|
|
171
|
+
import idc
|
|
172
|
+
|
|
173
|
+
name = _extract_name(definition)
|
|
174
|
+
|
|
175
|
+
# Check it doesn't already exist.
|
|
176
|
+
til = ida_typeinf.get_idati()
|
|
177
|
+
tif = ida_typeinf.tinfo_t()
|
|
178
|
+
if tif.get_named_type(til, name):
|
|
179
|
+
raise ToolError(f"Structure '{name}' already exists. Use edit_structure to modify it.")
|
|
180
|
+
|
|
181
|
+
result = idc.parse_decls(definition, idc.PT_SIL)
|
|
182
|
+
if result != 0:
|
|
183
|
+
raise ToolError(f"Failed to parse definition (error code {result}). Check C syntax.")
|
|
184
|
+
|
|
185
|
+
log.info("Created structure '%s'", name)
|
|
186
|
+
return _struct_to_dict(name)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def edit_structure(definition: str) -> dict:
|
|
190
|
+
"""Edit an existing structure by replacing its definition."""
|
|
191
|
+
session.require_open()
|
|
192
|
+
|
|
193
|
+
import ida_typeinf
|
|
194
|
+
import idc
|
|
195
|
+
|
|
196
|
+
name = _extract_name(definition)
|
|
197
|
+
|
|
198
|
+
# Check it exists.
|
|
199
|
+
til = ida_typeinf.get_idati()
|
|
200
|
+
tif = ida_typeinf.tinfo_t()
|
|
201
|
+
if not tif.get_named_type(til, name):
|
|
202
|
+
raise ToolError(f"Structure '{name}' not found. Use create_structure to create it.")
|
|
203
|
+
|
|
204
|
+
result = idc.parse_decls(definition, idc.PT_SIL | idc.PT_REPLACE)
|
|
205
|
+
if result != 0:
|
|
206
|
+
raise ToolError(f"Failed to parse definition (error code {result}). Check C syntax.")
|
|
207
|
+
|
|
208
|
+
log.info("Edited structure '%s'", name)
|
|
209
|
+
return _struct_to_dict(name)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def delete_structure(name: str) -> dict:
|
|
213
|
+
"""Delete a structure by name."""
|
|
214
|
+
session.require_open()
|
|
215
|
+
|
|
216
|
+
import ida_typeinf
|
|
217
|
+
|
|
218
|
+
# Check it exists.
|
|
219
|
+
til = ida_typeinf.get_idati()
|
|
220
|
+
tif = ida_typeinf.tinfo_t()
|
|
221
|
+
if not tif.get_named_type(til, name):
|
|
222
|
+
raise ToolError(f"Structure '{name}' not found.")
|
|
223
|
+
|
|
224
|
+
ida_typeinf.del_named_type(til, name, ida_typeinf.NTF_TYPE)
|
|
225
|
+
|
|
226
|
+
log.info("Deleted structure '%s'", name)
|
|
227
|
+
return {"status": "deleted", "name": name}
|