GP-4389: Fixes for Trace RMI lldb on macOS

Create local-lldh.sh launch script
Upgrade to JNA-5.14
Fix pty IOCTL numbers for macOS
Fix compile-spec mapping
Improv error report / clean-up after launch failure.
Write ERROR state on memory read failures
Convert Python exceptions to LLDB command errors
This commit is contained in:
Dan 2024-03-04 16:45:41 -05:00
parent bb8ec1cbe6
commit 973b9a8d4c
50 changed files with 1247 additions and 723 deletions

View file

@ -25,7 +25,7 @@ import agent.gdb.manager.breakpoint.GdbBreakpointInfo;
import agent.gdb.manager.breakpoint.GdbBreakpointInsertions; import agent.gdb.manager.breakpoint.GdbBreakpointInsertions;
import agent.gdb.manager.impl.GdbManagerImpl; import agent.gdb.manager.impl.GdbManagerImpl;
import ghidra.pty.PtyFactory; import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPty; import ghidra.pty.unix.UnixPty;
/** /**
* The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal * The controlling side of a GDB session, using GDB/MI, usually via a pseudo-terminal
@ -232,7 +232,7 @@ public interface GdbManager extends AutoCloseable, GdbConsoleOperations, GdbBrea
* Note: depending on the target, its output may not be communicated via this listener. Local * Note: depending on the target, its output may not be communicated via this listener. Local
* targets, e.g., tend to just print output to GDB's controlling TTY. See * targets, e.g., tend to just print output to GDB's controlling TTY. See
* {@link GdbInferior#setTty(String)} for a means to more reliably interact with a target's * {@link GdbInferior#setTty(String)} for a means to more reliably interact with a target's
* input and output. See also {@link LinuxPty} for a means to easily acquire a new TTY from * input and output. See also {@link UnixPty} for a means to easily acquire a new TTY from
* Java. * Java.
* *
* @param listener the listener to add * @param listener the listener to add

View file

@ -551,7 +551,7 @@ def putmem_state(address, length, state, pages=True):
inf = gdb.selected_inferior() inf = gdb.selected_inferior()
base, addr = STATE.trace.memory_mapper.map(inf, start) base, addr = STATE.trace.memory_mapper.map(inf, start)
if base != addr.space: if base != addr.space:
trace.create_overlay_space(base, addr.space) STATE.trace.create_overlay_space(base, addr.space)
STATE.trace.set_memory_state(addr.extend(end - start), state) STATE.trace.set_memory_state(addr.extend(end - start), state)

View file

@ -17,11 +17,10 @@ from concurrent.futures import Future, Executor
from contextlib import contextmanager from contextlib import contextmanager
import re import re
import gdb
from ghidratrace import sch from ghidratrace import sch
from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange
import gdb
from . import commands, hooks, util from . import commands, hooks, util
@ -690,8 +689,8 @@ def read_mem(inferior: sch.Schema('Inferior'), range: AddressRange):
gdb.execute( gdb.execute(
f'ghidra trace putmem 0x{offset_start:x} {range.length()}') f'ghidra trace putmem 0x{offset_start:x} {range.length()}')
except: except:
commands.putmem_state( gdb.execute(
offset_start, offset_start+range.length() - 1, 'error') f'ghidra trace putmem-state 0x{offset_start:x} {range.length()} error')
@REGISTRY.method @REGISTRY.method

View file

@ -23,7 +23,8 @@ import org.junit.Ignore;
import agent.gdb.manager.GdbManager; import agent.gdb.manager.GdbManager;
import ghidra.pty.PtySession; import ghidra.pty.PtySession;
import ghidra.pty.linux.LinuxPty; import ghidra.pty.linux.LinuxIoctls;
import ghidra.pty.unix.UnixPty;
import ghidra.util.Msg; import ghidra.util.Msg;
@Ignore("Need compatible GDB version for CI") @Ignore("Need compatible GDB version for CI")
@ -45,13 +46,13 @@ public class JoinedGdbManagerTest extends AbstractGdbManagerTest {
} }
} }
protected LinuxPty ptyUserGdb; protected UnixPty ptyUserGdb;
protected PtySession gdb; protected PtySession gdb;
@Override @Override
protected CompletableFuture<Void> startManager(GdbManager manager) { protected CompletableFuture<Void> startManager(GdbManager manager) {
try { try {
ptyUserGdb = LinuxPty.openpty(); ptyUserGdb = UnixPty.openpty(LinuxIoctls.INSTANCE);
manager.start(null); manager.start(null);
Msg.debug(this, "Starting GDB and invoking new-ui mi2 " + manager.getMi2PtyName()); Msg.debug(this, "Starting GDB and invoking new-ui mi2 " + manager.getMi2PtyName());

View file

@ -0,0 +1,75 @@
#!/usr/bin/env 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 lldb
#@desc <html><body width="300px">
#@desc <h3>Launch with <tt>lldb</tt></h3>
#@desc <p>This will launch the target on the local machine using <tt>lldb</tt>. LLDB must already
#@desc be installed on your system, and it must embed the Python 3 interpreter. You will also
#@desc need <tt>protobuf</tt> and <tt>psutil</tt> installed for Python 3.</p>
#@desc </body></html>
#@menu-group local
#@icon icon.debugger
#@help TraceRmiLauncherServicePlugin#lldb
#@enum StartCmd:str run "process launch" "process launch --stop-at-entry"
#@arg :str "Image" "The target binary executable image"
#@args "Arguments" "Command-line arguments to pass to the target"
#@env OPT_LLDB_PATH:str="lldb" "Path to lldb" "The path to lldb. Omit the full path to resolve using the system PATH."
#@env OPT_START_CMD:StartCmd="process launch" "Run command" "The lldb command to actually run the target."
#@env OPT_EXTRA_TTY:bool=false "Target 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-lldb/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-lldb/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-lldb/pypkg/src:$PYTHONPATH
export PYTHONPATH=$GHIDRA_HOME/Ghidra/Debug/Debugger-rmi-trace/pypkg/src:$PYTHONPATH
fi
target_image="$1"
shift
target_args="$@"
if [ -z "$target_args" ]
then
argspart=
else
argspart=-o "settings set target.run-args $target_args"
fi
if [ -z "$TARGET_TTY" ]
then
ttypart=
else
ttypart=-o "settings set target.output-path $TTY_TARGET" -o "settings set target.input-path $TTY_TARGET"
fi
"$OPT_LLDB_PATH" \
-o "version" \
-o "script import ghidralldb" \
-o "target create \"$target_image\"" \
$argspart \
$ttypart \
-o "ghidra trace connect \"$GHIDRA_TRACE_RMI_ADDR\"" \
-o "ghidra trace start" \
-o "ghidra trace sync-enable" \
-o "$OPT_START_CMD"

View file

@ -14,20 +14,20 @@
# limitations under the License. # limitations under the License.
## ##
from ghidratrace.client import Address, RegVal from ghidratrace.client import Address, RegVal
import lldb import lldb
from . import util from . import util
# NOTE: This map is derived from the ldefs using a script # NOTE: This map is derived from the ldefs using a script
language_map = { language_map = {
'aarch64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'], 'aarch64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:AppleSilicon', 'AARCH64:LE:64:v8A'],
'armv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'armv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
'armv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'armv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
'armv7s': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'armv7s': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
'arm64': ['ARM:BE:64:v8', 'ARM:LE:64:v8'], 'arm64': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:v8A'],
'arm64_32': ['ARM:BE:32:v8', 'ARM:LE:32:v8'], 'arm64_32': ['ARM:BE:32:v8', 'ARM:LE:32:v8'],
'arm64e': ['ARM:BE:64:v8', 'ARM:LE:64:v8'], 'arm64e': ['AARCH64:BE:64:v8A', 'AARCH64:LE:64:v8A'],
'i386': ['x86:LE:32:default'], 'i386': ['x86:LE:32:default'],
'thumbv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'thumbv7': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
'thumbv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'], 'thumbv7k': ['ARM:BE:32:v7', 'ARM:LE:32:v7'],
@ -40,7 +40,7 @@ data64_compiler_map = {
None: 'pointer64', None: 'pointer64',
} }
x86_compiler_map = { default_compiler_map = {
'freebsd': 'gcc', 'freebsd': 'gcc',
'linux': 'gcc', 'linux': 'gcc',
'netbsd': 'gcc', 'netbsd': 'gcc',
@ -55,10 +55,12 @@ x86_compiler_map = {
} }
compiler_map = { compiler_map = {
'DATA:BE:64:default': data64_compiler_map, 'DATA:BE:64:': data64_compiler_map,
'DATA:LE:64:default': data64_compiler_map, 'DATA:LE:64:': data64_compiler_map,
'x86:LE:32:default': x86_compiler_map, 'x86:LE:32:': default_compiler_map,
'x86:LE:64:default': x86_compiler_map, 'x86:LE:64:': default_compiler_map,
'ARM:LE:32:': default_compiler_map,
'ARM:LE:64:': default_compiler_map,
} }
@ -132,12 +134,20 @@ def compute_ghidra_compiler(lang):
return comp return comp
# Check if the selected lang has specific compiler recommendations # Check if the selected lang has specific compiler recommendations
if not lang in compiler_map: matched_lang = sorted(
(l for l in compiler_map if l in lang),
key=lambda l: compiler_map[l]
)
if len(matched_lang) == 0:
return 'default' return 'default'
comp_map = compiler_map[lang] comp_map = compiler_map[matched_lang[0]]
osabi = get_osabi() osabi = get_osabi()
if osabi in comp_map: matched_osabi = sorted(
return comp_map[osabi] (l for l in comp_map if l in osabi),
key=lambda l: comp_map[l]
)
if len(matched_osabi) > 0:
return comp_map[matched_osabi[0]]
if None in comp_map: if None in comp_map:
return comp_map[None] return comp_map[None]
return 'default' return 'default'
@ -161,7 +171,8 @@ class DefaultMemoryMapper(object):
def map_back(self, proc: lldb.SBProcess, address: Address) -> int: def map_back(self, proc: lldb.SBProcess, address: Address) -> int:
if address.space == self.defaultSpace: if address.space == self.defaultSpace:
return address.offset return address.offset
raise ValueError(f"Address {address} is not in process {proc.GetProcessID()}") raise ValueError(
f"Address {address} is not in process {proc.GetProcessID()}")
DEFAULT_MEMORY_MAPPER = DefaultMemoryMapper('ram') DEFAULT_MEMORY_MAPPER = DefaultMemoryMapper('ram')
@ -203,11 +214,11 @@ class DefaultRegisterMapper(object):
def map_value(self, proc, name, value): def map_value(self, proc, name, value):
try: try:
### TODO: this seems half-baked # TODO: this seems half-baked
av = value.to_bytes(8, "big") av = value.to_bytes(8, "big")
except e: except e:
raise ValueError("Cannot convert {}'s value: '{}', type: '{}'" raise ValueError("Cannot convert {}'s value: '{}', type: '{}'"
.format(name, value, value.type)) .format(name, value, value.type))
return RegVal(self.map_name(proc, name), av) return RegVal(self.map_name(proc, name), av)
def map_name_back(self, proc, name): def map_name_back(self, proc, name):
@ -258,4 +269,3 @@ def compute_register_mapper(lang):
if ':LE:' in lang: if ':LE:' in lang:
return DEFAULT_LE_REGISTER_MAPPER return DEFAULT_LE_REGISTER_MAPPER
return register_mappers[lang] return register_mappers[lang]

View file

@ -13,15 +13,17 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
## ##
import time
import threading import threading
import time
import lldb import lldb
from . import commands, util from . import commands, util
ALL_EVENTS = 0xFFFF ALL_EVENTS = 0xFFFF
class HookState(object): class HookState(object):
__slots__ = ('installed', 'mem_catchpoint') __slots__ = ('installed', 'mem_catchpoint')
@ -31,7 +33,8 @@ class HookState(object):
class ProcessState(object): class ProcessState(object):
__slots__ = ('first', 'regions', 'modules', 'threads', 'breaks', 'watches', 'visited') __slots__ = ('first', 'regions', 'modules', 'threads',
'breaks', 'watches', 'visited')
def __init__(self): def __init__(self):
self.first = True self.first = True
@ -64,9 +67,10 @@ class ProcessState(object):
hashable_frame = (thread.GetThreadID(), frame.GetFrameID()) hashable_frame = (thread.GetThreadID(), frame.GetFrameID())
if first or hashable_frame not in self.visited: if first or hashable_frame not in self.visited:
banks = frame.GetRegisters() banks = frame.GetRegisters()
commands.putreg(frame, banks.GetFirstValueByName(commands.DEFAULT_REGISTER_BANK)) commands.putreg(frame, banks.GetFirstValueByName(
commands.putmem("$pc", "1", from_tty=False) commands.DEFAULT_REGISTER_BANK))
commands.putmem("$sp", "1", from_tty=False) commands.putmem("$pc", "1", result=None)
commands.putmem("$sp", "1", result=None)
self.visited.add(hashable_frame) self.visited.add(hashable_frame)
if first or self.regions or self.threads or self.modules: if first or self.regions or self.threads or self.modules:
# Sections, memory syscalls, or stack allocations # Sections, memory syscalls, or stack allocations
@ -117,10 +121,11 @@ HOOK_STATE = HookState()
BRK_STATE = BrkState() BRK_STATE = BrkState()
PROC_STATE = {} PROC_STATE = {}
def process_event(self, listener, event): def process_event(self, listener, event):
try: try:
desc = util.get_description(event) desc = util.get_description(event)
#event_process = lldb.SBProcess_GetProcessFromEvent(event) # print('Event:', desc)
event_process = util.get_process() event_process = util.get_process()
if event_process not in PROC_STATE: if event_process not in PROC_STATE:
PROC_STATE[event_process.GetProcessID()] = ProcessState() PROC_STATE[event_process.GetProcessID()] = ProcessState()
@ -128,35 +133,29 @@ def process_event(self, listener, event):
if rc is False: if rc is False:
print("add listener for process failed") print("add listener for process failed")
commands.put_state(event_process) # NB: Calling put_state on running leaves an open transaction
if event_process.is_running is False:
commands.put_state(event_process)
type = event.GetType() type = event.GetType()
if lldb.SBTarget.EventIsTargetEvent(event): if lldb.SBTarget.EventIsTargetEvent(event):
print('Event:', desc)
if (type & lldb.SBTarget.eBroadcastBitBreakpointChanged) != 0: if (type & lldb.SBTarget.eBroadcastBitBreakpointChanged) != 0:
print("eBroadcastBitBreakpointChanged")
return on_breakpoint_modified(event) return on_breakpoint_modified(event)
if (type & lldb.SBTarget.eBroadcastBitWatchpointChanged) != 0: if (type & lldb.SBTarget.eBroadcastBitWatchpointChanged) != 0:
print("eBroadcastBitWatchpointChanged")
return on_watchpoint_modified(event) return on_watchpoint_modified(event)
if (type & lldb.SBTarget.eBroadcastBitModulesLoaded) != 0: if (type & lldb.SBTarget.eBroadcastBitModulesLoaded) != 0:
print("eBroadcastBitModulesLoaded")
return on_new_objfile(event) return on_new_objfile(event)
if (type & lldb.SBTarget.eBroadcastBitModulesUnloaded) != 0: if (type & lldb.SBTarget.eBroadcastBitModulesUnloaded) != 0:
print("eBroadcastBitModulesUnloaded")
return on_free_objfile(event) return on_free_objfile(event)
if (type & lldb.SBTarget.eBroadcastBitSymbolsLoaded) != 0: if (type & lldb.SBTarget.eBroadcastBitSymbolsLoaded) != 0:
print("eBroadcastBitSymbolsLoaded")
return True return True
if lldb.SBProcess.EventIsProcessEvent(event): if lldb.SBProcess.EventIsProcessEvent(event):
if (type & lldb.SBProcess.eBroadcastBitStateChanged) != 0: if (type & lldb.SBProcess.eBroadcastBitStateChanged) != 0:
print("eBroadcastBitStateChanged")
if not event_process.is_alive: if not event_process.is_alive:
return on_exited(event) return on_exited(event)
if event_process.is_stopped: if event_process.is_stopped:
return on_stop(event) return on_stop(event)
return True return True
if (type & lldb.SBProcess.eBroadcastBitInterrupt) != 0: if (type & lldb.SBProcess.eBroadcastBitInterrupt) != 0:
print("eBroadcastBitInterrupt")
if event_process.is_stopped: if event_process.is_stopped:
return on_stop(event) return on_stop(event)
if (type & lldb.SBProcess.eBroadcastBitSTDOUT) != 0: if (type & lldb.SBProcess.eBroadcastBitSTDOUT) != 0:
@ -164,138 +163,100 @@ def process_event(self, listener, event):
if (type & lldb.SBProcess.eBroadcastBitSTDERR) != 0: if (type & lldb.SBProcess.eBroadcastBitSTDERR) != 0:
return True return True
if (type & lldb.SBProcess.eBroadcastBitProfileData) != 0: if (type & lldb.SBProcess.eBroadcastBitProfileData) != 0:
print("eBroadcastBitProfileData")
return True return True
if (type & lldb.SBProcess.eBroadcastBitStructuredData) != 0: if (type & lldb.SBProcess.eBroadcastBitStructuredData) != 0:
print("eBroadcastBitStructuredData")
return True return True
# NB: Thread events not currently processes # NB: Thread events not currently processes
if lldb.SBThread.EventIsThreadEvent(event): if lldb.SBThread.EventIsThreadEvent(event):
print('Event:', desc)
if (type & lldb.SBThread.eBroadcastBitStackChanged) != 0: if (type & lldb.SBThread.eBroadcastBitStackChanged) != 0:
print("eBroadcastBitStackChanged")
return on_frame_selected() return on_frame_selected()
if (type & lldb.SBThread.eBroadcastBitThreadSuspended) != 0: if (type & lldb.SBThread.eBroadcastBitThreadSuspended) != 0:
print("eBroadcastBitThreadSuspended")
if event_process.is_stopped: if event_process.is_stopped:
return on_stop(event) return on_stop(event)
if (type & lldb.SBThread.eBroadcastBitThreadResumed) != 0: if (type & lldb.SBThread.eBroadcastBitThreadResumed) != 0:
print("eBroadcastBitThreadResumed")
return on_cont(event) return on_cont(event)
if (type & lldb.SBThread.eBroadcastBitSelectedFrameChanged) != 0: if (type & lldb.SBThread.eBroadcastBitSelectedFrameChanged) != 0:
print("eBroadcastBitSelectedFrameChanged")
return on_frame_selected() return on_frame_selected()
if (type & lldb.SBThread.eBroadcastBitThreadSelected) != 0: if (type & lldb.SBThread.eBroadcastBitThreadSelected) != 0:
print("eBroadcastBitThreadSelected")
return on_thread_selected() return on_thread_selected()
if lldb.SBBreakpoint.EventIsBreakpointEvent(event): if lldb.SBBreakpoint.EventIsBreakpointEvent(event):
print('Event:', desc) btype = lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event)
btype = lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event); bpt = lldb.SBBreakpoint.GetBreakpointFromEvent(event)
bpt = lldb.SBBreakpoint.GetBreakpointFromEvent(event);
if btype is lldb.eBreakpointEventTypeAdded: if btype is lldb.eBreakpointEventTypeAdded:
print("eBreakpointEventTypeAdded")
return on_breakpoint_created(bpt) return on_breakpoint_created(bpt)
if btype is lldb.eBreakpointEventTypeAutoContinueChanged: if btype is lldb.eBreakpointEventTypeAutoContinueChanged:
print("elldb.BreakpointEventTypeAutoContinueChanged")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeCommandChanged: if btype is lldb.eBreakpointEventTypeCommandChanged:
print("eBreakpointEventTypeCommandChanged")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeConditionChanged: if btype is lldb.eBreakpointEventTypeConditionChanged:
print("eBreakpointEventTypeConditionChanged")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeDisabled: if btype is lldb.eBreakpointEventTypeDisabled:
print("eBreakpointEventTypeDisabled")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeEnabled: if btype is lldb.eBreakpointEventTypeEnabled:
print("eBreakpointEventTypeEnabled")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeIgnoreChanged: if btype is lldb.eBreakpointEventTypeIgnoreChanged:
print("eBreakpointEventTypeIgnoreChanged")
return True return True
if btype is lldb.eBreakpointEventTypeInvalidType: if btype is lldb.eBreakpointEventTypeInvalidType:
print("eBreakpointEventTypeInvalidType")
return True return True
if btype is lldb.eBreakpointEventTypeLocationsAdded: if btype is lldb.eBreakpointEventTypeLocationsAdded:
print("eBreakpointEventTypeLocationsAdded")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeLocationsRemoved: if btype is lldb.eBreakpointEventTypeLocationsRemoved:
print("eBreakpointEventTypeLocationsRemoved")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeLocationsResolved: if btype is lldb.eBreakpointEventTypeLocationsResolved:
print("eBreakpointEventTypeLocationsResolved")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
if btype is lldb.eBreakpointEventTypeRemoved: if btype is lldb.eBreakpointEventTypeRemoved:
print("eBreakpointEventTypeRemoved")
return on_breakpoint_deleted(bpt) return on_breakpoint_deleted(bpt)
if btype is lldb.eBreakpointEventTypeThreadChanged: if btype is lldb.eBreakpointEventTypeThreadChanged:
print("eBreakpointEventTypeThreadChanged")
return on_breakpoint_modified(bpt) return on_breakpoint_modified(bpt)
print("UNKNOWN BREAKPOINT EVENT") print("UNKNOWN BREAKPOINT EVENT")
return True return True
if lldb.SBWatchpoint.EventIsWatchpointEvent(event): if lldb.SBWatchpoint.EventIsWatchpointEvent(event):
print('Event:', desc) btype = lldb.SBWatchpoint.GetWatchpointEventTypeFromEvent(event)
btype = lldb.SBWatchpoint.GetWatchpointEventTypeFromEvent(event); bpt = lldb.SBWatchpoint.GetWatchpointFromEvent(eventt)
bpt = lldb.SBWatchpoint.GetWatchpointFromEvent(eventt);
if btype is lldb.eWatchpointEventTypeAdded: if btype is lldb.eWatchpointEventTypeAdded:
print("eWatchpointEventTypeAdded")
return on_watchpoint_added(bpt) return on_watchpoint_added(bpt)
if btype is lldb.eWatchpointEventTypeCommandChanged: if btype is lldb.eWatchpointEventTypeCommandChanged:
print("eWatchpointEventTypeCommandChanged")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeConditionChanged: if btype is lldb.eWatchpointEventTypeConditionChanged:
print("eWatchpointEventTypeConditionChanged")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeDisabled: if btype is lldb.eWatchpointEventTypeDisabled:
print("eWatchpointEventTypeDisabled")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeEnabled: if btype is lldb.eWatchpointEventTypeEnabled:
print("eWatchpointEventTypeEnabled")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeIgnoreChanged: if btype is lldb.eWatchpointEventTypeIgnoreChanged:
print("eWatchpointEventTypeIgnoreChanged")
return True return True
if btype is lldb.eWatchpointEventTypeInvalidType: if btype is lldb.eWatchpointEventTypeInvalidType:
print("eWatchpointEventTypeInvalidType")
return True return True
if btype is lldb.eWatchpointEventTypeRemoved: if btype is lldb.eWatchpointEventTypeRemoved:
print("eWatchpointEventTypeRemoved")
return on_watchpoint_deleted(bpt) return on_watchpoint_deleted(bpt)
if btype is lldb.eWatchpointEventTypeThreadChanged: if btype is lldb.eWatchpointEventTypeThreadChanged:
print("eWatchpointEventTypeThreadChanged")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
if btype is lldb.eWatchpointEventTypeTypeChanged: if btype is lldb.eWatchpointEventTypeTypeChanged:
print("eWatchpointEventTypeTypeChanged")
return on_watchpoint_modified(bpt) return on_watchpoint_modified(bpt)
print("UNKNOWN WATCHPOINT EVENT") print("UNKNOWN WATCHPOINT EVENT")
return True return True
if lldb.SBCommandInterpreter.EventIsCommandInterpreterEvent(event): if lldb.SBCommandInterpreter.EventIsCommandInterpreterEvent(event):
print('Event:', desc)
if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousErrorData) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousErrorData) != 0:
print("eBroadcastBitAsynchronousErrorData")
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousOutputData) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitAsynchronousOutputData) != 0:
print("eBroadcastBitAsynchronousOutputData")
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitQuitCommandReceived) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitQuitCommandReceived) != 0:
print("eBroadcastBitQuitCommandReceived")
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitResetPrompt) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitResetPrompt) != 0:
print("eBroadcastBitResetPrompt")
return True return True
if (type & lldb.SBCommandInterpreter.eBroadcastBitThreadShouldExit) != 0: if (type & lldb.SBCommandInterpreter.eBroadcastBitThreadShouldExit) != 0:
print("eBroadcastBitThreadShouldExit")
return True return True
print("UNKNOWN EVENT") print("UNKNOWN EVENT")
return True return True
except RuntimeError as e: except RuntimeError as e:
print(e) print(e)
class EventThread(threading.Thread): class EventThread(threading.Thread):
func = process_event func = process_event
event = lldb.SBEvent() event = lldb.SBEvent()
def run(self): def run(self):
# Let's only try at most 4 times to retrieve any kind of event. # Let's only try at most 4 times to retrieve any kind of event.
# After that, the thread exits. # After that, the thread exits.
listener = lldb.SBListener('eventlistener') listener = lldb.SBListener('eventlistener')
@ -314,7 +275,7 @@ class EventThread(threading.Thread):
if rc is False: if rc is False:
print("add listener for process failed") print("add listener for process failed")
return return
# Not sure what effect this logic has # Not sure what effect this logic has
rc = cli.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS) rc = cli.GetBroadcaster().AddInitialEventsToListener(listener, ALL_EVENTS)
if rc is False: if rc is False:
@ -329,12 +290,13 @@ class EventThread(threading.Thread):
print("add listener for process failed") print("add listener for process failed")
return return
rc = listener.StartListeningForEventClass(util.get_debugger(), lldb.SBThread.GetBroadcasterClassName(), ALL_EVENTS) rc = listener.StartListeningForEventClass(
util.get_debugger(), lldb.SBThread.GetBroadcasterClassName(), ALL_EVENTS)
if rc is False: if rc is False:
print("add listener for threads failed") print("add listener for threads failed")
return return
# THIS WILL NOT WORK: listener = util.get_debugger().GetListener() # THIS WILL NOT WORK: listener = util.get_debugger().GetListener()
while True: while True:
event_recvd = False event_recvd = False
while event_recvd is False: while event_recvd is False:
@ -344,13 +306,14 @@ class EventThread(threading.Thread):
while listener.GetNextEvent(self.event): while listener.GetNextEvent(self.event):
self.func(listener, self.event) self.func(listener, self.event)
event_recvd = True event_recvd = True
except Exception as e: except BaseException as e:
print(e) print(e)
proc = util.get_process() proc = util.get_process()
if proc is not None and not proc.is_alive: if proc is not None and not proc.is_alive:
break break
return return
""" """
# Not sure if this is possible in LLDB... # Not sure if this is possible in LLDB...
@ -475,7 +438,7 @@ def on_memory_changed(event):
with commands.STATE.client.batch(): with commands.STATE.client.batch():
with trace.open_tx("Memory *0x{:08x} changed".format(event.address)): with trace.open_tx("Memory *0x{:08x} changed".format(event.address)):
commands.put_bytes(event.address, event.address + event.length, commands.put_bytes(event.address, event.address + event.length,
pages=False, is_mi=False, from_tty=False) pages=False, is_mi=False, result=None)
def on_register_changed(event): def on_register_changed(event):
@ -547,11 +510,13 @@ def on_exited(event):
commands.put_event_thread() commands.put_event_thread()
commands.activate() commands.activate()
def notify_others_breaks(proc): def notify_others_breaks(proc):
for num, state in PROC_STATE.items(): for num, state in PROC_STATE.items():
if num != proc.GetProcessID(): if num != proc.GetProcessID():
state.breaks = True state.breaks = True
def notify_others_watches(proc): def notify_others_watches(proc):
for num, state in PROC_STATE.items(): for num, state in PROC_STATE.items():
if num != proc.GetProcessID(): if num != proc.GetProcessID():
@ -697,6 +662,7 @@ def remove_hooks():
return return
HOOK_STATE.installed = False HOOK_STATE.installed = False
def enable_current_process(): def enable_current_process():
proc = util.get_process() proc = util.get_process()
PROC_STATE[proc.GetProcessID()] = ProcessState() PROC_STATE[proc.GetProcessID()] = ProcessState()

View file

@ -18,7 +18,6 @@ import re
from ghidratrace import sch from ghidratrace import sch
from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange from ghidratrace.client import MethodRegistry, ParamDesc, Address, AddressRange
import lldb import lldb
from . import commands, util from . import commands, util
@ -66,9 +65,7 @@ def find_proc_by_num(procnum):
def find_proc_by_pattern(object, pattern, err_msg): def find_proc_by_pattern(object, pattern, err_msg):
print(object.path)
mat = pattern.fullmatch(object.path) mat = pattern.fullmatch(object.path)
print(mat)
if mat is None: if mat is None:
raise TypeError(f"{object} is not {err_msg}") raise TypeError(f"{object} is not {err_msg}")
procnum = int(mat['procnum']) procnum = int(mat['procnum'])
@ -81,11 +78,12 @@ def find_proc_by_obj(object):
def find_proc_by_procbreak_obj(object): def find_proc_by_procbreak_obj(object):
return find_proc_by_pattern(object, PROC_BREAKS_PATTERN, return find_proc_by_pattern(object, PROC_BREAKS_PATTERN,
"a BreakpointLocationContainer") "a BreakpointLocationContainer")
def find_proc_by_procwatch_obj(object): def find_proc_by_procwatch_obj(object):
return find_proc_by_pattern(object, PROC_WATCHES_PATTERN, return find_proc_by_pattern(object, PROC_WATCHES_PATTERN,
"a WatchpointContainer") "a WatchpointContainer")
def find_proc_by_env_obj(object): def find_proc_by_env_obj(object):
@ -108,7 +106,8 @@ def find_thread_by_num(proc, tnum):
for t in proc.threads: for t in proc.threads:
if t.GetThreadID() == tnum: if t.GetThreadID() == tnum:
return t return t
raise KeyError(f"Processes[{proc.GetProcessID()}].Threads[{tnum}] does not exist") raise KeyError(
f"Processes[{proc.GetProcessID()}].Threads[{tnum}] does not exist")
def find_thread_by_pattern(pattern, object, err_msg): def find_thread_by_pattern(pattern, object, err_msg):
@ -166,7 +165,7 @@ def find_reg_by_name(f, name):
# I could keep my own cache in a dict, but why? # I could keep my own cache in a dict, but why?
def find_bpt_by_number(breaknum): def find_bpt_by_number(breaknum):
# TODO: If len exceeds some threshold, use binary search? # TODO: If len exceeds some threshold, use binary search?
for i in range(0,util.get_target().GetNumBreakpoints()): for i in range(0, util.get_target().GetNumBreakpoints()):
b = util.get_target().GetBreakpointAtIndex(i) b = util.get_target().GetBreakpointAtIndex(i)
if b.GetID() == breaknum: if b.GetID() == breaknum:
return b return b
@ -189,7 +188,7 @@ def find_bpt_by_obj(object):
# I could keep my own cache in a dict, but why? # I could keep my own cache in a dict, but why?
def find_wpt_by_number(watchnum): def find_wpt_by_number(watchnum):
# TODO: If len exceeds some threshold, use binary search? # TODO: If len exceeds some threshold, use binary search?
for i in range(0,util.get_target().GetNumWatchpoints()): for i in range(0, util.get_target().GetNumWatchpoints()):
w = util.get_target().GetWatchpointAtIndex(i) w = util.get_target().GetWatchpointAtIndex(i)
if w.GetID() == watchnum: if w.GetID() == watchnum:
return w return w
@ -203,6 +202,7 @@ def find_wpt_by_pattern(pattern, object, err_msg):
watchnum = int(mat['watchnum']) watchnum = int(mat['watchnum'])
return find_wpt_by_number(watchnum) return find_wpt_by_number(watchnum)
def find_wpt_by_obj(object): def find_wpt_by_obj(object):
return find_wpt_by_pattern(PROC_WATCHLOC_PATTERN, object, "a WatchpointSpec") return find_wpt_by_pattern(PROC_WATCHLOC_PATTERN, object, "a WatchpointSpec")
@ -244,7 +244,7 @@ def execute(cmd: str, to_string: bool=False):
def refresh_available(node: sch.Schema('AvailableContainer')): def refresh_available(node: sch.Schema('AvailableContainer')):
"""List processes on lldb's host system.""" """List processes on lldb's host system."""
with commands.open_tracked_tx('Refresh Available'): with commands.open_tracked_tx('Refresh Available'):
util.get_debugger().HandleCommand('ghidra_trace_put_available') util.get_debugger().HandleCommand('ghidra trace put-available')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -254,14 +254,14 @@ def refresh_breakpoints(node: sch.Schema('BreakpointContainer')):
process). process).
""" """
with commands.open_tracked_tx('Refresh Breakpoints'): with commands.open_tracked_tx('Refresh Breakpoints'):
util.get_debugger().HandleCommand('ghidra_trace_put_breakpoints') util.get_debugger().HandleCommand('ghidra trace put-breakpoints')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
def refresh_processes(node: sch.Schema('ProcessContainer')): def refresh_processes(node: sch.Schema('ProcessContainer')):
"""Refresh the list of processes.""" """Refresh the list of processes."""
with commands.open_tracked_tx('Refresh Processes'): with commands.open_tracked_tx('Refresh Processes'):
util.get_debugger().HandleCommand('ghidra_trace_put_threads') util.get_debugger().HandleCommand('ghidra trace put-threads')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -273,7 +273,7 @@ def refresh_proc_breakpoints(node: sch.Schema('BreakpointLocationContainer')):
refreshed. refreshed.
""" """
with commands.open_tracked_tx('Refresh Breakpoint Locations'): with commands.open_tracked_tx('Refresh Breakpoint Locations'):
util.get_debugger().HandleCommand('ghidra_trace_put_breakpoints'); util.get_debugger().HandleCommand('ghidra trace put-breakpoints')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -285,20 +285,21 @@ def refresh_proc_watchpoints(node: sch.Schema('WatchpointContainer')):
refreshed. refreshed.
""" """
with commands.open_tracked_tx('Refresh Watchpoint Locations'): with commands.open_tracked_tx('Refresh Watchpoint Locations'):
util.get_debugger().HandleCommand('ghidra_trace_put_watchpoints'); util.get_debugger().HandleCommand('ghidra trace put-watchpoints')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
def refresh_environment(node: sch.Schema('Environment')): def refresh_environment(node: sch.Schema('Environment')):
"""Refresh the environment descriptors (arch, os, endian).""" """Refresh the environment descriptors (arch, os, endian)."""
with commands.open_tracked_tx('Refresh Environment'): with commands.open_tracked_tx('Refresh Environment'):
util.get_debugger().HandleCommand('ghidra_trace_put_environment') util.get_debugger().HandleCommand('ghidra trace put-environment')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
def refresh_threads(node: sch.Schema('ThreadContainer')): def refresh_threads(node: sch.Schema('ThreadContainer')):
"""Refresh the list of threads in the process.""" """Refresh the list of threads in the process."""
with commands.open_tracked_tx('Refresh Threads'): with commands.open_tracked_tx('Refresh Threads'):
util.get_debugger().HandleCommand('ghidra_trace_put_threads') util.get_debugger().HandleCommand('ghidra trace put-threads')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -307,7 +308,7 @@ def refresh_stack(node: sch.Schema('Stack')):
t = find_thread_by_stack_obj(node) t = find_thread_by_stack_obj(node)
t.process.SetSelectedThread(t) t.process.SetSelectedThread(t)
with commands.open_tracked_tx('Refresh Stack'): with commands.open_tracked_tx('Refresh Stack'):
util.get_debugger().HandleCommand('ghidra_trace_put_frames'); util.get_debugger().HandleCommand('ghidra trace put-frames')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -317,14 +318,14 @@ def refresh_registers(node: sch.Schema('RegisterValueContainer')):
f.thread.SetSelectedFrame(f.GetFrameID()) f.thread.SetSelectedFrame(f.GetFrameID())
# TODO: Groups? # TODO: Groups?
with commands.open_tracked_tx('Refresh Registers'): with commands.open_tracked_tx('Refresh Registers'):
util.get_debugger().HandleCommand('ghidra_trace_putreg'); util.get_debugger().HandleCommand('ghidra trace putreg')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
def refresh_mappings(node: sch.Schema('Memory')): def refresh_mappings(node: sch.Schema('Memory')):
"""Refresh the list of memory regions for the process.""" """Refresh the list of memory regions for the process."""
with commands.open_tracked_tx('Refresh Memory Regions'): with commands.open_tracked_tx('Refresh Memory Regions'):
util.get_debugger().HandleCommand('ghidra_trace_put_regions'); util.get_debugger().HandleCommand('ghidra trace put-regions')
@REGISTRY.method(action='refresh') @REGISTRY.method(action='refresh')
@ -335,7 +336,7 @@ def refresh_modules(node: sch.Schema('ModuleContainer')):
This will refresh the sections for all modules, not just the selected one. This will refresh the sections for all modules, not just the selected one.
""" """
with commands.open_tracked_tx('Refresh Modules'): with commands.open_tracked_tx('Refresh Modules'):
util.get_debugger().HandleCommand('ghidra_trace_put_modules'); util.get_debugger().HandleCommand('ghidra trace put-modules')
@REGISTRY.method(action='activate') @REGISTRY.method(action='activate')
@ -343,6 +344,7 @@ def activate_process(process: sch.Schema('Process')):
"""Switch to the process.""" """Switch to the process."""
return return
@REGISTRY.method(action='activate') @REGISTRY.method(action='activate')
def activate_thread(thread: sch.Schema('Thread')): def activate_thread(thread: sch.Schema('Thread')):
"""Switch to the thread.""" """Switch to the thread."""
@ -376,11 +378,13 @@ def attach_obj(process: sch.Schema('Process'), target: sch.Schema('Attachable'))
pid = find_availpid_by_obj(target) pid = find_availpid_by_obj(target)
util.get_debugger().HandleCommand(f'process attach -p {pid}') util.get_debugger().HandleCommand(f'process attach -p {pid}')
@REGISTRY.method(action='attach') @REGISTRY.method(action='attach')
def attach_pid(process: sch.Schema('Process'), pid: int): def attach_pid(process: sch.Schema('Process'), pid: int):
"""Attach the process to the given target.""" """Attach the process to the given target."""
util.get_debugger().HandleCommand(f'process attach -p {pid}') util.get_debugger().HandleCommand(f'process attach -p {pid}')
@REGISTRY.method(action='attach') @REGISTRY.method(action='attach')
def attach_name(process: sch.Schema('Process'), name: str): def attach_name(process: sch.Schema('Process'), name: str):
"""Attach the process to the given target.""" """Attach the process to the given target."""
@ -395,23 +399,24 @@ def detach(process: sch.Schema('Process')):
@REGISTRY.method(action='launch') @REGISTRY.method(action='launch')
def launch_loader(process: sch.Schema('Process'), def launch_loader(process: sch.Schema('Process'),
file: ParamDesc(str, display='File'), file: ParamDesc(str, display='File'),
args: ParamDesc(str, display='Arguments')=''): args: ParamDesc(str, display='Arguments')=''):
""" """
Start a native process with the given command line, stopping at 'main'. Start a native process with the given command line, stopping at 'main'.
If 'main' is not defined in the file, this behaves like 'run'. If 'main' is not defined in the file, this behaves like 'run'.
""" """
util.get_debugger().HandleCommand(f'file {file}') util.get_debugger().HandleCommand(f'file {file}')
if args is not '': if args != '':
util.get_debugger().HandleCommand(f'settings set target.run-args {args}') util.get_debugger().HandleCommand(
f'settings set target.run-args {args}')
util.get_debugger().HandleCommand(f'process launch --stop-at-entry') util.get_debugger().HandleCommand(f'process launch --stop-at-entry')
@REGISTRY.method(action='launch') @REGISTRY.method(action='launch')
def launch(process: sch.Schema('Process'), def launch(process: sch.Schema('Process'),
file: ParamDesc(str, display='File'), file: ParamDesc(str, display='File'),
args: ParamDesc(str, display='Arguments')=''): args: ParamDesc(str, display='Arguments')=''):
""" """
Run a native process with the given command line. Run a native process with the given command line.
@ -419,8 +424,9 @@ def launch(process: sch.Schema('Process'),
signaled. signaled.
""" """
util.get_debugger().HandleCommand(f'file {file}') util.get_debugger().HandleCommand(f'file {file}')
if args is not '': if args != '':
util.get_debugger().HandleCommand(f'settings set target.run-args {args}') util.get_debugger().HandleCommand(
f'settings set target.run-args {args}')
util.get_debugger().HandleCommand(f'run') util.get_debugger().HandleCommand(f'run')
@ -440,9 +446,9 @@ def _continue(process: sch.Schema('Process')):
def interrupt(): def interrupt():
"""Interrupt the execution of the debugged program.""" """Interrupt the execution of the debugged program."""
util.get_debugger().HandleCommand('process interrupt') util.get_debugger().HandleCommand('process interrupt')
#util.get_process().SendAsyncInterrupt() # util.get_process().SendAsyncInterrupt()
#util.get_debugger().HandleCommand('^c') # util.get_debugger().HandleCommand('^c')
#util.get_process().Signal(2) # util.get_process().Signal(2)
@REGISTRY.method(action='step_into') @REGISTRY.method(action='step_into')
@ -527,13 +533,15 @@ def break_read_range(process: sch.Schema('Process'), range: AddressRange):
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
sz = range.length() sz = range.length()
util.get_debugger().HandleCommand(f'watchpoint set expression -s {sz} -w read -- {offset_start}') util.get_debugger().HandleCommand(
f'watchpoint set expression -s {sz} -w read -- {offset_start}')
@REGISTRY.method(action='break_read') @REGISTRY.method(action='break_read')
def break_read_expression(expression: str): def break_read_expression(expression: str):
"""Set a read watchpoint.""" """Set a read watchpoint."""
util.get_debugger().HandleCommand(f'watchpoint set expression -w read -- {expression}') util.get_debugger().HandleCommand(
f'watchpoint set expression -w read -- {expression}')
@REGISTRY.method(action='break_write') @REGISTRY.method(action='break_write')
@ -543,13 +551,15 @@ def break_write_range(process: sch.Schema('Process'), range: AddressRange):
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
sz = range.length() sz = range.length()
util.get_debugger().HandleCommand(f'watchpoint set expression -s {sz} -- {offset_start}') util.get_debugger().HandleCommand(
f'watchpoint set expression -s {sz} -- {offset_start}')
@REGISTRY.method(action='break_write') @REGISTRY.method(action='break_write')
def break_write_expression(expression: str): def break_write_expression(expression: str):
"""Set a watchpoint.""" """Set a watchpoint."""
util.get_debugger().HandleCommand(f'watchpoint set expression -- {expression}') util.get_debugger().HandleCommand(
f'watchpoint set expression -- {expression}')
@REGISTRY.method(action='break_access') @REGISTRY.method(action='break_access')
@ -559,13 +569,15 @@ def break_access_range(process: sch.Schema('Process'), range: AddressRange):
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
sz = range.length() sz = range.length()
util.get_debugger().HandleCommand(f'watchpoint set expression -s {sz} -w read_write -- {offset_start}') util.get_debugger().HandleCommand(
f'watchpoint set expression -s {sz} -w read_write -- {offset_start}')
@REGISTRY.method(action='break_access') @REGISTRY.method(action='break_access')
def break_access_expression(expression: str): def break_access_expression(expression: str):
"""Set an access watchpoint.""" """Set an access watchpoint."""
util.get_debugger().HandleCommand(f'watchpoint set expression -w read_write -- {expression}') util.get_debugger().HandleCommand(
f'watchpoint set expression -w read_write -- {expression}')
@REGISTRY.method(action='break_ext') @REGISTRY.method(action='break_ext')
@ -580,12 +592,14 @@ def toggle_watchpoint(breakpoint: sch.Schema('WatchpointSpec'), enabled: bool):
wpt = find_wpt_by_obj(watchpoint) wpt = find_wpt_by_obj(watchpoint)
wpt.enabled = enabled wpt.enabled = enabled
@REGISTRY.method(action='toggle') @REGISTRY.method(action='toggle')
def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool): def toggle_breakpoint(breakpoint: sch.Schema('BreakpointSpec'), enabled: bool):
"""Toggle a breakpoint.""" """Toggle a breakpoint."""
bpt = find_bpt_by_obj(breakpoint) bpt = find_bpt_by_obj(breakpoint)
bpt.enabled = enabled bpt.enabled = enabled
@REGISTRY.method(action='toggle') @REGISTRY.method(action='toggle')
def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool): def toggle_breakpoint_location(location: sch.Schema('BreakpointLocation'), enabled: bool):
"""Toggle a breakpoint location.""" """Toggle a breakpoint location."""
@ -601,6 +615,7 @@ def delete_watchpoint(watchpoint: sch.Schema('WatchpointSpec')):
wptnum = wpt.GetID() wptnum = wpt.GetID()
util.get_debugger().HandleCommand(f'watchpoint delete {wptnum}') util.get_debugger().HandleCommand(f'watchpoint delete {wptnum}')
@REGISTRY.method(action='delete') @REGISTRY.method(action='delete')
def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')): def delete_breakpoint(breakpoint: sch.Schema('BreakpointSpec')):
"""Delete a breakpoint.""" """Delete a breakpoint."""
@ -615,8 +630,16 @@ def read_mem(process: sch.Schema('Process'), range: AddressRange):
proc = find_proc_by_obj(process) proc = find_proc_by_obj(process)
offset_start = process.trace.memory_mapper.map_back( offset_start = process.trace.memory_mapper.map_back(
proc, Address(range.space, range.min)) proc, Address(range.space, range.min))
ci = util.get_debugger().GetCommandInterpreter()
with commands.open_tracked_tx('Read Memory'): with commands.open_tracked_tx('Read Memory'):
util.get_debugger().HandleCommand(f'ghidra_trace_putmem 0x{offset_start:x} {range.length()}') result = lldb.SBCommandReturnObject()
ci.HandleCommand(
f'ghidra trace putmem 0x{offset_start:x} {range.length()}', result)
if result.Succeeded():
return
print(f"Could not read 0x{offset_start:x}: {result}")
util.get_debugger().HandleCommand(
f'ghidra trace putmem-state 0x{offset_start:x} {range.length()} error')
@REGISTRY.method @REGISTRY.method
@ -628,7 +651,7 @@ def write_mem(process: sch.Schema('Process'), address: Address, data: bytes):
@REGISTRY.method @REGISTRY.method
def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes): def write_reg(frame: sch.Schema('StackFrame'), name: str, value: bytes):
"""Write a register.""" """Write a register."""
f = find_frame_by_obj(frame) f = find_frame_by_obj(frame)
f.select() f.select()
@ -637,4 +660,5 @@ def write_reg(frame: sch.Schema('Frame'), name: str, value: bytes):
reg = find_reg_by_name(f, mname) reg = find_reg_by_name(f, mname)
size = int(lldb.parse_and_eval(f'sizeof(${mname})')) size = int(lldb.parse_and_eval(f'sizeof(${mname})'))
arr = '{' + ','.join(str(b) for b in mval) + '}' arr = '{' + ','.join(str(b) for b in mval) + '}'
util.get_debugger().HandleCommand(f'expr ((unsigned char[{size}])${mname}) = {arr};') util.get_debugger().HandleCommand(
f'expr ((unsigned char[{size}])${mname}) = {arr};')

View file

@ -1,46 +0,0 @@
## ###
# 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 lldb
# TODO: I don't know how to register a custom parameter prefix. I would rather
# these were 'ghidra language' and 'ghidra compiler'
class GhidraLanguageParameter(lldb.Parameter):
"""
The language id for Ghidra traces. Set this to 'auto' to try to derive it
from 'show arch' and 'show endian'. Otherwise, set it to a Ghidra
LanguageID.
"""
def __init__(self):
super().__init__('ghidra-language', lldb.COMMAND_DATA, lldb.PARAM_STRING)
self.value = 'auto'
GhidraLanguageParameter()
class GhidraCompilerParameter(lldb.Parameter):
"""
The compiler spec id for Ghidra traces. Set this to 'auto' to try to derive
it from 'show osabi'. Otherwise, set it to a Ghidra CompilerSpecID. Note
that valid compiler spec ids depend on the language id.
"""
def __init__(self):
super().__init__('ghidra-compiler', lldb.COMMAND_DATA, lldb.PARAM_STRING)
self.value = 'auto'
GhidraCompilerParameter()

View file

@ -27,7 +27,10 @@ LldbVersion = namedtuple('LldbVersion', ['full', 'major', 'minor'])
def _compute_lldb_ver(): def _compute_lldb_ver():
blurb = lldb.debugger.GetVersionString() blurb = lldb.debugger.GetVersionString()
top = blurb.split('\n')[0] top = blurb.split('\n')[0]
full = top.split(' ')[2] if ' version ' in top:
full = top.split(' ')[2] # "lldb version x.y.z"
else:
full = top.split('-')[1] # "lldb-x.y.z"
major, minor = full.split('.')[:2] major, minor = full.split('.')[:2]
return LldbVersion(full, int(major), int(minor)) return LldbVersion(full, int(major), int(minor))
@ -36,6 +39,7 @@ LLDB_VERSION = _compute_lldb_ver()
GNU_DEBUGDATA_PREFIX = ".gnu_debugdata for " GNU_DEBUGDATA_PREFIX = ".gnu_debugdata for "
class Module(namedtuple('BaseModule', ['name', 'base', 'max', 'sections'])): class Module(namedtuple('BaseModule', ['name', 'base', 'max', 'sections'])):
pass pass
@ -70,7 +74,7 @@ class ModuleInfoReader(object):
name = s.GetName() name = s.GetName()
attrs = s.GetPermissions() attrs = s.GetPermissions()
return Section(name, start, end, offset, attrs) return Section(name, start, end, offset, attrs)
def finish_module(self, name, sections): def finish_module(self, name, sections):
alloc = {k: s for k, s in sections.items()} alloc = {k: s for k, s in sections.items()}
if len(alloc) == 0: if len(alloc) == 0:
@ -96,7 +100,7 @@ class ModuleInfoReader(object):
fspec = module.GetFileSpec() fspec = module.GetFileSpec()
name = debracket(fspec.GetFilename()) name = debracket(fspec.GetFilename())
sections = {} sections = {}
for i in range(0, module.GetNumSections()): for i in range(0, module.GetNumSections()):
s = self.section_from_sbsection(module.GetSectionAtIndex(i)) s = self.section_from_sbsection(module.GetSectionAtIndex(i))
sname = debracket(s.name) sname = debracket(s.name)
sections[sname] = s sections[sname] = s
@ -107,8 +111,8 @@ class ModuleInfoReader(object):
def _choose_module_info_reader(): def _choose_module_info_reader():
return ModuleInfoReader() return ModuleInfoReader()
MODULE_INFO_READER = _choose_module_info_reader()
MODULE_INFO_READER = _choose_module_info_reader()
class Region(namedtuple('BaseRegion', ['start', 'end', 'offset', 'perms', 'objfile'])): class Region(namedtuple('BaseRegion', ['start', 'end', 'offset', 'perms', 'objfile'])):
@ -137,8 +141,8 @@ class RegionInfoReader(object):
reglist = get_process().GetMemoryRegions() reglist = get_process().GetMemoryRegions()
for i in range(0, reglist.GetSize()): for i in range(0, reglist.GetSize()):
module = get_target().GetModuleAtIndex(i) module = get_target().GetModuleAtIndex(i)
info = lldb.SBMemoryRegionInfo(); info = lldb.SBMemoryRegionInfo()
success = reglist.GetMemoryRegionAtIndex(i, info); success = reglist.GetMemoryRegionAtIndex(i, info)
if success: if success:
r = self.region_from_sbmemreg(info) r = self.region_from_sbmemreg(info)
regions.append(r) regions.append(r)
@ -177,28 +181,39 @@ def _choose_breakpoint_location_info_reader():
BREAKPOINT_LOCATION_INFO_READER = _choose_breakpoint_location_info_reader() BREAKPOINT_LOCATION_INFO_READER = _choose_breakpoint_location_info_reader()
def get_debugger(): def get_debugger():
return lldb.SBDebugger.FindDebuggerWithID(1) return lldb.SBDebugger.FindDebuggerWithID(1)
def get_target(): def get_target():
return get_debugger().GetTargetAtIndex(0) return get_debugger().GetTargetAtIndex(0)
def get_process(): def get_process():
return get_target().GetProcess() return get_target().GetProcess()
def selected_thread(): def selected_thread():
return get_process().GetSelectedThread() return get_process().GetSelectedThread()
def selected_frame(): def selected_frame():
return selected_thread().GetSelectedFrame() return selected_thread().GetSelectedFrame()
def parse_and_eval(expr, signed=False): def parse_and_eval(expr, signed=False):
if signed is True: if signed is True:
return get_target().EvaluateExpression(expr).GetValueAsSigned() return get_eval(expr).GetValueAsSigned()
return get_target().EvaluateExpression(expr).GetValueAsUnsigned() return get_eval(expr).GetValueAsUnsigned()
def get_eval(expr): def get_eval(expr):
return get_target().EvaluateExpression(expr) eval = get_target().EvaluateExpression(expr)
if eval.GetError().Fail():
raise ValueError(eval.GetError().GetCString())
return eval
def get_description(object, level=None): def get_description(object, level=None):
stream = lldb.SBStream() stream = lldb.SBStream()
@ -208,8 +223,10 @@ def get_description(object, level=None):
object.GetDescription(stream, level) object.GetDescription(stream, level)
return escape_ansi(stream.GetData()) return escape_ansi(stream.GetData())
conv_map = {} conv_map = {}
def get_convenience_variable(id): def get_convenience_variable(id):
#val = get_target().GetEnvironment().Get(id) #val = get_target().GetEnvironment().Get(id)
if id not in conv_map: if id not in conv_map:
@ -219,18 +236,20 @@ def get_convenience_variable(id):
return "auto" return "auto"
return val return val
def set_convenience_variable(id, value): def set_convenience_variable(id, value):
#env = get_target().GetEnvironment() #env = get_target().GetEnvironment()
#return env.Set(id, value, True) # return env.Set(id, value, True)
conv_map[id] = value conv_map[id] = value
def escape_ansi(line): def escape_ansi(line):
ansi_escape =re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
return ansi_escape.sub('', line) return ansi_escape.sub('', line)
def debracket(init): def debracket(init):
val = init val = init
val = val.replace("[","(") val = val.replace("[", "(")
val = val.replace("]",")") val = val.replace("]", ")")
return val return val

View file

@ -37,6 +37,13 @@ public interface TraceRmiAcceptor {
*/ */
TraceRmiConnection accept() throws IOException, CancelledException; TraceRmiConnection accept() throws IOException, CancelledException;
/**
* Check if the acceptor is actually still accepting.
*
* @return true if not accepting anymore
*/
boolean isClosed();
/** /**
* Get the address (and port) where the acceptor is listening * Get the address (and port) where the acceptor is listening
* *

View file

@ -57,16 +57,30 @@ public interface TraceRmiLaunchOffer {
* @param exception optional error, if failed * @param exception optional error, if failed
*/ */
public record LaunchResult(Program program, Map<String, TerminalSession> sessions, public record LaunchResult(Program program, Map<String, TerminalSession> sessions,
TraceRmiConnection connection, Trace trace, Throwable exception) TraceRmiAcceptor acceptor, TraceRmiConnection connection, Trace trace,
implements AutoCloseable { Throwable exception) implements AutoCloseable {
public LaunchResult(Program program, Map<String, TerminalSession> sessions,
TraceRmiAcceptor acceptor, TraceRmiConnection connection, Trace trace,
Throwable exception) {
this.program = program;
this.sessions = sessions;
this.acceptor = acceptor == null || acceptor.isClosed() ? null : acceptor;
this.connection = connection;
this.trace = trace;
this.exception = exception;
}
@Override @Override
public void close() throws Exception { public void close() throws Exception {
for (TerminalSession s : sessions.values()) {
s.close();
}
if (connection != null) { if (connection != null) {
connection.close(); connection.close();
} }
if (acceptor != null) {
acceptor.cancel();
}
for (TerminalSession s : sessions.values()) {
s.close();
}
} }
} }

