ghidraxdbg 12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ghidraxdbg/__init__.py +17 -0
- ghidraxdbg/arch.py +236 -0
- ghidraxdbg/commands.py +1349 -0
- ghidraxdbg/hooks.py +419 -0
- ghidraxdbg/methods.py +680 -0
- ghidraxdbg/py.typed +0 -0
- ghidraxdbg/schema.xml +317 -0
- ghidraxdbg/util.py +297 -0
- ghidraxdbg-12.0.dist-info/METADATA +20 -0
- ghidraxdbg-12.0.dist-info/RECORD +13 -0
- ghidraxdbg-12.0.dist-info/WHEEL +5 -0
- ghidraxdbg-12.0.dist-info/licenses/LICENSE +11 -0
- ghidraxdbg-12.0.dist-info/top_level.txt +1 -0
ghidraxdbg/hooks.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
## ###
|
|
2
|
+
# IP: GHIDRA
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
##
|
|
16
|
+
from bisect import bisect_left, bisect_right
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
import functools
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import traceback
|
|
23
|
+
from typing import Any, Callable, Collection, Dict, Optional, TypeVar, cast
|
|
24
|
+
|
|
25
|
+
from ghidratrace.client import Schedule
|
|
26
|
+
from x64dbg_automate.events import EventType
|
|
27
|
+
from x64dbg_automate.models import BreakpointType
|
|
28
|
+
|
|
29
|
+
from . import commands, util
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
ALL_EVENTS = 0xFFFF
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=False)
|
|
36
|
+
class HookState:
|
|
37
|
+
installed = False
|
|
38
|
+
mem_catchpoint = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=False)
|
|
42
|
+
class ProcessState:
|
|
43
|
+
first = True
|
|
44
|
+
# For things we can detect changes to between stops
|
|
45
|
+
regions = False
|
|
46
|
+
modules = False
|
|
47
|
+
threads = False
|
|
48
|
+
breaks = True
|
|
49
|
+
watches = False
|
|
50
|
+
# For frames and threads that have already been synced since last stop
|
|
51
|
+
visited: set[Any] = field(default_factory=set)
|
|
52
|
+
waiting = False
|
|
53
|
+
|
|
54
|
+
def record(self, description: Optional[str] = None,
|
|
55
|
+
time: Optional[Schedule] = None) -> None:
|
|
56
|
+
first = self.first
|
|
57
|
+
self.first = False
|
|
58
|
+
trace = commands.STATE.require_trace()
|
|
59
|
+
if description is not None:
|
|
60
|
+
trace.snapshot(description, time=time)
|
|
61
|
+
if first:
|
|
62
|
+
commands.put_available()
|
|
63
|
+
commands.put_processes()
|
|
64
|
+
commands.put_environment()
|
|
65
|
+
commands.put_threads()
|
|
66
|
+
if self.threads:
|
|
67
|
+
commands.put_threads()
|
|
68
|
+
self.threads = False
|
|
69
|
+
thread = util.selected_thread()
|
|
70
|
+
if thread is not None:
|
|
71
|
+
if first or thread not in self.visited:
|
|
72
|
+
try:
|
|
73
|
+
commands.putreg()
|
|
74
|
+
commands.putmem('0x{:x}'.format(util.get_pc()),
|
|
75
|
+
"1", display_result=False)
|
|
76
|
+
commands.putmem('0x{:x}'.format(util.get_sp()-1),
|
|
77
|
+
"2", display_result=False)
|
|
78
|
+
commands.put_breakpoints(BreakpointType.BpNormal)
|
|
79
|
+
commands.put_breakpoints(BreakpointType.BpHardware)
|
|
80
|
+
commands.put_breakpoints(BreakpointType.BpMemory)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
#commands.put_frames()
|
|
84
|
+
self.visited.add(thread)
|
|
85
|
+
# TODO: hoping to support this at some point once the relevant APIs are exposed
|
|
86
|
+
# frame = util.selected_frame()
|
|
87
|
+
# hashable_frame = (thread, frame)
|
|
88
|
+
# if first or hashable_frame not in self.visited:
|
|
89
|
+
# self.visited.add(hashable_frame)
|
|
90
|
+
try:
|
|
91
|
+
if first or self.regions or self.modules:
|
|
92
|
+
commands.put_regions()
|
|
93
|
+
self.regions = False
|
|
94
|
+
self.modules = False
|
|
95
|
+
except:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
def record_continued(self) -> None:
|
|
99
|
+
try:
|
|
100
|
+
proc = util.selected_process()
|
|
101
|
+
commands.put_state(proc)
|
|
102
|
+
commands.put_breakpoints(BreakpointType.BpNormal)
|
|
103
|
+
commands.put_breakpoints(BreakpointType.BpHardware)
|
|
104
|
+
commands.put_breakpoints(BreakpointType.BpMemory)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def record_exited(self, exit_code: Optional[str] = None,
|
|
109
|
+
time: Optional[Schedule] = None) -> None:
|
|
110
|
+
trace = commands.STATE.require_trace()
|
|
111
|
+
if exit_code is not None:
|
|
112
|
+
trace.snapshot(f"Exited {exit_code}", time=time)
|
|
113
|
+
ipath = commands.PROCESS_PATTERN.format(procnum=util.last_process)
|
|
114
|
+
procobj = trace.proxy_object_path(ipath)
|
|
115
|
+
procobj.set_value('Exit Code', exit_code)
|
|
116
|
+
procobj.set_value('State', 'TERMINATED')
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=False)
|
|
120
|
+
class BrkState:
|
|
121
|
+
break_loc_counts: Dict[int, int] = field(default_factory=dict)
|
|
122
|
+
|
|
123
|
+
def update_brkloc_count(self, b, count: int) -> None:
|
|
124
|
+
self.break_loc_counts[b.GetID()] = count
|
|
125
|
+
|
|
126
|
+
def get_brkloc_count(self, b) -> int:
|
|
127
|
+
return self.break_loc_counts.get(b.GetID(), 0)
|
|
128
|
+
|
|
129
|
+
def del_brkloc_count(self, b) -> int:
|
|
130
|
+
if b not in self.break_loc_counts:
|
|
131
|
+
return 0 # TODO: Print a warning?
|
|
132
|
+
count = self.break_loc_counts[b.GetID()]
|
|
133
|
+
del self.break_loc_counts[b.GetID()]
|
|
134
|
+
return count
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
HOOK_STATE = HookState()
|
|
138
|
+
BRK_STATE = BrkState()
|
|
139
|
+
PROC_STATE: Dict[int, ProcessState] = {}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
C = TypeVar('C', bound=Callable)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def log_errors(func: C) -> C:
|
|
146
|
+
"""Wrap a function in a try-except that prints and reraises the exception.
|
|
147
|
+
|
|
148
|
+
This is needed for exceptions that occur during event callbacks.
|
|
149
|
+
"""
|
|
150
|
+
@functools.wraps(func)
|
|
151
|
+
def _func(*args, **kwargs) -> Any:
|
|
152
|
+
try:
|
|
153
|
+
return func(*args, **kwargs)
|
|
154
|
+
except:
|
|
155
|
+
traceback.print_exc()
|
|
156
|
+
raise
|
|
157
|
+
return cast(C, _func)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@log_errors
|
|
161
|
+
def on_state_changed(*args) -> None:
|
|
162
|
+
# print("ON_STATE_CHANGED")
|
|
163
|
+
ev_type = args[0].event_type
|
|
164
|
+
# print(ev_type)
|
|
165
|
+
proc = util.selected_process()
|
|
166
|
+
trace = commands.STATE.require_trace()
|
|
167
|
+
with trace.client.batch():
|
|
168
|
+
with trace.open_tx("State changed proc {}".format(proc)):
|
|
169
|
+
commands.put_state(proc)
|
|
170
|
+
if proc not in PROC_STATE:
|
|
171
|
+
if ev_type == EventType.EVENT_EXIT_PROCESS:
|
|
172
|
+
on_process_deleted(args)
|
|
173
|
+
return
|
|
174
|
+
PROC_STATE[proc].waiting = False
|
|
175
|
+
try:
|
|
176
|
+
if ev_type == EventType.EVENT_RESUME_DEBUG:
|
|
177
|
+
on_cont()
|
|
178
|
+
elif ev_type == EventType.EVENT_PAUSE_DEBUG:
|
|
179
|
+
on_stop()
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@log_errors
|
|
185
|
+
def on_breakpoint_hit(*args) -> None:
|
|
186
|
+
# print("ON_THREADS_CHANGED")
|
|
187
|
+
proc = util.selected_process()
|
|
188
|
+
if proc not in PROC_STATE:
|
|
189
|
+
return
|
|
190
|
+
data = args[0].event_data
|
|
191
|
+
PROC_STATE[proc].breaks = True
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@log_errors
|
|
196
|
+
def on_new_process(*args) -> None:
|
|
197
|
+
# print("ON_NEW_PROCESS")
|
|
198
|
+
trace = commands.STATE.trace
|
|
199
|
+
if trace is None:
|
|
200
|
+
return
|
|
201
|
+
with trace.client.batch():
|
|
202
|
+
with trace.open_tx("New Process {}".format(util.selected_process())):
|
|
203
|
+
commands.put_processes()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def on_process_selected() -> None:
|
|
207
|
+
# print("PROCESS_SELECTED")
|
|
208
|
+
proc = util.selected_process()
|
|
209
|
+
if proc not in PROC_STATE:
|
|
210
|
+
return
|
|
211
|
+
trace = commands.STATE.trace
|
|
212
|
+
if trace is None:
|
|
213
|
+
return
|
|
214
|
+
with trace.client.batch():
|
|
215
|
+
with trace.open_tx("Process {} selected".format(proc)):
|
|
216
|
+
PROC_STATE[proc].record()
|
|
217
|
+
commands.activate()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@log_errors
|
|
221
|
+
def on_process_deleted(*args) -> None:
|
|
222
|
+
# print("PROCESS_DELETED: args={}".format(args))
|
|
223
|
+
proc = util.selected_process()
|
|
224
|
+
on_exited(args)
|
|
225
|
+
if proc in PROC_STATE:
|
|
226
|
+
del PROC_STATE[proc]
|
|
227
|
+
trace = commands.STATE.trace
|
|
228
|
+
if trace is None:
|
|
229
|
+
return
|
|
230
|
+
with trace.client.batch():
|
|
231
|
+
with trace.open_tx("Process {} deleted".format(proc)):
|
|
232
|
+
commands.put_processes() # TODO: Could just delete the one....
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@log_errors
|
|
236
|
+
def on_threads_changed(*args) -> None:
|
|
237
|
+
# print("ON_THREADS_CHANGED")
|
|
238
|
+
data = args[0].event_data
|
|
239
|
+
proc = util.selected_process()
|
|
240
|
+
if proc not in PROC_STATE:
|
|
241
|
+
return
|
|
242
|
+
util.threads[data.dwThreadId] = data
|
|
243
|
+
state = PROC_STATE[proc]
|
|
244
|
+
state.threads = True
|
|
245
|
+
state.waiting = False
|
|
246
|
+
trace = commands.STATE.require_trace()
|
|
247
|
+
with trace.client.batch():
|
|
248
|
+
with trace.open_tx("Threads changed proc {}".format(proc)):
|
|
249
|
+
#commands.put_threads()
|
|
250
|
+
commands.put_state(proc)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def on_thread_selected(*args) -> None:
|
|
255
|
+
# print("THREAD_SELECTED: args={}".format(args))
|
|
256
|
+
# sys.stdout.flush()
|
|
257
|
+
nthrd = args[0][1]
|
|
258
|
+
nproc = util.selected_process()
|
|
259
|
+
if nproc not in PROC_STATE:
|
|
260
|
+
return
|
|
261
|
+
trace = commands.STATE.trace
|
|
262
|
+
if trace is None:
|
|
263
|
+
return
|
|
264
|
+
with trace.client.batch():
|
|
265
|
+
with trace.open_tx("Thread {}.{} selected".format(nproc, nthrd)):
|
|
266
|
+
commands.put_state(nproc)
|
|
267
|
+
state = PROC_STATE[nproc]
|
|
268
|
+
if state.waiting:
|
|
269
|
+
state.record_continued()
|
|
270
|
+
else:
|
|
271
|
+
state.record()
|
|
272
|
+
commands.activate()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def on_register_changed(regnum) -> None:
|
|
276
|
+
# print("REGISTER_CHANGED")
|
|
277
|
+
proc = util.selected_process()
|
|
278
|
+
if proc not in PROC_STATE:
|
|
279
|
+
return
|
|
280
|
+
trace = commands.STATE.trace
|
|
281
|
+
if trace is None:
|
|
282
|
+
return
|
|
283
|
+
with trace.client.batch():
|
|
284
|
+
with trace.open_tx("Register {} changed".format(regnum)):
|
|
285
|
+
commands.putreg()
|
|
286
|
+
commands.activate()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def on_memory_changed(space) -> None:
|
|
290
|
+
proc = util.selected_process()
|
|
291
|
+
if proc not in PROC_STATE:
|
|
292
|
+
return
|
|
293
|
+
trace = commands.STATE.trace
|
|
294
|
+
if trace is None:
|
|
295
|
+
return
|
|
296
|
+
# Not great, but invalidate the whole space
|
|
297
|
+
# UI will only re-fetch what it needs
|
|
298
|
+
# But, some observations will not be recovered
|
|
299
|
+
try:
|
|
300
|
+
with trace.client.batch():
|
|
301
|
+
with trace.open_tx("Memory changed"):
|
|
302
|
+
commands.putmem_state(0, 2**64, 'unknown')
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def on_cont(*args) -> None:
|
|
308
|
+
# print("ON CONT")
|
|
309
|
+
proc = util.selected_process()
|
|
310
|
+
if proc not in PROC_STATE:
|
|
311
|
+
return
|
|
312
|
+
trace = commands.STATE.trace
|
|
313
|
+
if trace is None:
|
|
314
|
+
return
|
|
315
|
+
state = PROC_STATE[proc]
|
|
316
|
+
with trace.client.batch():
|
|
317
|
+
with trace.open_tx("Continued"):
|
|
318
|
+
state.record_continued()
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def on_stop(*args) -> None:
|
|
323
|
+
# print("ON STOP")
|
|
324
|
+
proc = util.selected_process()
|
|
325
|
+
if proc not in PROC_STATE:
|
|
326
|
+
return
|
|
327
|
+
trace = commands.STATE.trace
|
|
328
|
+
if trace is None:
|
|
329
|
+
return
|
|
330
|
+
state = PROC_STATE[proc]
|
|
331
|
+
state.visited.clear()
|
|
332
|
+
time = None
|
|
333
|
+
with trace.client.batch():
|
|
334
|
+
with trace.open_tx("Stopped"):
|
|
335
|
+
description = "Stopped"
|
|
336
|
+
state.record(description, time)
|
|
337
|
+
try:
|
|
338
|
+
commands.put_event_thread()
|
|
339
|
+
except:
|
|
340
|
+
pass
|
|
341
|
+
commands.activate()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def on_exited(*args) -> None:
|
|
345
|
+
# print("ON EXITED")
|
|
346
|
+
trace = commands.STATE.trace
|
|
347
|
+
if trace is None:
|
|
348
|
+
return
|
|
349
|
+
state = PROC_STATE[util.last_process]
|
|
350
|
+
state.visited.clear()
|
|
351
|
+
with trace.client.batch():
|
|
352
|
+
with trace.open_tx("Exited"):
|
|
353
|
+
exit_code = args[0][0].event_data.dwExitCode
|
|
354
|
+
state.record_exited(exit_code)
|
|
355
|
+
commands.activate()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@log_errors
|
|
359
|
+
def on_modules_changed(*args) -> None:
|
|
360
|
+
# print("ON_MODULES_CHANGED")
|
|
361
|
+
#data = args[0].event_data
|
|
362
|
+
proc = util.selected_process()
|
|
363
|
+
if proc not in PROC_STATE:
|
|
364
|
+
return
|
|
365
|
+
state = PROC_STATE[proc]
|
|
366
|
+
state.modules = True
|
|
367
|
+
state.waiting = False
|
|
368
|
+
trace = commands.STATE.require_trace()
|
|
369
|
+
with trace.client.batch():
|
|
370
|
+
with trace.open_tx("Modules changed proc {}".format(proc)):
|
|
371
|
+
#commands.put_modules()
|
|
372
|
+
commands.put_state(proc)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def install_hooks() -> None:
|
|
376
|
+
# print("Installing hooks")
|
|
377
|
+
if HOOK_STATE.installed:
|
|
378
|
+
return
|
|
379
|
+
HOOK_STATE.installed = True
|
|
380
|
+
|
|
381
|
+
dbg = util.dbg.client
|
|
382
|
+
dbg.watch_debug_event(EventType.EVENT_OUTPUT_DEBUG_STRING, lambda x: on_breakpoint_hit(x))
|
|
383
|
+
dbg.watch_debug_event(EventType.EVENT_BREAKPOINT, lambda x: on_state_changed(x))
|
|
384
|
+
dbg.watch_debug_event(EventType.EVENT_SYSTEMBREAKPOINT, lambda x: on_state_changed(x))
|
|
385
|
+
dbg.watch_debug_event(EventType.EVENT_EXCEPTION, lambda x: on_state_changed(x))
|
|
386
|
+
dbg.watch_debug_event(EventType.EVENT_CREATE_THREAD, lambda x: on_threads_changed(x))
|
|
387
|
+
dbg.watch_debug_event(EventType.EVENT_EXIT_THREAD, lambda x: on_threads_changed(x))
|
|
388
|
+
dbg.watch_debug_event(EventType.EVENT_LOAD_DLL, lambda x: on_modules_changed(x))
|
|
389
|
+
dbg.watch_debug_event(EventType.EVENT_UNLOAD_DLL, lambda x: on_modules_changed(x))
|
|
390
|
+
dbg.watch_debug_event(EventType.EVENT_STEPPED, lambda x: on_state_changed(x))
|
|
391
|
+
dbg.watch_debug_event(EventType.EVENT_PAUSE_DEBUG, lambda x: on_state_changed(x))
|
|
392
|
+
dbg.watch_debug_event(EventType.EVENT_RESUME_DEBUG, lambda x: on_state_changed(x))
|
|
393
|
+
dbg.watch_debug_event(EventType.EVENT_ATTACH, lambda x: on_state_changed(x))
|
|
394
|
+
dbg.watch_debug_event(EventType.EVENT_DETACH, lambda x: on_state_changed(x))
|
|
395
|
+
dbg.watch_debug_event(EventType.EVENT_INIT_DEBUG, lambda x: on_state_changed(x))
|
|
396
|
+
dbg.watch_debug_event(EventType.EVENT_STOP_DEBUG, lambda x: on_state_changed(x))
|
|
397
|
+
dbg.watch_debug_event(EventType.EVENT_CREATE_PROCESS, lambda x: on_state_changed(x))
|
|
398
|
+
dbg.watch_debug_event(EventType.EVENT_EXIT_PROCESS, lambda x: on_state_changed(x))
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def remove_hooks() -> None:
|
|
402
|
+
# print("Removing hooks")
|
|
403
|
+
if HOOK_STATE.installed:
|
|
404
|
+
HOOK_STATE.installed = False
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def enable_current_process() -> None:
|
|
408
|
+
# print("Enable current process")
|
|
409
|
+
proc = util.selected_process()
|
|
410
|
+
PROC_STATE[proc] = ProcessState()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def disable_current_process() -> None:
|
|
414
|
+
# print("Disable current process")
|
|
415
|
+
proc = util.selected_process()
|
|
416
|
+
if proc in PROC_STATE:
|
|
417
|
+
# Silently ignore already disabled
|
|
418
|
+
del PROC_STATE[proc]
|
|
419
|
+
|