Skip to content

Commit 6571ec7

Browse files
peace-makercclauss
andauthored
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 <cclauss@me.com> * 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 <cclauss@me.com>
1 parent 86cafce commit 6571ec7

3 files changed

Lines changed: 109 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ The table below shows which release corresponds to each branch, and what date th
141141
- [#2743][2743] fix(adb): add missing import for remote to fix NameError hidden by bare except
142142
- [#2746][2746] elf: point people at libc_start_main_return when they look up __libc_start_main_ret
143143
- [#2734][2734] ROP: Add labels to reference addresses relative to your chain
144+
- [#2735][2735] Add support for debugging with x64dbg on Windows
144145

145146
[2675]: https://github.com/Gallopsled/pwntools/pull/2675
146147
[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
207208
[2743]: https://github.com/Gallopsled/pwntools/pull/2743
208209
[2746]: https://github.com/Gallopsled/pwntools/pull/2746
209210
[2734]: https://github.com/Gallopsled/pwntools/pull/2734
211+
[2735]: https://github.com/Gallopsled/pwntools/pull/2735
210212

211213
## 4.15.1
212214

pwnlib/context/__init__.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ class ContextType:
359359
'gdb_binary': "",
360360
'windbg_binary': "",
361361
'windbgx_binary': "",
362+
'x64dbg_binary': "",
362363
'debugger': "auto",
363364
'kernel': None,
364365
'local_libcdb': "/var/lib/libc-database",
@@ -450,7 +451,7 @@ class ContextType:
450451
valid_signed = sorted(signednesses)
451452

452453
#: Valid values for :attr:`debugger`
453-
debugger_choices = ['auto', 'gdb', 'windbgx', 'windbg']
454+
debugger_choices = ['auto', 'gdb', 'windbgx', 'windbg', 'x64dbg']
454455

455456
def __init__(self, **kwargs):
456457
"""
@@ -1614,15 +1615,33 @@ def windbgx_binary(self, value):
16141615
"""
16151616
return str(value)
16161617

1618+
@_validator
1619+
def x64dbg_binary(self, value):
1620+
r"""Path to the binary that is used when running x64dbg locally.
1621+
1622+
Should be set to the x96dbg.exe launcher binary to handle 32-bit and 64-bit binaries.
1623+
1624+
This is useful when you have multiple versions of x64dbg installed or the x64dbg binary is
1625+
called something different.
1626+
1627+
If set to an empty string, pwntools will try to search for a reasonable x64dbg binary from
1628+
the path or based on the ``"Debug with x64dbg"`` shell extension if available.
1629+
1630+
Default value is ``""``.
1631+
"""
1632+
return str(value)
1633+
16171634
@_validator
16181635
def debugger(self, value):
16191636
"""Type of debugger to use when running locally.
16201637
16211638
Possible values are:
16221639
1640+
- ``auto``: Automatically select the available debugger.
16231641
- ``gdb``: Use GDB as the debugger.
16241642
- ``windbg``: Use WinDbg as the debugger.
16251643
- ``windbgx``: Use WinDbgX as the debugger.
1644+
- ``x64dbg``: Use x64dbg as the debugger.
16261645
16271646
Defaults to ``windbgx`` on Windows and ``gdb`` on other platforms.
16281647

pwnlib/windbg.py

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
The order of preference is:
1111
- ``windbgx``
1212
- ``windbg``
13+
- ``x64dbg``
1314
1415
If automatic lookup fails, you can manually set :attr:`.context.debugger` to
1516
the debugger of your choice and provide the path to the debugger binary
16-
using :attr:`.context.windbgx_binary` or :attr:`.context.windbg_binary`.
17+
using :attr:`.context.windbgx_binary`, :attr:`.context.windbg_binary`, or
18+
:attr:`.context.x64dbg_binary`.
1719
1820
Useful Functions
1921
----------------
@@ -72,9 +74,9 @@
7274
import atexit
7375
import os
7476
import signal
75-
7677
import subprocess
7778
import tempfile
79+
from pathlib import Path
7880

7981
from pwnlib import tubes
8082
from pwnlib.context import LocalContext
@@ -125,44 +127,52 @@ def debug(args, dbgscript=None, exe=None, env=None, creationflags=0, **kwargs):
125127
log.warn_once("Skipping debugger since context.noptrace==True")
126128
return tubes.process.process(args, executable=exe, env=env, creationflags=creationflags)
127129

130+
debugger, _ = binary()
128131
dbgscript = dbgscript or ''
129132
if isinstance(dbgscript, str):
130133
dbgscript = dbgscript.split('\n')
131134
# resume main thread
132-
dbgscript = ['~0m'] + dbgscript
135+
if debugger in ('windbg', 'windbgx'):
136+
dbgscript = ['~0m'] + dbgscript
137+
elif debugger == 'x64dbg':
138+
dbgscript = ['threadresumeall'] + dbgscript
139+
log.info_once('x64dbg: To resume the main thread, you need to use the "threadresumeall" command manually before version 2025.08.19.')
133140
creationflags |= CREATE_SUSPENDED
134141
io = tubes.process.process(args, executable=exe, env=env, creationflags=creationflags)
135142
attach(target=io, dbgscript=dbgscript, **kwargs)
136143

137144
return io
138145

139146
def binary():
140-
"""binary() -> str
147+
"""binary() -> (str, str)
141148
142149
Returns the path to the debugger binary depending on the context.
143150
:attr:`.context.debugger` is used to determine which debugger to use.
144151
145152
Returns:
153+
str: Name of the selected debugger.
146154
str: Path to the appropriate ``windbg`` binary to use.
147155
"""
148156
if context.debugger == 'auto':
149157
for debugger in context.debugger_choices:
158+
if debugger == 'auto':
159+
continue
150160
with context.local(debugger=debugger, log_level='critical'):
151161
try:
152162
return binary()
153163
except Exception:
154164
pass
155165
else:
156166
log.error('No debugger found. Please set context.debugger to one of: %s\n'
157-
'You might have to specify the path to the debugger binary with context.windbg_binary or context.windbgx_binary.',
167+
'You might have to specify the path to the debugger binary with context.x64dbg_binary, context.windbg_binary or context.windbgx_binary.',
158168
', '.join(context.debugger_choices))
159169

160-
if context.debugger == 'windbg':
170+
elif context.debugger == 'windbg':
161171
if context.windbg_binary:
162172
windbg = misc.which(context.windbg_binary)
163173
if not windbg:
164174
log.warn_once('Path to WinDbg binary `{}` not found'.format(context.windbg_binary))
165-
return windbg
175+
return context.debugger, windbg
166176

167177
windbg = misc.which('windbg.exe')
168178
if not windbg and os.environ.get('ProgramFiles(x86)'):
@@ -176,29 +186,74 @@ def binary():
176186
windbg = os.path.join(os.environ.get('ProgramFiles(x86)'), 'Windows Kits', '10', 'Debuggers', arch_str, 'windbg.exe')
177187
if not windbg or not os.path.exists(windbg):
178188
log.error('windbg is not installed or in system PATH. You can set context.windbg_binary to specify the path manually.')
179-
return windbg
189+
return context.debugger, windbg
180190

181-
if context.debugger == 'windbgx':
191+
elif context.debugger == 'windbgx':
182192
if context.windbgx_binary:
183193
windbg = misc.which(context.windbgx_binary)
184194
if not windbg:
185195
log.warn_once('Path to WinDbgx binary `{}` not found'.format(context.windbgx_binary))
186-
return windbg
196+
return context.debugger, windbg
187197

188198
windbg = misc.which('windbgx.exe')
189199
if not windbg and os.environ.get('LocalAppData'):
190200
windbg = os.path.join(os.environ.get('LocalAppData'), 'Microsoft', 'WindowsApps', 'WinDbgX.exe')
191201
if not windbg or not os.path.exists(windbg):
192202
log.error('windbgx is not installed or in system PATH. You can set context.windbgx_binary to specify the path manually.')
193-
return windbg
203+
return context.debugger, windbg
204+
205+
elif context.debugger == 'x64dbg':
206+
return context.debugger, _lookup_x64dbg()
194207

195208
log.error('Invalid debugger selection: %s', context.debugger)
196209

210+
def _lookup_x64dbg():
211+
def _select_arch_binary(path):
212+
# Select the appropriate x64dbg binary based on the architecture directly
213+
# instead of the x96dbg.exe selector binary.
214+
# The x96dbg.exe proxy launches the correct one but our `wait_for_debugger`
215+
# function doesn't follow that and reports the proxy binary exiting early.
216+
base_path = Path(path).resolve().parent
217+
if base_path.name in ('x32', 'x64'):
218+
base_path = base_path.parent
219+
if context.arch == 'i386':
220+
return base_path / 'x32' / 'x32dbg.exe'
221+
elif context.arch == 'amd64':
222+
return base_path / 'x64' / 'x64dbg.exe'
223+
else:
224+
log.error('Unsupported architecture for x64dbg: %s', context.arch)
225+
if context.x64dbg_binary:
226+
x64dbg = misc.which(context.x64dbg_binary)
227+
if not x64dbg:
228+
log.warn_once('Path to x64dbg binary `{}` not found'.format(context.x64dbg_binary))
229+
return _select_arch_binary(x64dbg)
230+
231+
x64dbg = misc.which('x96dbg.exe')
232+
if x64dbg:
233+
return _select_arch_binary(x64dbg)
234+
235+
# See if the "Debug with x64dbg" shell extension is installed
236+
try:
237+
import winreg # pylint: disable=import-error winreg is only available on Windows
238+
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r'exefile\shell\Debug with x64dbg\Command') as key:
239+
regcmd = winreg.QueryValueEx(key, None)
240+
# ('"C:\\Users\\User\\Downloads\\x64dbg\\bin\\x96dbg.exe" "%1"', 2)
241+
if regcmd[1] != winreg.REG_EXPAND_SZ:
242+
log.error('x64dbg registry key is not REG_EXPAND_SZ')
243+
command = regcmd[0].split('"')[1]
244+
if not os.path.exists(command):
245+
log.error('x64dbg path from registry does not exist')
246+
return _select_arch_binary(command)
247+
except FileNotFoundError:
248+
pass
249+
250+
log.error('x64dbg is not installed or in system PATH')
251+
197252
@LocalContext
198253
def attach(target, dbgscript=None, dbg_args=[]):
199254
"""attach(target, dbgscript=None, dbg_args=[]) -> int
200255
201-
Attach to a running process with WinDbg.
256+
Attach to a running process with WinDbg or x64dbg.
202257
203258
Arguments:
204259
target(int, str, process): Process to attach to.
@@ -262,31 +317,41 @@ def attach(target, dbgscript=None, dbg_args=[]):
262317
if not pid:
263318
log.error('could not find target process')
264319

265-
cmd = [binary()]
320+
debugger, debugger_path = binary()
321+
cmd = [debugger_path]
266322
if dbg_args:
267323
cmd.extend(dbg_args)
268-
324+
269325
cmd.extend(['-p', str(pid)])
270326

271327
dbgscript = dbgscript or ''
272328
if isinstance(dbgscript, str):
273329
dbgscript = dbgscript.split('\n')
274-
with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.dbg') as tmp:
275-
tmp.write('\n'.join(script.strip() for script in dbgscript if script.strip()))
276-
tmp.flush()
277-
dbgscript_file = tmp.name
278-
330+
dbgscript_file = None
279331
if dbgscript:
280-
cmd.extend(['-c', '$<{}'.format(dbgscript_file)])
332+
with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='.dbg') as tmp:
333+
tmp.write('\n'.join(script.strip() for script in dbgscript if script.strip()))
334+
tmp.flush()
335+
dbgscript_file = tmp.name
336+
337+
if debugger in ('windbg', 'windbgx'):
338+
cmd.extend(['-c', '$<{}'.format(dbgscript_file)])
339+
# x64dbg got support to run commands on startup in version 2025.08.19.
340+
# But absolute paths were not supported until version 2026.05.27.
341+
elif debugger == 'x64dbg':
342+
cmd.extend(['-cf', dbgscript_file])
343+
else:
344+
log.warn_once('dbgscript is not supported for %s', debugger)
281345

282-
log.info("Launching a new process: %r" % cmd)
346+
log.info("Launching a new process: %r", cmd)
283347

284348
io = subprocess.Popen(cmd)
285349
debugger_pid = io.pid
286350

287351
def kill():
288352
try:
289-
os.unlink(dbgscript_file)
353+
if dbgscript_file is not None:
354+
os.unlink(dbgscript_file)
290355
os.kill(debugger_pid, signal.SIGTERM)
291356
except OSError:
292357
pass

0 commit comments

Comments
 (0)