Merge remote-tracking branch 'origin/GP-4732_d-millar_remote_dbgeng_RB250130--SQUASHED'

This commit is contained in:
Ryan Kurtz 2025-01-30 14:37:51 -05:00
commit c693e963eb
10 changed files with 272 additions and 67 deletions

View file

@ -9,6 +9,7 @@ data/debugger-launchers/local-dbgeng-ext.bat||GHIDRA||||END|
data/debugger-launchers/local-dbgeng.bat||GHIDRA||||END| data/debugger-launchers/local-dbgeng.bat||GHIDRA||||END|
data/debugger-launchers/local-ttd.bat||GHIDRA||||END| data/debugger-launchers/local-ttd.bat||GHIDRA||||END|
data/debugger-launchers/remote-dbgeng.bat||GHIDRA||||END| data/debugger-launchers/remote-dbgeng.bat||GHIDRA||||END|
data/debugger-launchers/svrcx-dbgeng.bat||GHIDRA||||END|
src/main/py/LICENSE||GHIDRA||||END| src/main/py/LICENSE||GHIDRA||||END|
src/main/py/MANIFEST.in||GHIDRA||||END| src/main/py/MANIFEST.in||GHIDRA||||END|
src/main/py/README.md||GHIDRA||||END| src/main/py/README.md||GHIDRA||||END|

View file

@ -12,7 +12,7 @@
::@enum Connection:str Remote Local EXDI ::@enum Connection:str Remote Local EXDI
::@env OPT_PYTHON_EXE:file!="python" "Python command" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH." ::@env OPT_PYTHON_EXE:file!="python" "Python command" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH."
:: Use env instead of args, because "all args except first" is terrible to implement in batch :: Use env instead of args, because "all args except first" is terrible to implement in batch
::@env OPT_TARGET_ARGS:str="" "Arguments" "Connection-string arguments (a la .server)" ::@env OPT_KCONNECT_STRING:str="" "Arguments" "Connection-string arguments (a la .server)"
::@env OPT_TARGET_FLAGS:Connection="Remote" "Type" "Type/flags for connection (Remote/Local/EXDI)." ::@env OPT_TARGET_FLAGS:Connection="Remote" "Type" "Type/flags for connection (Remote/Local/EXDI)."
::@env OPT_USE_DBGMODEL:bool=true "Use dbgmodel" "Load and use dbgmodel.dll if it is available." ::@env OPT_USE_DBGMODEL:bool=true "Use dbgmodel" "Load and use dbgmodel.dll if it is available."
::@env WINDBG_DIR:dir="" "Path to dbgeng.dll directory" "Path containing dbgeng and associated DLLS (if not Windows Kits)." ::@env WINDBG_DIR:dir="" "Path to dbgeng.dll directory" "Path containing dbgeng and associated DLLS (if not Windows Kits)."

View file

@ -1,9 +1,8 @@
::@title dbgeng-remote ::@title dbgeng-remote
::@image-opt env:OPT_TARGET_IMG
::@desc <html><body width="300px"> ::@desc <html><body width="300px">
::@desc <h3>Launch with <tt>dbgeng</tt> remotely (in a Python interpreter)</h3> ::@desc <h3>Connect to a remote debugger (via the .server interface)</h3>
::@desc <p> ::@desc <p>
::@desc This will launch the target on a remote machine using <tt>dbgeng.dll</tt>. ::@desc This will connect to a remote machine using the .server interface.
::@desc For setup instructions, press <b>F1</b>. ::@desc For setup instructions, press <b>F1</b>.
::@desc </p> ::@desc </p>
::@desc </body></html> ::@desc </body></html>
@ -11,11 +10,7 @@
::@icon icon.debugger ::@icon icon.debugger
::@help TraceRmiLauncherServicePlugin#dbgeng_remote ::@help TraceRmiLauncherServicePlugin#dbgeng_remote
::@env OPT_PYTHON_EXE:file!="python" "Python command" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH." ::@env OPT_PYTHON_EXE:file!="python" "Python command" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH."
:: Use env instead of args, because "all args except first" is terrible to implement in batch ::@env OPT_CONNECT_STRING:str="" "Connection" "Connection-string arguments (a la .server)"
::@env OPT_TARGET_IMG:str!="" "Image" "The target binary executable image"
::@env OPT_TARGET_ARGS:str="" "Arguments" "Command-line arguments to pass to the target"
::@env OPT_CONNECT_STRING:str="" "Connection" "Connection-string arguments (a la dbgsrv args)"
::@env OPT_USE_DBGMODEL:bool=true "Use dbgmodel" "Load and use dbgmodel.dll if it is available."
::@env WINDBG_DIR:dir="" "Path to dbgeng.dll directory" "Path containing dbgeng and associated DLLS (if not Windows Kits)." ::@env WINDBG_DIR:dir="" "Path to dbgeng.dll directory" "Path containing dbgeng and associated DLLS (if not Windows Kits)."
@echo off @echo off

