Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support to debug processes on Windows #2327

Merged
merged 7 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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