diff --git a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/qemu-gdb.sh b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/qemu-gdb.sh index 54bc6932b1..16e6f5eb78 100755 --- a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/qemu-gdb.sh +++ b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/qemu-gdb.sh @@ -23,7 +23,7 @@ #@desc (you may install gdb-multiarch), and it must embed the Python 3 interpreter. You #@desc will also need protobuf installed for Python 3. #@desc -#@menu-group qemu +#@menu-group cross #@icon icon.debugger #@help TraceRmiLauncherServicePlugin#gdb #@enum StartCmd:str run start starti diff --git a/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/wine-gdb.sh b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/wine-gdb.sh new file mode 100755 index 0000000000..f21c4bd5e6 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/data/debugger-launchers/wine-gdb.sh @@ -0,0 +1,75 @@ +#!/usr/bin/bash +## ### +# 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. +## +#@title wine + gdb +#@desc +#@desc

Launch with gdb and wine

+#@desc

This will launch the target on the local machine using gdb and wine. +#@desc GDB and Wine must already be installed on your system, and GDB must embed the Python 3 +#@desc interpreter. You will also need protobuf and psutil installed for Python +#@desc 3.

+#@desc

This operates by starting GDB on the Wine executable and passing arguments to launch a +#@desc Windows target. This may prevent GDB from processing the object file, because it is a PE +#@desc file, and most copies of GDB for UNIX will support only ELF. Nevertheless, Ghidra should +#@desc recognize the target and map it, giving you symbols and debug info in the front end, even +#@desc if not in the GDB CLI.

+#@desc

You will need to locate the wine executable on your system, not the script. To +#@desc find it, either dissect the wine script or consult online documentation for your +#@desc distribution of Wine. There are often two executables, one for 32-bit targets and one for +#@desc 64-bit targets. You must select the correct one.

