mirror of
https://github.com/Gallopsled/pwntools.git
synced 2026-10-12 02:02:27 +00:00
Add support for debugging with x64dbg on Windows (#2735)
* Add support for debugging with x64dbg on Windows Add x64dbg option to `context.debugger` to attach using x64dbg in `windbg.debug()`. The path to the x96dbg.exe is looked up in the path and through the "Debug with x64dbg" shell extension you can install optionally when first launching the x96dbg.exe launcher. You can specify the path using `context.x64dbg_binary` too. * Refactor exclusion of `context.debugger="auto"` while handling `"auto"` Co-authored-by: Christian Clauss <[email protected]> * Update CHANGELOG * Remove absolute path workaround x64dbg 2026.05.27 fixed passing absolute paths to a script file using -cf. * Remove bare raise log.error raises already. * Fix linting error of winreg import on linux The module is only available on Windows. So ignore the error when linting on another platform. --------- Co-authored-by: Christian Clauss <[email protected]>
This commit is contained in:
co-authored by
Christian Clauss
parent
86cafce196
commit
6571ec7de5
@@ -141,6 +141,7 @@ The table below shows which release corresponds to each branch, and what date th
|
||||
- [#2743][2743] fix(adb): add missing import for remote to fix NameError hidden by bare except
|
||||
- [#2746][2746] elf: point people at libc_start_main_return when they look up __libc_start_main_ret
|
||||
- [#2734][2734] ROP: Add labels to reference addresses relative to your chain
|
||||
- [#2735][2735] Add support for debugging with x64dbg on Windows
|
||||
|
||||
[2675]: https://github.com/Gallopsled/pwntools/pull/2675
|
||||
[2652]: https://github.com/Gallopsled/pwntools/pull/2652
|
||||
@@ -207,6 +208,7 @@ The table below shows which release corresponds to each branch, and what date th
|
||||
[2743]: https://github.com/Gallopsled/pwntools/pull/2743
|
||||
[2746]: https://github.com/Gallopsled/pwntools/pull/2746
|
||||
[2734]: https://github.com/Gallopsled/pwntools/pull/2734
|
||||
[2735]: https://github.com/Gallopsled/pwntools/pull/2735
|
||||
|
||||
## 4.15.1
|
||||
|
||||
|
||||
@@ -359,6 +359,7 @@ class ContextType:
|
||||
'gdb_binary': "",
|
||||
'windbg_binary': "",
|
||||
'windbgx_binary': "",
|
||||
'x64dbg_binary': "",
|
||||
'debugger': "auto",
|
||||
'kernel': None,
|
||||
'local_libcdb': "/var/lib/libc-database",
|
||||
@@ -450,7 +451,7 @@ class ContextType:
|
||||
valid_signed = sorted(signednesses)
|
||||
|
||||
#: Valid values for :attr:`debugger`
|
||||
debugger_choices = ['auto', 'gdb', 'windbgx', 'windbg']
|
||||
debugger_choices = ['auto', 'gdb', 'windbgx', 'windbg', 'x64dbg']
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
@@ -1614,15 +1615,33 @@ class ContextType:
|
||||
"""
|
||||
return str(value)
|
||||
|
||||
@_validator
|
||||
def x64dbg_binary(self, value):
|
||||
r"""Path to the binary that is used when running x64dbg locally.
|
||||
|
||||
Should be set to the x96dbg.exe launcher binary to handle 32-bit and 64-bit binaries.
|
||||
|
||||
This is useful when you have multiple versions of x64dbg installed or the x64dbg binary is
|
||||
called something different.
|
||||
|
||||
If set to an empty string, pwntools will try to search for a reasonable x64dbg binary from
|
||||
the path or based on the ``"Debug with x64dbg"`` shell extension if available.
|
||||
|
||||
Default value is ``""``.
|
||||
"""
|
||||
return str(value)
|
||||
|
||||
@_validator
|
||||
def debugger(self, value):
|
||||
"""Type of debugger to use when running locally.
|
||||
|
||||
Possible values are:
|
||||
|
||||
- ``auto``: Automatically select the available debugger.
|
||||
- ``gdb``: Use GDB as the debugger.
|
||||
- ``windbg``: Use WinDbg as the debugger.
|
||||
- ``windbgx``: Use WinDbgX as the debugger.
|
||||
- ``x64dbg``: Use x64dbg as the debugger.
|
||||
|
||||
Defaults to ``windbgx`` on Windows and ``gdb`` on other platforms.
|
||||
|
||||
|
||||
+87
-22
@@ -10,10 +10,12 @@ available debuggers on the system.
|
||||
The order of preference is:
|
||||
- ``windbgx``
|
||||
- ``windbg``
|
||||
- ``x64dbg``
|
||||
|
||||
If automatic lookup fails, you can manually set :attr:`.context.debugger` to
|
||||
the debugger of your choice and provide the path to the debugger binary
|
||||
using :attr:`.context.windbgx_binary` or :attr:`.context.windbg_binary`.
|
||||
using :attr:`.context.windbgx_binary`, :attr:`.context.windbg_binary`, or
|
||||
:attr:`.context.x64dbg_binary`.
|
||||
|
||||
Useful Functions
|
||||
----------------
|
||||
@@ -72,9 +74,9 @@ Member Documentation
|
||||
import atexit
|
||||
import os
|
||||
import signal
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from pwnlib import tubes
|
||||
from pwnlib.context import LocalContext
|
||||
@@ -125,11 +127,16 @@ def debug(args, dbgscript=None, exe=None, env=None, creationflags=0, **kwargs):
|
||||
log.warn_once("Skipping debugger since context.noptrace==True")
|
||||
return tubes.process.process(args, executable=exe, env=env, creationflags=creationflags)
|
||||
|
||||
debugger, _ = binary()
|
||||
dbgscript = dbgscript or ''
|
||||
if isinstance(dbgscript, str):
|
||||
dbgscript = dbgscript.split('\n')
|
||||
# resume main thread
|
||||
dbgscript = ['~0m'] + dbgscript
|
||||
if debugger in ('windbg', 'windbgx'):
|
||||
dbgscript = ['~0m'] + dbgscript
|
||||
elif debugger == 'x64dbg':
|
||||
dbgscript = ['threadresumeall'] + dbgscript
|
||||
log.info_once('x64dbg: To resume the main thread, you need to use the "threadresumeall" command manually before version 2025.08.19.')
|
||||
creationflags |= CREATE_SUSPENDED
|
||||
io = tubes.process.process(args, executable=exe, env=env, creationflags=creationflags)
|
||||
attach(target=io, dbgscript=dbgscript, **kwargs)
|
||||
@@ -137,16 +144,19 @@ def debug(args, dbgscript=None, exe=None, env=None, creationflags=0, **kwargs):
|
||||
return io
|
||||
|
||||
def binary():
|
||||
"""binary() -> str
|
||||
"""binary() -> (str, str)
|
||||
|
||||
Returns the path to the debugger binary depending on the context.
|
||||
:attr:`.context.debugger` is used to determine which debugger to use.
|
||||
|
||||
Returns:
|
||||
str: Name of the selected debugger.
|
||||
str: Path to the appropriate ``windbg`` binary to use.
|
||||
"""
|
||||
if context.debugger == 'auto':
|
||||
for debugger in context.debugger_choices:
|
||||
if debugger == 'auto':
|
||||
continue
|
||||
with context.local(debugger=debugger, log_level='critical'):
|
||||
try:
|
||||
return binary()
|
||||
@@ -154,15 +164,15 @@ def binary():
|
||||
pass
|
||||
else:
|
||||
log.error('No debugger found. Please set context.debugger to one of: %s\n'
|
||||
'You might have to specify the path to the debugger binary with context.windbg_binary or context.windbgx_binary.',
|
||||
'You might have to specify the path to the debugger binary with context.x64dbg_binary, context.windbg_binary or context.windbgx_binary.',
|
||||
', '.join(context.debugger_choices))
|
||||
|
||||
if context.debugger == 'windbg':
|
||||
elif context.debugger == 'windbg':
|
||||
if context.windbg_binary:
|
||||
windbg = misc.which(context.windbg_binary)
|
||||
if not windbg:
|
||||
log.warn_once('Path to WinDbg binary `{}` not found'.format(context.windbg_binary))
|
||||
return windbg
|
||||
return context.debugger, windbg
|
||||
|
||||
windbg = misc.which('windbg.exe')
|
||||
if not windbg and os.environ.get('ProgramFiles(x86)'):
|
||||
@@ -176,29 +186,74 @@ def binary():
|
||||
windbg = os.path.join(os.environ.get('ProgramFiles(x86)'), 'Windows Kits', '10', 'Debuggers', arch_str, 'windbg.exe')
|
||||
if not windbg or not os.path.exists(windbg):
|
||||
log.error('windbg is not installed or in system PATH. You can set context.windbg_binary to specify the path manually.')
|
||||
return windbg
|
||||
return context.debugger, windbg
|
||||
|
||||
if context.debugger == 'windbgx':
|
||||
elif context.debugger == 'windbgx':
|
||||
if context.windbgx_binary:
|
||||
windbg = misc.which(context.windbgx_binary)
|
||||
if not windbg:
|
||||
log.warn_once('Path to WinDbgx binary `{}` not found'.format(context.windbgx_binary))
|
||||
return windbg
|
||||
return context.debugger, windbg
|
||||
|
||||
windbg = misc.which('windbgx.exe')
|
||||
if not windbg and os.environ.get('LocalAppData'):
|
||||
windbg = os.path.join(os.environ.get('LocalAppData'), 'Microsoft', 'WindowsApps', 'WinDbgX.exe')
|
||||
if not windbg or not os.path.exists(windbg):
|
||||
log.error('windbgx is not installed or in system PATH. You can set context.windbgx_binary to specify the path manually.')
|
||||
return windbg
|
||||
return context.debugger, windbg
|
||||
|
||||
elif context.debugger == 'x64dbg':
|
||||
return context.debugger, _lookup_x64dbg()
|
||||
|
||||
log.error('Invalid debugger selection: %s', context.debugger)
|
||||
|
||||
def _lookup_x64dbg():
|
||||
def _select_arch_binary(path):
|
||||
# Select the appropriate x64dbg binary based on the architecture directly
|
||||
# instead of the x96dbg.exe selector binary.
|
||||
# The x96dbg.exe proxy launches the correct one but our `wait_for_debugger`
|
||||
# function doesn't follow that and reports the proxy binary exiting early.
|
||||
base_path = Path(path).resolve().parent
|
||||
if base_path.name in ('x32', 'x64'):
|
||||
base_path = base_path.parent
|
||||
if context.arch == 'i386':
|
||||
return base_path / 'x32' / 'x32dbg.exe'
|
||||
elif context.arch == 'amd64':
|
||||
return base_path / 'x64' / 'x64dbg.exe'
|
||||
else:
|
||||
log.error('Unsupported architecture for x64dbg: %s', context.arch)
|
||||
if context.x64dbg_binary:
|
||||
x64dbg = misc.which(context.x64dbg_binary)
|
||||
if not x64dbg:
|
||||
log.warn_once('Path to x64dbg binary `{}` not found'.format(context.x64dbg_binary))
|
||||
return _select_arch_binary(x64dbg)
|
||||
|
||||
x64dbg = misc.which('x96dbg.exe')
|
||||
if x64dbg:
|
||||
return _select_arch_binary(x64dbg)
|
||||
|
||||
# See if the "Debug with x64dbg" shell extension is installed
|
||||
try:
|
||||
import winreg # pylint: disable=import-error winreg is only available on Windows
|
||||
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r'exefile\shell\Debug with x64dbg\Command') as key:
|
||||
regcmd = winreg.QueryValueEx(key, None)
|
||||
# ('"C:\\Users\\User\\Downloads\\x64dbg\\bin\\x96dbg.exe" "%1"', 2)
|
||||
if regcmd[1] != winreg.REG_EXPAND_SZ:
|
||||
log.error('x64dbg registry key is not REG_EXPAND_SZ')
|
||||
command = regcmd[0].split('"')[1]
|
||||
if not os.path.exists(command):
|
||||
log.error('x64dbg path from registry does not exist')
|
||||
return _select_arch_binary(command)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
log.error('x64dbg is not installed or in system PATH')
|
||||
|
||||
@LocalContext
|
||||
def attach(target, dbgscript=None, dbg_args=[]):
|
||||
"""attach(target, dbgscript=None, dbg_args=[]) -> int
|
||||
|
||||
Attach to a running process with WinDbg.
|
||||
Attach to a running process with WinDbg or x64dbg.
|
||||
|
||||
Arguments:
|
||||
target(int, str, process): Process to attach to.
|
||||
@@ -262,31 +317,41 @@ def attach(target, dbgscript=None, dbg_args=[]):
|
||||
if not pid:
|
||||
log.error('could not find target process')
|
||||
|
||||
cmd = [binary()]
|
||||
debugger, debugger_path = binary()
|
||||
cmd = [debugger_path]
|
||||
if dbg_args:
|
||||
cmd.extend(dbg_args)
|
||||
|
||||
|
||||
cmd.extend(['-p', str(pid)])
|
||||
|
||||
dbgscript = dbgscript or ''
|
||||
if isinstance(dbgscript, str):
|
||||
dbgscript = dbgscript.split('\n')
|
||||
with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.dbg') as tmp:
|
||||
tmp.write('\n'.join(script.strip() for script in dbgscript if script.strip()))
|
||||
tmp.flush()
|
||||
dbgscript_file = tmp.name
|
||||
|
||||
dbgscript_file = None
|
||||
if dbgscript:
|
||||
cmd.extend(['-c', '$<{}'.format(dbgscript_file)])
|
||||
with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.dbg') as tmp:
|
||||
tmp.write('\n'.join(script.strip() for script in dbgscript if script.strip()))
|
||||
tmp.flush()
|
||||
dbgscript_file = tmp.name
|
||||
|
||||
log.info("Launching a new process: %r" % cmd)
|
||||
if debugger in ('windbg', 'windbgx'):
|
||||
cmd.extend(['-c', '$<{}'.format(dbgscript_file)])
|
||||
# x64dbg got support to run commands on startup in version 2025.08.19.
|
||||
# But absolute paths were not supported until version 2026.05.27.
|
||||
elif debugger == 'x64dbg':
|
||||
cmd.extend(['-cf', dbgscript_file])
|
||||
else:
|
||||
log.warn_once('dbgscript is not supported for %s', debugger)
|
||||
|
||||
log.info("Launching a new process: %r", cmd)
|
||||
|
||||
io = subprocess.Popen(cmd)
|
||||
debugger_pid = io.pid
|
||||
|
||||
def kill():
|
||||
try:
|
||||
os.unlink(dbgscript_file)
|
||||
if dbgscript_file is not None:
|
||||
os.unlink(dbgscript_file)
|
||||
os.kill(debugger_pid, signal.SIGTERM)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user