View file

@ -0,0 +1,23 @@
::@title dbgeng-svrcx
::@image-opt env:OPT_TARGET_IMG
::@desc <html><body width="300px">
::@desc <h3>Connect to a remote <tt>dbgeng</tt> connection server and launch the target (in a Python interpreter)</h3>
::@desc <p>
::@desc This will launch the target via a connection server on a remote machine.
::@desc For setup instructions, press <b>F1</b>.
::@desc </p>
::@desc </body></html>
::@menu-group local
::@icon icon.debugger
::@help TraceRmiLauncherServicePlugin#dbgeng_svrcx
::@env OPT_PYTHON_EXE:file!="python" "Python command" "The path to the Python 3 interpreter. Omit the full path to resolve using the system PATH."
:: Use env instead of args, because "all args except first" is terrible to implement in batch
::@env OPT_TARGET_IMG:str!="" "Image" "The target binary executable image"
::@env OPT_TARGET_ARGS:str="" "Arguments" "Command-line arguments to pass to the target"
::@env OPT_CONNECT_STRING:str="" "Connection" "Connection-string arguments (a la dbgsrv args)"
::@env OPT_USE_DBGMODEL:bool=true "Use dbgmodel" "Load and use dbgmodel.dll if it is available."
::@env WINDBG_DIR:dir="" "Path to dbgeng.dll directory" "Path containing dbgeng and associated DLLS (if not Windows Kits)."
@echo off
"%OPT_PYTHON_EXE%" -i ..\support\svrcx-dbgeng.py

View file

@ -57,7 +57,7 @@ def main():
print(f"RegMapFile: {os.getenv('EXDI_SYSTEM_REGISTERS_MAP_XML_FILE')}") print(f"RegMapFile: {os.getenv('EXDI_SYSTEM_REGISTERS_MAP_XML_FILE')}")
util.set_convenience_variable('output-radix', 16) util.set_convenience_variable('output-radix', 16)
flags = 2 flags = 2
args = os.getenv('OPT_TARGET_ARGS') args = os.getenv('OPT_KCONNECT_STRING')
cmd.ghidra_trace_attach_kernel(args, flags, start_trace=False) cmd.ghidra_trace_attach_kernel(args, flags, start_trace=False)
# TODO: HACK # TODO: HACK

View file

