diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/build.gradle b/Ghidra/Debug/Debugger-agent-dbgeng/build.gradle index 6f73a8fd56..35fc966118 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/build.gradle +++ b/Ghidra/Debug/Debugger-agent-dbgeng/build.gradle @@ -20,6 +20,7 @@ apply from: "$rootProject.projectDir/gradle/nativeProject.gradle" apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle" apply from: "$rootProject.projectDir/gradle/debugger/hasNodepJar.gradle" +apply from: "$rootProject.projectDir/gradle/debugger/hasPythonPackage.gradle" apply plugin: 'eclipse' eclipse.project.name = 'Debug Debugger-agent-dbgeng' diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest b/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest index e9bc58dcf9..7689ea7e59 100644 --- a/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest +++ b/Ghidra/Debug/Debugger-agent-dbgeng/certification.manifest @@ -3,3 +3,7 @@ Module.manifest||GHIDRA||||END| src/javaprovider/def/javaprovider.def||GHIDRA||||END| src/javaprovider/rc/javaprovider.rc||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/schema.xml||GHIDRA||||END| diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/LICENSE b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/LICENSE new file mode 100644 index 0000000000..c026b6b79a --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/LICENSE @@ -0,0 +1,11 @@ +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. diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/README.md b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/README.md new file mode 100644 index 0000000000..2a84727524 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/README.md @@ -0,0 +1,8 @@ +# Ghidra Trace RMI + +Package for connecting dbgeng to Ghidra via Trace RMI. + +This connector requires Pybag 2.2.8 or better: + + https://pypi.org/project/Pybag + https://github.com/dshikashio/Pybag \ No newline at end of file diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/pyproject.toml b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/pyproject.toml new file mode 100644 index 0000000000..9972ad4aec --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "ghidradbg" +version = "10.4" +authors = [ + { name="Ghidra Development Team" }, +] +description = "Ghidra's Plugin for dbgeng" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", +] +dependencies = [ + "ghidratrace==10.4", + "pybag>=2.2.8" +] + +[project.urls] +"Homepage" = "https://github.com/NationalSecurityAgency/ghidra" +"Bug Tracker" = "https://github.com/NationalSecurityAgency/ghidra/issues" diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/__init__.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/__init__.py new file mode 100644 index 0000000000..389cafd711 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/__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. +## +import ctypes +ctypes.windll.kernel32.SetErrorMode(0x0001|0x0002|0x8000) + +from . import util, commands, methods, hooks diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/arch.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/arch.py new file mode 100644 index 0000000000..f7b2f35fef --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/arch.py @@ -0,0 +1,240 @@ +## ### +# 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(): + try: + type = util.get_debugger()._control.GetActualProcessorType() + except Exception: + return "Unknown" + if type is None: + return "x86_64" + if type == 0x8664: + return "x86_64" + if type == 0x014c: + return "x86" + if type == 0x01c0: + return "ARM" + if type == 0x0200: + return "Itanium" + if type == 0x0EBC: + return "EFI" + return "Unknown" + + +def get_endian(): + parm = util.get_convenience_variable('endian') + if parm != 'auto': + return parm + return 'little' + + +def get_osabi(): + parm = util.get_convenience_variable('osabi') + if not parm in ['auto', 'default']: + return parm + try: + os = util.get_debugger().cmd("vertarget") + if "Windows" not in os: + return "default" + except Exception: + pass + 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/ghidradbg/commands.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py new file mode 100644 index 0000000000..f40d8b5933 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/commands.py @@ -0,0 +1,1307 @@ +## ### +# 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 + +from ghidratrace import sch +from ghidratrace.client import Client, Address, AddressRange, TraceObject + +from pybag import pydbg, userdbg, kerneldbg +from pybag.dbgeng import core as DbgEng +from pybag.dbgeng import exception + +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 + +# 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))) + STATE.client = Client(c, methods.REGISTRY) + 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, 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/' + progname.split('/')[-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. + """ + + util.base = userdbg.UserDbg() + if command != None: + if timeout != None: + util.base._client.CreateProcess(command, DbgEng.DEBUG_PROCESS) + if initial_break: + util.base._control.AddEngineOptions(DbgEng.DEBUG_ENGINITIAL_BREAK) + util.base.wait(timeout) + else: + util.base.create(command, initial_break) + if start_trace: + ghidra_trace_start(command) + + +def ghidra_trace_kill(): + """ + Kill a session. + """ + + dbg()._client.TerminateCurrentProcess() + + +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("Connected to Ghidra at {}:{}\n".format(host, port)) + 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): + """ + 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)} + + +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(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().reg + for i in range(0, len(regs)): + name = regs._reg.GetDescription(i)[0] + value = regs._get_register_by_index(i) + try: + values.append(mapper.map_value(nproc, name, value)) + robj.set_value(name, 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 = [] + regs = dbg().reg + for i in range(0, len(regs)): + name = regs._reg.GetDescription(i)[0] + names.append(mapper.map_name(nproc, name)) + 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): + status = dbg()._control.GetExecutionStatus() + if status == DbgEng.DEBUG_STATUS_BREAK: + return 'STOPPED' + return 'RUNNING' + + +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(i) + procobj.set_value('_state', istate) + if running == False: + procobj.set_value('_pid', p[0]) + pidstr = ('0x{:x}' if radix == 16 else '0{:o}' if radix == 8 else '{}').format(p[0]) + procobj.set_value('_display', pidstr) + procobj.set_value('Name', str(p[1])) + procobj.set_value('PEB', hex(p[2])) + 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() + 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.GetId()) + brkobj = STATE.trace.create_object(bpath) + if bp.GetFlags() & DbgEng.DEBUG_BREAKPOINT_ENABLED: + status = True + else: + status = False + if bp.GetFlags() & DbgEng.DEBUG_BREAKPOINT_DEFERRED: + offset = "[Deferred]" + expr = bp.GetOffsetExpression() + else: + address = bp.GetOffset() + offset = "%016x" % address + expr = dbg().get_name_by_offset(address) + try: + tid = bp.GetMatchThreadId() + tid = "%04x" % tid + except exception.E_NOINTERFACE_Error: + tid = "****" + + if bp.GetType()[0] == DbgEng.DEBUG_BREAKPOINT_DATA: + width, prot = bp.GetDataParameters() + width = str(width) + prot = {4: 'HW_EXECUTE', 2: 'READ', 1: 'WRITE'}[prot] + else: + width = ' ' + prot = 'SW_EXECUTE' + + 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('Pass Count', bp.GetPassCount()) + brkobj.set_value('Current Pass Count', bp.GetCurrentPassCount()) + brkobj.set_value('Enabled', status) + brkobj.set_value('Flags', bp.GetFlags()) + if tid != None: + brkobj.set_value('Match TID', tid) + brkobj.set_value('Command', bp.GetCommand()) + brkobj.insert() + + k = PROC_BREAK_KEY_PATTERN.format(breaknum=bp.GetId()) + 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 dbg().breakpoints] + for bpid in ids: + try: + bp = dbg()._control.GetBreakpointById(bpid) + except exception.E_NOINTERFACE_Error: + dbg().breakpoints._remove_stale(bpid) + continue + keys.append(PROC_BREAK_KEY_PATTERN.format(breaknum=bpid)) + 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', 'pydbg') + 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: + regions = dbg().memory_list() + except Exception: + regions = [] + if len(regions) == 0 and util.selected_thread() != None: + regions = [util.REGION_INFO_READER.full_mem()] + mapper = STATE.trace.memory_mapper + keys = [] + # r : MEMORY_BASIC_INFORMATION64 + for r in regions: + rpath = REGION_PATTERN.format(procnum=nproc, start=r.BaseAddress) + keys.append(REGION_KEY_PATTERN.format(start=r.BaseAddress)) + regobj = STATE.trace.create_object(rpath) + start_base, start_addr = mapper.map(nproc, r.BaseAddress) + if start_base != start_addr.space: + STATE.trace.create_overlay_space(start_base, start_addr.space) + regobj.set_value('_range', start_addr.extend(r.RegionSize)) + regobj.set_value('_readable', r.Protect == None or r.Protect&0x66 != 0) + regobj.set_value('_writable', r.Protect == None or r.Protect&0xCC != 0) + regobj.set_value('_executable', r.Protect == None or r.Protect&0xF0 != 0) + regobj.set_value('_offset', hex(r.BaseAddress)) + regobj.set_value('Base', hex(r.BaseAddress)) + regobj.set_value('Size', hex(r.RegionSize)) + regobj.set_value('AllocationBase', hex(r.AllocationBase)) + regobj.set_value('Protect', hex(r.Protect)) + regobj.set_value('Type', hex(r.Type)) + 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 = dbg().module_list() + mapper = STATE.trace.memory_mapper + mod_keys = [] + for m in modules: + name = m[0][0] + # m[1] : _DEBUG_MODULE_PARAMETERS + base = m[1].Base + hbase = hex(base) + size = m[1].Size + flags = m[1].Flags + mpath = MODULE_PATTERN.format(procnum=nproc, modpath=hbase) + modobj = STATE.trace.create_object(mpath) + mod_keys.append(MODULE_KEY_PATTERN.format(modpath=hbase)) + modobj.set_value('_module_name', name) + 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('Base', hbase) + modobj.set_value('Size', hex(size)) + modobj.set_value('Flags', hex(size)) + modobj.insert() + + # TODO: would be nice to list sections, but currently we have no API for + # it as far as I am aware + # sec_keys = [] + # STATE.trace.proxy_object_path( + # mpath + SECTIONS_ADD_PATTERN).retain_values(sec_keys) + + STATE.trace.proxy_object_path(MODULES_PATTERN.format( + procnum=nproc)).retain_values(mod_keys) + + +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 convert_tid(t): + if t[1] == 0: + return t[2] + return t[1] + + +def compute_thread_display(tidstr, t): + return '[{} {}]'.format(tidstr, t[2]) + + +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 i, t in enumerate(util.thread_list(running)): + tpath = THREAD_PATTERN.format(procnum=nproc, tnum=i) + tobj = STATE.trace.create_object(tpath) + keys.append(THREAD_KEY_PATTERN.format(tnum=i)) + #tobj.set_value('_state', convert_state(t)) + if running == False: + tobj.set_value('_name', t[2]) + tid = t[0] + tobj.set_value('_tid', tid) + tidstr = ('0x{:x}' if radix == + 16 else '0{:o}' if radix == 8 else '{}').format(tid) + tobj.set_value('_short_display', '[{}.{}:{}]'.format( + nproc, i, tidstr)) + tobj.set_value('_display', compute_thread_display(tidstr, t)) + tobj.set_value('TEB', hex(t[1])) + tobj.set_value('Name', t[2]) + tobj.insert() + STATE.trace.proxy_object_path( + THREADS_PATTERN.format(procnum=nproc)).retain_values(keys) + + +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() + + +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() diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/hooks.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/hooks.py new file mode 100644 index 0000000000..67ddd78230 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/hooks.py @@ -0,0 +1,439 @@ +## ### +# 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') + + 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() + + def record(self, description=None): + first = self.first + self.first = False + if description is not None: + commands.STATE.trace.snapshot(description) + 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", from_tty=False) + commands.putmem("$sp", "1", from_tty=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 or self.threads or self.modules: + 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): + if description is not None: + commands.STATE.trace.snapshot(description) + 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") + #print(args[0]) + 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: + if args[1] & DbgEng.DEBUG_STATUS_INSIDE_WAIT: + return DbgEng.DEBUG_STATUS_GO + 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)): + PROC_STATE[nproc].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() + with commands.STATE.client.batch(): + with trace.open_tx("Stopped"): + state.record("Stopped") + 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.GetId())): + 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(bpid): + 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=bpid) + with commands.STATE.client.batch(): + with trace.open_tx("Breakpoint {} deleted".format(bpid)): + 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 + + events = dbg().events + + events.engine_state(handler=on_state_changed) + events.debuggee_state(handler=on_debuggee_changed) + events.session_status(handler=on_session_status_changed) + events.symbol_state(handler=on_symbol_state_changed) + events.system_error(handler=on_system_error) + + events.create_process(handler=on_new_process) + events.exit_process(handler=on_process_deleted) + events.create_thread(handler=on_threads_changed) + events.exit_thread(handler=on_threads_changed) + events.module_load(handler=on_modules_changed) + events.unload_module(handler=on_modules_changed) + + #events.breakpoint(handler=on_breakpoint_hit) + #events.exception(handler=on_exception) + + +def remove_hooks(): + if not HOOK_STATE.installed: + return + HOOK_STATE.installed = False + dbg()._reset_callbacks() + + +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/ghidradbg/methods.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py new file mode 100644 index 0000000000..ae9bd6b33a --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/methods.py @@ -0,0 +1,522 @@ +## ### +# 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 pybag import pydbg +from pybag.dbgeng import core as DbgEng + +from . import util, commands +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('Available\[(?P\\d*)\]') +WATCHPOINT_PATTERN = re.compile('Watchpoints\[(?P\\d*)\]') +BREAKPOINT_PATTERN = re.compile('Breakpoints\[(?P\\d*)\]') +BREAK_LOC_PATTERN = extre(BREAKPOINT_PATTERN, '\[(?P\\d*)\]') +PROCESS_PATTERN = re.compile('Processes\[(?P\\d*)\]') +PROC_BREAKS_PATTERN = extre(PROCESS_PATTERN, '\.Breakpoints') +PROC_BREAKBPT_PATTERN = extre(PROC_BREAKS_PATTERN, '\[(?P\\d*)\]') +ENV_PATTERN = extre(PROCESS_PATTERN, '\.Environment') +THREADS_PATTERN = extre(PROCESS_PATTERN, '\.Threads') +THREAD_PATTERN = extre(THREADS_PATTERN, '\[(?P\\d*)\]') +STACK_PATTERN = extre(THREAD_PATTERN, '\.Stack') +FRAME_PATTERN = extre(STACK_PATTERN, '\[(?P\\d*)\]') +REGS_PATTERN0 = extre(THREAD_PATTERN, '.Registers') +REGS_PATTERN = extre(FRAME_PATTERN, '.Registers') +MEMORY_PATTERN = extre(PROCESS_PATTERN, '\.Memory') +MODULES_PATTERN = extre(PROCESS_PATTERN, '\.Modules') + + +def find_availpid_by_pattern(pattern, object, err_msg): + mat = pattern.fullmatch(object.path) + if mat is None: + raise TypeError(f"{object} is not {err_msg}") + pid = int(mat['pid']) + return pid + + +def find_availpid_by_obj(object): + return find_availpid_by_pattern(AVAILABLE_PATTERN, object, "an Available") + + +def find_proc_by_num(id): + if id != util.selected_process(): + util.select_process(id) + return util.selected_process() + + +def find_proc_by_pattern(object, pattern, err_msg): + mat = pattern.fullmatch(object.path) + if mat is None: + raise TypeError(f"{object} is not {err_msg}") + procnum = int(mat['procnum']) + return find_proc_by_num(procnum) + + +def find_proc_by_obj(object): + return find_proc_by_pattern(object, PROCESS_PATTERN, "an Process") + + +def find_proc_by_procbreak_obj(object): + return find_proc_by_pattern(object, PROC_BREAKS_PATTERN, + "a BreakpointLocationContainer") + +def find_proc_by_procwatch_obj(object): + return find_proc_by_pattern(object, PROC_WATCHES_PATTERN, + "a WatchpointContainer") + + +def find_proc_by_env_obj(object): + return find_proc_by_pattern(object, ENV_PATTERN, "an Environment") + + +def find_proc_by_threads_obj(object): + return find_proc_by_pattern(object, THREADS_PATTERN, "a ThreadContainer") + + +def find_proc_by_mem_obj(object): + return find_proc_by_pattern(object, MEMORY_PATTERN, "a Memory") + + +def find_proc_by_modules_obj(object): + return find_proc_by_pattern(object, MODULES_PATTERN, "a ModuleContainer") + + +def find_thread_by_num(id): + if id != util.selected_thread(): + util.select_thread(id) + return util.selected_thread() + + +def find_thread_by_pattern(pattern, object, err_msg): + mat = pattern.fullmatch(object.path) + if mat is None: + raise TypeError(f"{object} is not {err_msg}") + pnum = int(mat['procnum']) + tnum = int(mat['tnum']) + find_proc_by_num(pnum) + return find_thread_by_num(tnum) + + +def find_thread_by_obj(object): + return find_thread_by_pattern(THREAD_PATTERN, object, "a Thread") + + +def find_thread_by_stack_obj(object): + return find_thread_by_pattern(STACK_PATTERN, object, "a Stack") + + +def find_thread_by_regs_obj(object): + return find_thread_by_pattern(REGS_PATTERN0, object, "a RegisterValueContainer") + + +def find_frame_by_level(level): + return dbg().backtrace_list()[level] + + +def find_frame_by_pattern(pattern, object, err_msg): + mat = pattern.fullmatch(object.path) + if mat is None: + raise TypeError(f"{object} is not {err_msg}") + pnum = int(mat['procnum']) + tnum = int(mat['tnum']) + level = int(mat['level']) + find_proc_by_num(pnum) + find_thread_by_num(tnum) + return find_frame_by_level(level) + + +def find_frame_by_obj(object): + return find_frame_by_pattern(FRAME_PATTERN, object, "a StackFrame") + + +def find_bpt_by_number(breaknum): + try: + bp = dbg()._control.GetBreakpointById(breaknum) + return bp + except exception.E_NOINTERFACE_Error: + raise KeyError(f"Breakpoints[{breaknum}] does not exist") + + +def find_bpt_by_pattern(pattern, object, err_msg): + mat = pattern.fullmatch(object.path) + if mat is None: + raise TypeError(f"{object} is not {err_msg}") + breaknum = int(mat['breaknum']) + return find_bpt_by_number(breaknum) + + +def find_bpt_by_obj(object): + return find_bpt_by_pattern(PROC_BREAKBPT_PATTERN, object, "a BreakpointSpec") + + +shared_globals = dict() + +@REGISTRY.method +def execute(cmd: str, to_string: bool=False): + """Execute a CLI command.""" + #print("***{}***".format(cmd)) + #sys.stderr.flush() + #sys.stdout.flush() + if to_string: + data = StringIO() + with redirect_stdout(data): + exec("{}".format(cmd), shared_globals) + return data.getvalue() + else: + exec("{}".format(cmd), shared_globals) + + +@REGISTRY.method +def evaluate(expr: str): + """Execute a CLI command.""" + return str(eval("{}".format(expr), shared_globals)) + + +@REGISTRY.method(action='refresh') +def refresh_available(node: sch.Schema('AvailableContainer')): + """List processes on pydbg's host system.""" + with commands.open_tracked_tx('Refresh Available'): + commands.ghidra_trace_put_available() + + +@REGISTRY.method(action='refresh') +def refresh_breakpoints(node: sch.Schema('BreakpointContainer')): + """ + Refresh the list of breakpoints (including locations for the current + process). + """ + with commands.open_tracked_tx('Refresh Breakpoints'): + commands.ghidra_trace_put_breakpoints() + + +@REGISTRY.method(action='refresh') +def refresh_processes(node: sch.Schema('ProcessContainer')): + """Refresh the list of processes.""" + with commands.open_tracked_tx('Refresh Processes'): + commands.ghidra_trace_put_threads() + + +@REGISTRY.method(action='refresh') +def refresh_proc_breakpoints(node: sch.Schema('BreakpointLocationContainer')): + """ + Refresh the breakpoint locations for the process. + + In the course of refreshing the locations, the breakpoint list will also be + refreshed. + """ + with commands.open_tracked_tx('Refresh Breakpoint Locations'): + commands.ghidra_trace_put_breakpoints() + + +@REGISTRY.method(action='refresh') +def refresh_environment(node: sch.Schema('Environment')): + """Refresh the environment descriptors (arch, os, endian).""" + with commands.open_tracked_tx('Refresh Environment'): + commands.ghidra_trace_put_environment() + +@REGISTRY.method(action='refresh') +def refresh_threads(node: sch.Schema('ThreadContainer')): + """Refresh the list of threads in the process.""" + with commands.open_tracked_tx('Refresh Threads'): + commands.ghidra_trace_put_threads() + + +@REGISTRY.method(action='refresh') +def refresh_stack(node: sch.Schema('Stack')): + """Refresh the backtrace for the thread.""" + tnum = find_thread_by_stack_obj(node) + with commands.open_tracked_tx('Refresh Stack'): + commands.ghidra_trace_put_frames() + + +@REGISTRY.method(action='refresh') +def refresh_registers(node: sch.Schema('RegisterValueContainer')): + """Refresh the register values for the frame.""" + tnum = find_thread_by_regs_obj(node) + with commands.open_tracked_tx('Refresh Registers'): + commands.ghidra_trace_putreg() + + +@REGISTRY.method(action='refresh') +def refresh_mappings(node: sch.Schema('Memory')): + """Refresh the list of memory regions for the process.""" + with commands.open_tracked_tx('Refresh Memory Regions'): + commands.ghidra_trace_put_regions() + + +@REGISTRY.method(action='refresh') +def refresh_modules(node: sch.Schema('ModuleContainer')): + """ + Refresh the modules and sections list for the process. + + This will refresh the sections for all modules, not just the selected one. + """ + with commands.open_tracked_tx('Refresh Modules'): + commands.ghidra_trace_put_modules() + + +@REGISTRY.method(action='activate') +def activate_process(process: sch.Schema('Process')): + """Switch to the process.""" + find_proc_by_obj(process) + +@REGISTRY.method(action='activate') +def activate_thread(thread: sch.Schema('Thread')): + """Switch to the thread.""" + find_thread_by_obj(thread) + + +@REGISTRY.method(action='activate') +def activate_frame(frame: sch.Schema('StackFrame')): + """Select the frame.""" + find_frame_by_obj(frame) + + +@REGISTRY.method(action='delete') +def remove_process(process: sch.Schema('Process')): + """Remove the process.""" + find_proc_by_obj(process) + dbg().detach() + + +@REGISTRY.method(action='connect') +def target(process: sch.Schema('Process'), spec: str): + """Connect to a target machine or process.""" + find_proc_by_obj(process) + dbg().attach(spec) + + +@REGISTRY.method(action='attach') +def attach_obj(target: sch.Schema('Attachable')): + """Attach the process to the given target.""" + pid = find_availpid_by_obj(target) + dbg().attach(pid) + +@REGISTRY.method(action='attach') +def attach_pid(pid: int): + """Attach the process to the given target.""" + dbg().attach(pid) + +@REGISTRY.method(action='attach') +def attach_name(process: sch.Schema('Process'), name: str): + """Attach the process to the given target.""" + dbg().atach(name) + + +@REGISTRY.method +def detach(process: sch.Schema('Process')): + """Detach the process's target.""" + dbg().detach() + + +@REGISTRY.method(action='launch') +def launch_loader( + file: ParamDesc(str, display='File'), + args: ParamDesc(str, display='Arguments')=''): + """ + Start a native process with the given command line, stopping at the ntdll initial breakpoint. + """ + command = file + if args != None: + command += " "+args + commands.ghidra_trace_create(command=file, start_trace=False) + + +@REGISTRY.method(action='launch') +def launch( + timeout: ParamDesc(int, display='Timeout'), + file: ParamDesc(str, display='File'), + args: ParamDesc(str, display='Arguments')=''): + """ + Run a native process with the given command line. + """ + command = file + if args != None: + command += " "+args + commands.ghidra_trace_create(command, initial_break=False, timeout=timeout, start_trace=False) + + +@REGISTRY.method +def kill(process: sch.Schema('Process')): + """Kill execution of the process.""" + dbg().terminate() + + +@REGISTRY.method(name='continue', action='resume') +def _continue(process: sch.Schema('Process')): + """Continue execution of the process.""" + dbg().go() + + +@REGISTRY.method +def interrupt(): + """Interrupt the execution of the debugged program.""" + dbg()._control.SetInterrupt(DbgEng.DEBUG_INTERRUPT_ACTIVE) + + +@REGISTRY.method(action='step_into') +def step_into(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1): + """Step on instruction exactly.""" + find_thread_by_obj(thread) + dbg().stepi(n) + + +@REGISTRY.method(action='step_over') +def step_over(thread: sch.Schema('Thread'), n: ParamDesc(int, display='N')=1): + """Step one instruction, but proceed through subroutine calls.""" + find_thread_by_obj(thread) + dbg().stepo(n) + + +@REGISTRY.method(action='step_out') +def step_out(thread: sch.Schema('Thread')): + """Execute until the current stack frame returns.""" + find_thread_by_obj(thread) + dbg().stepout() + + +@REGISTRY.method(action='step_to') +def step_to(thread: sch.Schema('Thread'), address: Address, max=None): + """Continue execution up to the given address.""" + find_thread_by_obj(thread) + return dbg().stepto(address.offset, max) + + +@REGISTRY.method(action='break_sw_execute') +def break_address(process: sch.Schema('Process'), address: Address): + """Set a breakpoint.""" + find_proc_by_obj(process) + dbg().bp(expr=address.offset) + + +@REGISTRY.method(action='break_sw_execute') +def break_expression(expression: str): + """Set a breakpoint.""" + # TODO: Escape? + dbg().bp(expr=expression) + + +@REGISTRY.method(action='break_hw_execute') +def break_hw_address(process: sch.Schema('Process'), address: Address): + """Set a hardware-assisted breakpoint.""" + find_proc_by_obj(process) + dbg().ba(expr=address.offset) + + +@REGISTRY.method(action='break_hw_execute') +def break_hw_expression(expression: str): + """Set a hardware-assisted breakpoint.""" + dbg().ba(expr=expression) + + +@REGISTRY.method(action='break_read') +def break_read_range(process: sch.Schema('Process'), range: AddressRange): + """Set a read watchpoint.""" + find_proc_by_obj(process) + dbg().ba(expr=range.min, size=range.length(), access=DbgEng.DEBUG_BREAK_READ) + + +@REGISTRY.method(action='break_read') +def break_read_expression(expression: str): + """Set a read watchpoint.""" + dbg().ba(expr=expression, access=DbgEng.DEBUG_BREAK_READ) + + +@REGISTRY.method(action='break_write') +def break_write_range(process: sch.Schema('Process'), range: AddressRange): + """Set a watchpoint.""" + find_proc_by_obj(process) + dbg().ba(expr=range.min, size=range.length(), access=DbgEng.DEBUG_BREAK_WRITE) + + +@REGISTRY.method(action='break_write') +def break_write_expression(expression: str): + """Set a watchpoint.""" + dbg().ba(expr=expression, access=DbgEng.DEBUG_BREAK_WRITE) + + +@REGISTRY.method(action='break_access') +def break_access_range(process: sch.Schema('Process'), range: AddressRange): + """Set an access watchpoint.""" + find_proc_by_obj(process) + dbg().ba(expr=range.min, size=range.length(), access=DbgEng.DEBUG_BREAK_READ|DbgEng.DEBUG_BREAK_WRITE) + + +@REGISTRY.method(action='break_access') +def break_access_expression(expression: str): + """Set an access watchpoint.""" + dbg().ba(expr=expression, access=DbgEng.DEBUG_BREAK_READ|DbgEng.DEBUG_BREAK_WRITE) + + +@REGISTRY.method(action='toggle') +def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool): + """Toggle a breakpoint.""" + bpt = find_bpt_by_obj(breakpoint) + if enabled: + dbg().be(bpt.GetId()) + else: + dbg().bd(bpt.GetId()) + + +@REGISTRY.method(action='delete') +def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')): + """Delete a breakpoint.""" + bpt = find_bpt_by_obj(breakpoint) + dbg().cmd("bc {}".format(bpt.GetId())) + + +@REGISTRY.method +def read_mem(process: sch.Schema('Process'), range: AddressRange): + """Read memory.""" + nproc = find_proc_by_obj(process) + offset_start = process.trace.memory_mapper.map_back( + nproc, Address(range.space, range.min)) + with commands.open_tracked_tx('Read Memory'): + dbg().read(range.min, range.length()) + + +@REGISTRY.method +def write_mem(process: sch.Schema('Process'), address: Address, data: bytes): + """Write memory.""" + nproc = find_proc_by_obj(process) + offset = process.trace.memory_mapper.map_back(nproc, address) + dbg().write(offset, data) + + +@REGISTRY.method +def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes): + """Write a register.""" + util.select_frame() + nproc = pydbg.selected_process() + dbg().reg._set_register(name, value) + + +def dbg(): + return util.get_debugger() diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/schema.xml b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/schema.xml new file mode 100644 index 0000000000..96da2775d9 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/schema.xml @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/util.py b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/util.py new file mode 100644 index 0000000000..1099b843ac --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-dbgeng/src/main/py/src/ghidradbg/util.py @@ -0,0 +1,260 @@ +## ### +# 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 collections import namedtuple +import os +import re +import sys + +from ctypes import * +from pybag import pydbg +from pybag.dbgeng import core as DbgEng +from pybag.dbgeng import exception +from pybag.dbgeng import util as DbgUtil + +base = pydbg.DebuggerBase() +DbgVersion = namedtuple('DbgVersion', ['full', 'major', 'minor']) + + +def _compute_pydbg_ver(): + blurb = "" #base._control.GetActualProcessorType() + full = "" + major = 0 + minor = 0 + return DbgVersion(full, int(major), int(minor)) + + +DBG_VERSION = _compute_pydbg_ver() + + +def get_debugger(): + return base + +def get_target(): + return 0 #get_debugger()._systems.GetCurrentSystemId() + +def get_inst(addr): + dbg = get_debugger() + ins = DbgUtil.disassemble_instruction(dbg.bitness(), addr, dbg.read(addr, 15)) + return str(ins) + +def get_inst_sz(addr): + dbg = get_debugger() + ins = DbgUtil.disassemble_instruction(dbg.bitness(), addr, dbg.read(addr, 15)) + return str(ins.size) + +def get_breakpoints(): + ids = [bpid for bpid in get_debugger().breakpoints] + offset_set = [] + expr_set = [] + prot_set = [] + width_set = [] + stat_set = [] + for bpid in ids: + try: + bp = get_debugger()._control.GetBreakpointById(bpid) + except exception.E_NOINTERFACE_Error: + continue + + if bp.GetFlags() & DbgEng.DEBUG_BREAKPOINT_DEFERRED: + offset = "[Deferred]" + expr = bp.GetOffsetExpression() + else: + offset = "%016x" % bp.GetOffset() + expr = get_debugger().get_name_by_offset(bp.GetOffset()) + + if bp.GetType()[0] == DbgEng.DEBUG_BREAKPOINT_DATA: + width, prot = bp.GetDataParameters() + width = ' sz={}'.format(str(width)) + prot = {4: 'type=x', 3: 'type=rw', 2: 'type=w', 1: 'type=r'}[prot] + else: + width = '' + prot = '' + + if bp.GetFlags() & DbgEng.DEBUG_BREAKPOINT_ENABLED: + status = 'enabled' + else: + status = 'disabled' + + offset_set.append(offset) + expr_set.append(expr) + prot_set.append(prot) + width_set.append(width) + stat_set.append(status) + return zip(offset_set, expr_set, prot_set, width_set, stat_set) + +def selected_process(): + try: + return get_debugger()._systems.GetCurrentProcessId() + #return current_process + except Exception: + return None + +def selected_thread(): + try: + return get_debugger()._systems.GetCurrentThreadId() + except Exception: + return None + +def selected_frame(): + return 0 #selected_thread().GetSelectedFrame() + +def select_process(id: int): + return get_debugger()._systems.SetCurrentProcessId(id) + +def select_thread(id: int): + return get_debugger()._systems.SetCurrentThreadId(id) + +def select_frame(id: int): + #TODO: this needs to be fixed + return id + +def parse_and_eval(expr): + regs = get_debugger().reg + if expr == "$pc": + return regs.get_pc() + if expr == "$sp": + return regs.get_sp() + return get_eval(expr) + +def get_eval(expr, type=None): + ctrl = get_debugger()._control._ctrl + ctrl.SetExpressionSyntax(1) + value = DbgEng._DEBUG_VALUE() + index = c_ulong() + if type == None: + type = DbgEng.DEBUG_VALUE_INT64 + hr = ctrl.Evaluate(Expression="{}".format(expr).encode(),DesiredType=type,Value=byref(value),RemainderIndex=byref(index)) + exception.check_err(hr) + if type == DbgEng.DEBUG_VALUE_INT8: + return value.u.I8 + if type == DbgEng.DEBUG_VALUE_INT16: + return value.u.I16 + if type == DbgEng.DEBUG_VALUE_INT32: + return value.u.I32 + if type == DbgEng.DEBUG_VALUE_INT64: + return value.u.I64.I64 + if type == DbgEng.DEBUG_VALUE_FLOAT32: + return value.u.F32 + if type == DbgEng.DEBUG_VALUE_FLOAT64: + return value.u.F64 + if type == DbgEng.DEBUG_VALUE_FLOAT80: + return value.u.F80Bytes + if type == DbgEng.DEBUG_VALUE_FLOAT82: + return value.u.F82Bytes + if type == DbgEng.DEBUG_VALUE_FLOAT128: + return value.u.F128Bytes + +def GetProcessIdsByIndex(count=0): + if count == 0: + try : + count = get_debugger()._systems.GetNumberProcesses() + except Exception: + count = 0 + ids = (c_ulong * count)() + sysids = (c_ulong * count)() + if count != 0: + hr = get_debugger()._systems._sys.GetProcessIdsByIndex(0, count, ids, sysids) + exception.check_err(hr) + return (tuple(ids), tuple(sysids)) + + +def GetCurrentProcessExecutableName(): + dbg = get_debugger() + size = c_ulong() + exesize = c_ulong() + hr = dbg._systems._sys.GetCurrentProcessExecutableName(None, size, byref(exesize)) + exception.check_err(hr) + buffer = create_string_buffer(exesize.value) + size = exesize + hr = dbg._systems._sys.GetCurrentProcessExecutableName(buffer, size, None) + exception.check_err(hr) + buffer = buffer[:size.value] + buffer = buffer.rstrip(b'\x00') + return buffer + + +def GetCurrentProcessPeb(): + dbg = get_debugger() + offset = c_ulonglong() + hr = dbg._systems._sys.GetCurrentProcessPeb(byref(offset)) + exception.check_err(hr) + return offset.value + + +def GetExitCode(): + exit_code = c_ulong() + hr = get_debugger()._client._cli.GetExitCode(byref(exit_code)) + return exit_code.value + + +def process_list(running=False): + """process_list() -> list of all processes""" + dbg = get_debugger() + ids, sysids = GetProcessIdsByIndex() + pebs = [] + names = [] + + try : + curid = dbg._systems.GetCurrentProcessId() + if running == False: + for id in ids: + dbg._systems.SetCurrentProcessId(id) + names.append(GetCurrentProcessExecutableName()) + pebs.append(GetCurrentProcessPeb()) + if running == False: + dbg._systems.SetCurrentProcessId(curid) + return zip(sysids, names, pebs) + except Exception: + pass + return zip(sysids) + +def thread_list(running=False): + """thread_list() -> list of all threads""" + dbg = get_debugger() + try : + ids, sysids = dbg._systems.GetThreadIdsByIndex() + except Exception: + return zip([]) + tebs = [] + syms = [] + + curid = dbg._systems.GetCurrentThreadId() + if running == False: + for id in ids: + dbg._systems.SetCurrentThreadId(id) + tebs.append(dbg._systems.GetCurrentThreadTeb()) + addr = dbg.reg.get_pc() + syms.append(dbg.get_name_by_offset(addr)) + if running == False: + dbg._systems.SetCurrentThreadId(curid) + return zip(sysids, tebs, syms) + return zip(sysids) + +conv_map = {} + +def get_convenience_variable(id): + #val = get_target().GetEnvironment().Get(id) + if id not in conv_map: + return "auto" + val = conv_map[id] + if val is None: + return "auto" + return val + +def set_convenience_variable(id, value): + #env = get_target().GetEnvironment() + #return env.Set(id, value, True) + conv_map[id] = value diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java index e6d1363a15..49a8f0cb76 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/rmi/trace/TraceRmiHandler.java @@ -249,7 +249,7 @@ public class TraceRmiHandler { } public boolean isClosed() { - return socket.isClosed(); + return socket.isClosed() && closed.isDone(); } public void waitClosed() throws InterruptedException, ExecutionException { diff --git a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DummyProc.java b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DummyProc.java index 272faf5f16..376014d77e 100644 --- a/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DummyProc.java +++ b/Ghidra/Debug/Framework-Debugging/src/test/java/ghidra/dbg/testutil/DummyProc.java @@ -52,6 +52,12 @@ public class DummyProc implements AutoCloseable { if (platformExe.exists() && platformExe.getFile(false).canExecute()) { return platformExe.getAbsolutePath(); } + platformExe = new ResourceFile(modRoot, + "build/exe/" + cmd + "/" + Platform.CURRENT_PLATFORM.getDirectoryName() + "/" + + cmd + ".exe"); + if (platformExe.exists() && platformExe.getFile(false).canExecute()) { + return platformExe.getAbsolutePath(); + } } } catch (Exception e) { diff --git a/Ghidra/Test/IntegrationTest/build.gradle b/Ghidra/Test/IntegrationTest/build.gradle index 2dfab73f13..bb6bdc5e41 100644 --- a/Ghidra/Test/IntegrationTest/build.gradle +++ b/Ghidra/Test/IntegrationTest/build.gradle @@ -82,4 +82,5 @@ integrationTest { dependsOn { project(':Debugger-rmi-trace').assemblePyPackage } dependsOn { project(':Debugger-agent-gdb').assemblePyPackage } dependsOn { project(':Debugger-agent-lldb').assemblePyPackage } + dependsOn { project(':Debugger-agent-dbgeng').assemblePyPackage } } diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/AbstractDbgEngTraceRmiTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/AbstractDbgEngTraceRmiTest.java new file mode 100644 index 0000000000..6631cb0507 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/AbstractDbgEngTraceRmiTest.java @@ -0,0 +1,424 @@ +/* ### + * 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. + */ +package agent.dbgeng.rmi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketTimeoutException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.junit.Before; + +import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest; +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteAsyncResult; +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiAcceptor; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiPlugin; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.app.services.TraceRmiService; +import ghidra.dbg.testutil.DummyProc; +import ghidra.framework.Application; +import ghidra.framework.OperatingSystem; +import ghidra.framework.TestApplicationUtils; +import ghidra.framework.main.ApplicationLevelOnlyPlugin; +import ghidra.framework.model.DomainFile; +import ghidra.framework.plugintool.Plugin; +import ghidra.framework.plugintool.PluginsConfiguration; +import ghidra.framework.plugintool.util.PluginDescription; +import ghidra.framework.plugintool.util.PluginException; +import ghidra.framework.plugintool.util.PluginPackage; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRangeImpl; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; + +public abstract class AbstractDbgEngTraceRmiTest extends AbstractGhidraHeadedDebuggerGUITest { + /** + * Some features have to be disabled to avoid permissions issues in the test container. Namely, + * don't try to disable ASLR. + */ + public static final String PREAMBLE = """ + from ghidradbg.commands import * + """; + // Connecting should be the first thing the script does, so use a tight timeout. + protected static final int CONNECT_TIMEOUT_MS = 3000; + protected static final int TIMEOUT_SECONDS = 300; + protected static final int QUIT_TIMEOUT_MS = 1000; + + protected TraceRmiService traceRmi; + private Path pythonPath; + private Path outFile; + private Path errFile; + + //@BeforeClass + public static void setupPython() throws Throwable { + new ProcessBuilder("gradle", "Debugger-agent-dbgeng:assemblePyPackage") + .directory(TestApplicationUtils.getInstallationDirectory()) + .inheritIO() + .start() + .waitFor(); + } + + protected void setPythonPath(ProcessBuilder pb) throws IOException { + String sep = + OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS ? ";" : ":"; + String rmiPyPkg = Application.getModuleSubDirectory("Debugger-rmi-trace", + "build/pypkg/src").getAbsolutePath(); + String gdbPyPkg = Application.getModuleSubDirectory("Debugger-agent-dbgeng", + "build/pypkg/src").getAbsolutePath(); + String add = rmiPyPkg + sep + gdbPyPkg; + pb.environment().compute("PYTHONPATH", (k, v) -> v == null ? add : (v + sep + add)); + } + + @Before + public void setupTraceRmi() throws Throwable { + traceRmi = addPlugin(tool, TraceRmiPlugin.class); + + try { + pythonPath = Paths.get(DummyProc.which("python3")); + } + catch (RuntimeException e) { + pythonPath = Paths.get(DummyProc.which("python")); + } + outFile = Files.createTempFile("pydbgout", null); + errFile = Files.createTempFile("pydbgerr", null); + } + + protected void addAllDebuggerPlugins() throws PluginException { + PluginsConfiguration plugConf = new PluginsConfiguration() { + @Override + protected boolean accepts(Class pluginClass) { + return !ApplicationLevelOnlyPlugin.class.isAssignableFrom(pluginClass); + } + }; + + for (PluginDescription pd : plugConf + .getPluginDescriptions(PluginPackage.getPluginPackage("Debugger"))) { + addPlugin(tool, pd.getPluginClass()); + } + } + + protected static String addrToStringForPython(InetAddress address) { + if (address.isAnyLocalAddress()) { + return "127.0.0.1"; // Can't connect to 0.0.0.0 as such. Choose localhost. + } + return address.getHostAddress(); + } + + protected static String sockToStringForPython(SocketAddress address) { + if (address instanceof InetSocketAddress tcp) { + return addrToStringForPython(tcp.getAddress()) + ":" + tcp.getPort(); + } + throw new AssertionError("Unhandled address type " + address); + } + + protected record PythonResult(boolean timedOut, int exitCode, String stdout, String stderr) { + protected String handle() { + if (stderr.contains("Error") || (0 != exitCode && 1 != exitCode && 143 != exitCode)) { + throw new PythonError(exitCode, stdout, stderr); + } + return stdout; + } + } + + protected record ExecInPython(Process python, CompletableFuture future) { + } + + @SuppressWarnings("resource") // Do not close stdin + protected ExecInPython execInPython(String script) throws IOException { + ProcessBuilder pb = new ProcessBuilder(pythonPath.toString(), "-i"); + setPythonPath(pb); + + // If commands come from file, Python will quit after EOF. + Msg.info(this, "outFile: " + outFile); + Msg.info(this, "errFile: " + errFile); + + //pb.inheritIO(); + pb.redirectInput(ProcessBuilder.Redirect.PIPE); + pb.redirectOutput(outFile.toFile()); + pb.redirectError(errFile.toFile()); + Process pyproc = pb.start(); + OutputStream stdin = pyproc.getOutputStream(); + stdin.write(script.getBytes()); + stdin.flush(); + //stdin.close(); + return new ExecInPython(pyproc, CompletableFuture.supplyAsync(() -> { + try { + if (!pyproc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + Msg.error(this, "Timed out waiting for Python"); + pyproc.destroyForcibly(); + pyproc.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return new PythonResult(true, -1, Files.readString(outFile), + Files.readString(errFile)); + } + Msg.info(this, "Python exited with code " + pyproc.exitValue()); + return new PythonResult(false, pyproc.exitValue(), Files.readString(outFile), + Files.readString(errFile)); + } + catch (Exception e) { + return ExceptionUtils.rethrow(e); + } + finally { + pyproc.destroyForcibly(); + } + })); + } + + public static class PythonError extends RuntimeException { + public final int exitCode; + public final String stdout; + public final String stderr; + + public PythonError(int exitCode, String stdout, String stderr) { + super(""" + exitCode=%d: + ----stdout---- + %s + ----stderr---- + %s + """.formatted(exitCode, stdout, stderr)); + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + } + + protected String runThrowError(String script) throws Exception { + CompletableFuture result = execInPython(script).future; + return result.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle(); + } + + protected record PythonAndHandler(ExecInPython exec, TraceRmiHandler handler) + implements AutoCloseable { + protected RemoteMethod getMethod(String name) { + return Objects.requireNonNull(handler.getMethods().get(name)); + } + + public void execute(String cmd) { + RemoteMethod execute = getMethod("execute"); + execute.invoke(Map.of("cmd", cmd)); + } + + public RemoteAsyncResult executeAsync(String cmd) { + RemoteMethod execute = getMethod("execute"); + return execute.invokeAsync(Map.of("cmd", cmd)); + } + + public String executeCapture(String expr) { + RemoteMethod execute = getMethod("evaluate"); + return (String) execute.invoke(Map.of("expr", expr)); + } + + @Override + public void close() throws Exception { + Msg.info(this, "Cleaning up python"); + exec.python().destroy(); + try { + PythonResult r = exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + r.handle(); + waitForPass(() -> assertTrue(handler.isClosed())); + } + finally { + exec.python.destroyForcibly(); + } + } + } + + protected PythonAndHandler startAndConnectPython(Function scriptSupplier) + throws Exception { + TraceRmiAcceptor acceptor = traceRmi.acceptOne(null); + ExecInPython exec = + execInPython(scriptSupplier.apply(sockToStringForPython(acceptor.getAddress()))); + acceptor.setTimeout(CONNECT_TIMEOUT_MS); + try { + TraceRmiHandler handler = acceptor.accept(); + return new PythonAndHandler(exec, handler); + } + catch (SocketTimeoutException e) { + exec.python.destroyForcibly(); + exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).handle(); + throw e; + } + } + + protected PythonAndHandler startAndConnectPython() throws Exception { + return startAndConnectPython(addr -> """ + %s + ghidra_trace_connect('%s') + """.formatted(PREAMBLE, addr)); + } + + @SuppressWarnings("resource") + protected String runThrowError(Function scriptSupplier) + throws Exception { + PythonAndHandler conn = startAndConnectPython(scriptSupplier); + PythonResult r = conn.exec.future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + String stdout = r.handle(); + waitForPass(() -> assertTrue(conn.handler.isClosed())); + return stdout; + } + + protected void waitStopped() { + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0))); + waitForPass(() -> assertEquals("STOPPED", tb.objValue(proc, 0, "_state"))); + waitTxDone(); + } + + protected void waitRunning() { + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]", Lifespan.at(0))); + waitForPass(() -> assertEquals("RUNNING", tb.objValue(proc, 0, "_state"))); + waitTxDone(); + } + + protected String extractOutSection(String out, String head) { + String[] split = out.split("\n"); + String xout = ""; + for (String s : split) { + if (!s.startsWith("(python)") && !s.equals("")) { + xout += s + "\n"; + } + } + return xout.split(head)[1].split("---")[0].replace("(python)", "").trim(); + } + + record MemDump(long address, byte[] data) { + } + + protected MemDump parseHexDump(String dump) throws IOException { + // First, get the address. Assume contiguous, so only need top line. + List lines = List.of(dump.split("\n")); + List toksLine0 = List.of(lines.get(0).split("\\s+")); + String addrstr = toksLine0.get(0); + if (addrstr.contains(":")) { + addrstr = addrstr.substring(0, addrstr.indexOf(":")); + } + long address = Long.parseLong(addrstr, 16); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + for (String l : lines) { + List parts = List.of(l.split(":")); + assertEquals(2, parts.size()); + String hex = parts.get(1).substring(0, 48); + byte[] lineData = NumericUtilities.convertStringToBytes(hex); + assertNotNull("Converted to null: " + hex, parts.get(1)); + buf.write(lineData); + } + return new MemDump(address, buf.toByteArray()); + } + + record RegDump() { + } + + protected RegDump parseRegDump(String dump) { + return new RegDump(); + } + + protected ManagedDomainObject openDomainObject(String path) throws Exception { + DomainFile df = env.getProject().getProjectData().getFile(path); + assertNotNull(df); + return new ManagedDomainObject(df, false, false, monitor); + } + + protected ManagedDomainObject waitDomainObject(String path) throws Exception { + DomainFile df; + long start = System.currentTimeMillis(); + while (true) { + df = env.getProject().getProjectData().getFile(path); + if (df != null) { + return new ManagedDomainObject(df, false, false, monitor); + } + Thread.sleep(1000); + if (System.currentTimeMillis() - start > 30000) { + throw new TimeoutException("30 seconds expired waiting for domain file"); + } + } + } + + protected void assertBreakLoc(TraceObjectValue locVal, String key, Address addr, int len, + Set kinds, String expression) throws Exception { + assertEquals(key, locVal.getEntryKey()); + TraceObject loc = locVal.getChild(); + TraceObject spec = loc; + assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue()); + assertEquals(TraceBreakpointKindSet.encode(kinds), spec.getValue(0, "_kinds").getValue()); + assertTrue(spec.getValue(0, "_expression").getValue().toString().contains(expression)); + } + + protected void assertWatchLoc(TraceObjectValue locVal, String key, Address addr, int len, + Set kinds, String expression) throws Exception { + assertEquals(key, locVal.getEntryKey()); + TraceObject loc = locVal.getChild(); + assertEquals(new AddressRangeImpl(addr, len), loc.getValue(0, "_range").getValue()); + assertEquals(TraceBreakpointKindSet.encode(kinds), loc.getValue(0, "_kinds").getValue()); + } + + protected void waitTxDone() { + waitFor(() -> tb.trace.getCurrentTransactionInfo() == null); + } + + public static void waitForPass(Runnable runnable, long timeoutMs, long retryDelayMs) { + long start = System.currentTimeMillis(); + AssertionError lastError = null; + while (System.currentTimeMillis() - start < timeoutMs) { + try { + runnable.run(); + return; + } + catch (AssertionError e) { + lastError = e; + } + try { + Thread.sleep(retryDelayMs); + } + catch (InterruptedException e) { + // Retry sooner, I guess. + } + } + if (lastError == null) { + throw new AssertionError("Timed out before first try?"); + } + throw lastError; + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngCommandsTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngCommandsTest.java new file mode 100644 index 0000000000..d8fda64266 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngCommandsTest.java @@ -0,0 +1,1223 @@ +/* ### + * 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. + */ +package agent.dbgeng.rmi; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.Test; + +import generic.Unique; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiAcceptor; +import ghidra.app.plugin.core.debug.service.rmi.trace.TraceRmiHandler; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.dbg.util.PathPredicates; +import ghidra.framework.model.DomainFile; +import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressRange; +import ghidra.program.model.address.AddressRangeImpl; +import ghidra.program.model.address.AddressSpace; +import ghidra.program.model.lang.RegisterValue; +import ghidra.program.model.listing.CodeUnit; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.ImmutableTraceAddressSnapRange; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.TraceAddressSnapRange; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.memory.TraceMemoryState; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectKeyPath; +import ghidra.trace.model.target.TraceObjectValue; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSnapshot; +import ghidra.util.Msg; + +public class DbgEngCommandsTest extends AbstractDbgEngTraceRmiTest { + + //@Test + public void testManual() throws Exception { + TraceRmiAcceptor acceptor = traceRmi.acceptOne(null); + Msg.info(this, + "Use: ghidra_trace_connect(" + sockToStringForPython(acceptor.getAddress()) + ")"); + TraceRmiHandler handler = acceptor.accept(); + Msg.info(this, "Connected: " + sockToStringForPython(handler.getRemoteAddress())); + handler.waitClosed(); + Msg.info(this, "Closed"); + } + + @Test + public void testConnectErrorNoArg() throws Exception { + try { + runThrowError(""" + from ghidradbg.commands import * + ghidra_trace_connect() + quit() + """); + fail(); + } + catch (PythonError e) { + assertThat(e.stderr, containsString("'ghidra_trace_connect'")); + assertThat(e.stderr, containsString("'address'")); + } + } + + @Test + public void testConnect() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + quit() + """.formatted(PREAMBLE, addr)); + } + + @Test + public void testDisconnect() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_disconnect() + quit() + """.formatted(PREAMBLE, addr)); + } + + @Test + public void testStartTraceDefaults() throws Exception { + // Default name and lcsp + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals("x86:LE:64:default", + tb.trace.getBaseLanguage().getLanguageID().getIdAsString()); + assertEquals("windows", + tb.trace.getBaseCompilerSpec().getCompilerSpecID().getIdAsString()); + } + } + + @Test + public void testStartTraceDefaultNoFile() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_start() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + assertThat(mdo.get(), instanceOf(Trace.class)); + } + } + + @Test + public void testStartTraceCustomize() throws Exception { + runThrowError( + addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe', start_trace=False) + util.set_convenience_variable('ghidra-language','Toy:BE:64:default') + util.set_convenience_variable('ghidra-compiler','default') + ghidra_trace_start('myToy') + quit() + """ + .formatted(PREAMBLE, addr)); + DomainFile dfMyToy = env.getProject().getProjectData().getFile("/New Traces/pydbg/myToy"); + assertNotNull(dfMyToy); + try (ManagedDomainObject mdo = new ManagedDomainObject(dfMyToy, false, false, monitor)) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals("Toy:BE:64:default", + tb.trace.getBaseLanguage().getLanguageID().getIdAsString()); + assertEquals("default", + tb.trace.getBaseCompilerSpec().getCompilerSpecID().getIdAsString()); + } + } + + @Test + public void testStopTrace() throws Exception { + // TODO: This test assumes pydbg and the target file notepad are x86-64 + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_stop() + quit() + """.formatted(PREAMBLE, addr)); + DomainFile dfBash = env.getProject().getProjectData().getFile("/New Traces/pydbg/notepad.exe"); + assertNotNull(dfBash); + // TODO: Given the 'quit' command, I'm not sure this assertion is checking anything. + assertFalse(dfBash.isOpen()); + } + + @Test + public void testInfo() throws Exception { + AtomicReference refAddr = new AtomicReference<>(); + String out = runThrowError(addr -> { + refAddr.set(addr); + return """ + %s + print('---Import---') + ghidra_trace_info() + ghidra_trace_connect('%s') + print('---Connect---') + ghidra_trace_info() + ghidra_trace_create('notepad.exe') + print('---Start---') + ghidra_trace_info() + ghidra_trace_stop() + print('---Stop---') + ghidra_trace_info() + ghidra_trace_disconnect() + print('---Disconnect---') + ghidra_trace_info() + quit() + """.formatted(PREAMBLE, addr); + }); + + assertEquals(""" + Not connected to Ghidra""", + extractOutSection(out, "---Import---")); + assertEquals(""" + Connected to Ghidra at %s + + No trace""".formatted(refAddr.get()), + extractOutSection(out, "---Connect---").replaceAll("\r", "").substring(0,48)); + String expected = """ + Connected to Ghidra at %s + + Trace active""".formatted(refAddr.get()); + String actual = extractOutSection(out, "---Start---").replaceAll("\r", ""); + assertEquals(expected, actual.substring(0,expected.length())); + assertEquals(""" + Connected to Ghidra at %s + + No trace""".formatted(refAddr.get()), + extractOutSection(out, "---Stop---").replaceAll("\r", "")); + assertEquals(""" + Not connected to Ghidra""", + extractOutSection(out, "---Disconnect---")); + } + + @Test + public void testLcsp() throws Exception { + // TODO: This test assumes x86-64 on test system + String out = runThrowError( + """ + %s + print('---Import---') + ghidra_trace_info_lcsp() + print('---') + ghidra_trace_create('notepad.exe', start_trace=False) + print('---File---') + ghidra_trace_info_lcsp() + util.set_convenience_variable('ghidra-language','Toy:BE:64:default') + print('---Language---') + ghidra_trace_info_lcsp() + util.set_convenience_variable('ghidra-compiler','posStack') + print('---Compiler---') + ghidra_trace_info_lcsp() + quit() + """.formatted(PREAMBLE)); + + assertTrue( + extractOutSection(out, "---File---").replaceAll("\r", "").contains( + """ + Selected Ghidra language: x86:LE:64:default + + Selected Ghidra compiler: windows""")); + assertEquals(""" + Selected Ghidra language: Toy:BE:64:default + + Selected Ghidra compiler: default""", + extractOutSection(out, "---Language---").replaceAll("\r", "")); + assertEquals(""" + Selected Ghidra language: Toy:BE:64:default + + Selected Ghidra compiler: posStack""", + extractOutSection(out, "---Compiler---").replaceAll("\r", "")); + } + + //@Test TODO - revisit after rebasing on master + public void testSave() throws Exception { + traceManager.setSaveTracesByDefault(false); + + // For sanity check, verify failing to save drops data + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create snapshot') + ghidra_trace_new_snap('Scripted snapshot') + ghidra_trace_txcommit() + ghidra_trace_stop() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(0, tb.trace.getTimeManager().getAllSnapshots().size()); + } + + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create snapshot') + ghidra_trace_new_snap('Scripted snapshot') + ghidra_trace_txcommit() + ghidra_trace_save() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + assertEquals(1, tb.trace.getTimeManager().getAllSnapshots().size()); + } + } + + @Test + public void testSnapshot() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create snapshot') + ghidra_trace_new_snap('Scripted snapshot') + ghidra_trace_txcommit() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceSnapshot snapshot = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()); + assertEquals(0, snapshot.getKey()); + assertEquals("Scripted snapshot", snapshot.getDescription()); + } + } + + @Test + public void testPutmem() throws Exception { + String out = runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create snapshot') + ghidra_trace_new_snap('Scripted snapshot') + ghidra_trace_putmem('$pc 16') + ghidra_trace_txcommit() + print('---Dump---') + pc = util.get_debugger().reg.get_pc() + util.get_debugger().dd(pc, count=1) + print('---') + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + MemDump dump = parseHexDump(extractOutSection(out, "---Dump---")); + ByteBuffer buf = ByteBuffer.allocate(dump.data().length); + tb.trace.getMemoryManager().getBytes(snap, tb.addr(dump.address()), buf); + + assertArrayEquals(dump.data(), buf.array()); + } + } + + @Test + public void testPutmemState() throws Exception { + String out = runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create snapshot') + ghidra_trace_new_snap('Scripted snapshot') + ghidra_trace_putmem_state('$pc 16 error') + ghidra_trace_txcommit() + print('---Start---') + pc = util.get_debugger().reg.get_pc() + util.get_debugger().dd(pc, count=1) + print('---') + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + String eval = extractOutSection(out, "---Start---"); + String addrstr = eval.substring(0, eval.indexOf(":")).trim(); + Address addr = tb.addr(Long.parseLong(addrstr, 16)); + + Entry entry = + tb.trace.getMemoryManager().getMostRecentStateEntry(snap, addr); + assertEquals(Map.entry(new ImmutableTraceAddressSnapRange( + new AddressRangeImpl(addr, 16), Lifespan.at(0)), TraceMemoryState.ERROR), entry); + } + } + + @Test + public void testDelmem() throws Exception { + String out = runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create snapshot') + ghidra_trace_new_snap('Scripted snapshot') + ghidra_trace_putmem('$pc 16') + ghidra_trace_delmem('$pc 8') + ghidra_trace_txcommit() + print('---Dump---') + pc = util.get_debugger().reg.get_pc() + util.get_debugger().dd(pc, count=1) + print('---') + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + + MemDump dump = parseHexDump(extractOutSection(out, "---Dump---")); + Arrays.fill(dump.data(), 0, 8, (byte) 0); + ByteBuffer buf = ByteBuffer.allocate(dump.data().length); + tb.trace.getMemoryManager().getBytes(snap, tb.addr(dump.address()), buf); + + assertArrayEquals(dump.data(), buf.array()); + } + } + + @Test + public void testPutreg() throws Exception { + String count = IntStream.iterate(0, i -> i < 32, i -> i + 1) + .mapToObj(Integer::toString) + .collect(Collectors.joining(",", "{", "}")); + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + regs = util.get_debugger().reg + regs._set_register("rax", int(0xdeadbeef)) + regs._set_register("st0", int(1.5)) + ghidra_trace_txstart('Create snapshot') + ghidra_trace_new_snap('Scripted snapshot') + ghidra_trace_putreg() + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr, count)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + List regVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Threads[].Registers")) + .map(p -> p.getLastEntry()) + .toList(); + TraceObjectValue tobj = regVals.get(0); + AddressSpace t1f0 = tb.trace.getBaseAddressFactory() + .getAddressSpace(tobj.getCanonicalPath().toString()); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false); + + RegisterValue rax = regs.getValue(snap, tb.reg("rax")); + assertEquals("deadbeef", rax.getUnsignedValue().toString(16)); + + // TODO: Pybag currently doesn't suppport non-int assignments + /* + * // RegisterValue ymm0 = regs.getValue(snap, tb.reg("ymm0")); // // LLDB + * treats registers in arch's endian // assertEquals( + * "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100", // + * ymm0.getUnsignedValue().toString(16)); + * + * // TraceData st0; // try (Transaction tx = + * tb.trace.openTransaction("Float80 unit")) { // TraceCodeSpace code = + * tb.trace.getCodeManager().getCodeSpace(t1f0, true); // st0 = + * code.definedData() // .create(Lifespan.nowOn(0), tb.reg("st0"), + * Float10DataType.dataType); // } // assertEquals("1.5", + * st0.getDefaultValueRepresentation()); + */ + } + } + + @Test + public void testDelreg() throws Exception { + String count = IntStream.iterate(0, i -> i < 32, i -> i + 1) + .mapToObj(Integer::toString) + .collect(Collectors.joining(",", "{", "}")); + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + regs = util.get_debugger().reg + regs._set_register("st0", int(1.5)) + ghidra_trace_txstart('Create snapshot') + ghidra_trace_new_snap('Scripted snapshot') + ghidra_trace_putreg() + ghidra_trace_delreg() + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr, count)); + // The spaces will be left over, but the values should be zeroed + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + long snap = Unique.assertOne(tb.trace.getTimeManager().getAllSnapshots()).getKey(); + List regVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Threads[].Registers")) + .map(p -> p.getLastEntry()) + .toList(); + TraceObjectValue tobj = regVals.get(0); + AddressSpace t1f0 = tb.trace.getBaseAddressFactory() + .getAddressSpace(tobj.getCanonicalPath().toString()); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false); + + RegisterValue rax = regs.getValue(snap, tb.reg("rax")); + assertEquals("0", rax.getUnsignedValue().toString(16)); + + // TODO: As above, not currently supported by pybag + /* + * // RegisterValue ymm0 = regs.getValue(snap, tb.reg("ymm0")); // + * assertEquals("0", ymm0.getUnsignedValue().toString(16)); + * + * // TraceData st0; // try (Transaction tx = + * tb.trace.openTransaction("Float80 unit")) { // TraceCodeSpace code = + * tb.trace.getCodeManager().getCodeSpace(t1f0, true); // st0 = + * code.definedData() // .create(Lifespan.nowOn(0), tb.reg("st0"), + * Float10DataType.dataType); // } // assertEquals("0.0", + * st0.getDefaultValueRepresentation()); + */ + } + } + + @Test + public void testCreateObj() throws Exception { + String out = runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_start() + ghidra_trace_txstart('Create Object') + print('---Id---') + ghidra_trace_create_obj('Test.Objects[1]') + print('---') + ghidra_trace_txcommit() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + String created = extractOutSection(out, "---Id---"); + long id = Long.parseLong(created.split("id=")[1].split(",")[0]); + assertEquals(object.getKey(), id); + } + } + + @Test + public void testInsertObj() throws Exception { + String out = runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_start() + ghidra_trace_txstart('Create Object') + ghidra_trace_create_obj('Test.Objects[1]') + print('---Lifespan---') + ghidra_trace_insert_obj('Test.Objects[1]') + print('---') + ghidra_trace_txcommit() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + Lifespan life = Unique.assertOne(object.getLife().spans()); + assertEquals(Lifespan.nowOn(0), life); + String expected = "Inserted object: lifespan=[0,+inf)"; + String actual = extractOutSection(out, "---Lifespan---"); + assertEquals(expected, actual.substring(0, expected.length())); + } + } + + @Test + public void testRemoveObj() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create Object') + ghidra_trace_create_obj('Test.Objects[1]') + ghidra_trace_insert_obj('Test.Objects[1]') + ghidra_trace_set_snap(1) + ghidra_trace_remove_obj('Test.Objects[1]') + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + Lifespan life = Unique.assertOne(object.getLife().spans()); + assertEquals(Lifespan.at(0), life); + } + } + + @SuppressWarnings("unchecked") + protected T runTestSetValue(String extra, String pydbgExpr, String gtype) + throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create Object') + ghidra_trace_create_obj('Test.Objects[1]') + ghidra_trace_insert_obj('Test.Objects[1]') + %s + ghidra_trace_set_value('Test.Objects[1]', 'test', %s, '%s') + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr, extra, pydbgExpr, gtype)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + TraceObjectValue value = object.getValue(0, "test"); + return value == null ? null : (T) value.getValue(); + } + } + + @Test + public void testSetValueNull() throws Exception { + assertNull(runTestSetValue("", "None", "VOID")); + } + + @Test + public void testSetValueBool() throws Exception { + assertEquals(Boolean.TRUE, runTestSetValue("", "True", "BOOL")); + } + + @Test + public void testSetValueByte() throws Exception { + assertEquals(Byte.valueOf((byte) 1), runTestSetValue("", "'(char)1'", "BYTE")); + } + + @Test + public void testSetValueChar() throws Exception { + assertEquals(Character.valueOf('A'), runTestSetValue("", "\"'A'\"", "CHAR")); + } + + @Test + public void testSetValueShort() throws Exception { + assertEquals(Short.valueOf((short) 1), runTestSetValue("", "'(short)1'", "SHORT")); + } + + @Test + public void testSetValueInt() throws Exception { + assertEquals(Integer.valueOf(1), runTestSetValue("", "'(int)1'", "INT")); + } + + @Test + public void testSetValueLong() throws Exception { + assertEquals(Long.valueOf(1), runTestSetValue("", "'(long)1'", "LONG")); + } + + @Test + public void testSetValueString() throws Exception { + assertEquals("HelloWorld!", runTestSetValue("", "\'HelloWorld!\'", "STRING")); + } + + @Test //- how do we input long strings in python + public void testSetValueStringWide() throws Exception { + assertEquals("HelloWorld!", runTestSetValue("", "u\'HelloWorld!\'", "STRING")); + } + + @Test + public void testSetValueBoolArr() throws Exception { + assertArrayEquals(new boolean[] { true, false }, + runTestSetValue("", "[True,False]", "BOOL_ARR")); + } + + @Test + public void testSetValueByteArrUsingString() throws Exception { + assertArrayEquals(new byte[] { 'H', 1, 'W' }, + runTestSetValue("", "'H\\1W'", "BYTE_ARR")); + } + + @Test + public void testSetValueByteArrUsingArray() throws Exception { + assertArrayEquals(new byte[] { 'H', 0, 'W' }, + runTestSetValue("", "['H',0,'W']", "BYTE_ARR")); + } + + @Test + public void testSetValueCharArrUsingString() throws Exception { + assertArrayEquals(new char[] { 'H', 1, 'W' }, + runTestSetValue("", "'H\\1W'", "CHAR_ARR")); + } + + @Test + public void testSetValueCharArrUsingArray() throws Exception { + assertArrayEquals(new char[] { 'H', 0, 'W' }, + runTestSetValue("", "['H',0,'W']", "CHAR_ARR")); + } + + @Test + public void testSetValueShortArrUsingString() throws Exception { + assertArrayEquals(new short[] { 'H', 1, 'W' }, + runTestSetValue("", "'H\\1W'", "SHORT_ARR")); + } + + @Test + public void testSetValueShortArrUsingArray() throws Exception { + assertArrayEquals(new short[] { 'H', 0, 'W' }, + runTestSetValue("", "['H',0,'W']", "SHORT_ARR")); + } + + @Test + public void testSetValueIntArrayUsingMixedArray() throws Exception { + // Because explicit array type is chosen, we get null terminator + assertArrayEquals(new int[] { 'H', 0, 'W' }, + runTestSetValue("", "['H',0,'W']", "INT_ARR")); + } + + @Test + public void testSetValueIntArrUsingArray() throws Exception { + assertArrayEquals(new int[] { 1, 2, 3, 4 }, + runTestSetValue("", "[1,2,3,4]", "INT_ARR")); + } + + @Test + public void testSetValueLongArr() throws Exception { + assertArrayEquals(new long[] { 1, 2, 3, 4 }, + runTestSetValue("", "[1,2,3,4]", "LONG_ARR")); + } + + @Test + public void testSetValueStringArr() throws Exception { + assertArrayEquals(new String[] { "1", "A", "dead", "beef" }, + runTestSetValue("", "['1','A','dead','beef']", "STRING_ARR")); + } + + @Test + public void testSetValueAddress() throws Exception { + Address address = runTestSetValue("", "'(void*)0xdeadbeef'", "ADDRESS"); + // Don't have the address factory to create expected address + assertEquals(0xdeadbeefL, address.getOffset()); + assertEquals("ram", address.getAddressSpace().getName()); + } + + @Test + public void testSetValueObject() throws Exception { + TraceObject object = runTestSetValue("", "'Test.Objects[1]'", "OBJECT"); + assertEquals("Test.Objects[1]", object.getCanonicalPath().toString()); + } + + @Test + public void testRetainValues() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create Object') + ghidra_trace_create_obj('Test.Objects[1]') + ghidra_trace_insert_obj('Test.Objects[1]') + ghidra_trace_set_value('Test.Objects[1]', '[1]', '"A"', 'STRING') + ghidra_trace_set_value('Test.Objects[1]', '[2]', '"B"', 'STRING') + ghidra_trace_set_value('Test.Objects[1]', '[3]', '"C"', 'STRING') + ghidra_trace_set_snap(10) + ghidra_trace_retain_values('Test.Objects[1]', '[1] [3]') + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + assertEquals(Map.ofEntries( + Map.entry("[1]", Lifespan.nowOn(0)), + Map.entry("[2]", Lifespan.span(0, 9)), + Map.entry("[3]", Lifespan.nowOn(0))), + object.getValues() + .stream() + .collect(Collectors.toMap(v -> v.getEntryKey(), v -> v.getLifespan()))); + } + } + + @Test + public void testGetObj() throws Exception { + String out = runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_start() + ghidra_trace_txstart('Create Object') + print('---Id---') + ghidra_trace_create_obj('Test.Objects[1]') + print('---') + ghidra_trace_txcommit() + print('---GetObject---') + ghidra_trace_get_obj('Test.Objects[1]') + print('---') + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject object = tb.trace.getObjectManager() + .getObjectByCanonicalPath(TraceObjectKeyPath.parse("Test.Objects[1]")); + assertNotNull(object); + String expected = "1\tTest.Objects[1]"; + String actual = extractOutSection(out, "---GetObject---"); + assertEquals(expected, actual.substring(0,expected.length())); + } + } + + @Test + public void testGetValues() throws Exception { + String out = runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create Object') + ghidra_trace_create_obj('Test.Objects[1]') + ghidra_trace_insert_obj('Test.Objects[1]') + ghidra_trace_set_value('Test.Objects[1]', 'vnull', None, 'VOID') + ghidra_trace_set_value('Test.Objects[1]', 'vbool', True, 'BOOL') + ghidra_trace_set_value('Test.Objects[1]', 'vbyte', '(char)1', 'BYTE') + ghidra_trace_set_value('Test.Objects[1]', 'vchar', "'A'", 'CHAR') + ghidra_trace_set_value('Test.Objects[1]', 'vshort', 2, 'SHORT') + ghidra_trace_set_value('Test.Objects[1]', 'vint', 3, 'INT') + ghidra_trace_set_value('Test.Objects[1]', 'vlong', 4, 'LONG') + ghidra_trace_set_value('Test.Objects[1]', 'vstring', 'Hello', 'STRING') + vboolarr = [True, False] + ghidra_trace_set_value('Test.Objects[1]', 'vboolarr', vboolarr, 'BOOL_ARR') + vbytearr = [1, 2, 3] + ghidra_trace_set_value('Test.Objects[1]', 'vbytearr', vbytearr, 'BYTE_ARR') + vchararr = 'Hello' + ghidra_trace_set_value('Test.Objects[1]', 'vchararr', vchararr, 'CHAR_ARR') + vshortarr = [1, 2, 3] + ghidra_trace_set_value('Test.Objects[1]', 'vshortarr', vshortarr, 'SHORT_ARR') + vintarr = [1, 2, 3] + ghidra_trace_set_value('Test.Objects[1]', 'vintarr', vintarr, 'INT_ARR') + vlongarr = [1, 2, 3] + ghidra_trace_set_value('Test.Objects[1]', 'vlongarr', vlongarr, 'LONG_ARR') + ghidra_trace_set_value('Test.Objects[1]', 'vaddr', '(void*)0xdeadbeef', 'ADDRESS') + ghidra_trace_set_value('Test.Objects[1]', 'vobj', 'Test.Objects[1]', 'OBJECT') + ghidra_trace_txcommit() + print('---GetValues---') + ghidra_trace_get_values('Test.Objects[1].') + print('---') + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + String expected = """ + Parent Key Span Value Type + Test.Objects[1] vbool [0,+inf) True BOOL + Test.Objects[1] vboolarr [0,+inf) [True, False] BOOL_ARR + Test.Objects[1] vbyte [0,+inf) 1 BYTE + Test.Objects[1] vbytearr [0,+inf) b'\\x01\\x02\\x03' BYTE_ARR + Test.Objects[1] vchar [0,+inf) 'A' CHAR + Test.Objects[1] vchararr [0,+inf) 'Hello' CHAR_ARR + Test.Objects[1] vint [0,+inf) 3 INT + Test.Objects[1] vintarr [0,+inf) [1, 2, 3] INT_ARR + Test.Objects[1] vlong [0,+inf) 4 LONG + Test.Objects[1] vlongarr [0,+inf) [1, 2, 3] LONG_ARR + Test.Objects[1] vobj [0,+inf) Test.Objects[1] OBJECT + Test.Objects[1] vshort [0,+inf) 2 SHORT + Test.Objects[1] vshortarr [0,+inf) [1, 2, 3] SHORT_ARR + Test.Objects[1] vstring [0,+inf) 'Hello' STRING + Test.Objects[1] vaddr [0,+inf) ram:deadbeef ADDRESS""" + .replaceAll(" ", "") + .replaceAll("\n", ""); + String actual = extractOutSection(out, "---GetValues---").replaceAll(" ", "").replaceAll("\r", "").replaceAll("\n", ""); + assertEquals( + expected, + actual.substring(0,expected.length())); + } + } + + @Test + public void testGetValuesRng() throws Exception { + String out = runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Create Object') + ghidra_trace_create_obj('Test.Objects[1]') + ghidra_trace_insert_obj('Test.Objects[1]') + ghidra_trace_set_value('Test.Objects[1]', 'vaddr', '(void*)0xdeadbeef', 'ADDRESS') + ghidra_trace_txcommit() + print('---GetValues---') + ghidra_trace_get_values_rng('(void*)0xdeadbeef 10') + print('---') + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + String expected = """ + Parent Key Span Value Type + + Test.Objects[1] vaddr [0,+inf) ram:deadbeef ADDRESS"""; + String actual = extractOutSection(out, "---GetValues---").replaceAll("\r", ""); + assertEquals(expected, actual.substring(0, expected.length())); + } + } + + @Test + public void testActivateObject() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + #set language c++ + ghidra_trace_txstart('Create Object') + ghidra_trace_create_obj('Test.Objects[1]') + ghidra_trace_insert_obj('Test.Objects[1]') + ghidra_trace_txcommit() + ghidra_trace_activate('Test.Objects[1]') + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + assertSame(mdo.get(), traceManager.getCurrentTrace()); + assertEquals("Test.Objects[1]", + traceManager.getCurrentObject().getCanonicalPath().toString()); + } + } + + @Test + public void testDisassemble() throws Exception { + String out = runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Tx') + ghidra_trace_putmem('$pc 16') + print('---Disassemble---') + ghidra_trace_disassemble('$pc') + print('---') + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Not concerned about specifics, so long as disassembly occurs + long total = 0; + for (CodeUnit cu : tb.trace.getCodeManager().definedUnits().get(0, true)) { + total += cu.getLength(); + } + String extract = extractOutSection(out, "---Disassemble---"); + String [] split = extract.split("\r\n"); + assertEquals("Disassembled %d bytes".formatted(total), + split[0]); + } + } + + @Test + public void testPutProcesses() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_start() + ghidra_trace_txstart('Tx') + ghidra_trace_put_processes() + ghidra_trace_txcommit() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection processes = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Processes[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(0, processes.size()); + } + } + + @Test + public void testPutAvailable() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_start() + ghidra_trace_txstart('Tx') + ghidra_trace_put_available() + ghidra_trace_txcommit() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection available = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Available[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(available.size(), greaterThan(2)); + } + } + + @Test + public void testPutBreakpoints() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + dbg = util.get_debugger() + pc = dbg.reg.get_pc() + dbg.bp(expr=pc) + dbg.ba(expr=pc+4) + ghidra_trace_txstart('Tx') + ghidra_trace_put_breakpoints() + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + List procBreakLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(2, procBreakLocVals.size()); + AddressRange rangeMain = + procBreakLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address bp1 = rangeMain.getMinAddress(); + + assertBreakLoc(procBreakLocVals.get(0), "[0]", bp1, 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), + "ntdll!LdrInit"); + assertBreakLoc(procBreakLocVals.get(1), "[1]", bp1.add(4), 1, + Set.of(TraceBreakpointKind.HW_EXECUTE), + "ntdll!LdrInit"); + } + } + + @Test + public void testPutBreakpoints2() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Tx') + dbg = util.get_debugger() + pc = dbg.reg.get_pc() + dbg.ba(expr=pc, access=DbgEng.DEBUG_BREAK_EXECUTE) + dbg.ba(expr=pc+4, access=DbgEng.DEBUG_BREAK_READ) + dbg.ba(expr=pc+8, access=DbgEng.DEBUG_BREAK_WRITE) + ghidra_trace_put_breakpoints() + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + List procBreakVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(3, procBreakVals.size()); + AddressRange rangeMain0 = + procBreakVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main0 = rangeMain0.getMinAddress(); + AddressRange rangeMain1 = + procBreakVals.get(1).getChild().getValue(0, "_range").castValue(); + Address main1 = rangeMain1.getMinAddress(); + AddressRange rangeMain2 = + procBreakVals.get(2).getChild().getValue(0, "_range").castValue(); + Address main2 = rangeMain2.getMinAddress(); + + assertWatchLoc(procBreakVals.get(0), "[0]", main0, (int) rangeMain0.getLength(), + Set.of(TraceBreakpointKind.HW_EXECUTE), "ntdll!LdrInit"); + assertWatchLoc(procBreakVals.get(1), "[1]", main1, (int) rangeMain1.getLength(), + Set.of(TraceBreakpointKind.WRITE), "ntdll!LdrInit"); + assertWatchLoc(procBreakVals.get(2), "[2]", main2, (int) rangeMain2.getLength(), + Set.of(TraceBreakpointKind.READ), "ntdll!LdrInit"); + } + } + + @Test + public void testPutEnvironment() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Tx') + ghidra_trace_put_environment() + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Assumes LLDB on Linux amd64 + TraceObject env = + Objects.requireNonNull(tb.objAny("Processes[].Environment", Lifespan.at(0))); + assertEquals("pydbg", env.getValue(0, "_debugger").getValue()); + assertEquals("x86_64", env.getValue(0, "_arch").getValue()); + assertEquals("windows", env.getValue(0, "_os").getValue()); + assertEquals("little", env.getValue(0, "_endian").getValue()); + } + } + + @Test + public void testPutRegions() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Tx') + ghidra_trace_put_regions() + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection all = + tb.trace.getMemoryManager().getAllRegions(); + assertThat(all.size(), greaterThan(2)); + } + } + + @Test + public void testPutModules() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Tx') + ghidra_trace_put_modules() + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection all = tb.trace.getModuleManager().getAllModules(); + TraceModule modBash = + Unique.assertOne(all.stream().filter(m -> m.getName().contains("notepad"))); + assertNotEquals(tb.addr(0), Objects.requireNonNull(modBash.getBase())); + } + } + + @Test + public void testPutThreads() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Tx') + ghidra_trace_put_threads() + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + Collection threads = tb.trace.getThreadManager().getAllThreads(); + assertThat(threads.size(), greaterThan(2)); + } + } + + @Test + public void testPutFrames() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + ghidra_trace_create('notepad.exe') + ghidra_trace_txstart('Tx') + ghidra_trace_put_frames() + ghidra_trace_txcommit() + ghidra_trace_kill() + quit() + """.formatted(PREAMBLE, addr)); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + // Would be nice to control / validate the specifics + List stack = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[0].Threads[0].Stack[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(stack.size(), greaterThan(2)); + } + } + + @Test + public void testMinimal() throws Exception { + runThrowError(addr -> """ + %s + ghidra_trace_connect('%s') + print('FINISHED') + quit() + """.formatted(PREAMBLE, addr)); + } + + @Test + public void testMinimal2() throws Exception { + Function scriptSupplier = addr -> """ + %s + ghidra_trace_connect('%s') + """.formatted(PREAMBLE, addr); + try (PythonAndHandler conn = startAndConnectPython(scriptSupplier)) { + conn.execute("print('FINISHED')"); + conn.close(); + } + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngHooksTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngHooksTest.java new file mode 100644 index 0000000000..de1af8159b --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngHooksTest.java @@ -0,0 +1,380 @@ +/* ### + * 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. + */ +package agent.dbgeng.rmi; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; + +import org.junit.Ignore; +import org.junit.Test; + +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.dbg.testutil.DummyProc; +import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathPredicates; +import ghidra.program.model.address.AddressSpace; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.thread.TraceThread; +import ghidra.trace.model.time.TraceSnapshot; + +public class DbgEngHooksTest extends AbstractDbgEngTraceRmiTest { + private static final long RUN_TIMEOUT_MS = 20000; + private static final long RETRY_MS = 500; + + record PythonAndTrace(PythonAndHandler conn, ManagedDomainObject mdo) implements AutoCloseable { + public void execute(String cmd) { + conn.execute(cmd); + } + + public String executeCapture(String cmd) { + return conn.executeCapture(cmd); + } + + @Override + public void close() throws Exception { + conn.close(); + mdo.close(); + } + } + + @SuppressWarnings("resource") + protected PythonAndTrace startAndSyncPython(String exec) throws Exception { + PythonAndHandler conn = startAndConnectPython(); + try { + ManagedDomainObject mdo; + conn.execute("from ghidradbg.commands import *"); + conn.execute( + "util.set_convenience_variable('ghidra-language', 'x86:LE:64:default')"); + if (exec != null) { + start(conn, exec); + mdo = waitDomainObject("/New Traces/pydbg/"+exec); + } + else { + conn.execute("ghidra_trace_start()"); + mdo = waitDomainObject("/New Traces/pydbg/noname"); + } + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + return new PythonAndTrace(conn, mdo); + } + catch (Exception e) { + conn.close(); + throw e; + } + } + + protected long lastSnap(PythonAndTrace conn) { + return conn.conn.handler().getLastSnapshot(tb.trace); + } + + @Test // The 10s wait makes this a pretty expensive test + public void testOnNewThread() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) { + conn.execute("from ghidradbg.commands import *"); + txPut(conn, "processes"); + + waitForPass(() -> { + TraceObject proc = tb.objAny("Processes[]"); + assertNotNull(proc); + assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + + txPut(conn, "threads"); + waitForPass(() -> assertEquals(4, + tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()), + RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("dbg().go(10)"); + + waitForPass(() -> assertTrue(tb.objValues(lastSnap(conn), "Processes[].Threads[]").size() > 4), + RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnThreadSelected() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) { + txPut(conn, "processes"); + + waitForPass(() -> { + TraceObject inf = tb.objAny("Processes[]"); + assertNotNull(inf); + assertEquals("STOPPED", tb.objValue(inf, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + + txPut(conn, "threads"); + waitForPass(() -> assertEquals(4, + tb.objValues(lastSnap(conn), "Processes[].Threads[]").size()), + RUN_TIMEOUT_MS, RETRY_MS); + + // Now the real test + conn.execute("util.select_thread(1)"); + waitForPass(() -> { + String tnum = conn.executeCapture("util.selected_thread()"); + assertTrue(tnum.contains("1")); + String threadIndex = threadIndex(traceManager.getCurrentObject()); + assertTrue(tnum.contains(threadIndex)); + }, RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("util.select_thread(2)"); + waitForPass(() -> { + String tnum = conn.executeCapture("util.selected_thread()"); + assertTrue(tnum.contains("2")); + String threadIndex = threadIndex(traceManager.getCurrentObject()); + assertTrue(tnum.contains(threadIndex)); + }, RUN_TIMEOUT_MS, RETRY_MS); + + conn.execute("util.select_thread(0)"); + waitForPass(() -> { + String tnum = conn.executeCapture("util.selected_thread()"); + assertTrue(tnum.contains("0")); + String threadIndex = threadIndex(traceManager.getCurrentObject()); + assertTrue(tnum.contains(threadIndex)); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + protected String getIndex(TraceObject object, String pattern, int n) { + if (object == null) { + return null; + } + PathPattern pat = PathPredicates.parse(pattern).getSingletonPattern(); +// if (pat.countWildcards() != 1) { +// throw new IllegalArgumentException("Exactly one wildcard required"); +// } + List path = object.getCanonicalPath().getKeyList(); + if (path.size() < pat.asPath().size()) { + return null; + } + List matched = pat.matchKeys(path.subList(0, pat.asPath().size())); + if (matched == null) { + return null; + } + if (matched.size() <= n) { + return null; + } + return matched.get(n); + } + + protected String threadIndex(TraceObject object) { + return getIndex(object, "Processes[].Threads[]", 1); + } + + protected String frameIndex(TraceObject object) { + return getIndex(object, "Processes[].Threads[].Stack[]", 2); + } + + @Test + @Ignore + public void testOnSyscallMemory() throws Exception { + // TODO: Need a specimen + // FWIW, I've already seen this getting exercised in other tests. + } + + //@Test - dbgeng has limited support via DEBUG_CDS_DATA, + // but expensive to implement anything here + public void testOnMemoryChanged() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) { + + conn.execute("ghidra_trace_txstart('Tx')"); + conn.execute("ghidra_trace_putmem('$pc 10')"); + conn.execute("ghidra_trace_txcommit()"); + long address = getAddressAtOffset(conn, 0); + conn.execute("util.get_debugger().write("+address+", b'\\x7f')"); + + waitForPass(() -> { + ByteBuffer buf = ByteBuffer.allocate(10); + tb.trace.getMemoryManager().getBytes(lastSnap(conn), tb.addr(address), buf); + assertEquals(0x7f, buf.get(0)); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnRegisterChanged() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) { + + conn.execute("ghidra_trace_txstart('Tx')"); + conn.execute("ghidra_trace_putreg()"); + conn.execute("ghidra_trace_txcommit()"); + conn.execute("util.get_debugger().reg._set_register('rax', 0x1234)"); + conn.execute("util.get_debugger().stepi()"); + + String path = "Processes[].Threads[].Registers"; + TraceObject registers = Objects.requireNonNull(tb.objAny(path, Lifespan.at(0))); + AddressSpace space = tb.trace.getBaseAddressFactory() + .getAddressSpace(registers.getCanonicalPath().toString()); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(space, false); + waitForPass(() -> assertEquals("1234", + regs.getValue(lastSnap(conn), tb.reg("RAX")).getUnsignedValue().toString(16))); + } + } + + @Test + public void testOnCont() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) { + txPut(conn, "processes"); + + conn.execute("util.get_debugger()._control.SetExecutionStatus(DbgEng.DEBUG_STATUS_GO)"); + waitRunning(); + + TraceObject proc = waitForValue(() -> tb.objAny("Processes[]")); + waitForPass(() -> { + assertEquals("RUNNING", tb.objValue(proc, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnStop() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) { + txPut(conn, "processes"); + + TraceObject proc = waitForValue(() -> tb.objAny("Processes[]")); + waitForPass(() -> { + assertEquals("STOPPED", tb.objValue(proc, lastSnap(conn), "_state")); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnExited() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("netstat.exe")) { + txPut(conn, "processes"); + waitStopped(); + + conn.execute("util.get_debugger().go()"); + + waitForPass(() -> { + TraceSnapshot snapshot = + tb.trace.getTimeManager().getSnapshot(lastSnap(conn), false); + assertNotNull(snapshot); + assertEquals("Exited with code 0", snapshot.getDescription()); + + TraceObject proc = tb.objAny("Processes[]"); + assertNotNull(proc); + Object val = tb.objValue(proc, lastSnap(conn), "_exit_code"); + assertThat(val, instanceOf(Number.class)); + assertEquals(0, ((Number) val).longValue()); + }, RUN_TIMEOUT_MS, RETRY_MS); + } + } + + @Test + public void testOnBreakpointCreated() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) { + assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size()); + + conn.execute("dbg = util.get_debugger()"); + conn.execute("pc = dbg.reg.get_pc()"); + conn.execute("dbg.bp(expr=pc)"); + conn.execute("dbg.stepi()"); + + waitForPass(() -> { + List brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]"); + assertEquals(1, brks.size()); + return (TraceObject) brks.get(0); + }); + } + } + + @Test + public void testOnBreakpointModified() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) { + assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size()); + + conn.execute("dbg = util.get_debugger()"); + conn.execute("pc = dbg.reg.get_pc()"); + conn.execute("dbg.bp(expr=pc)"); + conn.execute("dbg.stepi()"); + + TraceObject brk = waitForPass(() -> { + List brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]"); + assertEquals(1, brks.size()); + return (TraceObject) brks.get(0); + }); + assertEquals(true, tb.objValue(brk, lastSnap(conn), "Enabled")); + conn.execute("dbg.bd(0)"); + conn.execute("dbg.stepi()"); + assertEquals(false, tb.objValue(brk, lastSnap(conn), "Enabled")); + + /* Not currently enabled + assertEquals("", tb.objValue(brk, lastSnap(conn), "Command")); + conn.execute("dbg.bp(expr=pc, windbgcmd='bl')"); + conn.execute("dbg.stepi()"); + assertEquals("bl", tb.objValue(brk, lastSnap(conn), "Command")); + */ + } + } + + @Test + public void testOnBreakpointDeleted() throws Exception { + try (PythonAndTrace conn = startAndSyncPython("notepad.exe")) { + assertEquals(0, tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size()); + + conn.execute("dbg = util.get_debugger()"); + conn.execute("pc = dbg.reg.get_pc()"); + conn.execute("dbg.bp(expr=pc)"); + conn.execute("dbg.stepi()"); + + TraceObject brk = waitForPass(() -> { + List brks = tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]"); + assertEquals(1, brks.size()); + return (TraceObject) brks.get(0); + }); + + conn.execute("dbg.cmd('bc %s')".formatted(brk.getCanonicalPath().index())); + conn.execute("dbg.stepi()"); + + waitForPass( + () -> assertEquals(0, + tb.objValues(lastSnap(conn), "Processes[].Breakpoints[]").size())); + } + } + + private void start(PythonAndHandler conn, String obj) { + conn.execute("from ghidradbg.commands import *"); + if (obj != null) + conn.execute("ghidra_trace_create('"+obj+"')"); + else + conn.execute("ghidra_trace_create()"); + conn.execute("ghidra_trace_sync_enable()"); + } + + private void txPut(PythonAndTrace conn, String obj) { + conn.execute("ghidra_trace_txstart('Tx" + obj + "')"); + conn.execute("ghidra_trace_put_" + obj +"()"); + conn.execute("ghidra_trace_txcommit()"); + } + + private long getAddressAtOffset(PythonAndTrace conn, int offset) { + String inst = "util.get_inst(util.get_debugger().reg.get_pc()+"+offset+")"; + String ret = conn.executeCapture(inst); + String[] split = ret.split("\\s+"); // get target + return Long.decode(split[1]); + } +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngMethodsTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngMethodsTest.java new file mode 100644 index 0000000000..5dcc795318 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/dbgeng/rmi/DbgEngMethodsTest.java @@ -0,0 +1,959 @@ +/* ### + * 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. + */ +package agent.dbgeng.rmi; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import generic.Unique; +import ghidra.app.plugin.core.debug.service.rmi.trace.RemoteMethod; +import ghidra.app.plugin.core.debug.service.rmi.trace.ValueDecoder; +import ghidra.app.plugin.core.debug.utils.ManagedDomainObject; +import ghidra.dbg.testutil.DummyProc; +import ghidra.dbg.util.PathPattern; +import ghidra.dbg.util.PathPredicates; +import ghidra.program.model.address.*; +import ghidra.program.model.lang.RegisterValue; +import ghidra.trace.database.ToyDBTraceBuilder; +import ghidra.trace.model.Lifespan; +import ghidra.trace.model.Trace; +import ghidra.trace.model.breakpoint.TraceBreakpointKind; +import ghidra.trace.model.memory.TraceMemoryRegion; +import ghidra.trace.model.memory.TraceMemorySpace; +import ghidra.trace.model.modules.TraceModule; +import ghidra.trace.model.target.TraceObject; +import ghidra.trace.model.target.TraceObjectValue; + +public class DbgEngMethodsTest extends AbstractDbgEngTraceRmiTest { + + @Test + public void testEvaluate() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + RemoteMethod evaluate = conn.getMethod("evaluate"); + assertEquals("11", + evaluate.invoke(Map.of("expr", "3+4*2"))); + } + } + + @Test + public void testExecuteCapture() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + RemoteMethod execute = conn.getMethod("execute"); + assertEquals(false, + execute.parameters().get("to_string").defaultValue().get(ValueDecoder.DEFAULT)); + assertEquals("11\n", + execute.invoke(Map.of("cmd", "print(3+4*2)", "to_string", true))); + } + } + + @Test + public void testExecute() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + conn.execute("ghidra_trace_kill()"); + } + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + // Just confirm it's present + } + } + + @Test + public void testRefreshAvailable() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, null); + txCreate(conn, "Available"); + + RemoteMethod refreshAvailable = conn.getMethod("refresh_available"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject available = Objects.requireNonNull(tb.objAny("Available")); + + refreshAvailable.invoke(Map.of("node", available)); + + // Would be nice to control / validate the specifics + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Available[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertThat(list.size(), greaterThan(2)); + } + } + } + + @Test + public void testRefreshBreakpoints() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod refreshBreakpoints = conn.getMethod("refresh_breakpoints"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + conn.execute("dbg = util.get_debugger()"); + conn.execute("pc = dbg.reg.get_pc()"); + conn.execute("dbg.bp(expr=pc)"); + conn.execute("dbg.ba(expr=pc+4)"); + txPut(conn, "breakpoints"); + TraceObject breakpoints = Objects.requireNonNull(tb.objAny("Processes[].Breakpoints")); + refreshBreakpoints.invoke(Map.of("node", breakpoints)); + + List procBreakLocVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(2, procBreakLocVals.size()); + AddressRange rangeMain = + procBreakLocVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main = rangeMain.getMinAddress(); + + assertBreakLoc(procBreakLocVals.get(0), "[0]", main, 1, + Set.of(TraceBreakpointKind.SW_EXECUTE), + "ntdll!LdrInit"); + assertBreakLoc(procBreakLocVals.get(1), "[1]", main.add(4), 1, + Set.of(TraceBreakpointKind.HW_EXECUTE), + "ntdll!LdrInit"); + } + } + } + + @Test + public void testRefreshBreakpoints2() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "all"); + + RemoteMethod refreshProcWatchpoints = conn.getMethod("refresh_breakpoints"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + conn.execute("dbg = util.get_debugger()"); + conn.execute("pc = dbg.reg.get_pc()"); + conn.execute("dbg.ba(expr=pc, access=DbgEng.DEBUG_BREAK_EXECUTE)"); + conn.execute("dbg.ba(expr=pc+4, access=DbgEng.DEBUG_BREAK_READ)"); + conn.execute("dbg.ba(expr=pc+8, access=DbgEng.DEBUG_BREAK_WRITE)"); + TraceObject locations = + Objects.requireNonNull(tb.objAny("Processes[].Breakpoints")); + refreshProcWatchpoints.invoke(Map.of("node", locations)); + + List procBreakVals = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Breakpoints[]")) + .map(p -> p.getLastEntry()) + .toList(); + assertEquals(3, procBreakVals.size()); + AddressRange rangeMain0 = + procBreakVals.get(0).getChild().getValue(0, "_range").castValue(); + Address main0 = rangeMain0.getMinAddress(); + AddressRange rangeMain1 = + procBreakVals.get(1).getChild().getValue(0, "_range").castValue(); + Address main1 = rangeMain1.getMinAddress(); + AddressRange rangeMain2 = + procBreakVals.get(2).getChild().getValue(0, "_range").castValue(); + Address main2 = rangeMain2.getMinAddress(); + + assertWatchLoc(procBreakVals.get(0), "[0]", main0, (int) rangeMain0.getLength(), + Set.of(TraceBreakpointKind.HW_EXECUTE), + "main"); + assertWatchLoc(procBreakVals.get(1), "[1]", main1, (int) rangeMain1.getLength(), + Set.of(TraceBreakpointKind.WRITE), + "main+4"); + assertWatchLoc(procBreakVals.get(2), "[2]", main2, (int) rangeMain1.getLength(), + Set.of(TraceBreakpointKind.READ), + "main+8"); + } + } + } + + @Test + public void testRefreshProcesses() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, null); + txCreate(conn, "Processes"); + txCreate(conn, "Processes[1]"); + + RemoteMethod refreshProcesses = conn.getMethod("refresh_processes"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject processes = Objects.requireNonNull(tb.objAny("Processes")); + + refreshProcesses.invoke(Map.of("node", processes)); + + // Would be nice to control / validate the specifics + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), PathPredicates.parse("Processes[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(1, list.size()); + } + } + } + + @Test + public void testRefreshEnvironment() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + String path = "Processes[].Environment"; + start(conn, "notepad.exe"); + txPut(conn, "all"); + + RemoteMethod refreshEnvironment = conn.getMethod("refresh_environment"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject env = Objects.requireNonNull(tb.objAny(path)); + + refreshEnvironment.invoke(Map.of("node", env)); + + // Assumes pydbg on Windows amd64 + assertEquals("pydbg", env.getValue(0, "_debugger").getValue()); + assertEquals("x86_64", env.getValue(0, "_arch").getValue()); + assertEquals("windows", env.getValue(0, "_os").getValue()); + assertEquals("little", env.getValue(0, "_endian").getValue()); + } + } + } + + @Test + public void testRefreshThreads() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + String path = "Processes[].Threads"; + start(conn, "notepad.exe"); + txCreate(conn, path); + + RemoteMethod refreshThreads = conn.getMethod("refresh_threads"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject threads = Objects.requireNonNull(tb.objAny(path)); + + refreshThreads.invoke(Map.of("node", threads)); + + // Would be nice to control / validate the specifics + int listSize = tb.trace.getThreadManager().getAllThreads().size(); + assertEquals(4, listSize); + } + } + } + + @Test + public void testRefreshStack() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + String path = "Processes[].Threads[].Stack"; + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod refreshStack = conn.getMethod("refresh_stack"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + txPut(conn, "frames"); + TraceObject stack = Objects.requireNonNull(tb.objAny(path)); + refreshStack.invoke(Map.of("node", stack)); + + // Would be nice to control / validate the specifics + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), + PathPredicates.parse("Processes[].Threads[].Stack[]")) + .map(p -> p.getDestination(null)) + .toList(); + assertTrue(list.size() > 1); + } + } + } + + @Test + public void testRefreshRegisters() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + String path = "Processes[].Threads[].Registers"; + start(conn, "notepad.exe"); + conn.execute("ghidra_trace_txstart('Tx')"); + conn.execute("ghidra_trace_putreg()"); + conn.execute("ghidra_trace_txcommit()"); + + RemoteMethod refreshRegisters = conn.getMethod("refresh_registers"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + conn.execute("regs = util.get_debugger().reg"); + conn.execute("regs._set_register('rax', int(0xdeadbeef))"); + + TraceObject registers = Objects.requireNonNull(tb.objAny(path, Lifespan.at(0))); + refreshRegisters.invoke(Map.of("node", registers)); + + long snap = 0; + AddressSpace t1f0 = tb.trace.getBaseAddressFactory() + .getAddressSpace(registers.getCanonicalPath().toString()); + TraceMemorySpace regs = tb.trace.getMemoryManager().getMemorySpace(t1f0, false); + RegisterValue rax = regs.getValue(snap, tb.reg("rax")); + assertEquals("deadbeef", rax.getUnsignedValue().toString(16)); + } + } + } + + @Test + public void testRefreshMappings() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + String path = "Processes[].Memory"; + start(conn, "notepad.exe"); + txCreate(conn, path); + + RemoteMethod refreshMappings = conn.getMethod("refresh_mappings"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject memory = Objects.requireNonNull(tb.objAny(path)); + + refreshMappings.invoke(Map.of("node", memory)); + + // Would be nice to control / validate the specifics + Collection all = + tb.trace.getMemoryManager().getAllRegions(); + assertThat(all.size(), greaterThan(2)); + } + } + } + + @Test + public void testRefreshModules() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + String path = "Processes[].Modules"; + start(conn, "notepad.exe"); + txCreate(conn, path); + + RemoteMethod refreshModules = conn.getMethod("refresh_modules"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject modules = Objects.requireNonNull(tb.objAny(path)); + + refreshModules.invoke(Map.of("node", modules)); + + // Would be nice to control / validate the specifics + Collection all = tb.trace.getModuleManager().getAllModules(); + TraceModule modBash = + Unique.assertOne(all.stream().filter(m -> m.getName().contains("notepad.exe"))); + assertNotEquals(tb.addr(0), Objects.requireNonNull(modBash.getBase())); + } + } + } + + @Test + public void testActivateThread() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod activateThread = conn.getMethod("activate_thread"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + txPut(conn, "threads"); + + PathPattern pattern = + PathPredicates.parse("Processes[].Threads[]").getSingletonPattern(); + List list = tb.trace.getObjectManager() + .getValuePaths(Lifespan.at(0), pattern) + .map(p -> p.getDestination(null)) + .toList(); + assertEquals(4, list.size()); + + for (TraceObject t : list) { + activateThread.invoke(Map.of("thread", t)); + String out = conn.executeCapture("util.get_debugger().get_thread()"); + List indices = pattern.matchKeys(t.getCanonicalPath().getKeyList()); + assertEquals(out, "%s".formatted(indices.get(1))); + } + } + } + } + + @Test + public void testRemoveProcess() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "netstat.exe"); + txPut(conn, "processes"); + + RemoteMethod removeProcess = conn.getMethod("remove_process"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/netstat.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject proc2 = Objects.requireNonNull(tb.objAny("Processes[]")); + removeProcess.invoke(Map.of("process", proc2)); + + String out = conn.executeCapture("list(util.process_list())"); + assertThat(out, containsString("[]")); + } + } + } + + @Test + public void testAttachObj() throws Exception { + try (DummyProc dproc = DummyProc.run("notepad.exe")) { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, null); + txPut(conn, "available"); + + RemoteMethod attachObj = conn.getMethod("attach_obj"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + TraceObject target = + Objects.requireNonNull(tb.obj("Available[%d]".formatted(dproc.pid))); + attachObj.invoke(Map.of("target", target)); + + String out = conn.executeCapture("list(util.process_list())"); + assertThat(out, containsString("%d".formatted(dproc.pid))); + } + } + } + } + + @Test + public void testAttachPid() throws Exception { + try (DummyProc dproc = DummyProc.run("notepad.exe")) { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, null); + txPut(conn, "available"); + + RemoteMethod attachPid = conn.getMethod("attach_pid"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + Objects.requireNonNull(tb.objAny("Available["+dproc.pid+"]", Lifespan.at(0))); + attachPid.invoke(Map.of("pid", dproc.pid)); + + String out = conn.executeCapture("list(util.process_list())"); + assertThat(out, containsString("%d".formatted(dproc.pid))); + } + } + } + } + + @Test + public void testDetach() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "netstat.exe"); + txPut(conn, "processes"); + + RemoteMethod detach = conn.getMethod("detach"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/netstat.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + detach.invoke(Map.of("process", proc)); + + String out = conn.executeCapture("list(util.process_list())"); + assertThat(out, containsString("[]")); + } + } + } + + @Test + public void testLaunchEntry() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, null); + txPut(conn, "processes"); + + RemoteMethod launch = conn.getMethod("launch_loader"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + launch.invoke(Map.ofEntries( + Map.entry("file", "notepad.exe"))); + + String out = conn.executeCapture("list(util.process_list())"); + assertThat(out, containsString("notepad.exe")); + } + } + } + + @Test //Can't do this test because create(xxx, initial_break=False) doesn't return + public void testLaunch() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, null); + txPut(conn, "processes"); + + RemoteMethod launch = conn.getMethod("launch"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/noname")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + launch.invoke(Map.ofEntries( + Map.entry("timeout", 1L), + Map.entry("file", "notepad.exe"))); + + txPut(conn, "processes"); + + String out = conn.executeCapture("list(util.process_list())"); + assertThat(out, containsString("notepad.exe")); + } + } + } + + @Test + public void testKill() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod kill = conn.getMethod("kill"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + kill.invoke(Map.of("process", proc)); + + String out = conn.executeCapture("list(util.process_list())"); + assertThat(out, containsString("[]")); + } + } + } + + @Test + public void testStepInto() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod step_into = conn.getMethod("step_into"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + txPut(conn, "threads"); + + TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]")); + + while (!getInst(conn).contains("call")) { + step_into.invoke(Map.of("thread", thread)); + } + + String disCall = getInst(conn); + // lab0: + // -> addr0 + // + // lab1: + // addr1 + String[] split = disCall.split("\\s+"); // get target + long pcCallee = Long.decode(split[split.length-1]); + + step_into.invoke(Map.of("thread", thread)); + long pc = getAddressAtOffset(conn, 0); + assertEquals(pcCallee, pc); + } + } + } + + @Test + public void testStepOver() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod step_over = conn.getMethod("step_over"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + txPut(conn, "threads"); + + TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]")); + + while (!getInst(conn).contains("call")) { + step_over.invoke(Map.of("thread", thread)); + } + + String disCall = getInst(conn); + String[] split = disCall.split("\\s+"); // get target + long pcCallee = Long.decode(split[split.length-1]); + + step_over.invoke(Map.of("thread", thread)); + long pc = getAddressAtOffset(conn, 0); + assertNotEquals(pcCallee, pc); + } + } + } + + @Test + public void testStepTo() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + + RemoteMethod step_into = conn.getMethod("step_into"); + RemoteMethod step_to = conn.getMethod("step_to"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + txPut(conn, "threads"); + + TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]")); + while (!getInst(conn).contains("call")) { + step_into.invoke(Map.of("thread", thread)); + } + step_into.invoke(Map.of("thread", thread)); + + int sz = Integer.parseInt(getInstSizeAtOffset(conn, 0)); + for (int i = 0; i < 4; i++) { + sz += Integer.parseInt(getInstSizeAtOffset(conn, sz)); + } + + long pcNext = getAddressAtOffset(conn, sz); + + boolean success = (boolean) step_to.invoke(Map.of("thread", thread, "address", tb.addr(pcNext), "max", 10)); + assertTrue(success); + + long pc = getAddressAtOffset(conn, 0); + assertEquals(pcNext, pc); + } + } + } + + @Test + public void testStepOut() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod step_into = conn.getMethod("step_into"); + RemoteMethod step_out = conn.getMethod("step_out"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + txPut(conn, "threads"); + + TraceObject thread = Objects.requireNonNull(tb.objAny("Processes[].Threads[]")); + + while (!getInst(conn).contains("call")) { + step_into.invoke(Map.of("thread", thread)); + } + + int sz = Integer.parseInt(getInstSizeAtOffset(conn, 0)); + long pcNext = getAddressAtOffset(conn, sz); + + step_into.invoke(Map.of("thread", thread)); + step_out.invoke(Map.of("thread", thread)); + long pc = getAddressAtOffset(conn, 0); + assertEquals(pcNext, pc); + } + } + } + + @Test + public void testBreakAddress() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakAddress = conn.getMethod("break_address"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + + long address = getAddressAtOffset(conn, 0); + breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address))); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString(Long.toHexString(address))); + } + } + } + + @Test + public void testBreakExpression() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakExpression.invoke(Map.of("expression", "entry")); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString("entry")); + } + } + } + + @Test + public void testBreakHardwareAddress() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakAddress = conn.getMethod("break_hw_address"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + + long address = getAddressAtOffset(conn, 0); + breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address))); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString(Long.toHexString(address))); + } + } + } + + @Test + public void testBreakHardwareExpression() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_hw_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + breakExpression.invoke(Map.of("expression", "entry")); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString("entry")); + } + } + } + + @Test + public void testBreakReadRange() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakRange = conn.getMethod("break_read_range"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + long address = getAddressAtOffset(conn, 0); + AddressRange range = tb.range(address, address + 3); // length 4 + breakRange.invoke(Map.of("process", proc, "range", range)); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString("%x".formatted(address))); + assertThat(out, containsString("sz=4")); + assertThat(out, containsString("type=r")); + } + } + } + + @Test + public void testBreakReadExpression() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_read_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + breakExpression.invoke(Map.of("expression", "ntdll!LdrInitShimEngineDynamic")); + long address = getAddressAtOffset(conn, 0); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString(Long.toHexString(address>>24))); + assertThat(out, containsString("sz=1")); + assertThat(out, containsString("type=r")); + } + } + } + + @Test + public void testBreakWriteRange() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakRange = conn.getMethod("break_write_range"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + long address = getAddressAtOffset(conn, 0); + AddressRange range = tb.range(address, address + 3); // length 4 + breakRange.invoke(Map.of("process", proc, "range", range)); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString("%x".formatted(address))); + assertThat(out, containsString("sz=4")); + assertThat(out, containsString("type=w")); + } + } + } + + @Test + public void testBreakWriteExpression() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_write_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + breakExpression.invoke(Map.of("expression", "ntdll!LdrInitShimEngineDynamic")); + long address = getAddressAtOffset(conn, 0); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString(Long.toHexString(address>>24))); + assertThat(out, containsString("sz=1")); + assertThat(out, containsString("type=w")); + } + } + } + + @Test + public void testBreakAccessRange() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakRange = conn.getMethod("break_access_range"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + waitStopped(); + + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + long address = getAddressAtOffset(conn, 0); + AddressRange range = tb.range(address, address + 3); // length 4 + breakRange.invoke(Map.of("process", proc, "range", range)); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString("%x".formatted(address))); + assertThat(out, containsString("sz=4")); + assertThat(out, containsString("type=rw")); + } + } + } + + @Test + public void testBreakAccessExpression() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakExpression = conn.getMethod("break_access_expression"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + breakExpression.invoke(Map.of("expression", "ntdll!LdrInitShimEngineDynamic")); + long address = getAddressAtOffset(conn, 0); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString(Long.toHexString(address>>24))); + assertThat(out, containsString("sz=1")); + assertThat(out, containsString("type=rw")); + } + } + } + + @Test + public void testToggleBreakpoint() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakAddress = conn.getMethod("break_address"); + RemoteMethod toggleBreakpoint = conn.getMethod("toggle_breakpoint"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + long address = getAddressAtOffset(conn, 0); + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address))); + + txPut(conn, "breakpoints"); + TraceObject bpt = Objects.requireNonNull(tb.objAny("Processes[].Breakpoints[]")); + + toggleBreakpoint.invoke(Map.of("breakpoint", bpt, "enabled", false)); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString("disabled")); + } + } + } + + @Test + public void testDeleteBreakpoint() throws Exception { + try (PythonAndHandler conn = startAndConnectPython()) { + start(conn, "notepad.exe"); + txPut(conn, "processes"); + + RemoteMethod breakAddress = conn.getMethod("break_address"); + RemoteMethod deleteBreakpoint = conn.getMethod("delete_breakpoint"); + try (ManagedDomainObject mdo = openDomainObject("/New Traces/pydbg/notepad.exe")) { + tb = new ToyDBTraceBuilder((Trace) mdo.get()); + + long address = getAddressAtOffset(conn, 0); + TraceObject proc = Objects.requireNonNull(tb.objAny("Processes[]")); + breakAddress.invoke(Map.of("process", proc, "address", tb.addr(address))); + + txPut(conn, "breakpoints"); + TraceObject bpt = Objects.requireNonNull(tb.objAny("Processes[].Breakpoints[]")); + + deleteBreakpoint.invoke(Map.of("breakpoint", bpt)); + + String out = conn.executeCapture("list(util.get_breakpoints())"); + assertThat(out, containsString("[]")); + } + } + } + + private void start(PythonAndHandler conn, String obj) { + conn.execute("from ghidradbg.commands import *"); + if (obj != null) + conn.execute("ghidra_trace_create('"+obj+"')"); + else + conn.execute("ghidra_trace_create()"); } + + private void txPut(PythonAndHandler conn, String obj) { + conn.execute("ghidra_trace_txstart('Tx')"); + conn.execute("ghidra_trace_put_" + obj + "()"); + conn.execute("ghidra_trace_txcommit()"); + } + + private void txCreate(PythonAndHandler conn, String path) { + conn.execute("ghidra_trace_txstart('Fake')"); + conn.execute("ghidra_trace_create_obj('%s')".formatted(path)); + conn.execute("ghidra_trace_txcommit()"); + } + + private String getInst(PythonAndHandler conn) { + return getInstAtOffset(conn, 0); + } + + private String getInstAtOffset(PythonAndHandler conn, int offset) { + String inst = "util.get_inst(util.get_debugger().reg.get_pc()+"+offset+")"; + String ret = conn.executeCapture(inst); + return ret.substring(1, ret.length()-1); // remove <> + } + + private String getInstSizeAtOffset(PythonAndHandler conn, int offset) { + String instSize = "util.get_inst_sz(util.get_debugger().reg.get_pc()+"+offset+")"; + return conn.executeCapture(instSize); + } + + private long getAddressAtOffset(PythonAndHandler conn, int offset) { + String inst = "util.get_inst(util.get_debugger().reg.get_pc()+"+offset+")"; + String ret = conn.executeCapture(inst); + String[] split = ret.split("\\s+"); // get target + return Long.decode(split[1]); + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbCommandsTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbCommandsTest.java index fe63e4cec6..9f0cd8a94b 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbCommandsTest.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/agent/lldb/rmi/LldbCommandsTest.java @@ -134,7 +134,7 @@ public class LldbCommandsTest extends AbstractLldbTraceRmiTest { ghidra_trace_connect %s file bash script ghidralldb.util.set_convenience_variable('ghidra-language','Toy:BE:64:default') - script ghidralldb.util.set_convenience_varaible('ghidra-compiler','default') + script ghidralldb.util.set_convenience_variable('ghidra-compiler','default') ghidra_trace_start myToy quit """