From 68772de75c47cfd4a11f0abc663e44aebb7fda70 Mon Sep 17 00:00:00 2001 From: peace-maker Date: Wed, 3 Jan 2024 00:18:46 +0100 Subject: [PATCH 1/4] 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. --- docs/source/index.rst | 1 + docs/source/windbg.rst | 9 ++ pwnlib/__init__.py | 1 + pwnlib/util/proc.py | 43 +++++++- pwnlib/windbg.py | 239 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 docs/source/windbg.rst create mode 100644 pwnlib/windbg.py diff --git a/docs/source/index.rst b/docs/source/index.rst index bc2f2b3e2..040824ef8 100755 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -77,6 +77,7 @@ Each of the ``pwntools`` modules is documented here. update useragents util/* + windbg .. toctree:: :hidden: diff --git a/docs/source/windbg.rst b/docs/source/windbg.rst new file mode 100644 index 000000000..e08397205 --- /dev/null +++ b/docs/source/windbg.rst @@ -0,0 +1,9 @@ +.. testsetup:: * + + from pwn import * + +:mod:`pwnlib.windbg` --- Working with WinDbg +====================================== + +.. automodule:: pwnlib.windbg + :members: \ No newline at end of file diff --git a/pwnlib/__init__.py b/pwnlib/__init__.py index 446907c04..3dae1f00f 100644 --- a/pwnlib/__init__.py +++ b/pwnlib/__init__.py @@ -36,6 +36,7 @@ 'util', 'update', 'version', + 'windbg', ] from . import args diff --git a/pwnlib/util/proc.py b/pwnlib/util/proc.py index 9de9ac59e..7e405cc6c 100644 --- a/pwnlib/util/proc.py +++ b/pwnlib/util/proc.py @@ -3,6 +3,7 @@ import errno import socket +import sys import time import psutil @@ -304,6 +305,43 @@ 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 + + SYNCHRONIZE = 0x00100000 + STANDARD_RIGHTS_REQUIRED = 0x000F0000 + PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + OpenProcess = kernel32.OpenProcess + OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] + OpenProcess.restype = wintypes.HANDLE + + 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 + + proc_handle = OpenProcess( PROCESS_ALL_ACCESS, 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 @@ -317,7 +355,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): diff --git a/pwnlib/windbg.py b/pwnlib/windbg.py new file mode 100644 index 000000000..439b34519 --- /dev/null +++ b/pwnlib/windbg.py @@ -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 ucrtbase:puts + ... 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 From e7393e74b3120edc4825691969dd5de3e2e68c8f Mon Sep 17 00:00:00 2001 From: peace-maker Date: Wed, 3 Jan 2024 00:24:05 +0100 Subject: [PATCH 2/4] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf8ed4ce4..df6b03bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ The table below shows which release corresponds to each branch, and what date th - [#2308][2308] Fix WinExec shellcraft to make sure it's 16 byte aligned - [#2279][2279] Make `pwn template` always set context.binary - [#2310][2310] Add support to start a process on Windows +- [#2327][2327] Add basic support to debug processes on Windows [2242]: https://github.com/Gallopsled/pwntools/pull/2242 [2277]: https://github.com/Gallopsled/pwntools/pull/2277 @@ -91,6 +92,7 @@ The table below shows which release corresponds to each branch, and what date th [2308]: https://github.com/Gallopsled/pwntools/pull/2308 [2279]: https://github.com/Gallopsled/pwntools/pull/2279 [2310]: https://github.com/Gallopsled/pwntools/pull/2310 +[2327]: https://github.com/Gallopsled/pwntools/pull/2327 ## 4.12.0 (`beta`) From a3f8bb473de2df40c77f08521851e41c65443858 Mon Sep 17 00:00:00 2001 From: peace-maker Date: Wed, 17 Jan 2024 22:53:16 +0100 Subject: [PATCH 3/4] Cleanup CheckRemoteDebuggerPresent call Only require PROCESS_QUERY_INFORMATION access and check for errors when opening the process. --- pwnlib/util/proc.py | 7 +++---- pwnlib/windbg.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pwnlib/util/proc.py b/pwnlib/util/proc.py index 7e405cc6c..6133349a4 100644 --- a/pwnlib/util/proc.py +++ b/pwnlib/util/proc.py @@ -314,13 +314,11 @@ def _check_bool(result, func, args): raise ctypes.WinError(ctypes.get_last_error()) return args - SYNCHRONIZE = 0x00100000 - STANDARD_RIGHTS_REQUIRED = 0x000F0000 - PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF 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)] @@ -332,7 +330,8 @@ def _check_bool(result, func, args): CloseHandle.restype = wintypes.BOOL CloseHandle.errcheck = _check_bool - proc_handle = OpenProcess( PROCESS_ALL_ACCESS, False, pid ) + PROCESS_QUERY_INFORMATION = 0x0400 + proc_handle = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid) present = wintypes.BOOL() CheckRemoteDebuggerPresent(proc_handle, ctypes.byref(present)) ret = 0 diff --git a/pwnlib/windbg.py b/pwnlib/windbg.py index 439b34519..588714572 100644 --- a/pwnlib/windbg.py +++ b/pwnlib/windbg.py @@ -177,7 +177,7 @@ def attach(target, windbgscript=None, windbg_args=[]): >>> io = process('cmd') # doctest: +SKIP >>> pid = windbg.attach(io, windbgscript=''' - ... bp ucrtbase:puts + ... bp kernelbase!WriteFile ... g ... ''') # doctest: +SKIP """ From 2ee56648d98e603e2c0ff69ab8efb8cd35dccd55 Mon Sep 17 00:00:00 2001 From: peace-maker Date: Wed, 17 Jan 2024 23:57:36 +0100 Subject: [PATCH 4/4] process.close: Move closing of std fds after kill Windows processes would block on fd.close() when the main thread is suspended. --- pwnlib/tubes/process.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pwnlib/tubes/process.py b/pwnlib/tubes/process.py index af9879bd9..388a14630 100644 --- a/pwnlib/tubes/process.py +++ b/pwnlib/tubes/process.py @@ -798,15 +798,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() @@ -816,6 +807,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():