@ -48,22 +48,11 @@ def main():
repl = cmd.repl repl = cmd.repl
cmd.ghidra_trace_connect(os.getenv('GHIDRA_TRACE_RMI_ADDR')) cmd.ghidra_trace_connect(os.getenv('GHIDRA_TRACE_RMI_ADDR'))
args = os.getenv('OPT_TARGET_ARGS')
if args:
args = ' ' + args
cmd.ghidra_trace_connect_server(os.getenv('OPT_CONNECT_STRING'))
img = os.getenv('OPT_TARGET_IMG')
if img is not None and img != "":
cmd.ghidra_trace_create(img + args, start_trace=False)
# TODO: HACK os.environ['OPT_USE_DBGMODEL'] = "false"
try: cmd.ghidra_trace_start("Remote")
dbg.wait()
except KeyboardInterrupt as ki:
dbg.interrupt()
cmd.ghidra_trace_start(os.getenv('OPT_TARGET_IMG'))
cmd.ghidra_trace_sync_enable() cmd.ghidra_trace_sync_enable()
dbg.interrupt()
on_state_changed(DbgEng.DEBUG_CES_EXECUTION_STATUS, DbgEng.DEBUG_STATUS_BREAK) on_state_changed(DbgEng.DEBUG_CES_EXECUTION_STATUS, DbgEng.DEBUG_STATUS_BREAK)
cmd.repl() cmd.repl()

View file

@ -0,0 +1,73 @@
## ###
# IP: GHIDRA
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
import os
import sys
home = os.getenv('GHIDRA_HOME')
if os.path.isdir(f'{home}\\ghidra\\.git'):
sys.path.append(
f'{home}\\ghidra\\Ghidra\\Debug\\Debugger-agent-dbgeng\\build\\pypkg\\src')
sys.path.append(
f'{home}\\ghidra\\Ghidra\\Debug\\Debugger-rmi-trace\\build\\pypkg\\src')
elif os.path.isdir(f'{home}\\.git'):
sys.path.append(
f'{home}\\Ghidra\\Debug\\Debugger-agent-dbgeng\\build\\pypkg\\src')
sys.path.append(
f'{home}\\Ghidra\\Debug\\Debugger-rmi-trace\\build\\pypkg\\src')
else:
sys.path.append(
f'{home}\\Ghidra\\Debug\\Debugger-agent-dbgeng\\pypkg\\src')
sys.path.append(f'{home}\\Ghidra\\Debug\\Debugger-rmi-trace\\pypkg\\src')
def main():
# Delay these imports until sys.path is patched
from ghidradbg import commands as cmd
from pybag.dbgeng import core as DbgEng
from ghidradbg.hooks import on_state_changed
from ghidradbg.util import dbg
# So that the user can re-enter by typing repl()
global repl
repl = cmd.repl
cmd.ghidra_trace_connect(os.getenv('GHIDRA_TRACE_RMI_ADDR'))
args = os.getenv('OPT_TARGET_ARGS')
if args:
args = ' ' + args
cmd.ghidra_trace_connect_server(os.getenv('OPT_CONNECT_STRING'))
img = os.getenv('OPT_TARGET_IMG')
if img is not None and img != "":
cmd.ghidra_trace_create(img + args, start_trace=False)
# TODO: HACK
try:
dbg.wait()
except KeyboardInterrupt as ki:
dbg.interrupt()
cmd.ghidra_trace_start(os.getenv('OPT_TARGET_IMG'))
cmd.ghidra_trace_sync_enable()
on_state_changed(DbgEng.DEBUG_CES_EXECUTION_STATUS, DbgEng.DEBUG_STATUS_BREAK)
cmd.repl()
if __name__ == '__main__':
main()

View file