+#@desc +#@menu-group cross +#@icon icon.debugger +#@help TraceRmiLauncherServicePlugin#gdb +#@arg :str "Image" "The target binary executable image" +#@args "Arguments" "Command-line arguments to pass to the target" +#@env OPT_WINE_PATH:str="/usr/lib/wine/wine64" "Path to wine binary" "The path to the wine executable for your target architecture." +#@env OPT_GDB_PATH:str="gdb" "Path to gdb" "The path to gdb. Omit the full path to resolve using the system PATH." +#@env OPT_EXTRA_TTY:bool=false "Inferior TTY" "Provide a separate terminal emulator for the target." +#@tty TTY_TARGET if env:OPT_EXTRA_TTY + +if [ -d ${GHIDRA_HOME}/ghidra/.git ] +then + export PYTHONPATH=$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-agent-gdb/build/pypkg/src:$PYTHONPATH + export PYTHONPATH=$GHIDRA_HOME/ghidra/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH +elif [ -d ${GHIDRA_HOME}/.git ] +then + export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-gdb/build/pypkg/src:$PYTHONPATH + export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/build/pypkg/src:$PYTHONPATH +else + export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-agent-gdb/pypkg/src:$PYTHONPATH + export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/pypkg/src:$PYTHONPATH +fi + +# NOTE: Ghidra will leave TTY_TARGET empty, which gdb takes for the same terminal. + +"$OPT_GDB_PATH" \ + -q \ + -ex "set pagination off" \ + -ex "set confirm off" \ + -ex "show version" \ + -ex "python import ghidragdb.wine" \ + -ex "file \"$OPT_WINE_PATH\"" \ + -ex "set args $@" \ + -ex "set inferior-tty $TTY_TARGET" \ + -ex "starti" \ + -ex "ghidra wine run-to-image \"$1\"" \ + -ex "ghidra trace connect \"$GHIDRA_TRACE_RMI_ADDR\"" \ + -ex "ghidra trace start \"$1\"" \ + -ex "ghidra trace sync-enable" \ + -ex "ghidra trace sync-synth-stopped" \ + -ex "set confirm on" \ + -ex "set pagination on" diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/arch.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/arch.py index afceea5fee..f4e77e7972 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/arch.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/arch.py @@ -18,6 +18,7 @@ from ghidratrace.client import Address, RegVal import gdb # NOTE: This map is derived from the ldefs using a script +# i386 is hand-patched language_map = { 'aarch64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'], 'aarch64:ilp32': ['AARCH64:BE:32:ilp32', 'AARCH64:LE:32:ilp32', 'AARCH64:LE:64:AppleSilicon'], @@ -48,6 +49,7 @@ language_map = { 'avr:51': ['avr8:LE:16:atmega256'], 'avr:6': ['avr8:LE:16:atmega256'], 'hppa2.0w': ['pa-risc:BE:32:default'], + 'i386': ['x86:LE:32:default'], 'i386:intel': ['x86:LE:32:default'], 'i386:x86-64': ['x86:LE:64:default'], 'i386:x86-64:intel': ['x86:LE:64:default'], diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py index f861bb550f..e3578f6fbe 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/hooks.py @@ -13,7 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. ## +import functools import time +import traceback import gdb @@ -144,6 +146,27 @@ BRK_STATE = BrkState() INF_STATES = {} +def log_errors(func): + ''' + Wrap a function in a try-except that prints and reraises the + exception. + + This is needed because pybag and/or the COM wrappers do not print + exceptions that occur during event callbacks. + ''' + + @functools.wraps(func) + def _func(*args, **kwargs): + try: + return func(*args, **kwargs) + except: + traceback.print_exc() + raise + + return _func + + +@log_errors def on_new_inferior(event): trace = commands.STATE.trace if trace is None: @@ -166,6 +189,7 @@ def on_inferior_selected(): commands.activate() +@log_errors def on_inferior_deleted(event): trace = commands.STATE.trace if trace is None: @@ -177,6 +201,7 @@ def on_inferior_deleted(event): commands.put_inferiors() # TODO: Could just delete the one.... +@log_errors def on_new_thread(event): inf = gdb.selected_inferior() if inf.num not in INF_STATES: @@ -214,6 +239,7 @@ def on_frame_selected(): commands.activate() +@log_errors def on_syscall_memory(): inf = gdb.selected_inferior() if inf.num not in INF_STATES: @@ -221,6 +247,7 @@ def on_syscall_memory(): INF_STATES[inf.num].regions = True +@log_errors def on_memory_changed(event): inf = gdb.selected_inferior() if inf.num not in INF_STATES: @@ -234,6 +261,7 @@ def on_memory_changed(event): pages=False, is_mi=False, from_tty=False) +@log_errors def on_register_changed(event): inf = gdb.selected_inferior() if inf.num not in INF_STATES: @@ -249,6 +277,7 @@ def on_register_changed(event): commands.putreg(event.frame, event.frame.architecture().registers()) +@log_errors def on_cont(event): if (HOOK_STATE.check_skip_continue()): return @@ -264,6 +293,7 @@ def on_cont(event): state.record_continued() +@log_errors def on_stop(event): if hasattr(event, 'breakpoints') and HOOK_STATE.mem_catchpoint in event.breakpoints: HOOK_STATE.skip_continue = True @@ -284,6 +314,7 @@ def on_stop(event): HOOK_STATE.end_batch() +@log_errors def on_exited(event): inf = gdb.selected_inferior() if inf.num not in INF_STATES: @@ -320,18 +351,22 @@ def modules_changed(): INF_STATES[inf.num].modules = True +@log_errors def on_clear_objfiles(event): modules_changed() +@log_errors def on_new_objfile(event): modules_changed() +@log_errors def on_free_objfile(event): modules_changed() +@log_errors def on_breakpoint_created(b): inf = gdb.selected_inferior() notify_others_breaks(inf) @@ -349,6 +384,7 @@ def on_breakpoint_created(b): ibobj.insert() +@log_errors def on_breakpoint_modified(b): if b == HOOK_STATE.mem_catchpoint: return @@ -369,10 +405,11 @@ def on_breakpoint_modified(b): # NOTE: Location may not apply to inferior, but whatever. for i in range(new_count, old_count): ikey = commands.INF_BREAK_KEY_PATTERN.format( - breaknum=b.number, locnum=i+1) + breaknum=b.number, locnum=i + 1) ibobj.set_value(ikey, None) +@log_errors def on_breakpoint_deleted(b): inf = gdb.selected_inferior() notify_others_breaks(inf) @@ -390,16 +427,18 @@ def on_breakpoint_deleted(b): trace.proxy_object_path(bpath).remove(tree=True) for i in range(old_count): ikey = commands.INF_BREAK_KEY_PATTERN.format( - breaknum=b.number, locnum=i+1) + breaknum=b.number, locnum=i + 1) ibobj.set_value(ikey, None) +@log_errors def on_before_prompt(): HOOK_STATE.end_batch() # This will be called by a catchpoint class GhidraTraceEventMemoryCommand(gdb.Command): + def __init__(self): super().__init__('hooks-ghidra event-memory', gdb.COMMAND_NONE) @@ -412,8 +451,11 @@ GhidraTraceEventMemoryCommand() def cmd_hook(name): + def _cmd_hook(func): + class _ActiveCommand(gdb.Command): + def __init__(self): # It seems we can't hook commands using the Python API.... super().__init__(f"hooks-ghidra def-{name}", gdb.COMMAND_USER) @@ -432,9 +474,11 @@ def cmd_hook(name): define {name} end """) + func.hook = _ActiveCommand func.unhook = _unhook_command return func + return _cmd_hook @@ -463,7 +507,7 @@ def hook_frame_down(): on_frame_selected() -# TODO: Checks and workarounds for events missing in gdb 8 +# TODO: Checks and workarounds for events missing in gdb 9 def install_hooks(): if HOOK_STATE.installed: return diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/util.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/util.py index 0e7ec2df8d..87e8d2c46f 100644 --- a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/util.py +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/util.py @@ -289,7 +289,7 @@ class BreakpointLocationInfoReaderV8(object): pass def get_locations(self, breakpoint): - pass + return [] class BreakpointLocationInfoReaderV13(object): diff --git a/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/wine.py b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/wine.py new file mode 100644 index 0000000000..14e6025d32 --- /dev/null +++ b/Ghidra/Debug/Debugger-agent-gdb/src/main/py/src/ghidragdb/wine.py @@ -0,0 +1,91 @@ +## ### +# 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 gdb + +from . import util +from .commands import install, cmd + + +@install +class GhidraWinePrefix(gdb.Command): + """Commands for tracing Wine processes""" + + def __init__(self): + super().__init__('ghidra wine', gdb.COMMAND_SUPPORT, prefix=True) + + +def is_mapped(pe_file): + return pe_file in gdb.execute("info proc mappings", to_string=True) + + +def set_break(command): + breaks_before = set(gdb.breakpoints()) + gdb.execute(command) + return (set(gdb.breakpoints()) - breaks_before).pop() + + +@cmd('ghidra wine run-to-image', '-ghidra-wine-run-to-image', gdb.COMMAND_SUPPORT, False) +def ghidra_wine_run_to_image(pe_file, *, is_mi, **kwargs): + mprot_catchpoint = set_break(""" +catch syscall mprotect +commands +silent +end +""".strip()) + while not is_mapped(pe_file): + gdb.execute("continue") + mprot_catchpoint.delete() + + +ORIG_MODULE_INFO_READER = util.MODULE_INFO_READER + + +class Range(object): + + def expand(self, region): + if not hasattr(self, 'min'): + self.min = region.start + self.max = region.end + else: + self.min = min(self.min, region.start) + self.max = max(self.max, region.end) + return self + + +# There are more, but user can monkey patch this +MODULE_SUFFIXES = (".exe", ".dll") + + +class WineModuleInfoReader(object): + + def get_modules(self): + modules = ORIG_MODULE_INFO_READER.get_modules() + ranges = dict() + for region in util.REGION_INFO_READER.get_regions(): + if not region.objfile in ranges: + ranges[region.objfile] = Range().expand(region) + else: + ranges[region.objfile].expand(region) + for k, v in ranges.items(): + if k in modules: + continue + if not k.lower().endswith(MODULE_SUFFIXES): + continue + modules[k] = util.Module(k, v.min, v.max, {}) + return modules + + +util.MODULE_INFO_READER = WineModuleInfoReader() diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java index 4bd69ecb31..681947730c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/objects/components/DebuggerMethodInvocationDialog.java @@ -18,6 +18,7 @@ package ghidra.app.plugin.core.debug.gui.objects.components; import java.awt.*; import java.awt.event.ActionEvent; import java.beans.*; +import java.math.BigInteger; import java.util.*; import java.util.List; @@ -44,6 +45,28 @@ import ghidra.util.layout.PairLayout; public class DebuggerMethodInvocationDialog extends DialogComponentProvider implements PropertyChangeListener { + + public static class BigIntEditor extends PropertyEditorSupport { + @Override + public String getJavaInitializationString() { + Object value = getValue(); + return value == null + ? "null" + : "new BigInteger(\"%s\")".formatted(value); + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(text == null + ? null + : new BigInteger(text)); + } + } + + static { + PropertyEditorManager.registerEditor(BigInteger.class, BigIntEditor.class); + } + private static final String KEY_MEMORIZED_ARGUMENTS = "memorizedArguments"; static class ChoicesPropertyEditor implements PropertyEditor { diff --git a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/modules/TraceStaticMappingManager.java b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/modules/TraceStaticMappingManager.java index acddb4c61e..e829f06e3e 100644 --- a/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/modules/TraceStaticMappingManager.java +++ b/Ghidra/Debug/Framework-TraceModeling/src/main/java/ghidra/trace/model/modules/TraceStaticMappingManager.java @@ -26,11 +26,12 @@ import ghidra.trace.model.Lifespan; /** * Manages mappings from this trace into static images (Ghida {@link Program}s) * - * Most commonly, this is used to map sections listed by a connected debugger to those same sections - * of programs already imported into the same Ghidra project. It is vitally important that the image - * loaded by the target is an exact copy of the image imported by Ghidra, or else things may not be - * aligned. + *

