# coding=utf-8
# Copyright 2020-2024 Vector 35 Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ctypes
import traceback
import binaryninja
# import debugger
from . import _debuggercore as dbgcore
from .debugger_enums import *
from typing import Callable, List, Union
[docs]
class DebugProcess:
"""
DebugProcess represents a process in the target. It has the following fields:
* ``pid``: the ID of the process
* ``name``: the name of the process
"""
def __init__(self, pid, name):
self.pid = pid
self.name = name
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.pid == other.pid and self.name == other.name
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return not (self == other)
def __hash__(self):
return hash((self.pid, self.pid))
def __setattr__(self, name, value):
try:
object.__setattr__(self, name, value)
except AttributeError:
raise AttributeError(f"attribute '{name}' is read only")
def __repr__(self):
return f"<DebugProcess: {self.pid:#x}, {self.name}>"
[docs]
class DebugThread:
"""
DebugThread represents a thread in the target. It has the following fields:
* ``tid``: the ID of the thread. On different systems, this may be either the system thread ID, or a sequential\
index starting from zero.
* ``rip``: the current address (instruction pointer) of the thread
In the future, we should provide both the internal thread ID and the system thread ID.
"""
def __init__(self, tid, rip):
self.tid = tid
self.rip = rip
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.tid == other.tid and self.rip == other.rip
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return not (self == other)
def __hash__(self):
return hash((self.tid, self.rip))
def __setattr__(self, name, value):
try:
object.__setattr__(self, name, value)
except AttributeError:
raise AttributeError(f"attribute '{name}' is read only")
def __repr__(self):
return f"<DebugThread: {self.tid:#x} @ {self.rip:#x}>"
[docs]
class DebugModule:
"""
DebugModule represents a module in the target. It has the following fields:
* ``name``: the path of the module
* ``short_name``: the name of the module
* ``address``: the base load address of the module
* ``size``: the size of the module
* ``loaded``: not used
"""
def __init__(self, name, short_name, address, size, loaded):
self.name = name
self.short_name = short_name
self.address = address
self.size = size
self.loaded = loaded
[docs]
@staticmethod
def is_same_base_module(module1: Union[str, bytes], module2: Union[str, bytes]) -> bool:
return dbgcore.BNDebuggerIsSameBaseModule(module1, module2)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.name == other.name and self.short_name == other.short_name and self.address == other.address\
and self.size == other.size and self.loaded == other.loaded
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return not (self == other)
def __hash__(self):
return hash((self.name, self.short_name, self.address. self.size, self.loaded))
def __setattr__(self, name, value):
try:
object.__setattr__(self, name, value)
except AttributeError:
raise AttributeError(f"attribute '{name}' is read only")
def __repr__(self):
return f"<DebugModule: {self.name}, {self.address:#x}, {self.size:#x}>"
[docs]
class DebugRegister:
"""
DebugRegister represents a register in the target. It has the following fields:
* ``name``: the name of the register
* ``value``: the value of the register
* ``width``: the width of the register, in bits. E.g., ``rax`` register is 64-bits wide
* ``index``: the index of the register. This is reported by the DebugAdapter and should remain unchanged
* ``hint``: a string that shows the content of the memory pointed to by the register. It is empty if the register\
value do not point to a valid (mapped) memory region
"""
def __init__(self, name, value, width, index, hint):
self.name = name
self.value = value
self.width = width
self.index = index
self.hint = hint
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.name == other.name and self.value == other.value and self.width == other.width \
and self.index == other.index and self.hint == other.hint
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return not (self == other)
def __hash__(self):
return hash((self.name, self.value, self.width. self.index, self.hint))
def __setattr__(self, name, value):
try:
object.__setattr__(self, name, value)
except AttributeError:
raise AttributeError(f"attribute '{name}' is read only")
def __repr__(self):
hint_str = f", {self.hint}" if self.hint != '' else ''
return f"<DebugRegister: {self.name}, {self.value:#x}{hint_str}>"
[docs]
class DebugRegisters:
"""
DebugRegisters represents all registers of the target.
"""
def __init__(self, handle):
self.handle = handle
self.regs = {}
count = ctypes.c_ulonglong()
registers = dbgcore.BNDebuggerGetRegisters(handle, count)
for i in range(0, count.value):
bp = DebugRegister(registers[i].m_name, registers[i].m_value,
registers[i].m_width, registers[i].m_registerIndex, registers[i].m_hint)
self.regs[registers[i].m_name] = bp
dbgcore.BNDebuggerFreeRegisters(registers, count.value)
def __repr__(self) -> str:
return self.regs.__repr__()
def __getitem__(self, name):
if name not in self.regs:
return None
return self.regs[name]
def __setitem__(self, name, val):
dbgcore.BNDebuggerSetRegisterValue(self.handle, name, val)
def __len__(self):
return len(self.regs)
[docs]
class DebugBreakpoint:
"""
DebugBreakpoint represents a breakpoint in the target. It has the following fields:
* ``module``: the name of the module for which the breakpoint is in
* ``offset``: the offset of the breakpoint to the start of the module
* ``address``: the absolute address of the breakpoint
* ``enabled``: not used
"""
def __init__(self, module, offset, address, enabled):
self.module = module
self.offset = offset
self.address = address
self.enabled = enabled
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.module == other.module and self.offset == other.offset and self.address == other.address \
and self.enabled == other.enabled
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return not (self == other)
def __hash__(self):
return hash((self.module, self.offset, self.address. self.enabled))
def __setattr__(self, name, value):
try:
object.__setattr__(self, name, value)
except AttributeError:
raise AttributeError(f"attribute '{name}' is read only")
def __repr__(self):
return f"<DebugBreakpoint: {self.module}:{self.offset:#x}, {self.address:#x}>"
[docs]
class ModuleNameAndOffset:
"""
ModuleNameAndOffset represents an address that is relative to the start of module. It is useful when ASLR is on.
* ``module``: the name of the module for which the address is in
* ``offset``: the offset of the address to the start of the module
"""
def __init__(self, module, offset):
self.module = module
self.offset = offset
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.module == other.module and self.offset == other.offset
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return not (self == other)
def __lt__(self, other):
if self.module < other.module:
return True
elif self.module > other.module:
return False
return self.offset < other.offset
def __gt__(self, other):
if self.module > other.module:
return True
elif self.module < other.module:
return False
return self.offset > other.offset
def __hash__(self):
return hash((self.module, self.offset))
def __setattr__(self, name, value):
try:
object.__setattr__(self, name, value)
except AttributeError:
raise AttributeError(f"attribute '{name}' is read only")
[docs]
class DebugFrame:
"""
DebugFrame represents a frame in the stack trace. It has the following fields:
* ``index``: the index of the frame
* ``pc``: the program counter of the frame
* ``sp``: the stack pointer of the frame
* ``fp``: the frame pointer of the frame
* ``func_name``: the function name which the pc is in
* ``func_start``: the start of the function
* ``module``: the module of the pc
"""
def __init__(self, index, pc, sp, fp, func_name, func_start, module):
self.index = index
self.pc = pc
self.sp = sp
self.fp = fp
self.func_name = func_name
self.func_start = func_start
self.module = module
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.index == other.index and self.pc == other.pc and self.sp == other.sp \
and self.fp == other.fp and self.func_name == other.func_name and self.func_start == other.func_start \
and self.module == other.module
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return not (self == other)
def __hash__(self):
return hash((self.index, self.pc, self.sp, self.fp, self.func_name, self.func_start, self.module))
def __setattr__(self, name, value):
try:
object.__setattr__(self, name, value)
except AttributeError:
raise AttributeError(f"attribute '{name}' is read only")
def __repr__(self):
offset = self.pc - self.func_start
if self.func_name != '':
return f"<DebugFrame: {self.module}`{self.func_name} + {offset:#x}, sp: {self.sp:#x}, fp: {self.fp:#x}>"
else:
return f"<DebugFrame: {self.module} + {offset:#x}, sp: {self.sp:#x}, fp: {self.fp:#x}>"
[docs]
class TargetStoppedEventData:
"""
TargetStoppedEventData is the data associated with a TargetStoppedEvent
* ``reason``: the reason of the stop
* ``last_active_thread``: not used
* ``exit_code``: not used
* ``data``: extra data. Not used.
"""
def __init__(self, reason: DebugStopReason, last_active_thread: int, exit_code: int, data):
self.reason = reason
self.last_active_thread = last_active_thread
self.exit_code = exit_code
self.data = data
[docs]
class ErrorEventData:
"""
ErrorEventData is the data associated with a ErrorEvent
* ``error``: the error message
* ``data``: extra data. Not used.
"""
def __init__(self, error: str, data):
self.error = error
self.data = data
[docs]
class TargetExitedEventData:
"""
TargetExitedEventData is the data associated with a TargetExitedEvent
* ``exit_code``: the exit code of the target
"""
def __init__(self, exit_code: int):
self.exit_code = exit_code
[docs]
class StdOutMessageEventData:
"""
StdOutMessageEventData is the data associated with a StdOutMessageEvent
* ``message``: the message that the target writes to the stdout
"""
def __init__(self, message: str):
self.message = message
[docs]
class DebuggerEventData:
"""
DebuggerEventData is the collection of all possible data associated with the debugger events
* ``target_stopped_data``: the data associated with a TargetStoppedEvent
* ``error_data``: the data associated with an ErrorEvent
* ``absolute_address``: an integer address, which is used when an absolute breakpoint is added/removed
* ``relative_address``: a ModuleNameAndOffset, which is used when a relative breakpoint is added/removed
* ``exit_data``: the data associated with a TargetExitedEvent
* ``message_data``: message data, used by both StdOutMessageEvent and BackendMessageEvent
"""
def __init__(self, target_stopped_data: TargetStoppedEventData,
error_data: ErrorEventData,
absolute_address: int,
relative_address: ModuleNameAndOffset,
exit_data: TargetExitedEventData,
message_data: StdOutMessageEventData):
self.target_stopped_data = target_stopped_data
self.error_data = error_data
self.absolute_address = absolute_address
self.relative_address = relative_address
self.exit_data = exit_data
self.message_data = message_data
[docs]
class DebuggerEvent:
"""
DebuggerEvent is the event object that a debugger event callback receives
* ``type``: a DebuggerEventType that specifies the event type
* ``data``: a DebuggerEventData that specifies the event data
"""
def __init__(self, type: DebuggerEventType, data: DebuggerEventData):
self.type = type
self.data = data
DebuggerEventCallback = Callable[['DebuggerEvent'], None]
[docs]
class DebuggerEventWrapper:
# This has no functional purposes;
# we just need it to stop Python from prematurely freeing the object
_debugger_events = {}
[docs]
@classmethod
def register(cls, controller: 'DebuggerController', callback: DebuggerEventCallback, name: Union[str, bytes]) -> int:
callback_obj = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(dbgcore.BNDebuggerEvent))\
(lambda ctxt, event: cls._notify(event[0], callback))
handle = dbgcore.BNDebuggerRegisterEventCallback(controller.handle, callback_obj, name, None)
cls._debugger_events[handle] = callback_obj
return handle
[docs]
@classmethod
def remove(cls, controller: 'DebuggerController', index: int) -> None:
try:
dbgcore.BNDebuggerRemoveEventCallback(controller.handle, index)
del cls._debugger_events[index]
except:
binaryninja.log_error(f'invalid debugger event callback index {index}')
@staticmethod
def _notify(event: dbgcore.BNDebuggerEvent, callback: DebuggerEventCallback) -> None:
try:
data = event.data
target_stopped_data = TargetStoppedEventData(data.targetStoppedData.reason,
data.targetStoppedData.lastActiveThread,
data.targetStoppedData.exitCode,
data.targetStoppedData.data)
error_data = ErrorEventData(data.errorData.error, data.errorData.data)
absolute_addr = data.absoluteAddress
relative_addr = ModuleNameAndOffset(data.relativeAddress.module, data.relativeAddress.offset)
exit_data = TargetExitedEventData(data.exitData.exitCode)
message_data = StdOutMessageEventData(data.messageData.message)
event_data = DebuggerEventData(target_stopped_data, error_data, absolute_addr, relative_addr, exit_data,
message_data)
event = DebuggerEvent(event.type, event_data)
callback(event)
except:
binaryninja.log_error(traceback.format_exc())
[docs]
class DebuggerController:
"""
The ``DebuggerController`` object is the core of the debugger. Most debugger operations can be performed on it.
It takes in a ``BinaryView`` and creates a debugger for it. If a debugger is already existing for the very same
BinaryView object, the debugger is returned.
Most operations of the debugger are performed on this class. For example, we can launch the debugger as follows::
>>> bv = load("test/binaries/helloworld")
>>> dbg = DebuggerController(bv)
>>> dbg.launch()
True
When the ``launch()`` returns True, it means the debugger has launched the target successfully. The target breaks at
the entry point of the binary. Now we can perform other control operations on it, e.g., resume the target by calling
``go()``.
>>> dbg.go()
<DebugStopReason.ProcessExited: 2>
Since there are no other breakpoints in the target, the process executes and then exits.
All target control functions, e.g., ``go()``, ``step_into()``, etc, are blocking. They will not return until the
target breaks. In the future, we will switch to an asynchronous communication model where these functions return
before the operation is performed.
Starting from 4.1.5542-dev (0ad6b08b), the debugger no longer involves two binary views during debugging. Instead,
it always uses the incoming binary view that is used to create the controller, and memory regions that are not
present in the original binary view are represented using the new MemoryRegion API. The binary view can be accessed
by the ``data`` property.
"""
def __init__(self, bv: binaryninja.BinaryView):
# bv.handle has type binaryninja.core.BNBinaryView, which is different from dbgcore.BNBinaryView,
# so the casting here is necessary
# A different way to deal with is that instead of defining a BNBinaryView struct in the _debuggercore.py,
# do from binaryninja._binaryninjacore import BNBinaryView
bv_obj = ctypes.cast(bv.handle, ctypes.POINTER(dbgcore.BNBinaryView))
self.handle = dbgcore.BNGetDebuggerController(bv_obj)
[docs]
def destroy(self):
"""
Delete the DebuggerController object. Intended for internal use. Ordinary users do not need to call it.
"""
dbgcore.BNDebuggerDestroyController(self.handle)
@property
def data(self) -> binaryninja.BinaryView:
"""Get the (rebased) BinaryView of the debugger"""
result = dbgcore.BNDebuggerGetData(self.handle)
if result is None:
return None
result = ctypes.cast(result, ctypes.POINTER(binaryninja.core.BNBinaryView))
if result is None:
return None
return binaryninja.BinaryView(handle=result)
@property
@binaryninja.deprecation.deprecated(deprecated_in="4.1.5542", details='Debugger no longer uses the live view, Use :py:attr:`data` instead')
def live_view(self) -> binaryninja.BinaryView:
"""Get the live BinaryView of the debugger"""
return self.data
@property
def remote_arch(self) -> binaryninja.Architecture:
"""
Get the architecture of the running target (read-only). This could be different from the architecture of the original binary.
"""
result = dbgcore.BNDebuggerGetRemoteArchitecture(self.handle)
if result is None:
return None
result = ctypes.cast(result, ctypes.POINTER(binaryninja.core.BNArchitecture))
if result is None:
return None
return binaryninja.CoreArchitecture(handle=result)
@property
def connected(self) -> bool:
"""Whether the debugger has successfully connected to the target (read-only)"""
return dbgcore.BNDebuggerIsConnected(self.handle)
@property
def running(self) -> bool:
"""Whether the target is running (read-only)"""
return dbgcore.BNDebuggerIsRunning(self.handle)
@property
def stack_pointer(self) -> int:
"""The stack pointer of the target (read-only)"""
return dbgcore.BNDebuggerGetStackPointer(self.handle)
[docs]
def read_memory(self, address: int, size: int) -> binaryninja.DataBuffer:
"""
Read memory from the target.
One can also get the ``data`` BinaryView of the DebuggerController, and use ordinary read methods to read
its content.
:param address: address to read from
:param size: number of bytes to read
:return: always returns a DataBuffer. When the operation fails, the size of the DataBuffer is 0x0
"""
result = dbgcore.BNDebuggerReadMemory(self.handle, address, size)
if result is None:
return None
buffer = ctypes.cast(result, ctypes.POINTER(binaryninja.core.BNDataBuffer))
if buffer is None:
return None
return binaryninja.DataBuffer(handle=buffer)
[docs]
def write_memory(self, address: int, buffer) -> bool:
"""
Write memory of the target.
One can also get the ``data`` BinaryView of the DebuggerController, and use ordinary write methods to write
its content.
:param address: address to write to
:param buffer: buffer of data to write. It can be either bytes or a DataBuffer
:return: True on success, False on failure.
"""
if isinstance(buffer, bytes):
buffer = binaryninja.DataBuffer(buffer)
buffer_obj = ctypes.cast(buffer.handle, ctypes.POINTER(dbgcore.BNDataBuffer))
return dbgcore.BNDebuggerWriteMemory(self.handle, address, buffer_obj)
@property
def processes(self) -> List[DebugProcess]:
"""
The processes of the target.
"""
count = ctypes.c_ulonglong()
process_list = dbgcore.BNDebuggerGetProcessList(self.handle, count)
result = []
for i in range(0, count.value):
process = DebugProcess(process_list[i].m_pid, process_list[i].m_processName)
result.append(process)
dbgcore.BNDebuggerFreeProcessList(process_list, count.value)
return result
@property
def threads(self) -> List[DebugThread]:
"""
The threads of the target.
"""
count = ctypes.c_ulonglong()
threads = dbgcore.BNDebuggerGetThreads(self.handle, count)
result = []
for i in range(0, count.value):
bp = DebugThread(threads[i].m_tid, threads[i].m_rip)
result.append(bp)
dbgcore.BNDebuggerFreeThreads(threads, count.value)
return result
@property
def active_thread(self) -> DebugThread:
"""
The active thread of the target. (read/write)
:getter: returns the active thread of the target
:setter: sets the active thread of the target
"""
active_thread = dbgcore.BNDebuggerGetActiveThread(self.handle)
return DebugThread(active_thread.m_tid, active_thread.m_rip)
@active_thread.setter
def active_thread(self, thread: DebugThread) -> None:
dbgcore.BNDebuggerSetActiveThread(self.handle, dbgcore.BNDebugThread(thread.tid, thread.rip))
[docs]
def suspend_thread(self, tid: int) -> bool:
"""
Suspends a thread by thread id.
:param tid: thread id
"""
return dbgcore.BNDebuggerSuspendThread(self.handle, tid)
[docs]
def resume_thread(self, tid: int) -> bool:
"""
Resumes a thread by thread id.
:param tid: thread id
"""
return dbgcore.BNDebuggerResumeThread(self.handle, tid)
@property
def modules(self) -> List[DebugModule]:
"""
The modules of the target
:return: a list of ``DebugModule``
"""
count = ctypes.c_ulonglong()
modules = dbgcore.BNDebuggerGetModules(self.handle, count)
result = []
for i in range(0, count.value):
bp = DebugModule(modules[i].m_name, modules[i].m_short_name, modules[i].m_address, modules[i].m_size, modules[i].m_loaded)
result.append(bp)
dbgcore.BNDebuggerFreeModules(modules, count.value)
return result
@property
def regs(self) -> DebugRegisters:
"""
All registers of the target
:return: a list of ``DebugRegister``
"""
return DebugRegisters(self.handle)
[docs]
def get_reg_value(self, reg: Union[str, bytes]) -> int:
"""
Get the value of one register by its name
:param reg: the name of the register
"""
return dbgcore.BNDebuggerGetRegisterValue(self.handle, reg)
[docs]
def set_reg_value(self, reg: Union[str, bytes], value: int) -> bool:
"""
Set value of register
:param reg: the name of the register
:param value: new value of the register
"""
return dbgcore.BNDebuggerSetRegisterValue(self.handle, reg, value)
# target control
[docs]
def launch(self) -> bool:
"""
Launch the target
"""
return dbgcore.BNDebuggerLaunch(self.handle)
[docs]
def launch_and_wait(self) -> bool:
"""
Launch the target and wait for all debugger events to be processed
"""
return dbgcore.BNDebuggerLaunchAndWait(self.handle)
[docs]
def restart(self) -> None:
"""
Restart the target
"""
dbgcore.BNDebuggerRestart(self.handle)
[docs]
def quit(self) -> None:
"""
Terminate the target
"""
dbgcore.BNDebuggerQuit(self.handle)
[docs]
def quit_and_wait(self) -> None:
"""
Terminate the target, and wait for all callback to be called
"""
dbgcore.BNDebuggerQuitAndWait(self.handle)
[docs]
def connect(self) -> bool:
"""
Connect to a remote target (process)
The host and port of the remote target must first be specified by setting `remote_host` and `remote_port`
"""
return dbgcore.BNDebuggerConnect(self.handle)
[docs]
def connect_and_wait(self) -> bool:
"""
Connect to a remote target (process) and wait for all debugger events to be processed
The host and port of the remote target must first be specified by setting `remote_host` and `remote_port`
"""
return dbgcore.BNDebuggerConnectAndWait(self.handle)
[docs]
def connect_to_debug_server(self) -> bool:
"""
Connect to a debug server.
The host and port of the debug server must first be specified by setting `remote_host` and `remote_port`
"""
return dbgcore.BNDebuggerConnectToDebugServer(self.handle)
[docs]
def disconnect_from_debug_server(self) -> None:
"""`
Disconnect from a debug server.
"""
dbgcore.BNDebuggerDisconnectDebugServer(self.handle)
[docs]
def detach(self) -> None:
"""
Detach the target, and let it execute on its own.
"""
dbgcore.BNDebuggerQuit(self.handle)
[docs]
def pause(self) -> None:
"""
Pause a running target
"""
dbgcore.BNDebuggerPause(self.handle)
[docs]
def launch_or_connect(self) -> None:
"""
Launch or connect to the target. Intended for internal use. Ordinary users do not need to call it.
"""
dbgcore.BNDebuggerLaunchOrConnect(self.handle)
[docs]
def attach(self) -> bool:
"""
Attach to a running process
The PID of the target process must be set via DebuggerState.pid_attach
"""
return dbgcore.BNDebuggerAttach(self.handle)
[docs]
def attach_and_wait(self) -> bool:
"""
Attach to a running process and wait until all debugger events are processed
The PID of the target process must be set via DebuggerState.pid_attach
"""
return dbgcore.BNDebuggerAttachAndWait(self.handle)
[docs]
def go(self) -> bool:
"""
Resume the target.
The call is asynchronous and returns before the target stops.
:return: whether the operation is successfully requested
"""
return dbgcore.BNDebuggerGo(self.handle)
# TODO: Improve the documentation on reverse-execution functions
[docs]
def go_reverse(self) -> bool:
"""
Resume the target in reverse.
The call is asynchronous and returns before the target stops.
:return: whether the operation is successfully requested
"""
return dbgcore.BNDebuggerGoReverse(self.handle)
[docs]
def step_into(self, il: binaryninja.FunctionGraphType = binaryninja.FunctionGraphType.NormalFunctionGraph) -> bool:
"""
Perform a step into on the target.
When the next instruction is not a call, execute the next instruction. When the next instruction is a call,
follow the call the get into the first instruction of the call.
The operation can be performed on an IL level specified by the ``il`` parameter, which then either executes the
next IL instruction, or follow into the IL function. Note, the underlying operation is still performed at the
disassembly level because that is the only thing a debugger understands. The high-level operations are simulated
on top of the disassembly and analysis.
Some limitations are known with stepping into on IL.
The call is asynchronous and returns before the target stops.
:param il: optional IL level to perform the operation at
:return: whether the operation is successfully requested
"""
return dbgcore.BNDebuggerStepInto(self.handle, il)
[docs]
def step_into_reverse(self, il: binaryninja.FunctionGraphType = binaryninja.FunctionGraphType.NormalFunctionGraph) -> bool:
"""
Perform a step into on the target in reverse.
When the previous instruction is not a call, step backwards. When the previous instruction is a call,
follow the call into the last instruction of the function.
The operation can be performed on an IL level specified by the ``il`` parameter, which then either executes the
next IL instruction, or follow into the IL function. Note, the underlying operation is still performed at the
disassembly level because that is the only thing a debugger understands. The high-level operations are simulated
on top of the disassembly and analysis.
Some limitations are known with stepping into on IL.
The call is asynchronous and returns before the target stops.
:param il: optional IL level to perform the operation at
:return: whether the operation is successfully requested
"""
return dbgcore.BNDebuggerStepIntoReverse(self.handle, il)
[docs]
def step_over(self, il: binaryninja.FunctionGraphType = binaryninja.FunctionGraphType.NormalFunctionGraph) -> bool:
"""
Perform a step over on the target.
When the next instruction is not a call, execute the next instruction. When the next instruction is a call,
complete the execution of the function and break at next instruction.
The operation can be performed on an IL level specified by the ``il`` parameter, which then either executes the
next IL instruction, or completes the IL function. Note, the underlying operation is still performed at the
disassembly level because that is the only thing a debugger understands. The high-level operations are simulated
on top of the disassembly and analysis.
Some limitations are known with stepping over on IL.
The call is asynchronous and returns before the target stops.
:param il: optional IL level to perform the operation at
:return: whether the operation is successfully requested
"""
return dbgcore.BNDebuggerStepOver(self.handle, il)
[docs]
def step_over_reverse(self, il: binaryninja.FunctionGraphType = binaryninja.FunctionGraphType.NormalFunctionGraph) -> bool:
"""
Perform a step over on the target in reverse.
When the previous instruction is not a call, step backwards. When the previous instruction is a call,
step back over the call.
The operation can be performed on an IL level specified by the ``il`` parameter, which then either executes the
next IL instruction, or completes the IL function. Note, the underlying operation is still performed at the
disassembly level because that is the only thing a debugger understands. The high-level operations are simulated
on top of the disassembly and analysis.
Some limitations are known with stepping over on IL.
The call is asynchronous and returns before the target stops.
:param il: optional IL level to perform the operation at
:return: whether the operation is successfully requested
"""
return dbgcore.BNDebuggerStepOverReverse(self.handle, il)
[docs]
def step_return(self) -> bool:
"""
Perform a step return on the target.
Step return completes the execution of the current function and returns to its caller. This operation relies
heavily on stack frame analysis, which is done by the DebugAdapters.
If a DebugAdapter does not support (i.e., overload) this function, a fallback handling is provided by the
DebuggerController. It checks the MLIL function and put breakpoints on all returning instructions and then resume
the target. By the time it breaks, the target is about to return from the current function.
This fallback behavior is slightly different from that offered by the LLDB and DbgEng adapter, which returns
from the current function and break afterwards.
The call is asynchronous and returns before the target stops.
:return: whether the operation is successfully requested
"""
return dbgcore.BNDebuggerStepReturn(self.handle)
[docs]
def step_return_reverse(self) -> bool:
"""
Perform a step return on the target in reverse.
Step return reverses the execution of the current function and returns to its caller. This operation relies
heavily on stack frame analysis, which is done by the DebugAdapters.
If a DebugAdapter does not support (i.e., overload) this function, a fallback handling is provided by the
DebuggerController. It checks the MLIL function and put breakpoints on all returning instructions and then resume
the target. By the time it breaks, the target is about to return from the current function.
This fallback behavior is slightly different from that offered by the LLDB and DbgEng adapter, which returns
from the current function and break afterwards.
The call is asynchronous and returns before the target stops.
:return: whether the operation is successfully requested
"""
return dbgcore.BNDebuggerStepReturnReverse(self.handle)
[docs]
def run_to(self, address) -> bool:
"""
Resume the target, and wait for it to break at the given address(es).
The address parameter can be either an integer, or a list of integers.
Internally, the debugger places breakpoints on these addresses, resume the target, and wait for the target
to break. Then the debugger removes the added breakpoints.
The call is asynchronous and returns before the target stops.
:return: whether the operation is successfully requested
"""
if isinstance(address, int):
address = [address]
if not isinstance(address, list):
raise NotImplementedError
addr_list = (ctypes.c_uint64 * len(address))()
for i in range(len(address)):
addr_list[i] = address[i]
return dbgcore.BNDebuggerRunTo(self.handle, addr_list, len(address))
[docs]
def go_and_wait(self) -> DebugStopReason:
"""
Resume the target.
The call is blocking and only returns when the target stops.
:return: the reason for the stop
"""
return DebugStopReason(dbgcore.BNDebuggerGoAndWait(self.handle))
[docs]
def go_reverse_and_wait(self) -> DebugStopReason:
"""
Resume the target in reverse.
The call is blocking and only returns when the target stops.
:return: the reason for the stop
"""
return DebugStopReason(dbgcore.BNDebuggerGoReverseAndWait(self.handle))
[docs]
def step_into_and_wait(self, il: binaryninja.FunctionGraphType =
binaryninja.FunctionGraphType.NormalFunctionGraph) -> DebugStopReason:
"""
Perform a step into on the target in reverse.
When the next instruction is not a call, execute the next instruction. When the next instruction is a call,
follow the call the get into the first instruction of the call.
The operation can be performed on an IL level specified by the ``il`` parameter, which then either executes the
next IL instruction, or follow into the IL function. Note, the underlying operation is still performed at the
disassembly level because that is the only thing a debugger understands. The high-level operations are simulated
on top of the disassembly and analysis.
Some limitations are known with stepping into on IL.
The call is blocking and only returns when the target stops.
:param il: optional IL level to perform the operation at
:return: the reason for the stop
"""
return DebugStopReason(dbgcore.BNDebuggerStepIntoAndWait(self.handle, il))
[docs]
def step_into_reverse_and_wait(self, il: binaryninja.FunctionGraphType =
binaryninja.FunctionGraphType.NormalFunctionGraph) -> DebugStopReason:
"""
Perform a reverse step into on the target.
When the previous instruction is not a call, reverse the program to the previous state. When the previous instruction is a call,
follow the call to get into the last instruction of the call.
The operation can be performed on an IL level specified by the ``il`` parameter, which then either reverses the current IL instruction, or follow into the previous IL function. Note, the underlying operation is still performed at the
disassembly level because that is the only thing a debugger understands. The high-level operations are simulated
on top of the disassembly and analysis.
Some limitations are known with stepping into on IL.
The call is blocking and only returns when the target stops.
:param il: optional IL level to perform the operation at
:return: the reason for the stop
"""
return DebugStopReason(dbgcore.BNDebuggerStepIntoReverseAndWait(self.handle, il))
[docs]
def step_over_and_wait(self, il: binaryninja.FunctionGraphType =
binaryninja.FunctionGraphType.NormalFunctionGraph) -> DebugStopReason:
"""
Perform a step over on the target.
When the next instruction is not a call, execute the next instruction. When the next instruction is a call,
complete the execution of the function and break at next instruction.
The operation can be performed on an IL level specified by the ``il`` parameter, which then either executes the
next IL instruction, or completes the IL function. Note, the underlying operation is still performed at the
disassembly level because that is the only thing a debugger understands. The high-level operations are simulated
on top of the disassembly and analysis.
Some limitations are known with stepping over on IL.
The call is blocking and only returns when the target stops.
:param il: optional IL level to perform the operation at
:return: the reason for the stop
"""
return DebugStopReason(dbgcore.BNDebuggerStepOverAndWait(self.handle, il))
[docs]
def step_over_reverse_and_wait(self, il: binaryninja.FunctionGraphType =
binaryninja.FunctionGraphType.NormalFunctionGraph) -> DebugStopReason:
"""
Perform a step over on the target in reverse.
When the next instruction is not a call, execute the next instruction. When the next instruction is a call,
complete the execution of the function and break at next instruction.
The operation can be performed on an IL level specified by the ``il`` parameter, which then either executes the
next IL instruction, or completes the IL function. Note, the underlying operation is still performed at the
disassembly level because that is the only thing a debugger understands. The high-level operations are simulated
on top of the disassembly and analysis.
Some limitations are known with stepping over on IL.
The call is blocking and only returns when the target stops.
:param il: optional IL level to perform the operation at
:return: the reason for the stop
"""
return DebugStopReason(dbgcore.BNDebuggerStepOverReverseAndWait(self.handle, il))
[docs]
def step_return_and_wait(self) -> DebugStopReason:
"""
Perform a step return on the target.
Step return completes the execution of the current function and returns to its caller. This operation relies
heavily on stack frame analysis, which is done by the DebugAdapters.
If a DebugAdapter does not support (i.e., overload) this function, a fallback handling is provided by the
DebuggerController. It checks the MLIL function and put breakpoints on all returning instructions and then resume
the target. By the time it breaks, the target is about to return from the current function.
This fallback behavior is slightly different from that offered by the LLDB and DbgEng adapter, which returns
from the current function and break afterwards.
The call is blocking and only returns when the target stops.
:return: the reason for the stop
"""
return DebugStopReason(dbgcore.BNDebuggerStepReturnAndWait(self.handle))
[docs]
def run_to_and_wait(self, address) -> DebugStopReason:
"""
Resume the target, and wait for it to break at the given address(es).
The address parameter can be either an integer, or a list of integers.
Internally, the debugger places breakpoints on these addresses, resume the target, and wait for the target
to break. Then the debugger removes the added breakpoints.
The call is blocking and only returns when the target stops.
:return: the reason for the stop
"""
if isinstance(address, int):
address = [address]
if not isinstance(address, list):
raise NotImplementedError
addr_list = (ctypes.c_uint64 * len(address))()
for i in range(len(address)):
addr_list[i] = address[i]
return DebugStopReason(dbgcore.BNDebuggerRunToAndWait(self.handle, addr_list, len(address)))
[docs]
def pause_and_wait(self) -> None:
"""
Pause a running target.
The call is blocking and only returns when the target stops.
"""
dbgcore.BNDebuggerPauseAndWait(self.handle)
[docs]
def restart_and_wait(self) -> None:
"""
Restart a running target.
The call is blocking and only returns when the target stops again after the restart.
"""
dbgcore.BNDebuggerRestartAndWait(self.handle)
@property
def adapter_type(self) -> str:
"""
The name for the current DebugAdapter. (read/write)
:getter: returns the name of the current DebugAdapter
:setter: sets the DebugAdapter to use
"""
return dbgcore.BNDebuggerGetAdapterType(self.handle)
@adapter_type.setter
def adapter_type(self, adapter: Union[str, bytes]) -> None:
dbgcore.BNDebuggerSetAdapterType(self.handle, adapter)
@property
def connection_status(self) -> DebugAdapterConnectionStatus:
"""
Get the connection status of the debugger
"""
return DebugAdapterConnectionStatus(dbgcore.BNDebuggerGetConnectionStatus(self.handle))
@property
def target_status(self) -> DebugAdapterTargetStatus:
"""
Get the status of the target
"""
return DebugAdapterTargetStatus(dbgcore.BNDebuggerGetTargetStatus(self.handle))
@property
def remote_host(self) -> str:
"""
The remote host to connect to. (read/write)
``remote_host`` and ``remote_port`` are only useful for remote debugging.
:getter: returns the remote host
:setter: sets the remote host
"""
return dbgcore.BNDebuggerGetRemoteHost(self.handle)
@remote_host.setter
def remote_host(self, host: Union[str, bytes]) -> None:
dbgcore.BNDebuggerSetRemoteHost(self.handle, host)
@property
def remote_port(self) -> int:
"""
The remote port to connect to. (read/write)
``remote_host`` and ``remote_port`` are only useful for remote debugging.
:getter: returns the remote port
:setter: sets the remote port
"""
return dbgcore.BNDebuggerGetRemotePort(self.handle)
@remote_port.setter
def remote_port(self, port: int) -> None:
dbgcore.BNDebuggerSetRemotePort(self.handle, port)
@property
def pid_attach(self) -> int:
"""
The PID to attach to. (read/write)
``pid_attach`` is only useful for connecting to a running process using PID.
:getter: returns the remote port
:setter: sets the remote port
"""
return dbgcore.BNDebuggerGetPIDAttach(self.handle)
@remote_port.setter
def pid_attach(self, pid: int) -> None:
dbgcore.BNDebuggerSetPIDAttach(self.handle, pid)
@property
def executable_path(self) -> str:
"""
The path of the executable. (read/write)
This can be set before launching the target. Be default, it is the path of the FileMetadata
(``bv.file.filename``)
:getter: returns the executable path
:setter: sets the executable path
"""
return dbgcore.BNDebuggerGetExecutablePath(self.handle)
@executable_path.setter
def executable_path(self, path: Union[str, bytes]) -> None:
dbgcore.BNDebuggerSetExecutablePath(self.handle, path)
@property
def input_file(self) -> str:
"""
The input file used to create the database
This should be set before launching the target. Be default, it is the path of the FileMetadata
(``bv.file.filename``)
:getter: returns the input file path
:setter: sets the input file path
"""
return dbgcore.BNDebuggerGetInputFile(self.handle)
@input_file.setter
def input_file(self, path: Union[str, bytes]) -> None:
dbgcore.BNDebuggerSetInputFile(self.handle, path)
@property
def working_directory(self) -> str:
"""
The path of the target. (read/write)
This can be set before launching the target to configure a working directory. Be default, it is the path of the
binaryninja executable. In the future, we will change the default working directory to the folder that the
executable is in.
:getter: returns the working directory
:setter: sets the working directory
"""
return dbgcore.BNDebuggerGetWorkingDirectory(self.handle)
@working_directory.setter
def working_directory(self, path: Union[str, bytes]) -> None:
dbgcore.BNDebuggerSetWorkingDirectory(self.handle, path)
@property
def request_terminal_emulator(self) -> bool:
"""
Whether to run the target in a separate terminal. (read/write)
The default value is false.
This can be set before launching the target to configure whether the target should be executed in a separate
terminal. On Linux and macOS, when set, the target runs in its own terminal and the DebuggerController cannot
receive notification of stdout output, or write to its stdin. All input/output must be performed in the target's
console. On Windows, this option has no effect and the target always runs in its own terminal.
:getter: returns whether to run the target in a separate terminal
:setter: sets whether to run the target in a separate terminal
"""
return dbgcore.BNDebuggerGetRequestTerminalEmulator(self.handle)
@request_terminal_emulator.setter
def request_terminal_emulator(self, requested: bool) -> None:
dbgcore.BNDebuggerSetRequestTerminalEmulator(self.handle, requested)
@property
def cmd_line(self) -> str:
"""
The command line arguments of the target. (read/write)
This can be set before launching the target to specify the command line arguments. The arguments are supplied as
a single string. The string is NOT shell expanded, which means the user must properly escape it if needed.
:getter: returns the command line arguments
:setter: sets the command line arguments
"""
return dbgcore.BNDebuggerGetCommandLineArguments(self.handle)
@cmd_line.setter
def cmd_line(self, arguments: Union[str, bytes]) -> None:
dbgcore.BNDebuggerSetCommandLineArguments(self.handle, arguments)
@property
def breakpoints(self) -> List[DebugBreakpoint]:
"""
The list of breakpoints
"""
count = ctypes.c_ulonglong()
breakpoints = dbgcore.BNDebuggerGetBreakpoints(self.handle, count)
result = []
for i in range(0, count.value):
bp = DebugBreakpoint(breakpoints[i].module, breakpoints[i].offset, breakpoints[i].address, breakpoints[i].enabled)
result.append(bp)
dbgcore.BNDebuggerFreeBreakpoints(breakpoints, count.value)
return result
[docs]
def delete_breakpoint(self, address):
"""
Delete a breakpoint
The input can be either an absolute address, or a ModuleNameAndOffset, which specifies a relative address to the
start of a module. The latter is useful for ASLR.
:param address: the address of breakpoint to delete
"""
if isinstance(address, int):
dbgcore.BNDebuggerDeleteAbsoluteBreakpoint(self.handle, address)
elif isinstance(address, ModuleNameAndOffset):
dbgcore.BNDebuggerDeleteRelativeBreakpoint(self.handle, address.module, address.offset)
else:
raise NotImplementedError
[docs]
def add_breakpoint(self, address):
"""
Add a breakpoint
The input can be either an absolute address, or a ModuleNameAndOffset, which specifies a relative address to the
start of a module. The latter is useful for ASLR.
:param address: the address of breakpoint to add
"""
if isinstance(address, int):
dbgcore.BNDebuggerAddAbsoluteBreakpoint(self.handle, address)
elif isinstance(address, ModuleNameAndOffset):
dbgcore.BNDebuggerAddRelativeBreakpoint(self.handle, address.module, address.offset)
else:
raise NotImplementedError
[docs]
def has_breakpoint(self, address) -> bool:
"""
Checks whether a breakpoint exists at the specified address
The input can be either an absolute address, or a ModuleNameAndOffset, which specifies a relative address to the
start of a module. The latter is useful for ASLR.
:param address: the address of breakpoint to query
"""
if isinstance(address, int):
return dbgcore.BNDebuggerContainsAbsoluteBreakpoint(self.handle, address)
elif isinstance(address, ModuleNameAndOffset):
return dbgcore.BNDebuggerContainsRelativeBreakpoint(self.handle, address.module, address.offset)
else:
raise NotImplementedError
@property
def ip(self) -> int:
"""
The IP (instruction pointer) of the target
For x86_64, it returns the value of ``rip`` register.
For x86, it returns the value of ``eip`` register.
For armv7/aarch64, or any other architecture that is not native to BN, it returns the value of ``pc`` register.
"""
return dbgcore.BNDebuggerGetIP(self.handle)
@ip.setter
def ip(self, addr: int) -> bool:
"""
Overrides the IP (instruction pointer) of the target
For x86_64, it set the value of ``rip`` register.
For x86, it set the value of ``eip`` register.
For armv7/aarch64, or any other architecture that is not native to BN, it set the value of ``pc`` register.
:param addr: the new value of the IP
:return: whether the operation succeeds
"""
return dbgcore.BNDebuggerSetIP(self.handle, addr)
@property
def last_ip(self) -> int:
"""
The IP (instruction pointer) when the target breaks last time.
"""
return dbgcore.BNDebuggerGetLastIP(self.handle)
@property
def exit_code(self) -> int:
"""
The exit code of the target (read-only)
This is only meaningful after the target has executed and exited.
"""
return dbgcore.BNDebuggerGetExitCode(self.handle)
[docs]
def register_event_callback(self, callback: DebuggerEventCallback, name: Union[str, bytes] = '') -> int:
"""
Register a debugger event callback to receive notification when various events happen.
The callback receives DebuggerEvent object that contains the type of the event and associated data.
:param callback: the callback to register
:param name: name of the callback
:return: an integer handle to the registered event callback
"""
return DebuggerEventWrapper.register(self, callback, name)
[docs]
def remove_event_callback(self, index: int):
"""
Remove the debugger event callback from the DebuggerController
"""
DebuggerEventWrapper.remove(self, index)
[docs]
def frames_of_thread(self, tid: int) -> List[DebugFrame]:
"""
Get the stack frames of the thread specified by ``tid``
:param tid: thread id
:return: list of stack frames
"""
count = ctypes.c_ulonglong()
frames = dbgcore.BNDebuggerGetFramesOfThread(self.handle, tid, count)
result = []
for i in range(0, count.value):
bp = DebugFrame(frames[i].m_index, frames[i].m_pc, frames[i].m_sp, frames[i].m_fp, frames[i].m_functionName,
frames[i].m_functionStart, frames[i].m_module)
result.append(bp)
dbgcore.BNDebuggerFreeFrames(frames, count.value)
return result
@property
def stop_reason(self) -> DebugStopReason:
"""
The reason for the target to stop
This is the same value to the return value of the function that resumed the target, e.g., ``go()``
"""
return DebugStopReason(dbgcore.BNDebuggerGetStopReason(self.handle))
@property
def stop_reason_str(self) -> str:
"""
String description of the target stop reason
"""
return dbgcore.BNDebuggerGetStopReasonString(self.stop_reason)
[docs]
def write_stdin(self, data: Union[str, bytes]) -> None:
"""
Write to the stdin of the target. Only works on Linux and macOS.
"""
dbgcore.BNDebuggerWriteStdin(self.handle, data, len(data))
[docs]
def execute_backend_command(self, command: Union[str, bytes]) -> str:
"""
Execute a backend command and get the output
For LLDB adapter, any LLDB commands can be executed. The returned string is what gets printed if one executes
the command in the LLDB prompt.
For DbgEnd adapter, any WinDbg commands can be executed. The returned string is what gets printed if one
executes the command in WinDbg.
"""
return dbgcore.BNDebuggerInvokeBackendCommand(self.handle, command)
[docs]
def get_adapter_property(self, name: Union[str, bytes]) -> 'binaryninja.metadata.MetadataValueType':
md_handle = dbgcore.BNDebuggerGetAdapterProperty(self.handle, name)
if md_handle is None:
raise KeyError(name)
md_handle_BN = ctypes.cast(md_handle, ctypes.POINTER(binaryninja.core.BNMetadata))
return binaryninja.metadata.Metadata(handle=md_handle_BN).value
[docs]
def set_adapter_property(self, name: Union[str, bytes], value: binaryninja.metadata.MetadataValueType) -> bool:
_value = value
if not isinstance(_value, binaryninja.metadata.Metadata):
_value = binaryninja.metadata.Metadata(_value)
handle = ctypes.cast(_value.handle, ctypes.POINTER(dbgcore.BNMetadata))
return dbgcore.BNDebuggerSetAdapterProperty(self.handle, name, handle)
[docs]
def get_addr_info(self, addr: int):
return dbgcore.BNDebuggerGetAddressInformation(self.handle, addr)
@property
def is_first_launch(self):
return dbgcore.BNDebuggerIsFirstLaunch(self.handle)
@property
def is_ttd(self):
return dbgcore.BNDebuggerIsTTD(self.handle)
def __del__(self):
if dbgcore is not None:
dbgcore.BNDebuggerFreeController(self.handle)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return ctypes.addressof(self.handle.contents) == ctypes.addressof(other.handle.contents)
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return not (self == other)
def __hash__(self):
return hash(ctypes.addressof(self.handle.contents))