@ -489,6 +489,23 @@ def install_hooks():
events = util.dbg._base.events events = util.dbg._base.events
if util.is_remote():
events.engine_state(handler=on_state_changed_async)
events.debuggee_state(handler=on_debuggee_changed_async)
events.session_status(handler=on_session_status_changed_async)
events.symbol_state(handler=on_symbol_state_changed_async)
events.system_error(handler=on_system_error_async)
events.create_process(handler=on_new_process_async)
events.exit_process(handler=on_process_deleted_async)
events.create_thread(handler=on_threads_changed_async)
events.exit_thread(handler=on_threads_changed_async)
events.module_load(handler=on_modules_changed_async)
events.unload_module(handler=on_modules_changed_async)
events.breakpoint(handler=on_breakpoint_hit_async)
events.exception(handler=on_exception_async)
else:
events.engine_state(handler=on_state_changed) events.engine_state(handler=on_state_changed)
events.debuggee_state(handler=on_debuggee_changed) events.debuggee_state(handler=on_debuggee_changed)
events.session_status(handler=on_session_status_changed) events.session_status(handler=on_session_status_changed)
@ -527,3 +544,59 @@ def disable_current_process():
if proc in PROC_STATE: if proc in PROC_STATE:
# Silently ignore already disabled # Silently ignore already disabled
del PROC_STATE[proc] del PROC_STATE[proc]
@log_errors
def on_state_changed_async(*args):
util.dbg.run_async(on_state_changed, *args)
@log_errors
def on_debuggee_changed_async(*args):
util.dbg.run_async(on_debuggee_changed, *args)
@log_errors
def on_session_status_changed_async(*args):
util.dbg.run_async(on_session_status_changed, *args)
@log_errors
def on_symbol_state_changed_async(*args):
util.dbg.run_async(on_symbol_state_changed, *args)
@log_errors
def on_system_error_async(*args):
util.dbg.run_async(on_system_error, *args)
@log_errors
def on_new_process_async(*args):
util.dbg.run_async(on_new_process, *args)
@log_errors
def on_process_deleted_async(*args):
util.dbg.run_async(on_process_deleted, *args)
@log_errors
def on_threads_changed_async(*args):
util.dbg.run_async(on_threads_changed, *args)
@log_errors
def on_modules_changed_async(*args):
util.dbg.run_async(on_modules_changed, *args)
@log_errors
def on_breakpoint_hit_async(*args):
util.dbg.run_async(on_breakpoint_hit, *args)
@log_errors
def on_exception_async(*args):
util.dbg.run_async(on_exception, *args)

View file