View file

@ -27,7 +27,7 @@ public class RunBashInTerminalScript extends TerminalGhidraScript {
Map<String, String> env = new HashMap<>(System.getenv()); Map<String, String> env = new HashMap<>(System.getenv());
env.put("TERM", "xterm-256color"); env.put("TERM", "xterm-256color");
PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env); PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env);
displayInTerminal(pty.getParent(), () -> { displayInTerminal(pty, () -> {
try { try {
session.waitExited(); session.waitExited();
} }

View file

@ -37,14 +37,16 @@ public class TerminalGhidraScript extends GhidraScript {
return state.getTool().getService(TerminalService.class); return state.getTool().getService(TerminalService.class);
} }
protected void displayInTerminal(PtyParent parent, Runnable waiter) throws PluginException { protected void displayInTerminal(Pty pty, Runnable waiter) throws PluginException {
TerminalService terminalService = ensureTerminalService(); TerminalService terminalService = ensureTerminalService();
PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) { parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() { term.addTerminalListener(new TerminalListener() {
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
}); });
waiter.run(); waiter.run();
@ -55,7 +57,7 @@ public class TerminalGhidraScript extends GhidraScript {
Map<String, String> env = new HashMap<>(System.getenv()); Map<String, String> env = new HashMap<>(System.getenv());
env.put("TERM", "xterm-256color"); env.put("TERM", "xterm-256color");
pty.getChild().nullSession(); pty.getChild().nullSession();
displayInTerminal(pty.getParent(), () -> { displayInTerminal(pty, () -> {
while (true) { while (true) {
try { try {
Thread.sleep(100000); Thread.sleep(100000);

View file

@ -441,6 +441,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
Pty pty = factory.openpty(); Pty pty = factory.openpty();
PtyParent parent = pty.getParent(); PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"), Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream()); parent.getInputStream(), parent.getOutputStream());
terminal.setSubTitle(ShellUtils.generateLine(commandLine)); terminal.setSubTitle(ShellUtils.generateLine(commandLine));
@ -448,7 +449,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
try { try {
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
catch (Exception e) { catch (Exception e) {
Msg.error(this, "Could not resize pty: " + e); Msg.error(this, "Could not resize pty: " + e);
@ -490,12 +491,13 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
Pty pty = factory.openpty(); Pty pty = factory.openpty();
PtyParent parent = pty.getParent(); PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"), Terminal terminal = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream()); parent.getInputStream(), parent.getOutputStream());
TerminalListener resizeListener = new TerminalListener() { TerminalListener resizeListener = new TerminalListener() {
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
}; };
terminal.addTerminalListener(resizeListener); terminal.addTerminalListener(resizeListener);
@ -549,7 +551,7 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
if (lastExc == null) { if (lastExc == null) {
lastExc = new CancelledException(); lastExc = new CancelledException();
} }
return new LaunchResult(program, sessions, connection, trace, lastExc); return new LaunchResult(program, sessions, acceptor, connection, trace, lastExc);
} }
acceptor = null; acceptor = null;
sessions.clear(); sessions.clear();
@ -598,10 +600,16 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
} }
} }
catch (Exception e) { catch (Exception e) {
DebuggerConsoleService consoleService =
tool.getService(DebuggerConsoleService.class);
if (consoleService != null) {
consoleService.log(DebuggerResources.ICON_LOG_ERROR,
"Launch %s Failed".formatted(getTitle()), e);
}
lastExc = e; lastExc = e;
prompt = mode != PromptMode.NEVER; prompt = mode != PromptMode.NEVER;
LaunchResult result = LaunchResult result =
new LaunchResult(program, sessions, connection, trace, lastExc); new LaunchResult(program, sessions, acceptor, connection, trace, lastExc);
if (prompt) { if (prompt) {
switch (promptError(result)) { switch (promptError(result)) {
case KEEP: case KEEP:
@ -621,13 +629,13 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
catch (Exception e1) { catch (Exception e1) {
Msg.error(this, "Could not close", e1); Msg.error(this, "Could not close", e1);
} }
return new LaunchResult(program, Map.of(), null, null, lastExc); return new LaunchResult(program, Map.of(), null, null, null, lastExc);
} }
continue; continue;
} }
return result; return result;
} }
return new LaunchResult(program, sessions, connection, trace, null); return new LaunchResult(program, sessions, null, connection, trace, null);
} }
} }
@ -702,21 +710,25 @@ public abstract class AbstractTraceRmiLaunchOffer implements TraceRmiLaunchOffer
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (Entry<String, TerminalSession> ent : result.sessions().entrySet()) { for (Entry<String, TerminalSession> ent : result.sessions().entrySet()) {
TerminalSession session = ent.getValue(); TerminalSession session = ent.getValue();
sb.append("<li>Terminal: " + HTMLUtilities.escapeHTML(ent.getKey()) + " &rarr; <tt>" + sb.append("<li>Terminal: %s &rarr; <tt>%s</tt>".formatted(
HTMLUtilities.escapeHTML(session.description()) + "</tt>"); HTMLUtilities.escapeHTML(ent.getKey()),
HTMLUtilities.escapeHTML(session.description())));
if (session.isTerminated()) { if (session.isTerminated()) {
sb.append(" (Terminated)"); sb.append(" (Terminated)");
} }
sb.append("</li>\n"); sb.append("</li>\n");
} }
if (result.acceptor() != null) {
sb.append("<li>Acceptor: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.acceptor().getAddress().toString())));
}
if (result.connection() != null) { if (result.connection() != null) {
sb.append("<li>Connection: <tt>" + sb.append("<li>Connection: <tt>%s</tt></li>\n".formatted(
HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString()) + HTMLUtilities.escapeHTML(result.connection().getRemoteAddress().toString())));
"</tt></li>\n");
} }
if (result.trace() != null) { if (result.trace() != null) {
sb.append( sb.append("<li>Trace: %s</li>\n".formatted(
"<li>Trace: " + HTMLUtilities.escapeHTML(result.trace().getName()) + "</li>\n"); HTMLUtilities.escapeHTML(result.trace().getName())));
} }
return sb.toString(); return sb.toString();
} }

View file

@ -62,6 +62,11 @@ public class DefaultTraceRmiAcceptor extends AbstractTraceRmiListener implements
} }
} }
@Override
public boolean isClosed() {
return socket.isClosed();
}
@Override @Override
public void close() { public void close() {
plugin.removeAcceptor(this); plugin.removeAcceptor(this);

View file

@ -310,7 +310,11 @@ public class DebuggerMethodInvocationDialog extends DialogComponentProvider
PropertyEditor editor = getEditor(param); PropertyEditor editor = getEditor(param);
Object val = computeMemorizedValue(param); Object val = computeMemorizedValue(param);
editor.setValue(val); if (val == null) {
editor.setValue("");
} else {
editor.setValue(val);
}
editor.addPropertyChangeListener(this); editor.addPropertyChangeListener(this);
pairPanel.add(MiscellaneousUtils.getEditorComponent(editor)); pairPanel.add(MiscellaneousUtils.getEditorComponent(editor));
paramEditors.put(param, editor); paramEditors.put(param, editor);

View file

@ -28,8 +28,8 @@ dependencies {
api project(':SoftwareModeling') api project(':SoftwareModeling')
api project(':ProposedUtils') api project(':ProposedUtils')
api "net.java.dev.jna:jna:5.4.0" api "net.java.dev.jna:jna:5.14.0"
api "net.java.dev.jna:jna-platform:5.4.0" api "net.java.dev.jna:jna-platform:5.14.0"
testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts') testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts')
} }

View file

@ -101,4 +101,12 @@ public interface PtyChild extends PtyEndpoint {
default String nullSession(TermMode... mode) throws IOException { default String nullSession(TermMode... mode) throws IOException {
return nullSession(List.of(mode)); return nullSession(List.of(mode));
} }
/**
* Resize the terminal window to the given width and height, in characters
*
* @param cols the width in characters
* @param rows the height in characters
*/
void setWindowSize(short cols, short rows);
} }

