diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest b/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest index 1ff44acca4..497d451482 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest +++ b/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest @@ -2,8 +2,10 @@ ##MODULE IP: Apache License 2.0 Module.manifest||GHIDRA||||END| data/debugger-launchers/local-dbgeng.bat||GHIDRA||||END| +data/debugger-launchers/local-ttd.bat||GHIDRA||||END| src/main/py/LICENSE||GHIDRA||||END| src/main/py/README.md||GHIDRA||||END| src/main/py/pyproject.toml||GHIDRA||||END| src/main/py/src/ghidradbg/dbgmodel/DbgModel.idl||GHIDRA||||END| src/main/py/src/ghidradbg/schema.xml||GHIDRA||||END| +src/main/py/src/ghidrattd/schema.xml||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/data/debugger-launchers/local-ttd.bat b/Ghidra/Debug/Debugger-agent-dbgeng/data/debugger-launchers/local-ttd.bat new file mode 100644 index 0000000000..a6aa5b0f3e --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/data/debugger-launchers/local-ttd.bat @@ -0,0 +1,22 @@ +::@title ttd +::@desc
+::@descThis will launch the target on the local machine using dbgeng.dll. Typically, +::@desc Windows systems have this library pre-installed, but it may have limitations, e.g., you +::@desc cannot use .server. For the full capabilities, you must install WinDbg.
+::@descFurthermore, you must have Python 3 installed on your system, and it must have the +::@desc pybag and protobuf packages installed.
+::@desc +::@menu-group local +::@icon icon.debugger +::@help TraceRmiLauncherServicePlugin#dbgeng +::@env OPT_PYTHON_EXE:str="python" "Path to python" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH." +:: Use env instead of args, because "all args except first" is terrible to implement in batch +::@env OPT_TARGET_IMG:str="" "Trace (.run)" "A trace associated with the target binary executable" +::@env OPT_TARGET_ARGS:str="" "Arguments" "Command-line arguments to pass to the target" +::@env OPT_USE_DBGMODEL:bool=true "Use dbgmodel" "Load and use dbgmodel.dll if it is available." +::@env OPT_DBGMODEL_PATH:str="" "Path to dbgeng & \\ttd" "Path to dbgeng and associated DLLS (if not Windows Kits)." + +@echo off + +"%OPT_PYTHON_EXE%" -i ..\support\local-ttd.py diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/data/support/local-ttd.py b/Ghidra/Debug/Debugger-agent-dbgeng/data/support/local-ttd.py new file mode 100644 index 0000000000..d9f861a5b5 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/data/support/local-ttd.py @@ -0,0 +1,58 @@ +## ### +# IP: GHIDRA +# +# 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 os +import sys + + +home = os.getenv('GHIDRA_HOME') + +if os.path.isdir(f'{home}\\ghidra\\.git'): + sys.path.append( + f'{home}\\ghidra\\Ghidra\\Debug\\Debugger-agent-dbgeng\\build\\pypkg\\src') + sys.path.append( + f'{home}\\ghidra\\Ghidra\\Debug\\Debugger-rmi-trace\\build\\pypkg\\src') +elif os.path.isdir(f'{home}\\.git'): + sys.path.append( + f'{home}\\Ghidra\\Debug\\Debugger-agent-dbgeng\\build\\pypkg\\src') + sys.path.append( + f'{home}\\Ghidra\\Debug\\Debugger-rmi-trace\\build\\pypkg\\src') +else: + sys.path.append( + f'{home}\\Ghidra\\Debug\\Debugger-agent-dbgeng\\pypkg\\src') + sys.path.append(f'{home}\\Ghidra\\Debug\\Debugger-rmi-trace\\pypkg\\src') + + +def main(): + # Delay these imports until sys.path is patched + from ghidrattd import commands as cmd + from ghidrattd import hooks + ###from ghidrattd.util import dbg + + cmd.ghidra_trace_connect(os.getenv('GHIDRA_TRACE_RMI_ADDR')) + args = os.getenv('OPT_TARGET_ARGS') + if args: + args = ' ' + args + cmd.ghidra_trace_create( + os.getenv('OPT_TARGET_IMG') + args, start_trace=True) + cmd.ghidra_trace_sync_enable() + hooks.on_stop() + + cmd.repl() + + +if __name__ == '__main__': + main() diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/__init__.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/__init__.py new file mode 100644 index 0000000000..6c5fc1de71 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/__init__.py @@ -0,0 +1,19 @@ +## ### +# IP: GHIDRA +# +# 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. +## + +# NOTE: libraries must precede EVERYTHING, esp pybag and DbgMod + +from . import libraries, util, commands, methods, hooks diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/arch.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/arch.py new file mode 100644 index 0000000000..975e19c6ae --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/arch.py @@ -0,0 +1,212 @@ +## ### +# IP: GHIDRA +# +# 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. +## +from ghidratrace.client import Address, RegVal + +from pybag import pydbg + +from . import util + +language_map = { + 'ARM': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A', 'ARM:BE:64:v8', 'ARM:LE:64:v8'], + 'Itanium': [], + 'x86': ['x86:LE:32:default'], + 'x86_64': ['x86:LE:64:default'], + 'EFI': ['x86:LE:64:default'], +} + +data64_compiler_map = { + None: 'pointer64', +} + +x86_compiler_map = { + 'windows': 'windows', + 'Cygwin': 'windows', +} + +arm_compiler_map = { + 'windows': 'windows', +} + +compiler_map = { + 'DATA:BE:64:default': data64_compiler_map, + 'DATA:LE:64:default': data64_compiler_map, + 'x86:LE:32:default': x86_compiler_map, + 'x86:LE:64:default': x86_compiler_map, + 'AARCH64:BE:64:v8A': arm_compiler_map, + 'AARCH64:LE:64:AppleSilicon': arm_compiler_map, + 'AARCH64:LE:64:v8A': arm_compiler_map, + 'ARM:BE:64:v8': arm_compiler_map, + 'ARM:LE:64:v8': arm_compiler_map, +} + + +def get_arch(): + return "x86_64" + + +def get_endian(): + return 'little' + + +def get_osabi(): + return "windows" + + +def compute_ghidra_language(): + # First, check if the parameter is set + lang = util.get_convenience_variable('ghidra-language') + if lang != 'auto': + return lang + + # Get the list of possible languages for the arch. We'll need to sift + # through them by endian and probably prefer default/simpler variants. The + # heuristic for "simpler" will be 'default' then shortest variant id. + arch = get_arch() + endian = get_endian() + lebe = ':BE:' if endian == 'big' else ':LE:' + if not arch in language_map: + return 'DATA' + lebe + '64:default' + langs = language_map[arch] + matched_endian = sorted( + (l for l in langs if lebe in l), + key=lambda l: 0 if l.endswith(':default') else len(l) + ) + if len(matched_endian) > 0: + return matched_endian[0] + # NOTE: I'm disinclined to fall back to a language match with wrong endian. + return 'DATA' + lebe + '64:default' + + +def compute_ghidra_compiler(lang): + # First, check if the parameter is set + comp = util.get_convenience_variable('ghidra-compiler') + if comp != 'auto': + return comp + + # Check if the selected lang has specific compiler recommendations + if not lang in compiler_map: + return 'default' + comp_map = compiler_map[lang] + osabi = get_osabi() + if osabi in comp_map: + return comp_map[osabi] + if None in comp_map: + return comp_map[None] + return 'default' + + +def compute_ghidra_lcsp(): + lang = compute_ghidra_language() + comp = compute_ghidra_compiler(lang) + return lang, comp + + +class DefaultMemoryMapper(object): + + def __init__(self, defaultSpace): + self.defaultSpace = defaultSpace + + def map(self, proc: int, offset: int): + space = self.defaultSpace + return self.defaultSpace, Address(space, offset) + + def map_back(self, proc: int, address: Address) -> int: + if address.space == self.defaultSpace: + return address.offset + raise ValueError(f"Address {address} is not in process {proc.GetProcessID()}") + + +DEFAULT_MEMORY_MAPPER = DefaultMemoryMapper('ram') + +memory_mappers = {} + + +def compute_memory_mapper(lang): + if not lang in memory_mappers: + return DEFAULT_MEMORY_MAPPER + return memory_mappers[lang] + + +class DefaultRegisterMapper(object): + + def __init__(self, byte_order): + if not byte_order in ['big', 'little']: + raise ValueError("Invalid byte_order: {}".format(byte_order)) + self.byte_order = byte_order + self.union_winners = {} + + def map_name(self, proc, name): + return name + + + def map_value(self, proc, name, value): + try: + ### TODO: this seems half-baked + av = value.to_bytes(8, "big") + except Exception: + raise ValueError("Cannot convert {}'s value: '{}', type: '{}'" + .format(name, value, type(value))) + return RegVal(self.map_name(proc, name), av) + + def map_name_back(self, proc, name): + return name + + def map_value_back(self, proc, name, value): + return RegVal(self.map_name_back(proc, name), value) + + +class Intel_x86_64_RegisterMapper(DefaultRegisterMapper): + + def __init__(self): + super().__init__('little') + + def map_name(self, proc, name): + if name is None: + return 'UNKNOWN' + if name == 'efl': + return 'rflags' + if name.startswith('zmm'): + # Ghidra only goes up to ymm, right now + return 'ymm' + name[3:] + return super().map_name(proc, name) + + def map_value(self, proc, name, value): + rv = super().map_value(proc, name, value) + if rv.name.startswith('ymm') and len(rv.value) > 32: + return RegVal(rv.name, rv.value[-32:]) + return rv + + def map_name_back(self, proc, name): + if name == 'rflags': + return 'eflags' + + +DEFAULT_BE_REGISTER_MAPPER = DefaultRegisterMapper('big') +DEFAULT_LE_REGISTER_MAPPER = DefaultRegisterMapper('little') + +register_mappers = { + 'x86:LE:64:default': Intel_x86_64_RegisterMapper() +} + + +def compute_register_mapper(lang): + if not lang in register_mappers: + if ':BE:' in lang: + return DEFAULT_BE_REGISTER_MAPPER + if ':LE:' in lang: + return DEFAULT_LE_REGISTER_MAPPER + return register_mappers[lang] + diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/commands.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/commands.py new file mode 100644 index 0000000000..faae4a52a1 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/commands.py @@ -0,0 +1,1386 @@ +## ### +# IP: GHIDRA +# +# 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. +## +from contextlib import contextmanager +import inspect +import os.path +import socket +import time +import sys +import re + +from ghidratrace import sch +from ghidratrace.client import Client, Address, AddressRange, Lifespan, TraceObject + +#from pybag import pydbg, userdbg, kerneldbg +#from pybag.dbgeng import core as DbgEng +#from pybag.dbgeng import exception +from pyttd import pyTTD + +from . import util, arch, methods, hooks +import code + +PAGE_SIZE = 4096 + +AVAILABLES_PATH = 'Available' +AVAILABLE_KEY_PATTERN = '[{pid}]' +AVAILABLE_PATTERN = AVAILABLES_PATH + AVAILABLE_KEY_PATTERN +PROCESSES_PATH = 'Processes' +PROCESS_KEY_PATTERN = '[{procnum}]' +PROCESS_PATTERN = PROCESSES_PATH + PROCESS_KEY_PATTERN +PROC_BREAKS_PATTERN = PROCESS_PATTERN + '.Breakpoints' +PROC_BREAK_KEY_PATTERN = '[{breaknum}]' +PROC_BREAK_PATTERN = PROC_BREAKS_PATTERN + PROC_BREAK_KEY_PATTERN +ENV_PATTERN = PROCESS_PATTERN + '.Environment' +THREADS_PATTERN = PROCESS_PATTERN + '.Threads' +THREAD_KEY_PATTERN = '[{tnum}]' +THREAD_PATTERN = THREADS_PATTERN + THREAD_KEY_PATTERN +STACK_PATTERN = THREAD_PATTERN + '.Stack' +FRAME_KEY_PATTERN = '[{level}]' +FRAME_PATTERN = STACK_PATTERN + FRAME_KEY_PATTERN +REGS_PATTERN = THREAD_PATTERN + '.Registers' +MEMORY_PATTERN = PROCESS_PATTERN + '.Memory' +REGION_KEY_PATTERN = '[{start:08x}]' +REGION_PATTERN = MEMORY_PATTERN + REGION_KEY_PATTERN +MODULES_PATTERN = PROCESS_PATTERN + '.Modules' +MODULE_KEY_PATTERN = '[{modpath}]' +MODULE_PATTERN = MODULES_PATTERN + MODULE_KEY_PATTERN +SECTIONS_ADD_PATTERN = '.Sections' +SECTION_KEY_PATTERN = '[{secname}]' +SECTION_ADD_PATTERN = SECTIONS_ADD_PATTERN + SECTION_KEY_PATTERN +DESCRIPTION_PATTERN = '{major}:{minor} {type}' + +# TODO: Symbols + + +class ErrorWithCode(Exception): + def __init__(self, code): + self.code = code + + def __str__(self)->str: + return repr(self.code) + + +class State(object): + + def __init__(self): + self.reset_client() + + def require_client(self): + if self.client is None: + raise RuntimeError("Not connected") + return self.client + + def require_no_client(self): + if self.client != None: + raise RuntimeError("Already connected") + + def reset_client(self): + self.client = None + self.reset_trace() + + def require_trace(self): + if self.trace is None: + raise RuntimeError("No trace active") + return self.trace + + def require_no_trace(self): + if self.trace != None: + raise RuntimeError("Trace already started") + + def reset_trace(self): + self.trace = None + util.set_convenience_variable('_ghidra_tracing', "false") + self.reset_tx() + + def require_tx(self): + if self.tx is None: + raise RuntimeError("No transaction") + return self.tx + + def require_no_tx(self): + if self.tx != None: + raise RuntimeError("Transaction already started") + + def reset_tx(self): + self.tx = None + + +STATE = State() + + +def ghidra_trace_connect(address=None): + """ + Connect Python to Ghidra for tracing + + Address must be of the form 'host:port' + """ + + STATE.require_no_client() + if address is None: + raise RuntimeError( + "'ghidra_trace_connect': missing required argument 'address'") + + parts = address.split(':') + if len(parts) != 2: + raise RuntimeError("address must be in the form 'host:port'") + host, port = parts + try: + c = socket.socket() + c.connect((host, int(port))) + # TODO: Can we get version info from the DLL? + STATE.client = Client(c, "dbgeng.dll", methods.REGISTRY) + print(f"Connected to {STATE.client.description} at {address}") + except ValueError: + raise RuntimeError("port must be numeric") + + +def ghidra_trace_listen(address='0.0.0.0:0'): + """ + Listen for Ghidra to connect for tracing + + Takes an optional address for the host and port on which to listen. Either + the form 'host:port' or just 'port'. If omitted, it will bind to an + ephemeral port on all interfaces. If only the port is given, it will bind to + that port on all interfaces. This command will block until the connection is + established. + """ + + STATE.require_no_client() + parts = address.split(':') + if len(parts) == 1: + host, port = '0.0.0.0', parts[0] + elif len(parts) == 2: + host, port = parts + else: + raise RuntimeError("address must be 'port' or 'host:port'") + + try: + s = socket.socket() + s.bind((host, int(port))) + host, port = s.getsockname() + s.listen(1) + print("Listening at {}:{}...\n".format(host, port)) + c, (chost, cport) = s.accept() + s.close() + print("Connection from {}:{}\n".format(chost, cport)) + STATE.client = Client(c, "dbgeng.dll", methods.REGISTRY) + except ValueError: + raise RuntimeError("port must be numeric") + + +def ghidra_trace_disconnect(): + """Disconnect Python from Ghidra for tracing""" + + STATE.require_client().close() + STATE.reset_client() + + +def compute_name(progname=None): + if progname is None: + try: + buffer = util.GetCurrentProcessExecutableName() + progname = buffer.decode('utf-8') + except Exception: + return 'pydbg/noname' + return 'pydbg/' + re.split(r'/|\\', progname)[-1] + + +def start_trace(name): + language, compiler = arch.compute_ghidra_lcsp() + STATE.trace = STATE.client.create_trace(name, language, compiler) + # TODO: Is adding an attribute like this recommended in Python? + STATE.trace.memory_mapper = arch.compute_memory_mapper(language) + STATE.trace.register_mapper = arch.compute_register_mapper(language) + + parent = os.path.dirname(inspect.getfile(inspect.currentframe())) + schema_fn = os.path.join(parent, 'schema.xml') + with open(schema_fn, 'r') as schema_file: + schema_xml = schema_file.read() + with STATE.trace.open_tx("Create Root Object"): + root = STATE.trace.create_root_object(schema_xml, 'Session') + root.set_value('_display', 'pydbg(dbgeng) ' + util.DBG_VERSION.full) + util.set_convenience_variable('_ghidra_tracing', "true") + + +def ghidra_trace_start(name=None): + """Start a Trace in Ghidra""" + + STATE.require_client() + name = compute_name(name) + STATE.require_no_trace() + start_trace(name) + + +def ghidra_trace_stop(): + """Stop the Trace in Ghidra""" + + STATE.require_trace().close() + STATE.reset_trace() + + +def ghidra_trace_restart(name=None): + """Restart or start the Trace in Ghidra""" + + STATE.require_client() + if STATE.trace != None: + STATE.trace.close() + STATE.reset_trace() + name = compute_name(name) + start_trace(name) + + +def ghidra_trace_create(command=None, initial_break=True, timeout=None, start_trace=True): + """ + Create a session. + """ + + eng = pyTTD.ReplayEngine() + util.eng = eng + if command != None: + eng.initialize(command) + util.first = eng.get_first_position() + util.last = eng.get_last_position() + print(f"Trace from {util.first} to {util.last}") + cursor = eng.new_cursor() + cursor.set_position(util.first) + util.lastpos = util.first + util.base = cursor + if start_trace: + print(f"calling start with {command}") + ghidra_trace_start(command) + print(f"started") + events = sorted( + list((x, "modload") for x in eng.get_module_loaded_event_list()) + + list((x, "modunload") for x in eng.get_module_unloaded_event_list()) + + list((x, "threadcreated") + for x in eng.get_thread_created_event_list()) + + list((x, "threadterm") + for x in eng.get_thread_terminated_event_list()), + key=lambda event: event[0].position + ) + + keys = [] + radix = util.get_convenience_variable('output-radix') + if radix == 'auto': + radix = 16 + nproc = 0 + + for event, evtype in events: + pos = event.position + util.events[pos.major] = event + util.evttypes[pos.major] = evtype + with open_tracked_tx('Populate events'): + index = util.pos2snap(pos) + STATE.trace.snapshot(DESCRIPTION_PATTERN.format( + major=pos.major, minor=pos.minor, type=evtype), snap=index) + if evtype == "modload": + with open_tracked_tx(evtype): + id = event.info.base_addr + path = event.info.path + size = event.info.image_size + mobj = get_module(keys, nproc, path, id, size) + util.starts[id] = index + mobj.insert(span=Lifespan(index)) + print(f"[{event.position.major:x}:{event.position.minor:x}]", end=" ") + print(f"Module {event.info.path} loaded") + elif evtype == "modunload": + with open_tracked_tx(evtype): + id = event.info.base_addr + path = event.info.path + size = event.info.image_size + mobj = get_module(keys, nproc, path, id, size) + util.stops[id] = index + mobj.remove(span=Lifespan(index)) + #print(f"[{event.position.major:x}:{event.position.minor:x}]", end=" ") + #print(f"Module {event.info.path} unloaded") + elif evtype == "threadcreated": + with open_tracked_tx(evtype): + id = event.info.threadid + tobj = get_thread(keys, radix, nproc, id) + util.starts[id] = index + tobj.insert(span=Lifespan(index)) + print(f"[{event.position.major:x}:{event.position.minor:x}]", end=" ") + print(f"Thread {event.info.threadid:x} created") + elif evtype == "threadterm": + with open_tracked_tx(evtype): + id = event.info.threadid + tobj = get_thread(keys, radix, 0, id) + util.stops[id] = index + tobj.remove(span=Lifespan(index)) + #print(f"[{event.position.major:x}:{event.position.minor:x}]", end=" ") + #print(f"Thread {event.info.threadid:x} terminated") + ghidra_trace_set_snap(util.first.major) + + +def ghidra_trace_kill(): + """ + Kill a session. + """ + + print("ghidra_trace_kill") + + +def ghidra_trace_info(): + """Get info about the Ghidra connection""" + + result = {} + if STATE.client is None: + print("Not connected to Ghidra\n") + return + host, port = STATE.client.s.getpeername() + print(f"Connected to {STATE.client.description} at {host}:{port}\n") + if STATE.trace is None: + print("No trace\n") + return + print("Trace active\n") + return result + + +def ghidra_trace_info_lcsp(): + """ + Get the selected Ghidra language-compiler-spec pair. + """ + + language, compiler = arch.compute_ghidra_lcsp() + print("Selected Ghidra language: {}\n".format(language)) + print("Selected Ghidra compiler: {}\n".format(compiler)) + + +def ghidra_trace_txstart(description="tx"): + """ + Start a transaction on the trace + """ + + STATE.require_no_tx() + STATE.tx = STATE.require_trace().start_tx(description, undoable=False) + + +def ghidra_trace_txcommit(): + """ + Commit the current transaction + """ + + STATE.require_tx().commit() + STATE.reset_tx() + + +def ghidra_trace_txabort(): + """ + Abort the current transaction + + Use only in emergencies. + """ + + tx = STATE.require_tx() + print("Aborting trace transaction!\n") + tx.abort() + STATE.reset_tx() + + +@contextmanager +def open_tracked_tx(description): + with STATE.require_trace().open_tx(description) as tx: + STATE.tx = tx + yield tx + STATE.reset_tx() + + +def ghidra_trace_save(): + """ + Save the current trace + """ + + STATE.require_trace().save() + + +def ghidra_trace_new_snap(description=None, snap=None): + """ + Create a new snapshot + + Subsequent modifications to machine state will affect the new snapshot. + """ + + description = str(description) + STATE.require_tx() + return {'snap': STATE.require_trace().snapshot(description, snap=snap)} + + +def ghidra_trace_set_snap(snap=None): + """ + Go to a snapshot + + Subsequent modifications to machine state will affect the given snapshot. + """ + + STATE.require_trace().set_snap(int(snap)) + + +def put_bytes(start, end, pages, display_result): + trace = STATE.require_trace() + if pages: + start = start // PAGE_SIZE * PAGE_SIZE + end = (end + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE + nproc = util.selected_process() + if end - start <= 0: + return {'count': 0} + buf = dbg().read_mem(start, end - start) + + count = 0 + if buf != None: + base, addr = trace.memory_mapper.map(nproc, start) + if base != addr.space: + trace.create_overlay_space(base, addr.space) + count = trace.put_bytes(addr, buf) + if display_result: + print("Wrote {} bytes\n".format(count)) + return {'count': count} + + +def eval_address(address): + try: + return util.parse_and_eval(address) + except Exception: + raise RuntimeError("Cannot convert '{}' to address".format(address)) + + +def eval_range(address, length): + start = eval_address(address) + try: + end = start + util.parse_and_eval(length) + except Exception as e: + raise RuntimeError("Cannot convert '{}' to length".format(length)) + return start, end + + +def putmem(address, length, pages=True, display_result=True): + start, end = eval_range(address, length) + return put_bytes(start, end, pages, display_result) + + +def ghidra_trace_putmem(items): + """ + Record the given block of memory into the Ghidra trace. + """ + + items = items.split(" ") + address = items[0] + length = items[1] + pages = items[2] if len(items) > 2 else True + + STATE.require_tx() + return putmem(address, length, pages, True) + + +def ghidra_trace_putval(items): + """ + Record the given value into the Ghidra trace, if it's in memory. + """ + + items = items.split(" ") + value = items[0] + pages = items[1] if len(items) > 1 else True + + STATE.require_tx() + try: + start = util.parse_and_eval(value) + except e: + raise RuntimeError("Value '{}' has no address".format(value)) + end = start + int(start.GetType().GetByteSize()) + return put_bytes(start, end, pages, True) + + +def ghidra_trace_putmem_state(items): + """ + Set the state of the given range of memory in the Ghidra trace. + """ + + items = items.split(" ") + address = items[0] + length = items[1] + state = items[2] + + STATE.require_tx() + STATE.trace.validate_state(state) + start, end = eval_range(address, length) + nproc = util.selected_process() + base, addr = STATE.trace.memory_mapper.map(nproc, start) + if base != addr.space: + trace.create_overlay_space(base, addr.space) + STATE.trace.set_memory_state(addr.extend(end - start), state) + + +def ghidra_trace_delmem(items): + """ + Delete the given range of memory from the Ghidra trace. + + Why would you do this? Keep in mind putmem quantizes to full pages by + default, usually to take advantage of spatial locality. This command does + not quantize. You must do that yourself, if necessary. + """ + + items = items.split(" ") + address = items[0] + length = items[1] + + STATE.require_tx() + start, end = eval_range(address, length) + nproc = util.selected_process() + base, addr = STATE.trace.memory_mapper.map(nproc, start) + # Do not create the space. We're deleting stuff. + STATE.trace.delete_bytes(addr.extend(end - start)) + + +def putreg(): + nproc = util.selected_process() + if nproc < 0: + return + nthrd = util.selected_thread() + space = REGS_PATTERN.format(procnum=nproc, tnum=nthrd) + STATE.trace.create_overlay_space('register', space) + robj = STATE.trace.create_object(space) + robj.insert() + mapper = STATE.trace.register_mapper + values = [] + regs = dbg().get_context_x86_64() + keys = ["seg_cs", "seg_ds", "seg_es", "seg_fs", "seg_gs", "seg_ss", "rflags", + "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rsp", "rbp", "rip", + "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"] + vals = [regs.seg_cs, regs.seg_ds, regs.seg_es, regs.seg_fs, regs.seg_gs, + regs.seg_ss, regs.eflags, regs.rax, regs.rbx, regs.rcx, regs.rdx, + regs.rsi, regs.rdi, regs.rsp, regs.rbp, regs.rip, + regs.r8, regs.r9, regs.r10, regs.r11, regs.r12, regs.r13, regs.r14, + regs.r15] + for i in range(0, len(keys)): + name = keys[i] + value = vals[i] + try: + values.append(mapper.map_value(nproc, name, value)) + robj.set_value(name, hex(value)) + except Exception: + pass + return {'missing': STATE.trace.put_registers(space, values)} + + +def ghidra_trace_putreg(): + """ + Record the given register group for the current frame into the Ghidra trace. + + If no group is specified, 'all' is assumed. + """ + + STATE.require_tx() + putreg() + + +def ghidra_trace_delreg(group='all'): + """ + Delete the given register group for the curent frame from the Ghidra trace. + + Why would you do this? If no group is specified, 'all' is assumed. + """ + + STATE.require_tx() + nproc = util.selected_process() + nthrd = util.selected_thread() + space = REGS_PATTERN.format(procnum=nproc, tnum=nthrd) + mapper = STATE.trace.register_mapper + names = [] + names.append(mapper.map_name(nproc, group)) + return STATE.trace.delete_registers(space, names) + + +def ghidra_trace_create_obj(path=None): + """ + Create an object in the Ghidra trace. + + The new object is in a detached state, so it may not be immediately + recognized by the Debugger GUI. Use 'ghidra_trace_insert-obj' to finish the + object, after all its required attributes are set. + """ + + STATE.require_tx() + obj = STATE.trace.create_object(path) + obj.insert() + print("Created object: id={}, path='{}'\n".format(obj.id, obj.path)) + return {'id': obj.id, 'path': obj.path} + + +def ghidra_trace_insert_obj(path): + """ + Insert an object into the Ghidra trace. + """ + + # NOTE: id parameter is probably not necessary, since this command is for + # humans. + STATE.require_tx() + span = STATE.trace.proxy_object_path(path).insert() + print("Inserted object: lifespan={}\n".format(span)) + return {'lifespan': span} + + +def ghidra_trace_remove_obj(path): + """ + Remove an object from the Ghidra trace. + + This does not delete the object. It just removes it from the tree for the + current snap and onwards. + """ + + STATE.require_tx() + STATE.trace.proxy_object_path(path).remove() + + +def to_bytes(value): + return bytes(ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0, len(value))) + + +def to_string(value, encoding): + b = bytes(ord(value[i]) if type(value[i]) == str else int( + value[i]) for i in range(0, len(value))) + return str(b, encoding) + + +def to_bool_list(value): + return [bool(value[i]) for i in range(0, len(value))] + + +def to_int_list(value): + return [ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0, len(value))] + + +def to_short_list(value): + return [ord(value[i]) if type(value[i]) == str else int(value[i]) for i in range(0, len(value))] + + +def to_string_list(value, encoding): + return [to_string(value[i], encoding) for i in range(0, len(value))] + + +def eval_value(value, schema=None): + if schema == sch.CHAR or schema == sch.BYTE or schema == sch.SHORT or schema == sch.INT or schema == sch.LONG or schema == None: + value = util.get_eval(value) + return value, schema + if schema == sch.ADDRESS: + value = util.get_eval(value) + nproc = util.selected_process() + base, addr = STATE.trace.memory_mapper.map(nproc, value) + return (base, addr), sch.ADDRESS + if type(value) != str: + value = eval("{}".format(value)) + if schema == sch.BOOL_ARR: + return to_bool_list(value), schema + if schema == sch.BYTE_ARR: + return to_bytes(value), schema + if schema == sch.SHORT_ARR: + return to_short_list(value), schema + if schema == sch.INT_ARR: + return to_int_list(value), schema + if schema == sch.LONG_ARR: + return to_int_list(value), schema + if schema == sch.STRING_ARR: + return to_string_list(value, 'utf-8'), schema + if schema == sch.CHAR_ARR: + return to_string(value, 'utf-8'), sch.CHAR_ARR + if schema == sch.STRING: + return to_string(value, 'utf-8'), sch.STRING + + return value, schema + + +def ghidra_trace_set_value(path: str, key: str, value, schema=None): + """ + Set a value (attribute or element) in the Ghidra trace's object tree. + + A void value implies removal. + NOTE: The type of an expression may be subject to the dbgeng's current + language. which current defaults to DEBUG_EXPR_CPLUSPLUS (vs DEBUG_EXPR_MASM). + For most non-primitive cases, we are punting to the Python API. + """ + schema = None if schema is None else sch.Schema(schema) + STATE.require_tx() + if schema == sch.OBJECT: + val = STATE.trace.proxy_object_path(value) + else: + val, schema = eval_value(value, schema) + if schema == sch.ADDRESS: + base, addr = val + val = addr + if base != addr.space: + trace.create_overlay_space(base, addr.space) + STATE.trace.proxy_object_path(path).set_value(key, val, schema) + + +def ghidra_trace_retain_values(path: str, keys: str): + """ + Retain only those keys listed, settings all others to null. + + Takes a list of keys to retain. The first argument may optionally be one of + the following: + + --elements To set all other elements to null (default) + --attributes To set all other attributes to null + --both To set all other values (elements and attributes) to null + + If, for some reason, one of the keys to retain would be mistaken for this + switch, then the switch is required. Only the first argument is taken as the + switch. All others are taken as keys. + """ + + keys = keys.split(" ") + + STATE.require_tx() + kinds = 'elements' + if keys[0] == '--elements': + kinds = 'elements' + keys = keys[1:] + elif keys[0] == '--attributes': + kinds = 'attributes' + keys = keys[1:] + elif keys[0] == '--both': + kinds = 'both' + keys = keys[1:] + elif keys[0].startswith('--'): + raise RuntimeError("Invalid argument: " + keys[0]) + STATE.trace.proxy_object_path(path).retain_values(keys, kinds=kinds) + + +def ghidra_trace_get_obj(path): + """ + Get an object descriptor by its canonical path. + + This isn't the most informative, but it will at least confirm whether an + object exists and provide its id. + """ + + trace = STATE.require_trace() + object = trace.get_object(path) + print("{}\t{}\n".format(object.id, object.path)) + return object + + +class TableColumn(object): + def __init__(self, head): + self.head = head + self.contents = [head] + self.is_last = False + + def add_data(self, data): + self.contents.append(str(data)) + + def finish(self): + self.width = max(len(d) for d in self.contents) + 1 + + def print_cell(self, i): + print( + self.contents[i] if self.is_last else self.contents[i].ljust(self.width), end='') + + +class Tabular(object): + def __init__(self, heads): + self.columns = [TableColumn(h) for h in heads] + self.columns[-1].is_last = True + self.num_rows = 1 + + def add_row(self, datas): + for c, d in zip(self.columns, datas): + c.add_data(d) + self.num_rows += 1 + + def print_table(self): + for c in self.columns: + c.finish() + for rn in range(self.num_rows): + for c in self.columns: + c.print_cell(rn) + print('\n') + + +def val_repr(value): + if isinstance(value, TraceObject): + return value.path + elif isinstance(value, Address): + return '{}:{:08x}'.format(value.space, value.offset) + return repr(value) + + +def print_values(values): + table = Tabular(['Parent', 'Key', 'Span', 'Value', 'Type']) + for v in values: + table.add_row( + [v.parent.path, v.key, v.span, val_repr(v.value), v.schema]) + table.print_table() + + +def ghidra_trace_get_values(pattern): + """ + List all values matching a given path pattern. + """ + + trace = STATE.require_trace() + values = trace.get_values(pattern) + print_values(values) + return values + + +def ghidra_trace_get_values_rng(items): + """ + List all values intersecting a given address range. + """ + + items = items.split(" ") + address = items[0] + length = items[1] + + trace = STATE.require_trace() + start, end = eval_range(address, length) + nproc = util.selected_process() + base, addr = trace.memory_mapper.map(nproc, start) + # Do not create the space. We're querying. No tx. + values = trace.get_values_intersecting(addr.extend(end - start)) + print_values(values) + return values + + +def activate(path=None): + trace = STATE.require_trace() + if path is None: + nproc = util.selected_process() + if nproc is None: + path = PROCESSES_PATH + else: + nthrd = util.selected_thread() + if nthrd is None: + path = PROCESS_PATTERN.format(procnum=nproc) + else: + path = THREAD_PATTERN.format(procnum=nproc, tnum=nthrd) + trace.proxy_object_path(path).activate() + + +def ghidra_trace_activate(path=None): + """ + Activate an object in Ghidra's GUI. + + This has no effect if the current trace is not current in Ghidra. If path is + omitted, this will activate the current frame. + """ + + activate(path) + + +def ghidra_trace_disassemble(address): + """ + Disassemble starting at the given seed. + + Disassembly proceeds linearly and terminates at the first branch or unknown + memory encountered. + """ + + STATE.require_tx() + start = eval_address(address) + nproc = util.selected_process() + base, addr = STATE.trace.memory_mapper.map(nproc, start) + if base != addr.space: + trace.create_overlay_space(base, addr.space) + + length = STATE.trace.disassemble(addr) + print("Disassembled {} bytes\n".format(length)) + return {'length': length} + + +def compute_proc_state(nproc=None): + return 'STOPPED' + + +def put_processes(running=False): + radix = util.get_convenience_variable('output-radix') + if radix == 'auto': + radix = 16 + keys = [] + for i, p in enumerate(util.process_list(running)): + ipath = PROCESS_PATTERN.format(procnum=i) + keys.append(PROCESS_KEY_PATTERN.format(procnum=i)) + procobj = STATE.trace.create_object(ipath) + + istate = compute_proc_state(p) + procobj.set_value('_state', istate) + if running == False: + procobj.set_value('_pid', p) + pidstr = ('0x{:x}' if radix == + 16 else '0{:o}' if radix == 8 else '{}').format(p) + procobj.set_value('_display', pidstr) + #procobj.set_value('Name', str(p[1])) + procobj.set_value('PEB', hex(util.eng.get_peb_address())) + procobj.insert() + STATE.trace.proxy_object_path(PROCESSES_PATH).retain_values(keys) + + +def put_state(event_process): + STATE.require_no_tx() + STATE.tx = STATE.require_trace().start_tx("state", undoable=False) + ipath = PROCESS_PATTERN.format(procnum=event_process) + procobj = STATE.trace.create_object(ipath) + state = compute_proc_state(event_process) + procobj.set_value('_state', state) + procobj.insert() + tnum = util.selected_thread() + if tnum is not None: + ipath = THREAD_PATTERN.format(procnum=event_process, tnum=tnum) + threadobj = STATE.trace.create_object(ipath) + threadobj.set_value('_state', state) + threadobj.insert() + STATE.require_tx().commit() + STATE.reset_tx() + + +def ghidra_trace_put_processes(): + """ + Put the list of processes into the trace's Processes list. + """ + + STATE.require_tx() + with STATE.client.batch() as b: + put_processes() + + +def put_available(): + radix = util.get_convenience_variable('output-radix') + keys = [] + result = dbg().cmd(".tlist") + lines = result.split("\n") + for i in lines: + i = i.strip() + if i == "": + continue + if i.startswith("0n") is False: + continue + items = i.strip().split(" ") + id = items[0][2:] + name = items[1] + ppath = AVAILABLE_PATTERN.format(pid=id) + procobj = STATE.trace.create_object(ppath) + keys.append(AVAILABLE_KEY_PATTERN.format(pid=id)) + pidstr = ('0x{:x}' if radix == + 16 else '0{:o}' if radix == 8 else '{}').format(id) + procobj.set_value('_pid', id) + procobj.set_value('Name', name) + procobj.set_value('_display', '{} {}'.format(pidstr, name)) + procobj.insert() + STATE.trace.proxy_object_path(AVAILABLES_PATH).retain_values(keys) + + +def ghidra_trace_put_available(): + """ + Put the list of available processes into the trace's Available list. + """ + + STATE.require_tx() + with STATE.client.batch() as b: + put_available() + + +def put_single_breakpoint(bp, ibobj, nproc, ikeys): + mapper = STATE.trace.memory_mapper + bpath = PROC_BREAK_PATTERN.format(procnum=nproc, breaknum=bp.id) + brkobj = STATE.trace.create_object(bpath) + status = True + address = bp.addr + expr = bp.expr + offset = "%016x" % address + + prot = bp.flags + width = bp.size + prot = {4: 'HW_EXECUTE', 3: 'READ', 2: 'WRITE'}[prot] + + if address is not None: # Implies execution break + base, addr = mapper.map(nproc, address) + if base != addr.space: + STATE.trace.create_overlay_space(base, addr.space) + brkobj.set_value('_range', addr.extend(1)) + elif expr is not None: # Implies watchpoint + try: + address = int(util.parse_and_eval('&({})'.format(expr))) + base, addr = mapper.map(inf, address) + if base != addr.space: + STATE.trace.create_overlay_space(base, addr.space) + brkobj.set_value('_range', addr.extend(width)) + except Exception as e: + print("Error: Could not get range for breakpoint: {}\n".format(e)) + else: # I guess it's a catchpoint + pass + + brkobj.set_value('_expression', expr) + brkobj.set_value('_range', addr.extend(1)) + brkobj.set_value('_kinds', prot) + brkobj.set_value('_enabled', status) + brkobj.set_value('Enabled', status) + brkobj.set_value('Flags', prot) + brkobj.insert() + + k = PROC_BREAK_KEY_PATTERN.format(breaknum=bp.id) + ikeys.append(k) + + +def put_breakpoints(): + target = util.get_target() + nproc = util.selected_process() + ibpath = PROC_BREAKS_PATTERN.format(procnum=nproc) + ibobj = STATE.trace.create_object(ibpath) + keys = [] + ikeys = [] + #ids = [bpid for bpid in util.breakpoints] + for bp in util.breakpoints: + keys.append(PROC_BREAK_KEY_PATTERN.format(breaknum=bp.id)) + put_single_breakpoint(bp, ibobj, nproc, ikeys) + ibobj.insert() + STATE.trace.proxy_object_path(PROC_BREAKS_PATTERN).retain_values(keys) + ibobj.retain_values(ikeys) + + +def ghidra_trace_put_breakpoints(): + """ + Put the current process's breakpoints into the trace. + """ + + STATE.require_tx() + with STATE.client.batch() as b: + put_breakpoints() + + +def put_environment(): + epath = ENV_PATTERN.format(procnum=util.selected_process()) + envobj = STATE.trace.create_object(epath) + envobj.set_value('_debugger', 'pyttd') + envobj.set_value('_arch', arch.get_arch()) + envobj.set_value('_os', arch.get_osabi()) + envobj.set_value('_endian', arch.get_endian()) + envobj.insert() + + +def ghidra_trace_put_environment(): + """ + Put some environment indicators into the Ghidra trace + """ + + STATE.require_tx() + with STATE.client.batch() as b: + put_environment() + + +def put_regions(): + nproc = util.selected_process() + try: + modules = util.module_list() + except Exception: + modules = [] + if len(modules) == 0 and util.selected_thread() != None: + modules = [util.REGION_INFO_READER.full_mem()] + mapper = STATE.trace.memory_mapper + keys = [] + for m in modules: + rpath = REGION_PATTERN.format(procnum=nproc, start=m.base_addr) + keys.append(REGION_KEY_PATTERN.format(start=m.base_addr)) + regobj = STATE.trace.create_object(rpath) + start_base, start_addr = mapper.map(nproc, m.base_addr) + if start_base != start_addr.space: + STATE.trace.create_overlay_space(start_base, start_addr.space) + regobj.set_value('_range', start_addr.extend(m.image_size)) + regobj.set_value('_readable', True) + regobj.set_value('_writable', False) + regobj.set_value('_executable', False) + regobj.set_value('_offset', hex(m.base_addr)) + regobj.set_value('Base', hex(m.base_addr)) + regobj.set_value('Size', hex(m.image_size)) + regobj.insert() + STATE.trace.proxy_object_path( + MEMORY_PATTERN.format(procnum=nproc)).retain_values(keys) + + +def ghidra_trace_put_regions(): + """ + Read the memory map, if applicable, and write to the trace's Regions + """ + + STATE.require_tx() + with STATE.client.batch() as b: + put_regions() + + +def put_modules(): + target = util.get_target() + nproc = util.selected_process() + modules = util.module_list() + keys = [] + for m in modules: + mobj = get_module(keys, nproc, m.path, m.base_addr, m.image_size) + lspan = Lifespan(util.starts[m.base_addr], util.stops[m.base_addr]) + mobj.insert(span=lspan) + # STATE.trace.proxy_object_path(MODULES_PATTERN.format( + # procnum=nproc)).retain_values(keys) + + +def get_module(keys, nproc: int, path, base, size): + split = path.split("\\") + name = split[len(split)-1] + hbase = hex(base) + #flags = m[1].Flags + mpath = MODULE_PATTERN.format(procnum=nproc, modpath=hbase) + modobj = STATE.trace.create_object(mpath) + keys.append(MODULE_KEY_PATTERN.format(modpath=hbase)) + modobj.set_value('_module_name', name) + mapper = STATE.trace.memory_mapper + base_base, base_addr = mapper.map(nproc, base) + if base_base != base_addr.space: + STATE.trace.create_overlay_space(base_base, base_addr.space) + modobj.set_value('_range', base_addr.extend(size)) + modobj.set_value('Name', name) + modobj.set_value('Path', path) + modobj.set_value('Base', hbase) + modobj.set_value('Size', hex(size)) + return modobj + + +def ghidra_trace_put_modules(): + """ + Gather object files, if applicable, and write to the trace's Modules + """ + + STATE.require_tx() + with STATE.client.batch() as b: + put_modules() + + +def convert_state(t): + if t.IsSuspended(): + return 'SUSPENDED' + if t.IsStopped(): + return 'STOPPED' + return 'RUNNING' + + +def compute_thread_display(tidstr): + return '[{}]'.format(tidstr) + + +def put_threads(running=False): + radix = util.get_convenience_variable('output-radix') + if radix == 'auto': + radix = 16 + nproc = util.selected_process() + if nproc == None: + return + keys = [] + for t in util.thread_list(): + tobj = get_thread(keys, radix, nproc, t.threadid) + lspan = Lifespan(util.starts[t.threadid], util.stops[t.threadid]) + tobj.insert(span=lspan) + # STATE.trace.proxy_object_path( + # THREADS_PATTERN.format(procnum=nproc)).retain_values(keys) + + +def get_thread(keys, radix, pid: int, tid: int): + tpath = THREAD_PATTERN.format(procnum=pid, tnum=tid) + tobj = STATE.trace.create_object(tpath) + keys.append(THREAD_KEY_PATTERN.format(tnum=tid)) + tobj.set_value('_tid', tid, span=Lifespan(0)) + tidstr = ('0x{:x}' if radix == 16 else '0{:o}' if radix == + 8 else '{}').format(tid) + tobj.set_value('_short_display', '[{}:{}]'.format( + pid, tidstr), span=Lifespan(0)) + tobj.set_value('_display', compute_thread_display( + tidstr), span=Lifespan(0)) + return tobj + + +def put_event_thread(nthrd=None): + nproc = util.selected_process() + # Assumption: Event thread is selected by pydbg upon stopping + if nthrd is None: + nthrd = util.selected_thread() + if nthrd != None: + tpath = THREAD_PATTERN.format(procnum=nproc, tnum=nthrd) + tobj = STATE.trace.proxy_object_path(tpath) + else: + tobj = None + STATE.trace.proxy_object_path('').set_value('_event_thread', tobj) + + +def ghidra_trace_put_threads(): + """ + Put the current process's threads into the Ghidra trace + """ + + STATE.require_tx() + with STATE.client.batch() as b: + put_threads() + + +def put_frames(): + nproc = util.selected_process() + mapper = STATE.trace.memory_mapper + nthrd = util.selected_thread() + if nthrd is None: + return + keys = [] + # f : _DEBUG_STACK_FRAME + for f in dbg().backtrace_list(): + fpath = FRAME_PATTERN.format( + procnum=nproc, tnum=nthrd, level=f.FrameNumber) + fobj = STATE.trace.create_object(fpath) + keys.append(FRAME_KEY_PATTERN.format(level=f.FrameNumber)) + base, pc = mapper.map(nproc, f.InstructionOffset) + if base != pc.space: + STATE.trace.create_overlay_space(base, pc.space) + fobj.set_value('_pc', pc) + fobj.set_value('InstructionOffset', hex(f.InstructionOffset)) + fobj.set_value('StackOffset', hex(f.StackOffset)) + fobj.set_value('ReturnOffset', hex(f.ReturnOffset)) + fobj.set_value('FrameOffset', hex(f.FrameOffset)) + fobj.set_value('_display', "#{} {}".format( + f.FrameNumber, hex(f.InstructionOffset))) + fobj.insert() + STATE.trace.proxy_object_path(STACK_PATTERN.format( + procnum=nproc, tnum=nthrd)).retain_values(keys) + + +def ghidra_trace_put_frames(): + """ + Put the current thread's frames into the Ghidra trace + """ + + STATE.require_tx() + with STATE.client.batch() as b: + put_frames() + + +def ghidra_trace_put_all(): + """ + Put everything currently selected into the Ghidra trace + """ + + STATE.require_tx() + with STATE.client.batch() as b: + # put_available() + put_processes() + put_environment() + put_regions() + put_modules() + put_threads() + # put_frames() + put_breakpoints() + # put_available() + ghidra_trace_putreg() + ghidra_trace_putmem("$pc 1") + ghidra_trace_putmem("$sp 1") + + +def ghidra_trace_install_hooks(): + """ + Install hooks to trace in Ghidra + """ + + hooks.install_hooks() + + +def ghidra_trace_remove_hooks(): + """ + Remove hooks to trace in Ghidra + + Using this directly is not recommended, unless it seems the hooks are + preventing pydbg or other extensions from operating. Removing hooks will break + trace synchronization until they are replaced. + """ + + hooks.remove_hooks() + + +def ghidra_trace_sync_enable(): + """ + Synchronize the current process with the Ghidra trace + + This will automatically install hooks if necessary. The goal is to record + the current frame, thread, and process into the trace immediately, and then + to append the trace upon stopping and/or selecting new frames. This action + is effective only for the current process. This command must be executed + for each individual process you'd like to synchronize. In older versions of + pydbg, certain events cannot be hooked. In that case, you may need to execute + certain "trace put" commands manually, or go without. + + This will have no effect unless or until you start a trace. + """ + + hooks.install_hooks() + hooks.enable_current_process() + put_state(0) + + +def ghidra_trace_sync_disable(): + """ + Cease synchronizing the current process with the Ghidra trace + + This is the opposite of 'ghidra_trace_sync-disable', except it will not + automatically remove hooks. + """ + + hooks.disable_current_process() + + +def ghidra_util_wait_stopped(timeout=1): + """ + Spin wait until the selected thread is stopped. + """ + + start = time.time() + t = util.selected_thread() + if t is None: + return + while not t.IsStopped() and not t.IsSuspended(): + t = util.selected_thread() # I suppose it could change + time.sleep(0.1) + if time.time() - start > timeout: + raise RuntimeError('Timed out waiting for thread to stop') + + +def dbg(): + return util.get_debugger() + + +SHOULD_WAIT = ['GO', 'STEP_BRANCH', 'STEP_INTO', 'STEP_OVER'] + + +def repl(): + print("This is the dbgeng.dll (WinDbg) REPL. To drop to Python3, press Ctrl-C.") + while True: + # TODO: Implement prompt retrieval in PR to pybag? + print('dbg> ', end='') + try: + cmd = input().strip() + if not cmd: + continue + dbg().cmd(cmd, quiet=True) + stat = dbg().exec_status() + if stat != 'BREAK': + dbg().wait() + else: + pass + # dbg().dispatch_events() + except KeyboardInterrupt as e: + print("") + print("You have left the dbgeng REPL and are now at the Python3 interpreter.") + print("use repl() to re-enter.") + return + except: + # Assume cmd() has already output the error + pass diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/hooks.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/hooks.py new file mode 100644 index 0000000000..44147f76f6 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/hooks.py @@ -0,0 +1,441 @@ +## ### +# IP: GHIDRA +# +# 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 sys +import time +import threading + +from pybag import pydbg +from pybag.dbgeng.callbacks import EventHandler +from pybag.dbgeng import core as DbgEng +from pybag.dbgeng import exception +from pybag.dbgeng.idebugbreakpoint import DebugBreakpoint + +from . import commands, util + +ALL_EVENTS = 0xFFFF + +class HookState(object): + __slots__ = ('installed', 'mem_catchpoint') + + def __init__(self): + self.installed = False + self.mem_catchpoint = None + + +class ProcessState(object): + __slots__ = ('first', 'regions', 'modules', 'threads', 'breaks', 'watches', 'visited', 'waiting') + + def __init__(self): + self.first = True + # For things we can detect changes to between stops + self.regions = False + self.modules = False + self.threads = False + self.breaks = False + self.watches = False + # For frames and threads that have already been synced since last stop + self.visited = set() + self.waiting = True + + def record(self, description=None, snap=None): + first = self.first + self.first = False + if description is not None: + commands.STATE.trace.snapshot(description, snap=snap) + if first: + commands.put_processes() + commands.put_environment() + if self.threads: + commands.put_threads() + self.threads = False + thread = util.selected_thread() + if thread is not None: + if first or thread not in self.visited: + commands.putreg() + commands.putmem("$pc", "1", display_result=False) + commands.putmem("$sp", "1", display_result=False) + #commands.put_frames() + self.visited.add(thread) + #frame = util.selected_frame() + #hashable_frame = (thread, frame) + #if first or hashable_frame not in self.visited: + # self.visited.add(hashable_frame) + if first or self.regions: + commands.put_regions() + self.regions = False + if first or self.modules: + commands.put_modules() + self.modules = False + if first or self.breaks: + commands.put_breakpoints() + self.breaks = False + + def record_continued(self): + commands.put_processes(running=True) + commands.put_threads(running=True) + + def record_exited(self, exit_code, description=None, snap=None): + if description is not None: + commands.STATE.trace.snapshot(description, snap) + proc = util.selected_process() + ipath = commands.PROCESS_PATTERN.format(procnum=proc) + commands.STATE.trace.proxy_object_path( + ipath).set_value('_exit_code', exit_code) + + +class BrkState(object): + __slots__ = ('break_loc_counts',) + + def __init__(self): + self.break_loc_counts = {} + + def update_brkloc_count(self, b, count): + self.break_loc_counts[b.GetID()] = count + + def get_brkloc_count(self, b): + return self.break_loc_counts.get(b.GetID(), 0) + + def del_brkloc_count(self, b): + if b not in self.break_loc_counts: + return 0 # TODO: Print a warning? + count = self.break_loc_counts[b.GetID()] + del self.break_loc_counts[b.GetID()] + return count + + +HOOK_STATE = HookState() +BRK_STATE = BrkState() +PROC_STATE = {} + + +def on_state_changed(*args): + #print("ON_STATE_CHANGED") + if args[0] == DbgEng.DEBUG_CES_CURRENT_THREAD: + return on_thread_selected(args) + elif args[0] == DbgEng.DEBUG_CES_BREAKPOINTS: + return on_breakpoint_modified(args) + elif args[0] == DbgEng.DEBUG_CES_RADIX: + util.set_convenience_variable('output-radix', args[1]) + return DbgEng.DEBUG_STATUS_GO + elif args[0] == DbgEng.DEBUG_CES_EXECUTION_STATUS: + proc = util.selected_process() + if args[1] & DbgEng.DEBUG_STATUS_INSIDE_WAIT: + PROC_STATE[proc].waiting = True + return DbgEng.DEBUG_STATUS_GO + PROC_STATE[proc].waiting = False + commands.put_state(proc) + if args[1] == DbgEng.DEBUG_STATUS_BREAK: + return on_stop(args) + else: + return on_cont(args) + return DbgEng.DEBUG_STATUS_GO + + +def on_debuggee_changed(*args): + #print("ON_DEBUGGEE_CHANGED") + trace = commands.STATE.trace + if trace is None: + return + if args[1] == DbgEng.DEBUG_CDS_REGISTERS: + on_register_changed(args[0][1]) + #if args[1] == DbgEng.DEBUG_CDS_DATA: + # on_memory_changed(args[0][1]) + return DbgEng.DEBUG_STATUS_GO + + +def on_session_status_changed(*args): + #print("ON_STATUS_CHANGED") + trace = commands.STATE.trace + if trace is None: + return + if args[0] == DbgEng.DEBUG_SESSION_ACTIVE or args[0] == DbgEng.DEBUG_SSESION_REBOOT: + with commands.STATE.client.batch(): + with trace.open_tx("New Process {}".format(util.selected_process())): + commands.put_processes() + return DbgEng.DEBUG_STATUS_GO + + +def on_symbol_state_changed(*args): + #print("ON_SYMBOL_STATE_CHANGED") + trace = commands.STATE.trace + if trace is None: + return + if args[0] == 1 or args[0] == 2: + PROC_STATE[proc].modules = True + return DbgEng.DEBUG_STATUS_GO + + +def on_system_error(*args): + print("ON_SYSTEM_ERROR") + print(hex(args[0])) + trace = commands.STATE.trace + if trace is None: + return + with commands.STATE.client.batch(): + with trace.open_tx("New Process {}".format(util.selected_process())): + commands.put_processes() + return DbgEng.DEBUG_STATUS_BREAK + + +def on_new_process(*args): + #print("ON_NEW_PROCESS") + trace = commands.STATE.trace + if trace is None: + return + with commands.STATE.client.batch(): + with trace.open_tx("New Process {}".format(util.selected_process())): + commands.put_processes() + return DbgEng.DEBUG_STATUS_BREAK + + +def on_process_selected(): + #print("PROCESS_SELECTED") + proc = util.selected_process() + if proc not in PROC_STATE: + return + trace = commands.STATE.trace + if trace is None: + return + with commands.STATE.client.batch(): + with trace.open_tx("Process {} selected".format(proc)): + PROC_STATE[proc].record() + commands.activate() + + +def on_process_deleted(*args): + #print("ON_PROCESS_DELETED") + proc = args[0] + on_exited(proc) + if proc in PROC_STATE: + del PROC_STATE[proc] + trace = commands.STATE.trace + if trace is None: + return + with commands.STATE.client.batch(): + with trace.open_tx("Process {} deleted".format(proc)): + commands.put_processes() # TODO: Could just delete the one.... + return DbgEng.DEBUG_STATUS_BREAK + + +def on_threads_changed(*args): + #print("ON_THREADS_CHANGED") + proc = util.selected_process() + if proc not in PROC_STATE: + return DbgEng.DEBUG_STATUS_GO + PROC_STATE[proc].threads = True + return DbgEng.DEBUG_STATUS_GO + + +def on_thread_selected(*args): + #print("THREAD_SELECTED") + nthrd = args[0][1] + nproc = util.selected_process() + if nproc not in PROC_STATE: + return + trace = commands.STATE.trace + if trace is None: + return + with commands.STATE.client.batch(): + with trace.open_tx("Thread {}.{} selected".format(nproc, nthrd)): + commands.put_state(nproc) + state = PROC_STATE[nproc] + if state.waiting: + state.record_continued() + else: + state.record() + commands.activate() + + +def on_register_changed(regnum): + #print("REGISTER_CHANGED") + proc = util.selected_process() + if proc not in PROC_STATE: + return + trace = commands.STATE.trace + if trace is None: + return + with commands.STATE.client.batch(): + with trace.open_tx("Register {} changed".format(regnum)): + commands.putreg() + commands.activate() + + +def on_cont(*args): + proc = util.selected_process() + if proc not in PROC_STATE: + return + trace = commands.STATE.trace + if trace is None: + return + state = PROC_STATE[proc] + with commands.STATE.client.batch(): + with trace.open_tx("Continued"): + state.record_continued() + return DbgEng.DEBUG_STATUS_GO + + +def on_stop(*args): + proc = util.selected_process() + if proc not in PROC_STATE: + print("not in state") + return + trace = commands.STATE.trace + if trace is None: + print("no trace") + return + state = PROC_STATE[proc] + state.visited.clear() + pos = dbg().get_position() + rng = range(pos.major, util.lastpos.major) + if pos.major > util.lastpos.major: + rng = range(util.lastpos.major, pos.major) + for i in rng: + if util.evttypes.__contains__(i): + type = util.evttypes[i] + if type == "modload" or type == "modunload": + on_modules_changed() + if type == "threadcreated" or type == "threadterm": + on_threads_changed() + util.lastpos = pos + with commands.STATE.client.batch(): + with trace.open_tx("Stopped"): + state.record("Stopped", util.pos2snap(pos)) + commands.put_state(proc) + commands.put_event_thread() + commands.activate() + + +def on_exited(proc): + if proc not in PROC_STATE: + print("not in state") + return + trace = commands.STATE.trace + if trace is None: + return + state = PROC_STATE[proc] + state.visited.clear() + exit_code = util.GetExitCode() + description = "Exited with code {}".format(exit_code) + with commands.STATE.client.batch(): + with trace.open_tx(description): + state.record_exited(exit_code, description) + commands.activate() + + +def on_modules_changed(*args): + #print("ON_MODULES_CHANGED") + proc = util.selected_process() + if proc not in PROC_STATE: + return DbgEng.DEBUG_STATUS_GO + PROC_STATE[proc].modules = True + return DbgEng.DEBUG_STATUS_GO + + +def on_breakpoint_created(bp): + proc = util.selected_process() + if proc not in PROC_STATE: + return + PROC_STATE[proc].breaks = True + trace = commands.STATE.trace + if trace is None: + return + ibpath = commands.PROC_BREAKS_PATTERN.format(procnum=proc) + with commands.STATE.client.batch(): + with trace.open_tx("Breakpoint {} created".format(bp.id)): + ibobj = trace.create_object(ibpath) + # Do not use retain_values or it'll remove other locs + commands.put_single_breakpoint(bp, ibobj, proc, []) + ibobj.insert() + + +def on_breakpoint_modified(*args): + #print("BREAKPOINT_MODIFIED") + proc = util.selected_process() + if proc not in PROC_STATE: + return + PROC_STATE[proc].breaks = True + trace = commands.STATE.trace + if trace is None: + return + ibpath = commands.PROC_BREAKS_PATTERN.format(procnum=proc) + ibobj = trace.create_object(ibpath) + bpid = args[0][1] + try: + bp = dbg()._control.GetBreakpointById(bpid) + except exception.E_NOINTERFACE_Error: + dbg().breakpoints._remove_stale(bpid) + return on_breakpoint_deleted(bpid) + return on_breakpoint_created(bp) + + +def on_breakpoint_deleted(bpt): + proc = util.selected_process() + if proc not in PROC_STATE: + return + PROC_STATE[proc].breaks = True + trace = commands.STATE.trace + if trace is None: + return + bpath = commands.PROC_BREAK_PATTERN.format(procnum=proc, breaknum=bpt.id) + with commands.STATE.client.batch(): + with trace.open_tx("Breakpoint {} deleted".format(bpt.id)): + trace.proxy_object_path(bpath).remove(tree=True) + + +def on_breakpoint_hit(*args): + trace = commands.STATE.trace + if trace is None: + return + with commands.STATE.client.batch(): + with trace.open_tx("New Process {}".format(util.selected_process())): + commands.put_processes() + return DbgEng.DEBUG_STATUS_GO + + +def on_exception(*args): + trace = commands.STATE.trace + if trace is None: + return + with commands.STATE.client.batch(): + with trace.open_tx("New Process {}".format(util.selected_process())): + commands.put_processes() + return DbgEng.DEBUG_STATUS_GO + + +def install_hooks(): + if HOOK_STATE.installed: + return + HOOK_STATE.installed = True + +def remove_hooks(): + if not HOOK_STATE.installed: + return + HOOK_STATE.installed = False + + +def enable_current_process(): + proc = util.selected_process() + PROC_STATE[proc] = ProcessState() + + +def disable_current_process(): + proc = util.selected_process() + if proc in PROC_STATE: + # Silently ignore already disabled + del PROC_STATE[proc] + +def dbg(): + return util.get_debugger() diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/libraries.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/libraries.py new file mode 100644 index 0000000000..5d6b4bd9fc --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/libraries.py @@ -0,0 +1,78 @@ +## ### +# IP: GHIDRA +# +# 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 os +import platform + +import comtypes +import comtypes.client + + +ctypes.windll.kernel32.SetErrorMode(0x0001 | 0x0002 | 0x8000) + +if platform.architecture()[0] == '64bit': + dbgdirs = [os.getenv('OPT_DBGMODEL_PATH'), + r'C:\Program Files\Windows Kits\10\Debuggers\x64', + r'C:\Program Files (x86)\Windows Kits\10\Debuggers\x64'] +else: + dbgdirs = [os.getenv('OPT_DBGMODEL_PATH'), + r'C:\Program Files\Windows Kits\10\Debuggers\x86', + r'C:\Program Files (x86)\Windows Kits\10\Debuggers\x86'] +dbgdir = None +for _dir in dbgdirs: + if _dir is not None and os.path.exists(_dir): + dbgdir = _dir + break + +if not dbgdir: + raise RuntimeError("Windbg install directory not found!") + +print(f"Loading dbgeng and friends from {dbgdir}") + +# preload these to get correct DLLs loaded +try: + ctypes.windll.LoadLibrary(os.path.join(dbgdir, 'dbghelp.dll')) +except Exception as exc: + print(fr"LoadLibrary failed: {dbgdir}\dbghelp.dll {exc}") + pass +try: + ctypes.windll.LoadLibrary(os.path.join(dbgdir, 'dbgeng.dll')) +except Exception as exc: + print(fr"LoadLibrary failed: {dbgdir}\dbgeng.dll {exc}") + pass +try: + ctypes.windll.LoadLibrary(os.path.join(dbgdir, 'DbgModel.dll')) +except Exception as exc: + print(fr"LoadLibrary failed: {dbgdir}\dbgmodel.dll {exc}") + pass +try: + ctypes.windll.LoadLibrary(os.path.join(dbgdir, 'ttd/TTDReplay.dll')) +except Exception as exc: + print(fr"LoadLibrary failed: {dbgdir}\ttd\TTDReplay.dll {exc}") + pass +try: + ctypes.windll.LoadLibrary(os.path.join(dbgdir, 'ttd/TTDReplayCPU.dll')) +except Exception as exc: + print(fr"LoadLibrary failed: {dbgdir}\ttd\TTDReplayCPU.dll {exc}") + pass + +try: + from comtypes.gen import DbgMod +except: + tlb = os.path.join(dbgmodel.module_locator(), 'tlb', 'dbgmodel.tlb') + print(f"Loading TLB: {tlb}") + comtypes.client.GetModule(tlb) + from comtypes.gen import DbgMod diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/methods.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/methods.py new file mode 100644 index 0000000000..3c09616f5f --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidrattd/methods.py @@ -0,0 +1,536 @@ +## ### +# IP: GHIDRA +# +# 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. +## +from concurrent.futures import Future, ThreadPoolExecutor +import re +import sys + +from ghidratrace import sch +from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange + +from pyttd import pyTTD +#from pybag import pydbg +#from pybag.dbgeng import core as DbgEng + +from . import util, commands, hooks +from contextlib import redirect_stdout +from io import StringIO + + +REGISTRY = MethodRegistry(ThreadPoolExecutor(max_workers=1)) + + +def extre(base, ext): + return re.compile(base.pattern + ext) + + +AVAILABLE_PATTERN = re.compile(r'Available\[(?P