@ -29,12 +29,13 @@ import traceback
from comtypes import CoClass, GUID from comtypes import CoClass, GUID
import comtypes import comtypes
from comtypes.gen import DbgMod from comtypes.gen import DbgMod
from comtypes.hresult import S_OK from comtypes.hresult import S_OK, S_FALSE
from pybag import pydbg, userdbg, kerneldbg, crashdbg from pybag import pydbg, userdbg, kerneldbg, crashdbg
from pybag.dbgeng import core as DbgEng from pybag.dbgeng import core as DbgEng
from pybag.dbgeng import exception from pybag.dbgeng import exception
from pybag.dbgeng import util as DbgUtil from pybag.dbgeng import util as DbgUtil
from pybag.dbgeng.callbacks import DbgEngCallbacks from pybag.dbgeng.callbacks import DbgEngCallbacks
from pybag.dbgeng.idebugclient import DebugClient
from ghidradbg.dbgmodel.ihostdatamodelaccess import HostDataModelAccess from ghidradbg.dbgmodel.ihostdatamodelaccess import HostDataModelAccess
from _winapi import STILL_ACTIVE from _winapi import STILL_ACTIVE
@ -145,7 +146,7 @@ class DbgExecutor(object):
def _submit_no_exit(self, fn, /, *args, **kwargs): def _submit_no_exit(self, fn, /, *args, **kwargs):
f = Future() f = Future()
if self._executing: if self._executing and self._ghidra_dbg.IS_REMOTE == False:
f.set_exception(DebuggeeRunningException("Debuggee is Running")) f.set_exception(DebuggeeRunningException("Debuggee is Running"))
return f return f
w = _WorkItem(f, fn, args, kwargs) w = _WorkItem(f, fn, args, kwargs)
@ -163,6 +164,7 @@ class DbgExecutor(object):
def _state_execute(self): def _state_execute(self):
self._executing = True self._executing = True
if self._ghidra_dbg.IS_REMOTE == False:
self._clear_queue() self._clear_queue()
def _state_break(self): def _state_break(self):
@ -239,10 +241,26 @@ class GhidraDbg(object):
setattr(self, name, self.eng_thread(getattr(base, name))) setattr(self, name, self.eng_thread(getattr(base, name)))
self.IS_KERNEL = False self.IS_KERNEL = False
self.IS_EXDI = False self.IS_EXDI = False
self.IS_REMOTE = os.getenv('OPT_CONNECT_STRING') is not None
def _new_base(self): def _new_base(self):
remote = os.getenv('OPT_CONNECT_STRING')
if remote is not None:
remote_client = DbgEng.DebugConnect(remote)
debug_client = self._generate_client(remote_client)
self._protected_base = AllDbg(client=debug_client)
else:
self._protected_base = AllDbg() self._protected_base = AllDbg()
def _generate_client(self, original):
cli = POINTER(DbgEng.IDebugClient)()
cliptr = POINTER(POINTER(DbgEng.IDebugClient))(cli)
hr = original.CreateClient(cliptr)
exception.check_err(hr)
return DebugClient(client=cli)
@property @property
def _base(self): def _base(self):
if threading.current_thread() is not self._thread: if threading.current_thread() is not self._thread:
@ -655,6 +673,9 @@ def GetExitCode():
return STILL_ACTIVE return STILL_ACTIVE
exit_code = c_ulong() exit_code = c_ulong()
hr = dbg._base._client._cli.GetExitCode(byref(exit_code)) hr = dbg._base._client._cli.GetExitCode(byref(exit_code))
# DebugConnect targets return E_UNEXPECTED but the target is STILL_ACTIVE
if hr != S_OK and hr != S_FALSE:
return STILL_ACTIVE
return exit_code.value return exit_code.value
@ -915,8 +936,20 @@ def set_kernel(value):
def is_kernel(): def is_kernel():
return dbg.IS_KERNEL return dbg.IS_KERNEL
def set_exdi(value): def set_exdi(value):
dbg.IS_EXDI = value dbg.IS_EXDI = value
def is_exdi(): def is_exdi():
return dbg.IS_EXDI return dbg.IS_EXDI
def set_remote(value):
dbg.IS_REMOTE = value
def is_remote():
return dbg.IS_REMOTE

View file

@ -843,6 +843,24 @@ python3 -m pip install --no-index -f Debugger-rmi-trace\pypkg\dist -f Debugger-a
<H3><A name="dbgeng_remote"></A>dbgeng-remote</H3> <H3><A name="dbgeng_remote"></A>dbgeng-remote</H3>
<P>This launcher connects to a remote debugger that has opened a port for remote control.
</P>
<H4>Options</H4>
<UL>
<LI>
<B>Connection</B>: This is the connection string specifying the transport options for
communicating with the remote debugger. A typical example might be
'tcp:port=12345,server=192.168.0.2' for a debugger that has issued the command
<PRE>
.server tcp:port=12345
</PRE>
</LI>
</UL>
<H3><A name="dbgeng_svrcx"></A>dbgeng-svrcx</H3>
<P>This launcher extends the base dbgeng launcher adding an option for connecting through a <P>This launcher extends the base dbgeng launcher adding an option for connecting through a
remote process server.</P> remote process server.</P>
@ -852,11 +870,11 @@ python3 -m pip install --no-index -f Debugger-rmi-trace\pypkg\dist -f Debugger-a
<LI> <LI>
<B>Connection</B>: This is the connection string specifying the transport options for <B>Connection</B>: This is the connection string specifying the transport options for
communicating with the remote server. A typical example might be communicating with the remote server. A typical example might be
'tcp:port=12345,server=192.168.0.2'' for a process server launched on the machine at 'tcp:port=12345,server=192.168.0.2' for a process server launched on the machine at
192.168.0.2 using: 192.168.0.2 using:
<PRE> <PRE>
dbgsrv -t tcp:port=12345 dbgsrv -t tcp:port=12345
</PRE> </PRE>
</LI> </LI>
</UL> </UL>