View file

@ -19,11 +19,4 @@ package ghidra.pty;
* The parent (UNIX "master") end of a pseudo-terminal * The parent (UNIX "master") end of a pseudo-terminal
*/ */
public interface PtyParent extends PtyEndpoint { public interface PtyParent extends PtyEndpoint {
/**
* Resize the terminal window to the given width and height, in characters
*
* @param cols the width in characters
* @param rows the height in characters
*/
void setWindowSize(short cols, short rows);
} }

View file

@ -15,6 +15,9 @@
*/ */
package ghidra.pty; package ghidra.pty;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/** /**
* A session led by the child pty * A session led by the child pty
* *
@ -31,6 +34,8 @@ public interface PtySession {
*/ */
int waitExited() throws InterruptedException; int waitExited() throws InterruptedException;
int waitExited(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException;
/** /**
* Take the greatest efforts to terminate the session (leader and descendants) * Take the greatest efforts to terminate the session (leader and descendants)
* *

View file

@ -15,20 +15,24 @@
*/ */
package ghidra.pty.linux; package ghidra.pty.linux;
import ghidra.pty.PtyParent; import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.linux.PosixC.Winsize; import ghidra.pty.unix.UnixPtySessionLeader;
public class LinuxPtyParent extends LinuxPtyEndpoint implements PtyParent { public enum LinuxIoctls implements Ioctls {
LinuxPtyParent(int fd) { INSTANCE;
super(fd);
@Override
public Class<? extends UnixPtySessionLeader> leaderClass() {
return LinuxPtySessionLeader.class;
} }
@Override @Override
public void setWindowSize(short cols, short rows) { public long TIOCSCTTY() {
Winsize.ByReference ws = new Winsize.ByReference(); return 0x540eL;
ws.ws_col = cols; }
ws.ws_row = rows;
ws.write(); @Override
PosixC.INSTANCE.ioctl(fd, Winsize.TIOCSWINSZ, ws.getPointer()); public long TIOCSWINSZ() {
return 0x5414L;
} }
} }

View file

@ -19,15 +19,16 @@ import java.io.IOException;
import ghidra.pty.Pty; import ghidra.pty.Pty;
import ghidra.pty.PtyFactory; import ghidra.pty.PtyFactory;
import ghidra.pty.unix.UnixPty;
public enum LinuxPtyFactory implements PtyFactory { public enum LinuxPtyFactory implements PtyFactory {
INSTANCE; INSTANCE;
@Override @Override
public Pty openpty(short cols, short rows) throws IOException { public Pty openpty(short cols, short rows) throws IOException {
LinuxPty pty = LinuxPty.openpty(); UnixPty pty = UnixPty.openpty(LinuxIoctls.INSTANCE);
if (cols != 0 && rows != 0) { if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows); pty.getChild().setWindowSize(cols, rows);
} }
return pty; return pty;
} }

View file

@ -15,11 +15,10 @@
*/ */
package ghidra.pty.linux; package ghidra.pty.linux;
import java.util.List; import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.unix.UnixPtySessionLeader;
public class LinuxPtySessionLeader { public class LinuxPtySessionLeader extends UnixPtySessionLeader {
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
private static final int O_RDWR = 2; // TODO: Find this in libs
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
LinuxPtySessionLeader leader = new LinuxPtySessionLeader(); LinuxPtySessionLeader leader = new LinuxPtySessionLeader();
@ -27,61 +26,8 @@ public class LinuxPtySessionLeader {
leader.run(); leader.run();
} }
protected String ptyPath; @Override
protected List<String> subArgs; protected Ioctls ioctls() {
return LinuxIoctls.INSTANCE;
protected void parseArgs(String[] args) {
ptyPath = args[0];
subArgs = List.of(args).subList(1, args.length);
}
protected void run() throws Exception {
/** This tells Linux to make this process the leader of a new session. */
LIB_POSIX.setsid();
/**
* Open the TTY. On Linux, the first TTY opened since becoming a session leader becomes the
* session's controlling TTY. Other platforms, e.g., BSD may require an explicit IOCTL.
*/
int bk = -1;
try {
int fd = LIB_POSIX.open(ptyPath, O_RDWR, 0);
/** Copy stderr to a backup descriptor, in case something goes wrong. */
int bkt = fd + 1;
LIB_POSIX.dup2(2, bkt);
bk = bkt;
/**
* Copy the TTY fd over all standard streams. This effectively redirects the leader's
* standard streams to the TTY.
*/
LIB_POSIX.dup2(fd, 0);
LIB_POSIX.dup2(fd, 1);
LIB_POSIX.dup2(fd, 2);
/**
* At this point, we are the session leader and the named TTY is the controlling PTY.
* Now, exec the specified image with arguments as the session leader. Recall, this
* replaces the image of this process.
*/
LIB_POSIX.execv(subArgs.get(0), subArgs.toArray(new String[0]));
}
catch (Throwable t) {
// Print to both redirected and to inherited stderr
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
if (bk != -1) {
try {
int bkt = bk;
LIB_POSIX.dup2(bkt, 2);
}
catch (Throwable t2) {
// Catastrophic
System.exit(-1);
}
}
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
System.exit(127);
}
} }
} }