+ * Most commonly, this is used to map modules listed by a connected debugger to programs already + * imported into the same Ghidra project. It is vitally important that the image loaded by the + * target is an exact copy of the image imported by Ghidra, or else things may not be aligned. * + *

* Note, to best handle mapping ranges to a variety of programs, and to validate the addition of new * entries, it is unlikely a client should consume mapping entries directly. Instead, a service * should track the mappings among all open traces and programs, permitting clients to mutate and @@ -42,6 +43,7 @@ public interface TraceStaticMappingManager { /** * Add a new mapping, if not already covered * + *

* A new mapping may overlap an existing mapping, so long as they agree in address shift. * Furthermore, in such cases, the implementation may coalesce mappings to remove duplication. * @@ -50,7 +52,7 @@ public interface TraceStaticMappingManager { * @param toProgramURL the (Ghidra) URL of the static image ("to") * @param toAddress the starting address (in string form) in the static image ("to") * @throws TraceConflictedMappingException if an existing mapping conflicts. See - * {@link #isAnyConflicting(AddressRange, Lifespan, URL, String)} + * {@link #findAnyConflicting(AddressRange, Lifespan, URL, String)} * @return the new entry, or any entry which subsumes the specified mapping */ TraceStaticMapping add(AddressRange range, Lifespan lifespan, URL toProgramURL, @@ -75,14 +77,16 @@ public interface TraceStaticMappingManager { /** * Check if another mapping would conflict with the given prospective mapping * + *

* Mappings are allowed to overlap, but they must agree on the destination program and address * throughout all overlapping portions. * - * TODO: It'd be nice if the manager automatically merged overlapping mappings in agreement or - * provided a "deduplicate" method which optimized the entries in the database. This gets - * complicated, since we're dealing with overlapping rectangles, not strict one-dimensional - * ranges. Look into existing research for optimizing coverage of shapes by rectangles. The same - * is needed for property maps in 2 dimensions. + *

+ * TODO: It'd be nice if the manager automatically merged overlapping mappings in + * agreement or provided a "de-duplicate" method which optimized the entries in the database. + * This gets complicated, since we're dealing with overlapping rectangles, not strict + * one-dimensional ranges. Look into existing research for optimizing coverage of shapes by + * rectangles. The same is needed for property maps in 2 dimensions. * * @param range the range in the trace ("from") * @param lifespan the span of time in the trace @@ -96,6 +100,7 @@ public interface TraceStaticMappingManager { /** * Find all mappings which overlap the given adddress range and span of time * + *

* Note, this returns overlapping entries whether or not they conflict. * * @param range the range in the trace ("from")