-
-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathdebugger.py
More file actions
1189 lines (979 loc) · 49.6 KB
/
debugger.py
File metadata and controls
1189 lines (979 loc) · 49.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
# Copyright (c) 2023-2025 Gabriele Digregorio, Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
from __future__ import annotations
from contextlib import contextmanager
from typing import TYPE_CHECKING
from libdebug.data.argument_list import ArgumentList
from libdebug.data.env_dict import EnvDict
from libdebug.liblog import liblog
from libdebug.utils.arch_mappings import map_arch
from libdebug.utils.elf_utils import elf_architecture, resolve_argv_path
from libdebug.utils.oop.alias import check_alias, check_aliased_property
from libdebug.utils.signal_utils import (
get_all_signal_numbers,
resolve_signal_name,
resolve_signal_number,
)
from libdebug.utils.syscall_utils import (
resolve_syscall_name,
resolve_syscall_number,
)
if TYPE_CHECKING:
from collections.abc import Callable
from libdebug.commlink.pipe_manager import PipeManager
from libdebug.data.breakpoint import Breakpoint
from libdebug.data.gdb_resume_event import GdbResumeEvent
from libdebug.data.memory_map import MemoryMap
from libdebug.data.memory_map_list import MemoryMapList
from libdebug.data.registers import Registers
from libdebug.data.signal_catcher import SignalCatcher
from libdebug.data.symbol import Symbol
from libdebug.data.symbol_list import SymbolList
from libdebug.data.syscall_handler import SyscallHandler
from libdebug.debugger.internal_debugger import InternalDebugger
from libdebug.memory.abstract_memory_view import AbstractMemoryView
from libdebug.snapshots.process.process_snapshot import ProcessSnapshot
from libdebug.snapshots.snapshot import Snapshot
from libdebug.state.thread_context import ThreadContext
class Debugger:
"""The Debugger class is the main class of `libdebug`. It contains all the methods needed to run and interact with the process."""
_sentinel: object = object()
"""A sentinel object."""
_internal_debugger: InternalDebugger
"""The internal debugger object."""
_previous_argv: list[str]
"""A copy of the previous argv state, used internally to detect changes to argv[0]."""
def __init__(self: Debugger) -> None:
pass
def post_init_(self: Debugger, internal_debugger: InternalDebugger) -> None:
"""Do not use this constructor directly. Use the `debugger` function instead."""
self._internal_debugger = internal_debugger
self._internal_debugger.start_up()
# We need to install the proper callbacks on the ArgumentList
self._configure_argument_list(self._internal_debugger.argv)
self._configure_env_dict()
@check_alias("r")
def run(self: Debugger, timeout: float = -1, redirect_pipes: bool = True) -> PipeManager | None:
"""Starts the process and waits for it to stop.
Args:
timeout (float): The timeout for the process to run. If -1, the process will run indefinitely.
redirect_pipes (bool): Whether to hook and redirect the pipes of the process to a PipeManager.
"""
return self._internal_debugger.run(timeout, redirect_pipes)
def attach(self: Debugger, pid: int) -> None:
"""Attaches to an existing process."""
self._internal_debugger.attach(pid)
def detach(self: Debugger) -> None:
"""Detaches from the process."""
self._internal_debugger.detach()
def kill(self: Debugger) -> None:
"""Kills the process."""
self._internal_debugger.kill()
def terminate(self: Debugger) -> None:
"""Interrupts the process, kills it and then terminates the background thread.
The debugger object will not be usable after this method is called.
This method should only be called to free up resources when the debugger object is no longer needed.
"""
self._internal_debugger.terminate()
@check_alias("c")
def cont(self: Debugger) -> None:
"""Continues the process."""
self._internal_debugger.cont()
@check_alias("int")
def interrupt(self: Debugger) -> None:
"""Interrupts the process."""
self._internal_debugger.interrupt()
@check_alias("w")
def wait(self: Debugger) -> None:
"""Waits for the process to stop."""
self._internal_debugger.wait()
def print_maps(self: Debugger) -> None:
"""Prints the memory maps of the process."""
liblog.warning("The `print_maps` method is deprecated. Use `d.pprint_maps` instead.")
self._internal_debugger.pprint_maps()
def pprint_maps(self: Debugger) -> None:
"""Prints the memory maps of the process."""
self._internal_debugger.pprint_maps()
def resolve_symbol(self: Debugger, symbol: str, file: str = "binary") -> int:
"""Resolves the address of the specified symbol.
Args:
symbol (str): The symbol to resolve.
file (str): The backing file to resolve the symbol in. Defaults to "binary"
Returns:
int: The address of the symbol.
"""
return self._internal_debugger.resolve_symbol(symbol, file)
@property
def symbols(self: Debugger) -> SymbolList[Symbol]:
"""Get the symbols of the process."""
return self._internal_debugger.symbols
@check_alias("bp")
def breakpoint(
self: Debugger,
position: int | str,
hardware: bool = False,
condition: str = "x",
length: int = 1,
callback: None | bool | Callable[[ThreadContext, Breakpoint], None] = None,
file: str = "hybrid",
) -> Breakpoint:
"""Sets a breakpoint at the specified location.
Args:
position (int | bytes): The location of the breakpoint.
hardware (bool, optional): Whether the breakpoint should be hardware-assisted or purely software. Defaults to False.
condition (str, optional): The trigger condition for the breakpoint. Defaults to None.
length (int, optional): The length of the breakpoint. Only for watchpoints. Defaults to 1.
callback (None | bool | Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the breakpoint is hit. If True, an empty callback will be set. Defaults to None.
file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
return self._internal_debugger.breakpoint(position, hardware, condition, length, callback, file)
@check_alias("wp")
def watchpoint(
self: Debugger,
position: int | str,
condition: str = "w",
length: int = 1,
callback: None | bool | Callable[[ThreadContext, Breakpoint], None] = None,
file: str = "hybrid",
) -> Breakpoint:
"""Sets a watchpoint at the specified location. Internally, watchpoints are implemented as breakpoints.
Args:
position (int | bytes): The location of the breakpoint.
condition (str, optional): The trigger condition for the watchpoint (either "w", "rw" or "x"). Defaults to "w".
length (int, optional): The size of the word in being watched (1, 2, 4 or 8). Defaults to 1.
callback (None | bool | Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the watchpoint is hit. If True, an empty callback will be set. Defaults to None.
file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
return self._internal_debugger.breakpoint(
position,
hardware=True,
condition=condition,
length=length,
callback=callback,
file=file,
)
def catch_signal(
self: Debugger,
signal: int | str,
callback: None | bool | Callable[[ThreadContext, SignalCatcher], None] = None,
recursive: bool = False,
) -> SignalCatcher:
"""Catch a signal in the target process.
Args:
signal (int | str): The signal to catch. If "*", "ALL", "all" or -1 is passed, all signals will be caught.
callback (None | bool | Callable[[ThreadContext, SignalCatcher], None], optional): A callback to be called when the signal is caught. If True, an empty callback will be set. Defaults to None.
recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher associated with the new signal should be considered as well. Defaults to False.
Returns:
SignalCatcher: The SignalCatcher object.
"""
return self._internal_debugger.catch_signal(signal, callback, recursive)
def hijack_signal(
self: Debugger,
original_signal: int | str,
new_signal: int | str,
recursive: bool = False,
) -> SyscallHandler:
"""Hijack a signal in the target process.
Args:
original_signal (int | str): The signal to hijack. If "*", "ALL", "all" or -1 is passed, all signals will be hijacked.
new_signal (int | str): The signal to hijack the original signal with.
recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher associated with the new signal should be considered as well. Defaults to False.
Returns:
SignalCatcher: The SignalCatcher object.
"""
return self._internal_debugger.hijack_signal(original_signal, new_signal, recursive)
def handle_syscall(
self: Debugger,
syscall: int | str,
on_enter: None | bool | Callable[[ThreadContext, SyscallHandler], None] = None,
on_exit: None | bool | Callable[[ThreadContext, SyscallHandler], None] = None,
recursive: bool = False,
) -> SyscallHandler:
"""Handle a syscall in the target process.
Args:
syscall (int | str): The syscall name or number to handle. If "*", "ALL", "all" or -1 is passed, all syscalls will be handled.
on_enter (None | bool |Callable[[ThreadContext, SyscallHandler], None], optional): The callback to execute when the syscall is entered. If True, an empty callback will be set. Defaults to None.
on_exit (None | bool | Callable[[ThreadContext, SyscallHandler], None], optional): The callback to execute when the syscall is exited. If True, an empty callback will be set. Defaults to None.
recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler associated with the new syscall should be considered as well. Defaults to False.
Returns:
SyscallHandler: The SyscallHandler object.
"""
return self._internal_debugger.handle_syscall(syscall, on_enter, on_exit, recursive)
def hijack_syscall(
self: Debugger,
original_syscall: int | str,
new_syscall: int | str,
recursive: bool = False,
**kwargs: int,
) -> SyscallHandler:
"""Hijacks a syscall in the target process.
Args:
original_syscall (int | str): The syscall name or number to hijack. If "*", "ALL", "all" or -1 is passed, all syscalls will be hijacked.
new_syscall (int | str): The syscall name or number to hijack the original syscall with.
recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler associated with the new syscall should be considered as well. Defaults to False.
**kwargs: (int, optional): The arguments to pass to the new syscall.
Returns:
SyscallHandler: The SyscallHandler object.
"""
return self._internal_debugger.hijack_syscall(original_syscall, new_syscall, recursive, **kwargs)
def gdb(
self: Debugger,
migrate_breakpoints: bool = True,
open_in_new_process: bool = True,
blocking: bool = True,
) -> GdbResumeEvent:
"""Migrates the current debugging session to GDB.
Args:
migrate_breakpoints (bool): Whether to migrate over the breakpoints set in libdebug to GDB.
open_in_new_process (bool): Whether to attempt to open GDB in a new process instead of the current one.
blocking (bool): Whether to block the script until GDB is closed.
"""
return self._internal_debugger.gdb(migrate_breakpoints, open_in_new_process, blocking)
@property
def is_in_gdb(self: Debugger) -> bool:
"""Returns whether the process is in GDB."""
return self._internal_debugger._is_migrated_to_gdb
def wait_for_gdb(self: Debugger) -> None:
"""Waits for the GDB process to migrate back to libdebug."""
self._internal_debugger.wait_for_gdb()
def r(self: Debugger, timeout: float = -1, redirect_pipes: bool = True) -> PipeManager | None:
"""Alias for the `run` method.
Starts the process and waits for it to stop.
Args:
timeout (float): The timeout for the process to run. If -1, the process will run indefinitely.
redirect_pipes (bool): Whether to hook and redirect the pipes of the process to a PipeManager.
"""
return self._internal_debugger.run(timeout, redirect_pipes)
def c(self: Debugger) -> None:
"""Alias for the `cont` method.
Continues the process.
"""
self._internal_debugger.cont()
def int(self: Debugger) -> None:
"""Alias for the `interrupt` method.
Interrupts the process.
"""
self._internal_debugger.interrupt()
def w(self: Debugger) -> None:
"""Alias for the `wait` method.
Waits for the process to stop.
"""
self._internal_debugger.wait()
def bp(
self: Debugger,
position: int | str,
hardware: bool = False,
condition: str = "x",
length: int = 1,
callback: None | bool | Callable[[ThreadContext, Breakpoint], None] = None,
file: str = "hybrid",
) -> Breakpoint:
"""Alias for the `breakpoint` method.
Sets a breakpoint at the specified location.
Args:
position (int | bytes): The location of the breakpoint.
hardware (bool, optional): Whether the breakpoint should be hardware-assisted or purely software. Defaults to False.
condition (str, optional): The trigger condition for the breakpoint. Defaults to None.
length (int, optional): The length of the breakpoint. Only for watchpoints. Defaults to 1.
callback (None | bool | Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the breakpoint is hit. If True, an empty callback will be set. Defaults to None.
file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
return self._internal_debugger.breakpoint(position, hardware, condition, length, callback, file)
def wp(
self: Debugger,
position: int | str,
condition: str = "w",
length: int = 1,
callback: None | bool | Callable[[ThreadContext, Breakpoint], None] = None,
file: str = "hybrid",
) -> Breakpoint:
"""Alias for the `watchpoint` method.
Sets a watchpoint at the specified location. Internally, watchpoints are implemented as breakpoints.
Args:
position (int | bytes): The location of the breakpoint.
condition (str, optional): The trigger condition for the watchpoint (either "w", "rw" or "x"). Defaults to "w".
length (int, optional): The size of the word in being watched (1, 2, 4 or 8). Defaults to 1.
callback (None | bool | Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the watchpoint is hit. If True, an empty callback will be set. Defaults to None.
file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
return self._internal_debugger.breakpoint(
position,
hardware=True,
condition=condition,
length=length,
callback=callback,
file=file,
)
@property
def arch(self: Debugger) -> str:
"""Get the architecture of the process."""
return self._internal_debugger.arch
@arch.setter
def arch(self: Debugger, value: str) -> None:
"""Set the architecture of the process."""
self._internal_debugger.arch = map_arch(value)
def _configure_argument_list(self: Debugger, argv: ArgumentList) -> None:
"""Sets up the ArgumentList with the before/after callbacks, and freezes argv[0] if needed."""
# If the user has not specified a different path, and argv is not empty, we should freeze argv[0]
if not self._internal_debugger._has_path_different_from_argv0 and argv and argv[0]:
argv.prevent_empty = True
else:
argv.prevent_empty = False
# We register a _before_callback that stores a copy of the current argv state
def _before_callback(_: list[str]) -> None:
"""Store a copy of the current argv state."""
# Changing argv is not allowed while the process is being debugged.
if self._internal_debugger.is_debugging:
raise RuntimeError("Cannot change argv while the process is running. Please kill it first.")
self._previous_argv = list(self._internal_debugger.argv) if self._internal_debugger.argv else []
# The _after callback should check if argv[0] has changed and update the path accordingly
def _after_callback(new_argv: list[str]) -> None:
"""An after callback that updates the path if argv[0] has changed."""
if not hasattr(self, "_previous_argv"):
raise RuntimeError("The _previous_argv attribute is not set. This should not happen.")
try:
if (
not self._internal_debugger._has_path_different_from_argv0
and new_argv
and new_argv[0] != self._previous_argv[0]
):
self._internal_debugger.clear_all_caches()
# Changing path can also change the architecture, so we need to update it
resolved_path = resolve_argv_path(new_argv[0])
self.arch = elf_architecture(resolved_path)
self._internal_debugger.path = resolved_path
except Exception:
# We revert to the previous argv state if something goes wrong
self._internal_debugger.argv = ArgumentList(self._previous_argv)
raise
# Set the callbacks on the ArgumentList
argv.set_callbacks(_before_callback, _after_callback)
@property
def argv(self: Debugger) -> ArgumentList:
"""The command line arguments of the debugged process."""
self._internal_debugger._ensure_process_stopped()
return self._internal_debugger.argv
@argv.setter
def argv(self: Debugger, value: str | list[str] | ArgumentList) -> None:
"""Set the command line arguments of the debugged process."""
self._internal_debugger._ensure_process_stopped()
# Changing argv is not allowed while the process is being debugged.
if self._internal_debugger.is_debugging:
raise RuntimeError("Cannot change argv while the process is running. Please kill it first.")
if not isinstance(value, str | list | ArgumentList):
raise TypeError("argv must be a string or a list of strings")
if isinstance(value, str):
value = ArgumentList([value])
elif isinstance(value, list):
value = ArgumentList(value)
# We need to install on the ArgumentList the proper callbacks
self._configure_argument_list(value)
# We have to check whether argv[0] has changed
# if so, we should invalidate everything and resolve the path again
# but that should be done only if path depended on argv[0]
if (
not self._internal_debugger._has_path_different_from_argv0
and self._internal_debugger.argv
and value[0] != self._internal_debugger.argv[0]
):
self._internal_debugger.clear_all_caches()
# Changing path can also change the architecture, so we need to update it
resolved_path = resolve_argv_path(value[0])
self.arch = elf_architecture(resolved_path)
self._internal_debugger.path = resolved_path
self._internal_debugger.argv = value
def _configure_env_dict(self: Debugger) -> None:
"""Sets up the EnvDict with the before callback."""
# We register a _before_callback that ensure that the process
# is not being debugged when the environment is changed
def _before_callback() -> None:
"""Ensure that the process is not being debugged when the environment is changed."""
# Changing env is not allowed while the process is being debugged.
if self._internal_debugger.is_debugging:
raise RuntimeError("Cannot change env while the process is running. Please kill it first.")
if self._internal_debugger.env is not None:
# If the env is already set, we just need to set the callback
self._internal_debugger.env.set_callback(_before_callback)
@property
def env(self: Debugger) -> EnvDict | None:
"""The environment variables of the debugged process."""
self._internal_debugger._ensure_process_stopped()
return self._internal_debugger.env
@env.setter
def env(self: Debugger, value: dict[str, str] | None) -> None:
"""Set the environment variables of the debugged process."""
self._internal_debugger._ensure_process_stopped()
# Changing env is not allowed while the process is being debugged.
if self._internal_debugger.is_debugging:
raise RuntimeError("Cannot change env while the process is running. Please kill it first.")
if value is not None and not isinstance(value, dict):
raise TypeError("env must be a dictionary or None")
self._internal_debugger.env = EnvDict(value) if value is not None else None
self._configure_env_dict()
@property
def path(self: Debugger) -> str:
"""The resolved path to the debugged binary."""
self._internal_debugger._ensure_process_stopped()
return self._internal_debugger.path
@path.setter
def path(self: Debugger, value: str) -> None:
"""Set the path to the debugged binary."""
self._internal_debugger._ensure_process_stopped()
if self._internal_debugger.is_debugging:
raise RuntimeError("Cannot change path while the process is running. Please kill it first.")
if not isinstance(value, str):
raise TypeError("path must be a string")
self._internal_debugger.clear_all_caches()
# resolve_argv_path can fail if the path is not valid
resolved_path = resolve_argv_path(value)
# Changing path can also change the architecture, so we need to update it
self.arch = elf_architecture(resolved_path)
self._internal_debugger.path = resolved_path
# We can also unfreeze argv[0] if it was frozen
self._internal_debugger.argv.prevent_empty = False
# We must note inside the debugger if the path is different from the first argument in argv
# This must be done last, otherwise we might get in an inconsistent state
# if one of the previous checks fails
self._internal_debugger._has_path_different_from_argv0 = True
@property
def kill_on_exit(self: Debugger) -> bool:
"""Get whether the process will be killed when the debugger exits."""
return self._internal_debugger.kill_on_exit
@kill_on_exit.setter
def kill_on_exit(self: Debugger, value: bool) -> None:
if not isinstance(value, bool):
raise TypeError("kill_on_exit must be a boolean")
self._internal_debugger.kill_on_exit = value
@property
def threads(self: Debugger) -> list[ThreadContext]:
"""Get the list of threads in the process."""
return self._internal_debugger.threads
@property
def breakpoints(self: Debugger) -> dict[int, Breakpoint]:
"""Get the breakpoints set on the process."""
return self._internal_debugger.breakpoints
@property
def children(self: Debugger) -> list[Debugger]:
"""Get the list of child debuggers."""
return self._internal_debugger.children
@property
def handled_syscalls(self: InternalDebugger) -> dict[int, SyscallHandler]:
"""Get the handled syscalls dictionary.
Returns:
dict[int, SyscallHandler]: the handled syscalls dictionary.
"""
return self._internal_debugger.handled_syscalls
@property
def caught_signals(self: InternalDebugger) -> dict[int, SignalCatcher]:
"""Get the caught signals dictionary.
Returns:
dict[int, SignalCatcher]: the caught signals dictionary.
"""
return self._internal_debugger.caught_signals
@property
def maps(self: Debugger) -> MemoryMapList[MemoryMap]:
"""Get the memory maps of the process."""
return self._internal_debugger.maps
@property
def pprint_syscalls(self: Debugger) -> bool:
"""Get the state of the pprint_syscalls flag.
Returns:
bool: True if the debugger should pretty print syscalls, False otherwise.
"""
return self._internal_debugger.pprint_syscalls
@pprint_syscalls.setter
def pprint_syscalls(self: Debugger, value: bool) -> None:
"""Set the state of the pprint_syscalls flag.
Args:
value (bool): the value to set.
"""
if not isinstance(value, bool):
raise TypeError("pprint_syscalls must be a boolean")
if value:
self._internal_debugger.enable_pretty_print()
else:
self._internal_debugger.disable_pretty_print()
self._internal_debugger.pprint_syscalls = value
@contextmanager
def pprint_syscalls_context(self: Debugger, value: bool) -> ...:
"""A context manager to temporarily change the state of the pprint_syscalls flag.
Args:
value (bool): the value to set.
"""
old_value = self.pprint_syscalls
self.pprint_syscalls = value
yield
self.pprint_syscalls = old_value
@property
def syscalls_to_pprint(self: Debugger) -> list[str] | None:
"""Get the syscalls to pretty print.
Returns:
list[str]: The syscalls to pretty print.
"""
if self._internal_debugger.syscalls_to_pprint is None:
return None
else:
return [
resolve_syscall_name(self._internal_debugger.arch, v)
for v in self._internal_debugger.syscalls_to_pprint
]
@syscalls_to_pprint.setter
def syscalls_to_pprint(self: Debugger, value: list[int | str] | None) -> None:
"""Get the syscalls to pretty print.
Args:
value (list[int | str] | None): The syscalls to pretty print.
"""
if value is None:
self._internal_debugger.syscalls_to_pprint = None
elif isinstance(value, list):
self._internal_debugger.syscalls_to_pprint = [
v if isinstance(v, int) else resolve_syscall_number(self._internal_debugger.arch, v) for v in value
]
else:
raise ValueError(
"syscalls_to_pprint must be a list of integers or strings or None.",
)
if self._internal_debugger.pprint_syscalls:
self._internal_debugger.enable_pretty_print()
@property
def syscalls_to_not_pprint(self: Debugger) -> list[str] | None:
"""Get the syscalls to not pretty print.
Returns:
list[str]: The syscalls to not pretty print.
"""
if self._internal_debugger.syscalls_to_not_pprint is None:
return None
else:
return [
resolve_syscall_name(self._internal_debugger.arch, v)
for v in self._internal_debugger.syscalls_to_not_pprint
]
@syscalls_to_not_pprint.setter
def syscalls_to_not_pprint(self: Debugger, value: list[int | str] | None) -> None:
"""Get the syscalls to not pretty print.
Args:
value (list[int | str] | None): The syscalls to not pretty print.
"""
if value is None:
self._internal_debugger.syscalls_to_not_pprint = None
elif isinstance(value, list):
self._internal_debugger.syscalls_to_not_pprint = [
v if isinstance(v, int) else resolve_syscall_number(self._internal_debugger.arch, v) for v in value
]
else:
raise ValueError(
"syscalls_to_not_pprint must be a list of integers or strings or None.",
)
if self._internal_debugger.pprint_syscalls:
self._internal_debugger.enable_pretty_print()
@property
def signals_to_block(self: Debugger) -> list[str]:
"""Get the signals to not forward to the process.
Returns:
list[str]: The signals to block.
"""
return [resolve_signal_name(v) for v in self._internal_debugger.signals_to_block]
@signals_to_block.setter
def signals_to_block(self: Debugger, signals: list[int | str]) -> None:
"""Set the signal to not forward to the process.
Args:
signals (list[int | str]): The signals to block.
"""
if not isinstance(signals, list):
raise TypeError("signals_to_block must be a list of integers or strings")
signals = [v if isinstance(v, int) else resolve_signal_number(v) for v in signals]
if not set(signals).issubset(get_all_signal_numbers()):
raise ValueError("Invalid signal number.")
self._internal_debugger.signals_to_block = signals
@property
def fast_memory(self: Debugger) -> bool:
"""Get the state of the fast_memory flag.
It is used to determine if the debugger should use a faster memory access method.
Returns:
bool: True if the debugger should use a faster memory access method, False otherwise.
"""
return self._internal_debugger.fast_memory
@fast_memory.setter
def fast_memory(self: Debugger, value: bool) -> None:
"""Set the state of the fast_memory flag.
It is used to determine if the debugger should use a faster memory access method.
Args:
value (bool): the value to set.
"""
self._internal_debugger._ensure_process_stopped()
if not isinstance(value, bool):
raise TypeError("fast_memory must be a boolean")
# If the process is currently being debugged and we are enabling fast_memory, we must
# ensure that fast_memory is actually available
# Setting fast_memory to False is always allowed, and if the process is not being debugged
# we have to perform the check at startup instead
if (
value
and self._internal_debugger.is_debugging
and not self._internal_debugger._process_memory_manager.is_available()
):
raise RuntimeError(
"The procfs memory interface could not be accessed (it could be read-only or not mounted). "
"Fast memory access is not available for the current process.",
)
self._internal_debugger.fast_memory = value
@property
def instruction_pointer(self: Debugger) -> int:
"""Get the main thread's instruction pointer."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].instruction_pointer
@instruction_pointer.setter
def instruction_pointer(self: Debugger, value: int) -> None:
"""Set the main thread's instruction pointer."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].instruction_pointer = value
@property
def syscall_arg0(self: Debugger) -> int:
"""Get the main thread's syscall argument 0."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].syscall_arg0
@syscall_arg0.setter
def syscall_arg0(self: Debugger, value: int) -> None:
"""Set the main thread's syscall argument 0."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].syscall_arg0 = value
@property
def syscall_arg1(self: Debugger) -> int:
"""Get the main thread's syscall argument 1."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].syscall_arg1
@syscall_arg1.setter
def syscall_arg1(self: Debugger, value: int) -> None:
"""Set the main thread's syscall argument 1."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].syscall_arg1 = value
@property
def syscall_arg2(self: Debugger) -> int:
"""Get the main thread's syscall argument 2."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].syscall_arg2
@syscall_arg2.setter
def syscall_arg2(self: Debugger, value: int) -> None:
"""Set the main thread's syscall argument 2."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].syscall_arg2 = value
@property
def syscall_arg3(self: Debugger) -> int:
"""Get the main thread's syscall argument 3."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].syscall_arg3
@syscall_arg3.setter
def syscall_arg3(self: Debugger, value: int) -> None:
"""Set the main thread's syscall argument 3."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].syscall_arg3 = value
@property
def syscall_arg4(self: Debugger) -> int:
"""Get the main thread's syscall argument 4."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].syscall_arg4
@syscall_arg4.setter
def syscall_arg4(self: Debugger, value: int) -> None:
"""Set the main thread's syscall argument 4."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].syscall_arg4 = value
@property
def syscall_arg5(self: Debugger) -> int:
"""Get the main thread's syscall argument 5."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].syscall_arg5
@syscall_arg5.setter
def syscall_arg5(self: Debugger, value: int) -> None:
"""Set the main thread's syscall argument 5."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].syscall_arg5 = value
@property
def syscall_number(self: Debugger) -> int:
"""Get the main thread's syscall number."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].syscall_number
@syscall_number.setter
def syscall_number(self: Debugger, value: int) -> None:
"""Set the main thread's syscall number."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].syscall_number = value
@property
def syscall_return(self: Debugger) -> int:
"""Get the main thread's syscall return value."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].syscall_return
@syscall_return.setter
def syscall_return(self: Debugger, value: int) -> None:
"""Set the main thread's syscall return value."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].syscall_return = value
@property
def regs(self: Debugger) -> Registers:
"""Get the main thread's registers."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self._internal_debugger._ensure_process_stopped_regs()
return self.threads[0].regs
@property
def dead(self: Debugger) -> bool:
"""Whether the process is dead."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].dead
@property
def zombie(self: Debugger) -> None:
"""Whether the main thread is a zombie."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].zombie
@check_aliased_property("mem")
def memory(self: Debugger) -> AbstractMemoryView:
"""The memory view of the process."""
return self._internal_debugger.memory
@property
def mem(self: Debugger) -> AbstractMemoryView:
"""Alias for the `memory` property.
The memory view of the process.
"""
return self._internal_debugger.memory
@check_aliased_property("pid")
def process_id(self: Debugger) -> int:
"""The process ID."""
return self._internal_debugger.process_id
@property
def pid(self: Debugger) -> int:
"""Alias for the `process_id` property.
The process ID.
"""
return self._internal_debugger.process_id
@check_aliased_property("tid")
def thread_id(self: Debugger) -> int:
"""The thread ID of the main thread."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].tid
@property
def tid(self: Debugger) -> int:
"""Alias for the `thread_id` property.
The thread ID of the main thread.
"""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].tid
@property
def running(self: Debugger) -> bool:
"""Whether the process is running."""
return self._internal_debugger.running
@property
def saved_ip(self: Debugger) -> int:
"""Get the saved instruction pointer of the main thread."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].saved_ip
@property
def exit_code(self: Debugger) -> int | None:
"""The main thread's exit code."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].exit_code
@property
def exit_signal(self: Debugger) -> str | None:
"""The main thread's exit signal."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].exit_signal
@property
def signal(self: Debugger) -> str | None:
"""The signal to be forwarded to the main thread."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].signal
@signal.setter
def signal(self: Debugger, signal: str | int) -> None:
"""Set the signal to forward to the main thread."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
self.threads[0].signal = signal
@property
def signal_number(self: Debugger) -> int | None:
"""The signal number to be forwarded to the main thread."""
if not self.threads:
raise RuntimeError("No threads available. Did you call `run` or `attach`?")
return self.threads[0].signal_number
def backtrace(self: Debugger, as_symbols: bool = False) -> list:
"""Returns the current backtrace of the main thread.