View file

@ -15,6 +15,9 @@
*/ */
package ghidra.pty.local; package ghidra.pty.local;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import ghidra.pty.PtySession; import ghidra.pty.PtySession;
import ghidra.util.Msg; import ghidra.util.Msg;
@ -36,6 +39,15 @@ public class LocalProcessPtySession implements PtySession {
return process.waitFor(); return process.waitFor();
} }
@Override
public int waitExited(long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException {
if (!process.waitFor(timeout, unit)) {
throw new TimeoutException();
}
return process.exitValue();
}
@Override @Override
public void destroyForcibly() { public void destroyForcibly() {
process.destroyForcibly(); process.destroyForcibly();

View file

@ -15,6 +15,9 @@
*/ */
package ghidra.pty.local; package ghidra.pty.local;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.sun.jna.LastErrorException; import com.sun.jna.LastErrorException;
import com.sun.jna.platform.win32.Kernel32; import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase; import com.sun.jna.platform.win32.WinBase;
@ -42,10 +45,9 @@ public class LocalWindowsNativeProcessPtySession implements PtySession {
Msg.info(this, "local Windows Pty session. PID = " + pid); Msg.info(this, "local Windows Pty session. PID = " + pid);
} }
@Override protected int doWaitExited(int millis) throws TimeoutException {
public int waitExited() throws InterruptedException {
while (true) { while (true) {
switch (Kernel32.INSTANCE.WaitForSingleObject(processHandle.getNative(), -1)) { switch (Kernel32.INSTANCE.WaitForSingleObject(processHandle.getNative(), millis)) {
case Kernel32.WAIT_OBJECT_0: case Kernel32.WAIT_OBJECT_0:
case Kernel32.WAIT_ABANDONED: case Kernel32.WAIT_ABANDONED:
IntByReference lpExitCode = new IntByReference(); IntByReference lpExitCode = new IntByReference();
@ -54,13 +56,32 @@ public class LocalWindowsNativeProcessPtySession implements PtySession {
return lpExitCode.getValue(); return lpExitCode.getValue();
} }
case Kernel32.WAIT_TIMEOUT: case Kernel32.WAIT_TIMEOUT:
throw new AssertionError(); throw new TimeoutException();
case Kernel32.WAIT_FAILED: case Kernel32.WAIT_FAILED:
throw new LastErrorException(Kernel32.INSTANCE.GetLastError()); throw new LastErrorException(Kernel32.INSTANCE.GetLastError());
} }
} }
} }
@Override
public int waitExited() {
try {
return doWaitExited(-1);
}
catch (TimeoutException e) {
throw new AssertionError(e);
}
}
@Override
public int waitExited(long timeout, TimeUnit unit) throws TimeoutException {
long millis = TimeUnit.MILLISECONDS.convert(timeout, unit);
if (millis > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Too long a timeout");
}
return doWaitExited((int) millis);
}
@Override @Override
public void destroyForcibly() { public void destroyForcibly() {
if (!Kernel32.INSTANCE.TerminateProcess(processHandle.getNative(), 1)) { if (!Kernel32.INSTANCE.TerminateProcess(processHandle.getNative(), 1)) {

View file

@ -0,0 +1,38 @@
/* ###
* 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 ghidra.pty.macos;
import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.unix.UnixPtySessionLeader;
public enum MacosIoctls implements Ioctls {
INSTANCE;
@Override
public Class<? extends UnixPtySessionLeader> leaderClass() {
return MacosPtySessionLeader.class;
}
@Override
public long TIOCSCTTY() {
return 0x20007461L;
}
@Override
public long TIOCSWINSZ() {
return 0x80087467L;
}
}

View file

@ -19,22 +19,22 @@ import java.io.IOException;
import ghidra.pty.Pty; import ghidra.pty.Pty;
import ghidra.pty.PtyFactory; import ghidra.pty.PtyFactory;
import ghidra.pty.linux.LinuxPty; import ghidra.pty.unix.UnixPty;
public enum MacosPtyFactory implements PtyFactory { public enum MacosPtyFactory implements PtyFactory {
INSTANCE; INSTANCE;
@Override @Override
public Pty openpty(short cols, short rows) throws IOException { public Pty openpty(short cols, short rows) throws IOException {
LinuxPty pty = LinuxPty.openpty(); UnixPty pty = UnixPty.openpty(MacosIoctls.INSTANCE);
if (cols != 0 && rows != 0) { if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows); pty.getChild().setWindowSize(cols, rows);
} }
return pty; return pty;
} }
@Override @Override
public String getDescription() { public String getDescription() {
return "local (MacOS)"; return "local (macOS)";
} }
} }

View file

@ -0,0 +1,33 @@
/* ###
* 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 ghidra.pty.macos;
import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.pty.unix.UnixPtySessionLeader;
public class MacosPtySessionLeader extends UnixPtySessionLeader {
public static void main(String[] args) throws Exception {
MacosPtySessionLeader leader = new MacosPtySessionLeader();
leader.parseArgs(args);
leader.run();
}
@Override
protected Ioctls ioctls() {
return MacosIoctls.INSTANCE;
}
}

View file

@ -234,7 +234,7 @@ public class GhidraSshPtyFactory implements PtyFactory {
try { try {
SshPty pty = new SshPty((ChannelExec) session.openChannel("exec")); SshPty pty = new SshPty((ChannelExec) session.openChannel("exec"));
if (cols != 0 && rows != 0) { if (cols != 0 && rows != 0) {
pty.getParent().setWindowSize(cols, rows); pty.getChild().setWindowSize(cols, rows);
} }
return pty; return pty;
} }

View file

@ -118,4 +118,8 @@ public class SshPtyChild extends SshPtyEndpoint implements PtyChild {
public OutputStream getOutputStream() { public OutputStream getOutputStream() {
throw new UnsupportedOperationException("The child is not local"); throw new UnsupportedOperationException("The child is not local");
} }
@Override
public void setWindowSize(short cols, short rows) {
channel.setPtySize(Short.toUnsignedInt(cols), Short.toUnsignedInt(rows), 0, 0);
}
} }

View file

@ -26,9 +26,4 @@ public class SshPtyParent extends SshPtyEndpoint implements PtyParent {
public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) { public SshPtyParent(ChannelExec channel, OutputStream outputStream, InputStream inputStream) {
super(channel, outputStream, inputStream); super(channel, outputStream, inputStream);
} }
@Override
public void setWindowSize(short cols, short rows) {
channel.setPtySize(Short.toUnsignedInt(cols), Short.toUnsignedInt(rows), 0, 0);
}
} }

View file

@ -15,6 +15,9 @@
*/ */
package ghidra.pty.ssh; package ghidra.pty.ssh;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.jcraft.jsch.*; import com.jcraft.jsch.*;
import ghidra.pty.PtySession; import ghidra.pty.PtySession;
@ -27,16 +30,37 @@ public class SshPtySession implements PtySession {
this.channel = channel; this.channel = channel;
} }
@Override protected int doWaitExited(Long millis) throws InterruptedException, TimeoutException {
public int waitExited() throws InterruptedException { long startMs = System.currentTimeMillis();
// Doesn't look like there's a clever way to wait. So do the spin sleep :( // Doesn't look like there's a clever way to wait. So do the spin sleep :(
while (!channel.isEOF()) { while (!channel.isEOF()) {
Thread.sleep(1000); Thread.sleep(100);
long elapsed = System.currentTimeMillis() - startMs;
if (millis != null && elapsed > millis) {
throw new TimeoutException();
}
} }
// NB. May not be available // NB. May not be available
return channel.getExitStatus(); return channel.getExitStatus();
} }
@Override
public int waitExited() throws InterruptedException {
try {
return doWaitExited(null);
}
catch (TimeoutException e) {
throw new AssertionError(e);
}
}
@Override
public int waitExited(long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException {
long millis = TimeUnit.MILLISECONDS.convert(timeout, unit);
return doWaitExited(millis);
}
@Override @Override
public void destroyForcibly() { public void destroyForcibly() {
channel.disconnect(); channel.disconnect();

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import com.sun.jna.LastErrorException; import com.sun.jna.LastErrorException;
import com.sun.jna.Native; import com.sun.jna.Native;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import com.sun.jna.*; import com.sun.jna.*;
import com.sun.jna.Structure.FieldOrder; import com.sun.jna.Structure.FieldOrder;
@ -26,10 +26,16 @@ import com.sun.jna.Structure.FieldOrder;
*/ */
public interface PosixC extends Library { public interface PosixC extends Library {
interface Ioctls {
Class<? extends UnixPtySessionLeader> leaderClass();
long TIOCSCTTY();
long TIOCSWINSZ();
}
@FieldOrder({ "ws_row", "ws_col", "ws_xpixel", "ws_ypixel" }) @FieldOrder({ "ws_row", "ws_col", "ws_xpixel", "ws_ypixel" })
class Winsize extends Structure { class Winsize extends Structure {
public static final int TIOCSWINSZ = 0x5414; // This may actually be Linux-specific
public short ws_row; public short ws_row;
public short ws_col; public short ws_col;
public short ws_xpixel; // Unused public short ws_xpixel; // Unused
@ -39,11 +45,21 @@ public interface PosixC extends Library {
} }
} }
@FieldOrder({ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed", @FieldOrder({ "steal" })
"c_ospeed" }) class ControllingTty extends Structure {
class Termios extends Structure { public int steal;
public static final int TCSANOW = 0;
public static class ByReference extends ControllingTty implements Structure.ByReference {
}
}
@FieldOrder(
{ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed",
"c_ospeed" }
)
class Termios extends Structure {
// TCSANOW and ECHO are the same on Linux and macOS
public static final int TCSANOW = 0;
public static final int ECHO = 0000010; // Octal public static final int ECHO = 0000010; // Octal
public int c_iflag; public int c_iflag;
@ -110,7 +126,7 @@ public interface PosixC extends Library {
} }
@Override @Override
public int ioctl(int fd, int cmd, Pointer... args) { public int ioctl(int fd, long cmd, Pointer... args) {
return Err.checkLt0(BARE.ioctl(fd, cmd, args)); return Err.checkLt0(BARE.ioctl(fd, cmd, args));
} }
@ -141,7 +157,7 @@ public interface PosixC extends Library {
int execv(String path, String[] argv); int execv(String path, String[] argv);
int ioctl(int fd, int cmd, Pointer... args); int ioctl(int fd, long cmd, Pointer... args);
int tcgetattr(int fd, Termios.ByReference termios_p); int tcgetattr(int fd, Termios.ByReference termios_p);

View file

@ -13,53 +13,53 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.IOException; import java.io.IOException;
import com.sun.jna.*; import com.sun.jna.Memory;
import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.IntByReference;
import ghidra.pty.Pty; import ghidra.pty.Pty;
import ghidra.pty.unix.PosixC.Ioctls;
import ghidra.util.Msg; import ghidra.util.Msg;
public class LinuxPty implements Pty { public class UnixPty implements Pty {
static final PosixC LIB_POSIX = PosixC.INSTANCE; static final PosixC LIB_POSIX = PosixC.INSTANCE;
private final int aparent; private final int aparent;
private final int achild; private final int achild;
//private final String name;
private boolean closed = false; private boolean closed = false;
private final LinuxPtyParent parent; private final UnixPtyParent parent;
private final LinuxPtyChild child; private final UnixPtyChild child;
public static LinuxPty openpty() throws IOException { public static UnixPty openpty(Ioctls ioctls) throws IOException {
// TODO: Support termp and winp? // TODO: Support termp and winp?
IntByReference p = new IntByReference(); IntByReference p = new IntByReference();
IntByReference c = new IntByReference(); IntByReference c = new IntByReference();
Memory n = new Memory(1024); Memory n = new Memory(1024);
Util.INSTANCE.openpty(p, c, n, null, null); Util.INSTANCE.openpty(p, c, n, null, null);
return new LinuxPty(p.getValue(), c.getValue(), n.getString(0)); return new UnixPty(ioctls, p.getValue(), c.getValue(), n.getString(0));
} }
LinuxPty(int aparent, int achild, String name) { UnixPty(Ioctls ioctls, int aparent, int achild, String name) {
Msg.debug(this, "New Pty: " + name + " at (" + aparent + "," + achild + ")"); Msg.debug(this, "New Pty: " + name + " at (" + aparent + "," + achild + ")");
this.aparent = aparent; this.aparent = aparent;
this.achild = achild; this.achild = achild;
this.parent = new LinuxPtyParent(aparent); this.parent = new UnixPtyParent(ioctls, aparent);
this.child = new LinuxPtyChild(achild, name); this.child = new UnixPtyChild(ioctls, achild, name);
} }
@Override @Override
public LinuxPtyParent getParent() { public UnixPtyParent getParent() {
return parent; return parent;
} }
@Override @Override
public LinuxPtyChild getChild() { public UnixPtyChild getChild() {
return child; return child;
} }

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -21,17 +21,17 @@ import java.util.*;
import ghidra.pty.PtyChild; import ghidra.pty.PtyChild;
import ghidra.pty.PtySession; import ghidra.pty.PtySession;
import ghidra.pty.linux.PosixC.Termios;
import ghidra.pty.local.LocalProcessPtySession; import ghidra.pty.local.LocalProcessPtySession;
import ghidra.pty.unix.PosixC.*;
import ghidra.util.Msg; import ghidra.util.Msg;
public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild { public class UnixPtyChild extends UnixPtyEndpoint implements PtyChild {
static final PosixC LIB_POSIX = PosixC.INSTANCE; static final PosixC LIB_POSIX = PosixC.INSTANCE;
private final String name; private final String name;
LinuxPtyChild(int fd, String name) { UnixPtyChild(Ioctls ioctls, int fd, String name) {
super(fd); super(ioctls, fd);
this.name = name; this.name = name;
} }
@ -70,7 +70,7 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
argsList.add(javaCommand); argsList.add(javaCommand);
argsList.add("-cp"); argsList.add("-cp");
argsList.add(System.getProperty("java.class.path")); argsList.add(System.getProperty("java.class.path"));
argsList.add(LinuxPtySessionLeader.class.getCanonicalName()); argsList.add(ioctls.leaderClass().getCanonicalName());
argsList.add(name); argsList.add(name);
argsList.addAll(Arrays.asList(args)); argsList.addAll(Arrays.asList(args));
@ -106,4 +106,18 @@ public class LinuxPtyChild extends LinuxPtyEndpoint implements PtyChild {
tmios.c_lflag &= ~Termios.ECHO; tmios.c_lflag &= ~Termios.ECHO;
LIB_POSIX.tcsetattr(fd, Termios.TCSANOW, tmios); LIB_POSIX.tcsetattr(fd, Termios.TCSANOW, tmios);
} }
@Override
public void setWindowSize(short cols, short rows) {
Winsize.ByReference ws = new Winsize.ByReference();
ws.ws_col = cols;
ws.ws_row = rows;
ws.write();
try {
PosixC.INSTANCE.ioctl(fd, ioctls.TIOCSWINSZ(), ws.getPointer());
}
catch (Exception e) {
Msg.error(this, "Could not set terminal window size: " + e);
}
}
} }

View file

@ -13,19 +13,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import ghidra.pty.PtyEndpoint; import ghidra.pty.PtyEndpoint;
import ghidra.pty.unix.PosixC.Ioctls;
public class LinuxPtyEndpoint implements PtyEndpoint { public class UnixPtyEndpoint implements PtyEndpoint {
protected final Ioctls ioctls;
protected final int fd; protected final int fd;
private final FdOutputStream outputStream; private final FdOutputStream outputStream;
private final FdInputStream inputStream; private final FdInputStream inputStream;
LinuxPtyEndpoint(int fd) { UnixPtyEndpoint(Ioctls ioctls, int fd) {
this.ioctls = ioctls;
this.fd = fd; this.fd = fd;
this.outputStream = new FdOutputStream(fd); this.outputStream = new FdOutputStream(fd);
this.inputStream = new FdInputStream(fd); this.inputStream = new FdInputStream(fd);

View file

@ -0,0 +1,25 @@
/* ###
* 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 ghidra.pty.unix;
import ghidra.pty.PtyParent;
import ghidra.pty.unix.PosixC.Ioctls;
public class UnixPtyParent extends UnixPtyEndpoint implements PtyParent {
UnixPtyParent(Ioctls ioctls, int fd) {
super(ioctls, fd);
}
}

View file

@ -0,0 +1,93 @@
/* ###
* 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 ghidra.pty.unix;
import java.util.List;
import ghidra.pty.unix.PosixC.ControllingTty;
import ghidra.pty.unix.PosixC.Ioctls;
public abstract class UnixPtySessionLeader {
private static final PosixC LIB_POSIX = PosixC.INSTANCE;
private static final int O_RDWR = 2; // TODO: Find this in libs
protected String ptyPath;
protected List<String> subArgs;
protected abstract Ioctls ioctls();
protected void parseArgs(String[] args) {
ptyPath = args[0];
subArgs = List.of(args).subList(1, args.length);
}
protected void run() throws Exception {
/**
* Open the TTY. On Linux, the first TTY opened since becoming a session leader becomes the
* session's controlling TTY. Other platforms, e.g., BSD may require an explicit IOCTL.
*/
int bk = -1;
try {
int fd = LIB_POSIX.open(ptyPath, O_RDWR, 0);
/** Copy stderr to a backup descriptor, in case something goes wrong. */
int bkt = fd + 1;
LIB_POSIX.dup2(2, bkt);
bk = bkt;
/**
* Copy the TTY fd over all standard streams. This effectively redirects the leader's
* standard streams to the TTY.
*/
LIB_POSIX.close(0);
LIB_POSIX.close(1);
LIB_POSIX.close(2);
LIB_POSIX.dup2(fd, 0);
LIB_POSIX.dup2(fd, 1);
LIB_POSIX.dup2(fd, 2);
LIB_POSIX.close(fd);
/** This tells Linux to make this process the leader of a new session. */
LIB_POSIX.setsid();
ControllingTty.ByReference ctty = new ControllingTty.ByReference();
ctty.steal = 0;
LIB_POSIX.ioctl(0, ioctls().TIOCSCTTY(), ctty.getPointer());
/**
* At this point, we are the session leader and the named TTY is the controlling PTY.
* Now, exec the specified image with arguments as the session leader. Recall, this
* replaces the image of this process.
*/
LIB_POSIX.execv(subArgs.get(0), subArgs.toArray(new String[0]));
}
catch (Throwable t) {
// Print to both redirected and to inherited stderr
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
if (bk != -1) {
try {
int bkt = bk;
LIB_POSIX.dup2(bkt, 2);
}
catch (Throwable t2) {
// Catastrophic
System.exit(-1);
}
}
System.err.println("Could not execute " + subArgs.get(0) + ": " + t.getMessage());
System.exit(127);
}
}
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.pty.linux; package ghidra.pty.unix;
import com.sun.jna.*; import com.sun.jna.*;
import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.IntByReference;

View file

@ -111,4 +111,9 @@ public class ConPtyChild extends ConPtyEndpoint implements PtyChild {
public String nullSession(Collection<TermMode> mode) throws IOException { public String nullSession(Collection<TermMode> mode) throws IOException {
throw new UnsupportedOperationException("ConPTY does not have a name"); throw new UnsupportedOperationException("ConPTY does not have a name");
} }
@Override
public void setWindowSize(short cols, short rows) {
pseudoConsoleHandle.resize(rows, cols);
}
} }

View file

@ -22,9 +22,4 @@ public class ConPtyParent extends ConPtyEndpoint implements PtyParent {
PseudoConsoleHandle pseudoConsoleHandle) { PseudoConsoleHandle pseudoConsoleHandle) {
super(writeHandle, readHandle, pseudoConsoleHandle); super(writeHandle, readHandle, pseudoConsoleHandle);
} }
@Override
public void setWindowSize(short cols, short rows) {
pseudoConsoleHandle.resize(rows, cols);
}
} }

View file

@ -15,201 +15,24 @@
*/ */
package ghidra.pty.linux; package ghidra.pty.linux;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue; import static org.junit.Assume.assumeTrue;
import java.io.*; import java.io.IOException;
import java.util.*;
import org.junit.Before; import org.junit.Before;
import org.junit.Test;
import ghidra.dbg.testutil.DummyProc;
import ghidra.framework.OperatingSystem; import ghidra.framework.OperatingSystem;
import ghidra.pty.AbstractPtyTest; import ghidra.pty.unix.AbstractUnixPtyTest;
import ghidra.pty.PtyChild.Echo; import ghidra.pty.unix.UnixPty;
import ghidra.pty.PtySession;
public class LinuxPtyTest extends AbstractPtyTest { public class LinuxPtyTest extends AbstractUnixPtyTest {
@Before @Before
public void checkLinux() { public void checkLinux() {
assumeTrue(OperatingSystem.LINUX == OperatingSystem.CURRENT_OPERATING_SYSTEM); assumeTrue(OperatingSystem.LINUX == OperatingSystem.CURRENT_OPERATING_SYSTEM);
} }
@Test @Override
public void testOpenClosePty() throws IOException { protected UnixPty openpty() throws IOException {
LinuxPty pty = LinuxPty.openpty(); return UnixPty.openpty(LinuxIoctls.INSTANCE);
pty.close();
}
@Test
public void testParentToChild() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testChildToParent() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testSessionBash() throws IOException, InterruptedException {
try (LinuxPty pty = LinuxPty.openpty()) {
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash") }, null);
pty.getParent().getOutputStream().write("exit\n".getBytes());
assertEquals(0, bash.waitExited());
}
}
@Test
public void testForkIntoNonExistent() throws IOException, InterruptedException {
try (LinuxPty pty = LinuxPty.openpty()) {
PtySession dies =
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
/**
* Choice of 127 is based on bash setting "exit code" to 127 for "command not found"
*/
assertEquals(127, dies.waitExited());
}
}
@Test
public void testSessionBashEchoTest() throws IOException, InterruptedException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (LinuxPty pty = LinuxPty.openpty()) {
LinuxPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
line = reader.readLine();
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
Set.of("BASH:exit 3", "exit 3").contains(line));
assertEquals(3, bash.waitExited());
}
}
@Test
public void testSessionBashInterruptCat() throws IOException, InterruptedException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (LinuxPty pty = LinuxPty.openpty()) {
LinuxPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("cat");
writer.flush();
line = reader.readLine();
assertTrue("Not 'cat' or 'BASH:cat': '" + line + "'",
Set.of("BASH:cat", "cat").contains(line));
writer.println("Hello, cat!");
writer.flush();
assertEquals("Hello, cat!", reader.readLine()); // echo back
assertEquals("Hello, cat!", reader.readLine()); // cat back
writer.write(3); // should interrupt
writer.flush();
do {
line = reader.readLine();
}
while (!"^C".equals(line));
writer.println("echo test");
writer.flush();
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
assertEquals(3, bash.waitExited());
}
}
@Test
public void testLocalEchoOn() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession();
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testLocalEchoOff() throws IOException {
try (LinuxPty pty = LinuxPty.openpty()) {
pty.getChild().nullSession(Echo.OFF);
PrintWriter writerP = new PrintWriter(pty.getParent().getOutputStream());
PrintWriter writerC = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writerP.println("Hello, World!");
writerP.flush();
writerC.println("Good bye!");
writerC.flush();
assertEquals("Good bye!", reader.readLine());
}
} }
} }

View file

@ -0,0 +1,38 @@
/* ###
* 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 ghidra.pty.macos;
import static org.junit.Assume.assumeTrue;
import java.io.IOException;
import org.junit.Before;
import ghidra.framework.OperatingSystem;
import ghidra.pty.unix.AbstractUnixPtyTest;
import ghidra.pty.unix.UnixPty;
public class MacosPtyTest extends AbstractUnixPtyTest {
@Before
public void checkLinux() {
assumeTrue(OperatingSystem.MAC_OS_X == OperatingSystem.CURRENT_OPERATING_SYSTEM);
}
@Override
protected UnixPty openpty() throws IOException {
return UnixPty.openpty(MacosIoctls.INSTANCE);
}
}

View file

@ -0,0 +1,215 @@
/* ###
* 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 ghidra.pty.unix;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.Test;
import ghidra.dbg.testutil.DummyProc;
import ghidra.pty.AbstractPtyTest;
import ghidra.pty.PtyChild.Echo;
import ghidra.pty.PtySession;
public abstract class AbstractUnixPtyTest extends AbstractPtyTest {
protected abstract UnixPty openpty() throws IOException;
@Test
public void testOpenClosePty() throws IOException {
UnixPty pty = openpty();
pty.close();
}
@Test
public void testParentToChild() throws IOException {
try (UnixPty pty = openpty()) {
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getChild().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testChildToParent() throws IOException {
try (UnixPty pty = openpty()) {
PrintWriter writer = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testSessionBash() throws IOException, InterruptedException, TimeoutException {
try (UnixPty pty = openpty()) {
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash") }, null);
pty.getParent().getOutputStream().write("exit\n".getBytes());
assertEquals(0, bash.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testForkIntoNonExistent()
throws IOException, InterruptedException, TimeoutException {
try (UnixPty pty = openpty()) {
PtySession dies =
pty.getChild().session(new String[] { "thisHadBetterNotExist" }, null);
/**
* Choice of 127 is based on bash setting "exit code" to 127 for "command not found"
*/
assertEquals(127, dies.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testSessionBashEchoTest()
throws IOException, InterruptedException, TimeoutException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (UnixPty pty = openpty()) {
UnixPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
line = reader.readLine();
assertTrue("Not 'exit 3' or 'BASH:exit 3': '" + line + "'",
Set.of("BASH:exit 3", "exit 3").contains(line));
assertEquals(3, bash.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testSessionBashInterruptCat()
throws IOException, InterruptedException, TimeoutException {
Map<String, String> env = new HashMap<>();
env.put("PS1", "BASH:");
env.put("PROMPT_COMMAND", "");
env.put("TERM", "");
try (UnixPty pty = openpty()) {
UnixPtyParent parent = pty.getParent();
PrintWriter writer = new PrintWriter(parent.getOutputStream());
BufferedReader reader = loggingReader(parent.getInputStream());
PtySession bash =
pty.getChild().session(new String[] { DummyProc.which("bash"), "--norc" }, env);
runExitCheck(3, bash);
writer.println("echo test");
writer.flush();
String line;
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("cat");
writer.flush();
line = reader.readLine();
assertTrue("Not 'cat' or 'BASH:cat': '" + line + "'",
Set.of("BASH:cat", "cat").contains(line));
writer.println("Hello, cat!");
writer.flush();
assertEquals("Hello, cat!", reader.readLine()); // echo back
assertEquals("Hello, cat!", reader.readLine()); // cat back
writer.write(3); // should interrupt
writer.flush();
do {
line = reader.readLine();
}
while (!"^C".equals(line));
writer.println("echo test");
writer.flush();
do {
line = reader.readLine();
}
while (!"test".equals(line));
writer.println("exit 3");
writer.flush();
assertTrue(Set.of("BASH:exit 3", "exit 3").contains(reader.readLine()));
assertEquals(3, bash.waitExited(2, TimeUnit.SECONDS));
}
}
@Test
public void testLocalEchoOn() throws IOException {
try (UnixPty pty = openpty()) {
pty.getChild().nullSession();
PrintWriter writer = new PrintWriter(pty.getParent().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writer.println("Hello, World!");
writer.flush();
assertEquals("Hello, World!", reader.readLine());
}
}
@Test
public void testLocalEchoOff() throws IOException {
try (UnixPty pty = openpty()) {
pty.getChild().nullSession(Echo.OFF);
PrintWriter writerP = new PrintWriter(pty.getParent().getOutputStream());
PrintWriter writerC = new PrintWriter(pty.getChild().getOutputStream());
BufferedReader reader =
new BufferedReader(new InputStreamReader(pty.getParent().getInputStream()));
writerP.println("Hello, World!");
writerP.flush();
writerC.println("Good bye!");
writerC.flush();
assertEquals("Good bye!", reader.readLine());
}
}
}

View file

@ -70,13 +70,14 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest {
PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env); PtySession session = pty.getChild().session(new String[] { "/usr/bin/bash" }, env);
PtyParent parent = pty.getParent(); PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) { parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() { term.addTerminalListener(new TerminalListener() {
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
System.err.println("resized: " + cols + "x" + rows); System.err.println("resized: " + cols + "x" + rows);
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
}); });
session.waitExited(); session.waitExited();
@ -101,13 +102,14 @@ public class TerminalProviderTest extends AbstractGhidraHeadedDebuggerTest {
pty.getChild().session(new String[] { "C:\\Windows\\system32\\cmd.exe" }, env); pty.getChild().session(new String[] { "C:\\Windows\\system32\\cmd.exe" }, env);
PtyParent parent = pty.getParent(); PtyParent parent = pty.getParent();
PtyChild child = pty.getChild();
try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"), try (Terminal term = terminalService.createWithStreams(Charset.forName("UTF-8"),
parent.getInputStream(), parent.getOutputStream())) { parent.getInputStream(), parent.getOutputStream())) {
term.addTerminalListener(new TerminalListener() { term.addTerminalListener(new TerminalListener() {
@Override @Override
public void resized(short cols, short rows) { public void resized(short cols, short rows) {
System.err.println("resized: " + cols + "x" + rows); System.err.println("resized: " + cols + "x" + rows);
parent.setWindowSize(cols, rows); child.setWindowSize(cols, rows);
} }
}); });
session.waitExited(); session.waitExited();