Merge remote-tracking branch 'origin/GP-3891_gdbWineLauncher--SQUASHED'

This commit is contained in:
Ryan Kurtz 2024-03-07 09:15:44 -05:00
commit df29f50fa3
8 changed files with 255 additions and 15 deletions

View File

@ -23,7 +23,7 @@
#@desc (you may install <tt>gdb-multiarch</tt>), and it must embed the Python 3 interpreter. You
#@desc will also need <tt>protobuf</tt> installed for Python 3.
#@desc </body></html>
#@menu-group qemu
#@menu-group cross
#@icon icon.debugger
#@help TraceRmiLauncherServicePlugin#gdb
#@enum StartCmd:str run start starti

View File

@ -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 <html><body width="300px">
#@desc <h3>Launch with <tt>gdb</tt> and <tt>wine</tt></h3>
#@desc <p>This will launch the target on the local machine using <tt>gdb</tt> and <tt>wine</tt>.
#@desc GDB and Wine must already be installed on your system, and GDB must embed the Python 3
#@desc interpreter. You will also need <tt>protobuf</tt> and <tt>psutil</tt> installed for Python
#@desc 3.</p>
#@desc <p>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.</p>
#@desc <p>You will need to locate the <tt>wine</tt> executable on your system, not the script. To
#@desc find it, either dissect the <tt>wine</tt> 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.</p>
#@desc </body></html>
#@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"

View File

@ -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'],

View File

@ -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

View File

@ -289,7 +289,7 @@ class BreakpointLocationInfoReaderV8(object):
pass
def get_locations(self, breakpoint):
pass
return []
class BreakpointLocationInfoReaderV13(object):

View File

@ -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()

View File

@ -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 {

View File

@ -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.
* <p>
* 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.
*
* <p>
* 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
*
* <p>
* 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
*
* <p>
* 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.
* <p>
* <b>TODO</b>: 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
*
* <p>
* Note, this returns overlapping entries whether or not they conflict.
*
* @param range the range in the trace ("from")