Skip to content

Commit 8ba1bdf

Browse files
authored
Add basic support to debug processes on Windows (Gallopsled#2327)
* Add basic support to debug processes on Windows Currently only `windbg.debug()` and `windbg.attach()` are implemented, which open a WinDbg instance and attach to the process. * Update CHANGELOG * Cleanup CheckRemoteDebuggerPresent call Only require PROCESS_QUERY_INFORMATION access and check for errors when opening the process. * process.close: Move closing of std fds after kill Windows processes would block on fd.close() when the main thread is suspended.
1 parent 4ac98cd commit 8ba1bdf

File tree

7 files changed

+302
-10
lines changed

7 files changed

+302
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ The table below shows which release corresponds to each branch, and what date th
7474
- [#2360][2360] Add offline parameter for `search_by_hash` series function
7575
- [#2356][2356] Add local libc database provider for libcdb
7676
- [#2374][2374] libcdb.unstrip_libc: debug symbols are fetched only if not present
77+
- [#2327][2327] Add basic support to debug processes on Windows
7778

7879
[2360]: https://github.com/Gallopsled/pwntools/pull/2360
7980
[2356]: https://github.com/Gallopsled/pwntools/pull/2356
8081
[2374]: https://github.com/Gallopsled/pwntools/pull/2374
82+
[2327]: https://github.com/Gallopsled/pwntools/pull/2327
8183

8284
## 4.13.0 (`beta`)
8385

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Each of the ``pwntools`` modules is documented here.
7777
update
7878
useragents
7979
util/*
80+
windbg
8081

8182
.. toctree::
8283
:hidden:

docs/source/windbg.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.. testsetup:: *
2+
3+
from pwn import *
4+
5+
:mod:`pwnlib.windbg` --- Working with WinDbg
6+
======================================
7+
8+
.. automodule:: pwnlib.windbg
9+
:members:

pwnlib/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
'util',
3737
'update',
3838
'version',
39+
'windbg',
3940
]
4041

4142
from . import args

pwnlib/tubes/process.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -802,15 +802,6 @@ def close(self):
802802
# First check if we are already dead
803803
self.poll()
804804

805-
# close file descriptors
806-
for fd in [self.proc.stdin, self.proc.stdout, self.proc.stderr]:
807-
if fd is not None:
808-
try:
809-
fd.close()
810-
except IOError as e:
811-
if e.errno != errno.EPIPE and e.errno != errno.EINVAL:
812-
raise
813-
814805
if not self._stop_noticed:
815806
try:
816807
self.proc.kill()
@@ -820,6 +811,15 @@ def close(self):
820811
except OSError:
821812
pass
822813

814+
# close file descriptors
815+
for fd in [self.proc.stdin, self.proc.stdout, self.proc.stderr]:
816+
if fd is not None:
817+
try:
818+
fd.close()
819+
except IOError as e:
820+
if e.errno != errno.EPIPE and e.errno != errno.EINVAL:
821+
raise
822+
823823

824824
def fileno(self):
825825
if not self.connected():

pwnlib/util/proc.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import errno
55
import socket
6+
import sys
67
import time
78

89
import psutil
@@ -315,6 +316,42 @@ def status(pid):
315316
raise
316317
return out
317318

319+
def _tracer_windows(pid):
320+
import ctypes
321+
from ctypes import wintypes
322+
323+
def _check_bool(result, func, args):
324+
if not result:
325+
raise ctypes.WinError(ctypes.get_last_error())
326+
return args
327+
328+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
329+
OpenProcess = kernel32.OpenProcess
330+
OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
331+
OpenProcess.restype = wintypes.HANDLE
332+
OpenProcess.errcheck = _check_bool
333+
334+
CheckRemoteDebuggerPresent = kernel32.CheckRemoteDebuggerPresent
335+
CheckRemoteDebuggerPresent.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.BOOL)]
336+
CheckRemoteDebuggerPresent.restype = wintypes.BOOL
337+
CheckRemoteDebuggerPresent.errcheck = _check_bool
338+
339+
CloseHandle = kernel32.CloseHandle
340+
CloseHandle.argtypes = [wintypes.HANDLE]
341+
CloseHandle.restype = wintypes.BOOL
342+
CloseHandle.errcheck = _check_bool
343+
344+
PROCESS_QUERY_INFORMATION = 0x0400
345+
proc_handle = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid)
346+
present = wintypes.BOOL()
347+
CheckRemoteDebuggerPresent(proc_handle, ctypes.byref(present))
348+
ret = 0
349+
if present.value:
350+
ret = pid
351+
CloseHandle(proc_handle)
352+
353+
return ret
354+
318355
def tracer(pid):
319356
"""tracer(pid) -> int
320357
@@ -329,7 +366,10 @@ def tracer(pid):
329366
>>> tracer(os.getpid()) is None
330367
True
331368
"""
332-
tpid = int(status(pid)['TracerPid'])
369+
if sys.platform == 'win32':
370+
tpid = _tracer_windows(pid)
371+
else:
372+
tpid = int(status(pid)['TracerPid'])
333373
return tpid if tpid > 0 else None
334374

335375
def state(pid):

pwnlib/windbg.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"""
2+
During exploit development, it is frequently useful to debug the
3+
target binary under WinDbg. This module provides a simple interface
4+
to do so under Windows.
5+
6+
Useful Functions
7+
----------------
8+
9+
- :func:`attach` - Attach to an existing process
10+
11+
Debugging Tips
12+
--------------
13+
14+
The :func:`attach` and :func:`debug` functions will likely be your bread and
15+
butter for debugging.
16+
17+
Both allow you to provide a script to pass to WinDbg when it is started, so that
18+
it can automatically set your breakpoints.
19+
20+
Attaching to Processes
21+
~~~~~~~~~~~~~~~~~~~~~~
22+
23+
To attach to an existing process, just use :func:`attach`. You can pass a PID,
24+
a process name (including file extension), or a :class:`.process`.
25+
26+
Spawning New Processes
27+
~~~~~~~~~~~~~~~~~~~~~~
28+
29+
Attaching to processes with :func:`attach` is useful, but the state the process
30+
is in may vary. If you need to attach to a process very early, and debug it from
31+
the very first instruction (or even the start of ``main``), you instead should use
32+
:func:`debug`.
33+
34+
When you use :func:`debug`, the return value is a :class:`.tube` object
35+
that you interact with exactly like normal.
36+
37+
Tips and Troubleshooting
38+
------------------------
39+
40+
``NOPTRACE`` magic argument
41+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
42+
43+
It's quite cumbersom to comment and un-comment lines containing `attach`.
44+
45+
You can cause these lines to be a no-op by running your script with the
46+
``NOPTRACE`` argument appended, or with ``PWNLIB_NOPTRACE=1`` in the environment.
47+
(The name is borrowed from ptrace syscall on Linux.)
48+
49+
::
50+
51+
$ python exploit.py NOPTRACE
52+
[+] Starting local process 'chall.exe': Done
53+
[!] Skipping debug attach since context.noptrace==True
54+
...
55+
56+
Member Documentation
57+
===============================
58+
"""
59+
from __future__ import absolute_import
60+
import atexit
61+
import os
62+
import signal
63+
64+
import subprocess
65+
66+
import six
67+
68+
from pwnlib import tubes
69+
from pwnlib.context import LocalContext
70+
from pwnlib.context import context
71+
from pwnlib.log import getLogger
72+
from pwnlib.util import misc
73+
from pwnlib.util import proc
74+
75+
log = getLogger(__name__)
76+
77+
CREATE_SUSPENDED = 0x00000004
78+
79+
@LocalContext
80+
def debug(args, windbgscript=None, exe=None, env=None, creationflags=0, **kwargs):
81+
"""debug(args, windbgscript=None, exe=None, env=None, creationflags=0) -> tube
82+
83+
Launch a process in suspended state, attach debugger and resume process.
84+
85+
Arguments:
86+
args(list): Arguments to the process, similar to :class:`.process`.
87+
windbgscript(str): windbg script to run.
88+
exe(str): Path to the executable on disk.
89+
env(dict): Environment to start the binary in.
90+
creationflags(int): Flags to pass to :func:`.process.process`.
91+
92+
Returns:
93+
:class:`.process`: A tube connected to the target process.
94+
95+
Notes:
96+
97+
.. code-block: python
98+
99+
# Create a new process, and stop it at 'main'
100+
io = windbg.debug('calc', '''
101+
bp $exentry
102+
go
103+
''')
104+
105+
When WinDbg opens via :func:`.debug`, it will initially be stopped on the very first
106+
instruction of the entry point.
107+
"""
108+
if isinstance(
109+
args, six.integer_types + (tubes.process.process, tubes.ssh.ssh_channel)
110+
):
111+
log.error("Use windbg.attach() to debug a running process")
112+
113+
if context.noptrace:
114+
log.warn_once("Skipping debugger since context.noptrace==True")
115+
return tubes.process.process(args, executable=exe, env=env, creationflags=creationflags)
116+
117+
windbgscript = windbgscript or ''
118+
if isinstance(windbgscript, six.string_types):
119+
windbgscript = windbgscript.split('\n')
120+
# resume main thread
121+
windbgscript = ['~0m'] + windbgscript
122+
creationflags |= CREATE_SUSPENDED
123+
io = tubes.process.process(args, executable=exe, env=env, creationflags=creationflags)
124+
attach(target=io, windbgscript=windbgscript, **kwargs)
125+
126+
return io
127+
128+
def binary():
129+
"""binary() -> str
130+
131+
Returns the path to the WinDbg binary.
132+
133+
Returns:
134+
str: Path to the appropriate ``windbg`` binary to use.
135+
"""
136+
windbg = misc.which('windbgx.exe') or misc.which('windbg.exe')
137+
if not windbg:
138+
log.error('windbg is not installed or in system PATH')
139+
return windbg
140+
141+
@LocalContext
142+
def attach(target, windbgscript=None, windbg_args=[]):
143+
"""attach(target, windbgscript=None, windbg_args=[]) -> int
144+
145+
Attach to a running process with WinDbg.
146+
147+
Arguments:
148+
target(int, str, process): Process to attach to.
149+
windbgscript(str, list): WinDbg script to run after attaching.
150+
windbg_args(list): Additional arguments to pass to WinDbg.
151+
152+
Returns:
153+
int: PID of the WinDbg process.
154+
155+
Notes:
156+
157+
The ``target`` argument is very robust, and can be any of the following:
158+
159+
:obj:`int`
160+
PID of a process
161+
:obj:`str`
162+
Process name. The youngest process is selected.
163+
:class:`.process`
164+
Process to connect to
165+
166+
Examples:
167+
168+
Attach to a process by PID
169+
170+
>>> pid = windbg.attach(1234) # doctest: +SKIP
171+
172+
Attach to the youngest process by name
173+
174+
>>> pid = windbg.attach('cmd.exe') # doctest: +SKIP
175+
176+
Attach a debugger to a :class:`.process` tube and automate interaction
177+
178+
>>> io = process('cmd') # doctest: +SKIP
179+
>>> pid = windbg.attach(io, windbgscript='''
180+
... bp kernelbase!WriteFile
181+
... g
182+
... ''') # doctest: +SKIP
183+
"""
184+
if context.noptrace:
185+
log.warn_once("Skipping debug attach since context.noptrace==True")
186+
return
187+
188+
# let's see if we can find a pid to attach to
189+
pid = None
190+
if isinstance(target, six.integer_types):
191+
# target is a pid, easy peasy
192+
pid = target
193+
elif isinstance(target, str):
194+
# pidof picks the youngest process
195+
pids = list(proc.pidof(target))
196+
if not pids:
197+
log.error('No such process: %s', target)
198+
pid = pids[0]
199+
log.info('Attaching to youngest process "%s" (PID = %d)' %
200+
(target, pid))
201+
elif isinstance(target, tubes.process.process):
202+
pid = proc.pidof(target)[0]
203+
else:
204+
log.error("don't know how to attach to target: %r", target)
205+
206+
if not pid:
207+
log.error('could not find target process')
208+
209+
cmd = [binary()]
210+
if windbg_args:
211+
cmd.extend(windbg_args)
212+
213+
cmd.extend(['-p', str(pid)])
214+
215+
windbgscript = windbgscript or ''
216+
if isinstance(windbgscript, six.string_types):
217+
windbgscript = windbgscript.split('\n')
218+
if isinstance(windbgscript, list):
219+
windbgscript = ';'.join(script.strip() for script in windbgscript if script.strip())
220+
if windbgscript:
221+
cmd.extend(['-c', windbgscript])
222+
223+
log.info("Launching a new process: %r" % cmd)
224+
225+
io = subprocess.Popen(cmd)
226+
windbg_pid = io.pid
227+
228+
def kill():
229+
try:
230+
os.kill(windbg_pid, signal.SIGTERM)
231+
except OSError:
232+
pass
233+
234+
atexit.register(kill)
235+
236+
if context.native:
237+
proc.wait_for_debugger(pid, windbg_pid)
238+
239+
return windbg_pid

0 commit comments

Comments
 (0)