Skip to content

Commit

Permalink
Add basic support to debug processes on Windows (#2327)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
peace-maker committed Mar 29, 2024
1 parent 4ac98cd commit 8ba1bdf
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 10 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ The table below shows which release corresponds to each branch, and what date th
- [#2360][2360] Add offline parameter for `search_by_hash` series function
- [#2356][2356] Add local libc database provider for libcdb
- [#2374][2374] libcdb.unstrip_libc: debug symbols are fetched only if not present
- [#2327][2327] Add basic support to debug processes on Windows

[2360]: https://github.com/Gallopsled/pwntools/pull/2360
[2356]: https://github.com/Gallopsled/pwntools/pull/2356
[2374]: https://github.com/Gallopsled/pwntools/pull/2374
[2327]: https://github.com/Gallopsled/pwntools/pull/2327

## 4.13.0 (`beta`)

Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Each of the ``pwntools`` modules is documented here.
update
useragents
util/*
windbg

.. toctree::
:hidden:
Expand Down
9 changes: 9 additions & 0 deletions docs/source/windbg.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.. testsetup:: *

from pwn import *

:mod:`pwnlib.windbg` --- Working with WinDbg
======================================

.. automodule:: pwnlib.windbg
:members:
1 change: 1 addition & 0 deletions pwnlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
'util',
'update',
'version',
'windbg',
]

from . import args
18 changes: 9 additions & 9 deletions pwnlib/tubes/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,15 +802,6 @@ def close(self):
# First check if we are already dead
self.poll()

# close file descriptors
for fd in [self.proc.stdin, self.proc.stdout, self.proc.stderr]:
if fd is not None:
try:
fd.close()
except IOError as e:
if e.errno != errno.EPIPE and e.errno != errno.EINVAL:
raise

if not self._stop_noticed:
try:
self.proc.kill()
Expand All @@ -820,6 +811,15 @@ def close(self):
except OSError:
pass

# close file descriptors
for fd in [self.proc.stdin, self.proc.stdout, self.proc.stderr]:
if fd is not None:
try:
fd.close()
except IOError as e:
if e.errno != errno.EPIPE and e.errno != errno.EINVAL:
raise


def fileno(self):
if not self.connected():
Expand Down
42 changes: 41 additions & 1 deletion pwnlib/util/proc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import errno
import socket
import sys
import time

import psutil
Expand Down Expand Up @@ -315,6 +316,42 @@ def status(pid):
raise
return out

def _tracer_windows(pid):
import ctypes
from ctypes import wintypes

def _check_bool(result, func, args):
if not result:
raise ctypes.WinError(ctypes.get_last_error())
return args

kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
OpenProcess = kernel32.OpenProcess
OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
OpenProcess.restype = wintypes.HANDLE
OpenProcess.errcheck = _check_bool

CheckRemoteDebuggerPresent = kernel32.CheckRemoteDebuggerPresent
CheckRemoteDebuggerPresent.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.BOOL)]
CheckRemoteDebuggerPresent.restype = wintypes.BOOL
CheckRemoteDebuggerPresent.errcheck = _check_bool

CloseHandle = kernel32.CloseHandle
CloseHandle.argtypes = [wintypes.HANDLE]
CloseHandle.restype = wintypes.BOOL
CloseHandle.errcheck = _check_bool

PROCESS_QUERY_INFORMATION = 0x0400
proc_handle = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid)
present = wintypes.BOOL()
CheckRemoteDebuggerPresent(proc_handle, ctypes.byref(present))
ret = 0
if present.value:
ret = pid
CloseHandle(proc_handle)

return ret

def tracer(pid):
"""tracer(pid) -> int
Expand All @@ -329,7 +366,10 @@ def tracer(pid):
>>> tracer(os.getpid()) is None
True
"""
tpid = int(status(pid)['TracerPid'])
if sys.platform == 'win32':
tpid = _tracer_windows(pid)
else:
tpid = int(status(pid)['TracerPid'])
return tpid if tpid > 0 else None

def state(pid):
Expand Down
239 changes: 239 additions & 0 deletions pwnlib/windbg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
"""
During exploit development, it is frequently useful to debug the
target binary under WinDbg. This module provides a simple interface
to do so under Windows.
Useful Functions
----------------
- :func:`attach` - Attach to an existing process
Debugging Tips
--------------
The :func:`attach` and :func:`debug` functions will likely be your bread and
butter for debugging.
Both allow you to provide a script to pass to WinDbg when it is started, so that
it can automatically set your breakpoints.
Attaching to Processes
~~~~~~~~~~~~~~~~~~~~~~
To attach to an existing process, just use :func:`attach`. You can pass a PID,
a process name (including file extension), or a :class:`.process`.
Spawning New Processes
~~~~~~~~~~~~~~~~~~~~~~
Attaching to processes with :func:`attach` is useful, but the state the process
is in may vary. If you need to attach to a process very early, and debug it from
the very first instruction (or even the start of ``main``), you instead should use
:func:`debug`.
When you use :func:`debug`, the return value is a :class:`.tube` object
that you interact with exactly like normal.
Tips and Troubleshooting
------------------------
``NOPTRACE`` magic argument
~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's quite cumbersom to comment and un-comment lines containing `attach`.
You can cause these lines to be a no-op by running your script with the
``NOPTRACE`` argument appended, or with ``PWNLIB_NOPTRACE=1`` in the environment.
(The name is borrowed from ptrace syscall on Linux.)
::
$ python exploit.py NOPTRACE
[+] Starting local process 'chall.exe': Done
[!] Skipping debug attach since context.noptrace==True
...
Member Documentation
===============================
"""
from __future__ import absolute_import
import atexit
import os
import signal

import subprocess

import six

from pwnlib import tubes
from pwnlib.context import LocalContext
from pwnlib.context import context
from pwnlib.log import getLogger
from pwnlib.util import misc
from pwnlib.util import proc

log = getLogger(__name__)

CREATE_SUSPENDED = 0x00000004

@LocalContext
def debug(args, windbgscript=None, exe=None, env=None, creationflags=0, **kwargs):
"""debug(args, windbgscript=None, exe=None, env=None, creationflags=0) -> tube
Launch a process in suspended state, attach debugger and resume process.
Arguments:
args(list): Arguments to the process, similar to :class:`.process`.
windbgscript(str): windbg script to run.
exe(str): Path to the executable on disk.
env(dict): Environment to start the binary in.
creationflags(int): Flags to pass to :func:`.process.process`.
Returns:
:class:`.process`: A tube connected to the target process.
Notes:
.. code-block: python
# Create a new process, and stop it at 'main'
io = windbg.debug('calc', '''
bp $exentry
go
''')
When WinDbg opens via :func:`.debug`, it will initially be stopped on the very first
instruction of the entry point.
"""
if isinstance(
args, six.integer_types + (tubes.process.process, tubes.ssh.ssh_channel)
):
log.error("Use windbg.attach() to debug a running process")

if context.noptrace:
log.warn_once("Skipping debugger since context.noptrace==True")
return tubes.process.process(args, executable=exe, env=env, creationflags=creationflags)

windbgscript = windbgscript or ''
if isinstance(windbgscript, six.string_types):
windbgscript = windbgscript.split('\n')
# resume main thread
windbgscript = ['~0m'] + windbgscript
creationflags |= CREATE_SUSPENDED
io = tubes.process.process(args, executable=exe, env=env, creationflags=creationflags)
attach(target=io, windbgscript=windbgscript, **kwargs)

return io

def binary():
"""binary() -> str
Returns the path to the WinDbg binary.
Returns:
str: Path to the appropriate ``windbg`` binary to use.
"""
windbg = misc.which('windbgx.exe') or misc.which('windbg.exe')
if not windbg:
log.error('windbg is not installed or in system PATH')
return windbg

@LocalContext
def attach(target, windbgscript=None, windbg_args=[]):
"""attach(target, windbgscript=None, windbg_args=[]) -> int
Attach to a running process with WinDbg.
Arguments:
target(int, str, process): Process to attach to.
windbgscript(str, list): WinDbg script to run after attaching.
windbg_args(list): Additional arguments to pass to WinDbg.
Returns:
int: PID of the WinDbg process.
Notes:
The ``target`` argument is very robust, and can be any of the following:
:obj:`int`
PID of a process
:obj:`str`
Process name. The youngest process is selected.
:class:`.process`
Process to connect to
Examples:
Attach to a process by PID
>>> pid = windbg.attach(1234) # doctest: +SKIP
Attach to the youngest process by name
>>> pid = windbg.attach('cmd.exe') # doctest: +SKIP
Attach a debugger to a :class:`.process` tube and automate interaction
>>> io = process('cmd') # doctest: +SKIP
>>> pid = windbg.attach(io, windbgscript='''
... bp kernelbase!WriteFile
... g
... ''') # doctest: +SKIP
"""
if context.noptrace:
log.warn_once("Skipping debug attach since context.noptrace==True")
return

# let's see if we can find a pid to attach to
pid = None
if isinstance(target, six.integer_types):
# target is a pid, easy peasy
pid = target
elif isinstance(target, str):
# pidof picks the youngest process
pids = list(proc.pidof(target))
if not pids:
log.error('No such process: %s', target)
pid = pids[0]
log.info('Attaching to youngest process "%s" (PID = %d)' %
(target, pid))
elif isinstance(target, tubes.process.process):
pid = proc.pidof(target)[0]
else:
log.error("don't know how to attach to target: %r", target)

if not pid:
log.error('could not find target process')

cmd = [binary()]
if windbg_args:
cmd.extend(windbg_args)

cmd.extend(['-p', str(pid)])

windbgscript = windbgscript or ''
if isinstance(windbgscript, six.string_types):
windbgscript = windbgscript.split('\n')
if isinstance(windbgscript, list):
windbgscript = ';'.join(script.strip() for script in windbgscript if script.strip())
if windbgscript:
cmd.extend(['-c', windbgscript])

log.info("Launching a new process: %r" % cmd)

io = subprocess.Popen(cmd)
windbg_pid = io.pid

def kill():
try:
os.kill(windbg_pid, signal.SIGTERM)
except OSError:
pass

atexit.register(kill)

if context.native:
proc.wait_for_debugger(pid, windbg_pid)

return windbg_pid

0 comments on commit 8ba1bdf

Please sign in to comment.