command line support (partial) via PEB stomping

This update include support to passing command line parameters to unmanaged exe via PEB stomping.
This technique is not working with every executable since it depends on which functions are used to pass arguments.
Generally, to get a universally working technique would be required to hook GetCommandlineA GetCommandlineW __getmainargs and __wgetmainargs since PEB stomping won't cover all cases, more details here:
https://blog-30cm-tw.translate.goog/2020/08/windows-c-mainargc-argv.html?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=it&_x_tr_pto=wapp

However, during my testing I found that mimikatz and several go binaries are working just by doing PEB stomping.
On the other hand, cmdline passing via PEB stomping alone to mingw and VS compiled binaries won't likely work.
This commit is contained in:
naksyn
2023-07-27 06:44:29 -07:00
parent 63ebe1c4ba
commit db1893910c
160 changed files with 70537 additions and 42 deletions
@@ -0,0 +1,5 @@
from .debugger import Debugger, HXBreakpoint
from .symboldbg import SymbolDebugger
from .localdbg import LocalDebugger
from .breakpoints import *
from .breakpoints import *
@@ -0,0 +1,258 @@
from collections import OrderedDict
import windows
from windows.generated_def.winstructs import *
from windows.generated_def import windef
from windows.winobject.process import WinProcess, WinThread
from windows.pycompat import basestring
STANDARD_BP = "BP"
HARDWARE_EXEC_BP = "HXBP"
MEMORY_BREAKPOINT = "MEMBP"
class Breakpoint(object):
"""An standard (Int3) breakpoint (type == ``STANDARD_BP``)"""
type = STANDARD_BP # REAL BP
def __init__(self, addr):
self.addr = addr
def apply_to_target(self, target):
return isinstance(target, WinProcess)
def trigger(self, dbg, exception):
"""Called when breakpoint is hit"""
pass
class ProxyBreakpoint(Breakpoint):
def __init__(self, target, addr, type):
self.target = target
self.addr = addr
self.type = type
def trigger(self, dbg, exception):
return self.target(dbg, exception)
class HXBreakpoint(Breakpoint):
"""An hardware-execution breakpoint (type == ``HARDWARE_EXEC_BP``)"""
type = HARDWARE_EXEC_BP
def apply_to_target(self, target):
return isinstance(target, WinThread)
class MemoryBreakpoint(Breakpoint):
"""A memory breakpoint (type == ``MEMORY_BREAKPOINT``)"""
type = MEMORY_BREAKPOINT
DEFAULT_EVENTS = "RWX"
DEFAULT_SIZE = 0x1000
def __init__(self, addr, size=None, events=None):
"""``size``: the size of the memory breakpoint.
``events``: a string representing the events that interest the BP (any of "RWX")"""
super(MemoryBreakpoint, self).__init__(addr)
self.size = size if size is not None else self.DEFAULT_SIZE
events = events if events is not None else self.DEFAULT_EVENTS
self.events = set(events)
def trigger(self, dbg, exception):
"""Called when breakpoint is hit"""
pass
## Arguments Helper (need to move this elsewhere)
class X86ArgumentRetriever(object):
def get_arg(self, nb, proc, thread):
return proc.read_dword(thread.context.sp + 4 + (4 * nb))
def set_arg(self, nb, value, proc, thread):
return proc.write_dword(thread.context.sp + 4 + (4 * nb), value)
class X64ArgumentRetriever(object):
REG_ARGS = ["Rcx", "Rdx", "R8", "R9"]
def get_arg(self, nb, proc, thread):
if nb < len(self.REG_ARGS):
return getattr(thread.context, self.REG_ARGS[nb])
return proc.read_qword(thread.context.sp + 8 + (8 * nb))
def set_arg(self, nb, value, proc, thread):
if nb < len(self.REG_ARGS):
ctx = thread.context
setattr(ctx, self.REG_ARGS[nb], value)
return thread.set_context(ctx)
return proc.write_qword(thread.context.sp + 8 + (8 * nb), value)
## Behaviour breakpoint !
# class FunctionParamDumpBP(Breakpoint):
class FunctionParamDumpBPAbstract(object):
def __init__(self, addr=None, target=None):
if target is None:
try:
target = self.TARGET
except AttributeError as e:
raise ValueError("{0} bp without a <target> must have a <TARGET> class attribute")
if addr is None:
addr = "{0}!{1}".format(target.target_dll, target.target_func)
super(FunctionParamDumpBPAbstract, self).__init__(addr)
self.target = target
self.target_args = target.prototype._argtypes_
self.target_params = target.params
def extract_arguments_32bits(self, cproc, cthread):
x = windows.debug.X86ArgumentRetriever()
res = OrderedDict()
for i, (name, type) in enumerate(zip(self.target_params, self.target_args)):
value = x.get_arg(i, cproc, cthread)
rt = windows.remotectypes.transform_type_to_remote32bits(type)
if issubclass(rt, windows.remotectypes.RemoteValue):
t = rt(value, cproc)
else:
t = rt(value)
# Will fail in py3..
content = None
try:
content = t.contents
except Exception as e:
# contents will fail on basic type
# Not really an expected behavior
# But it works for now.. (and since a while)
pass
if content is None:
t = t.value
res[name[1]] = t
return res
def extract_arguments_64bits(self, cproc, cthread):
x = windows.debug.X64ArgumentRetriever()
res = OrderedDict()
for i, (name, type) in enumerate(zip(self.target_params, self.target_args)):
value = x.get_arg(i, cproc, cthread)
rt = windows.remotectypes.transform_type_to_remote64bits(type)
if issubclass(rt, windows.remotectypes.RemoteValue):
t = rt(value, cproc)
else:
t = rt(value)
if not hasattr(t, "contents"):
try:
t = t.value
except AttributeError:
pass
res[name[1]] = t
return res
def extract_arguments(self, cproc, cthread):
"""Extracts the functions parameters in an :class:`OrderedDict`"""
if windows.current_process.bitness == 32:
return self.extract_arguments_32bits(cproc, cthread)
if cproc.bitness == 64:
return self.extract_arguments_64bits(cproc, cthread)
# SysWow process from a 64bits debugger, handle bitness with CS
if cthread.context.SegCs == windows.syswow64.CS_32bits:
return self.extract_arguments_32bits(cproc, cthread)
return self.extract_arguments_64bits(cproc, cthread)
def arguments(self, dbg):
"TEST PARAM DICT"
if windows.current_process.bitness == 32:
extractor = windows.debug.X86ArgumentRetriever()
elif dbg.current_process.bitness == 64:
extractor = windows.debug.X64ArgumentRetriever()
elif dbg.current_thread.context.SegCs == windows.syswow64.CS_32bits:
extractor = windows.debug.X86ArgumentRetriever()
else:
extractor = windows.debug.X64ArgumentRetriever()
name_map = {name:i for i, name in enumerate(t[1] for t in self.target_params)}
return FunctionParameterProxy(extractor, name_map, self.target_args, dbg)
class FunctionParameterProxy(object):
# TODO: clean this + put more of the logic in the X64ArgumentRetriever
def __init__(self, extractor, name_map, parameters_type, x):
self.extractor = extractor
self.name_map = name_map
self.parameters_type = parameters_type
self.x = x
def __getitem__(self, x):
if isinstance(x, basestring):
x = self.name_map[x]
# import pdb;pdb.set_trace()
argtype = self.parameters_type[x]
value = self.extractor.get_arg(x, self.x.current_process, self.x.current_thread)
rt = windows.remotectypes.transform_type_to_remote32bits(argtype)
if issubclass(rt, windows.remotectypes.RemoteValue):
t = rt(value, self.x.current_process)
else:
t = rt(value)
if not hasattr(t, "contents"):
try:
t = t.value
except AttributeError:
pass
return t
def __setitem__(self, x, value):
if isinstance(x, basestring):
x = self.name_map[x]
try:
ctypes.cast(value, PVOID)
except ctypes.ArgumentError:
pass
value = getattr(value, "value", value)
return self.extractor.set_arg(x, value, self.x.current_process, self.x.current_thread)
class FunctionParamDumpBP(FunctionParamDumpBPAbstract, Breakpoint):
pass
class FunctionParamDumpHXBP(FunctionParamDumpBPAbstract, HXBreakpoint):
pass
class FunctionRetBP(Breakpoint):
def __init__(self, addr, initial_breakpoint):
super(FunctionRetBP, self).__init__(addr)
self.initial_breakpoint = initial_breakpoint
def trigger(self, dbg, exc):
dbg.del_bp(self, targets=[dbg.current_process])
return self.initial_breakpoint.ret_trigger(dbg, exc)
class FunctionCallBP(Breakpoint):
"""A Breakpoint that allow to trigger at the return of a function"""
def break_on_ret(self, dbg, exception):
"""Setup a breakpoint at the return address of the function, this breakpoint will call :func:`ret_trigger`"""
return_addr = self.get_ret_addr(dbg, exception)
dbg.add_bp(FunctionRetBP(return_addr, self), target=dbg.current_process)
def get_ret_addr(self, dbg, exception):
"""Get the return address of the current target, only valid in the trigger() function."""
cproc = dbg.current_process
return dbg.current_process.read_ptr(dbg.current_thread.context.sp)
def ret_trigger(self, dbg, exception):
"""Called at the return of the function if :func:`break_on_ret` was called"""
raise NotImplementedError("ret_trigger")
class FunctionBP(FunctionCallBP, FunctionParamDumpBP):
"""A breakpoint that accepts a function from :mod:`windows.winproxy` and able to:
- Extract the arguments of the functions
- Break at the return of the function
"""
class PrintBP(Breakpoint):
def __init__(self, addr, format, func=None):
super(PrintBP, self).__init__(addr)
self.format = format
self.func = func
def trigger(self, dbg, exc):
thread = dbg.current_thread
format_dict = {"dbg": dbg, "exc": exc, "proc": dbg.current_process, "thread": thread, "ctx": thread.context}
if self.func:
format_dict.update(self.func(**format_dict))
print(self.format.format(**format_dict))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,305 @@
from collections import defaultdict
from contextlib import contextmanager
import windows
import windows.winobject.exception as winexception
from windows import winproxy
from windows.generated_def import windef
from windows.generated_def.winstructs import *
from .breakpoints import *
class FakeDebuggerCurrentThread(object):
"""A pseudo thread representing the current thread at exception time"""
def __init__(self, dbg):
self.dbg = dbg
@property
def tid(self):
return windows.current_thread.tid
@property
def context(self):
"""!!! This context in-place modification will be effective without set_context"""
return self.dbg.get_exception_context()
def set_context(self, context):
# The context returned by 'self.context' already modify the return context in place..
pass
class LocalDebugger(object):
"""A debugger interface around :func:`AddVectoredExceptionHandler`.
Handle:
* Standard BP (int3)
* Hardware-Exec BP (DrX)
"""
def __init__(self):
self.breakpoints = {}
self._memory_save = {}
self._reput_breakpoint = {}
self._hxbp_breakpoint = defaultdict(dict)
self.callback_vectored = winexception.VectoredException(self.callback)
winproxy.AddVectoredExceptionHandler(0, self.callback_vectored)
self.setup_hxbp_callback_vectored = winexception.VectoredException(self.setup_hxbp_callback)
self.hxbp_info = None
self.code = windows.native_exec.create_function(b"\xcc\xc3", [PVOID])
self.veh_depth = 0
self.current_exception = None
self.exceptions_stack = [None]
self.current_process = windows.current_process
self.current_thread = FakeDebuggerCurrentThread(self)
@contextmanager
def NewCurrentException(self, exc):
try:
self.exceptions_stack.append(exc)
self.current_exception = exc
self.veh_depth += 1
yield exc
finally:
self.exceptions_stack.pop()
self.current_exception = self.exceptions_stack[-1]
self.veh_depth -= 1
def get_exception_code(self):
"""Return ExceptionCode of current exception"""
return self.current_exception[0].ExceptionRecord[0].ExceptionCode
def get_exception_context(self):
"""Return context of current exception"""
return self.current_exception[0].ContextRecord[0]
def single_step(self):
"""Make the current thread to single step"""
self.get_exception_context().EEFlags.TF = 1
return windef.EXCEPTION_CONTINUE_EXECUTION
def _pass_breakpoint(self, addr, single_step):
with windows.utils.VirtualProtected(addr, 1, PAGE_EXECUTE_READWRITE):
windows.current_process.write_memory(addr, self._memory_save[addr])
self._reput_breakpoint[windows.current_thread.tid] = self.breakpoints[addr], single_step
return self.single_step()
def _local_resolve(self, addr):
if not isinstance(addr, basestring):
return addr
dll, api = addr.split("!")
dll = dll.lower()
modules = {m.name[:-len(".dll")] if m.name.endswith(".dll") else m.name : m for m in windows.current_process.peb.modules}
mod = None
if dll in modules:
mod = [modules[dll]]
if not mod:
return None
# TODO: optim exports are the same for whole system (32 vs 64 bits)
# I don't have to reparse the exports each time..
# Try to interpret api as an int
try:
api_int = int(api, 0)
return mod[0].baseaddr + api_int
except ValueError:
pass
exports = mod[0].pe.exports
if api not in exports:
dbgprint("Error resolving <{0}> in local process".format(addr, target), "DBG")
raise ValueError("Unknown API <{0}> in DLL {1}".format(api, dll))
return exports[api]
def callback(self, exc):
with self.NewCurrentException(exc):
return self.handle_exception(exc)
def handle_exception(self, exc):
exp_code = self.get_exception_code()
context = self.get_exception_context()
exp_addr = context.pc
if exp_code == EXCEPTION_BREAKPOINT and exp_addr in self.breakpoints:
res = self.breakpoints[exp_addr].trigger(self, exc)
single_step = self.get_exception_context().EEFlags.TF # single step activated by breakpoint
if exp_addr in self.breakpoints: # Breakpoint deleted itself ?
return self._pass_breakpoint(exp_addr, single_step)
return EXCEPTION_CONTINUE_EXECUTION
if exp_code == EXCEPTION_SINGLE_STEP and windows.current_thread.tid in self._reput_breakpoint:
bp, single_step = self._reput_breakpoint[windows.current_thread.tid]
self._memory_save[bp._addr] = windows.current_process.read_memory(bp._addr, 1)
with windows.utils.VirtualProtected(bp._addr, 1, PAGE_EXECUTE_READWRITE):
windows.current_process.write_memory(bp._addr, b"\xcc")
del self._reput_breakpoint[windows.current_thread.tid]
if single_step:
return self.on_exception(exc)
return windef.EXCEPTION_CONTINUE_EXECUTION
elif exp_code == EXCEPTION_SINGLE_STEP and exp_addr in self._hxbp_breakpoint[windows.current_thread.tid]:
res = self._hxbp_breakpoint[windows.current_thread.tid][exp_addr].trigger(self, exc)
context.EEFlags.RF = 1
return EXCEPTION_CONTINUE_EXECUTION
return self.on_exception(exc)
def on_exception(self, exc):
"""Called on exception"""
if not self.get_exception_code() in winexception.exception_name_by_value:
return windef.EXCEPTION_CONTINUE_SEARCH
return windef.EXCEPTION_CONTINUE_EXECUTION
def del_bp(self, bp, targets=None):
"""Delete a breakpoint"""
# TODO: check targets..
if bp.type == STANDARD_BP:
with windows.utils.VirtualProtected(bp.addr, 1, PAGE_EXECUTE_READWRITE):
windows.current_process.write_memory(bp.addr, self._memory_save[bp.addr])
del self._memory_save[bp.addr]
del self.breakpoints[bp.addr]
return
if bp.type == HARDWARE_EXEC_BP:
threads_by_tid = {t.tid: t for t in windows.current_process.threads}
for tid in self._hxbp_breakpoint:
if bp.addr in self._hxbp_breakpoint[tid] and self._hxbp_breakpoint[tid][bp.addr] == bp:
if tid == windows.current_thread.tid:
self.remove_hxbp_self_thread(bp.addr)
else:
self.remove_hxbp_other_thread(bp.addr, threads_by_tid[tid])
del self._hxbp_breakpoint[tid][bp.addr]
return
raise NotImplementedError("Unknow BP type {0}".format(bp.type))
def add_bp(self, bp, target=None):
"""Add a breakpoint, bp is a "class:`Breakpoint`
If the ``bp`` type is ``STANDARD_BP``, target must be None.
If the ``bp`` type is ``HARDWARE_EXEC_BP``, target can be None (all threads), or some threads of the process
"""
if bp.type == HARDWARE_EXEC_BP:
return self.add_bp_hxbp(bp, target)
if bp.type != STANDARD_BP:
raise NotImplementedError("Unknow BP type {0}".format(bp.type))
if target not in [None, windows.current_process]:
raise ValueError("LocalDebugger: STANDARD_BP doest not support targets {0}".format(targets))
addr = self._local_resolve(bp.addr)
bp._addr = addr
self.breakpoints[addr] = bp
self._memory_save[addr] = windows.current_process.read_memory(addr, 1)
with windows.utils.VirtualProtected(addr, 1, PAGE_EXECUTE_READWRITE):
windows.current_process.write_memory(addr, b"\xcc")
return
def add_bp_hxbp(self, bp, targets=None):
if bp.type != HARDWARE_EXEC_BP:
raise NotImplementedError("Add non standard-BP in LocalDebugger")
if targets is None:
targets = windows.current_process.threads
for thread in targets:
if thread.owner.pid != windows.current_process.pid:
raise ValueError("Cannot add HXBP to target in remote process {0}".format(thread))
if thread.tid == windows.current_thread.tid:
self.setup_hxbp_self_thread(bp.addr)
else:
self.setup_hxbp_other_thread(bp.addr, thread)
self._hxbp_breakpoint[thread.tid][bp.addr] = bp
def setup_hxbp_callback(self, exc):
with self.NewCurrentException(exc):
exp_code = self.get_exception_code()
if exp_code != windef.EXCEPTION_BREAKPOINT:
return windef.EXCEPTION_CONTINUE_SEARCH
context = self.get_exception_context()
exp_addr = context.pc
hxbp_used = self.setup_hxbp_in_context(context, self.data)
windows.current_process.write_memory(exp_addr, b"\x90")
# Raising in the VEH is a bad idea..
# So better give the information to triggerer..
if hxbp_used is not None:
self.get_exception_context().func_result = exp_addr
else:
self.get_exception_context().func_result = 0
return windef.EXCEPTION_CONTINUE_EXECUTION
def remove_hxbp_callback(self, exc):
with self.NewCurrentException(exc):
exp_code = self.get_exception_code()
context = self.get_exception_context()
exp_addr = context.pc
hxbp_used = self.remove_hxbp_in_context(context, self.data)
windows.current_process.write_memory(exp_addr, b"\x90")
# Raising in the VEH is a bad idea..
# So better give the information to triggerer..
if hxbp_used is not None:
self.get_exception_context().Eax = exp_addr
else:
self.get_exception_context().Eax = 0
return windef.EXCEPTION_CONTINUE_EXECUTION
def setup_hxbp_in_context(self, context, addr):
for i in range(4):
is_used = getattr(context.EDr7, "L" + str(i))
empty_drx = str(i)
if not is_used:
context.EDr7.GE = 1
context.EDr7.LE = 1
setattr(context.EDr7, "L" + empty_drx, 1)
setattr(context, "Dr" + empty_drx, addr)
return i
return None
def remove_hxbp_in_context(self, context, addr):
for i in range(4):
target_drx = str(i)
is_used = getattr(context.EDr7, "L" + str(i))
draddr = getattr(context, "Dr" + target_drx)
if is_used and draddr == addr:
setattr(context.EDr7, "L" + target_drx, 0)
setattr(context, "Dr" + target_drx, 0)
return i
return None
def setup_hxbp_self_thread(self, addr):
if self.current_exception is not None:
x = self.setup_hxbp_in_context(self.get_exception_context(), addr)
if x is None:
raise ValueError("Could not setup HXBP")
return
self.data = addr
with winexception.VectoredExceptionHandler(1, self.setup_hxbp_callback):
x = self.code()
if x is None:
raise ValueError("Could not setup HXBP")
windows.current_process.write_memory(x, b"\xcc")
return
def setup_hxbp_other_thread(self, addr, thread):
thread.suspend()
ctx = thread.context
x = self.setup_hxbp_in_context(ctx, addr)
if x is None:
raise ValueError("Could not setup HXBP in {0}".format(thread))
thread.set_context(ctx)
thread.resume()
def remove_hxbp_self_thread(self, addr):
if self.current_exception is not None:
x = self.remove_hxbp_in_context(self.get_exception_context(), addr)
if x is None:
raise ValueError("Could not setup HXBP")
return
self.data = addr
with winexception.VectoredExceptionHandler(1, self.remove_hxbp_callback):
x = self.code()
if x is None:
raise ValueError("Could not remove HXBP")
windows.current_process.write_memory(x, b"\xcc")
return
def remove_hxbp_other_thread(self, addr, thread):
thread.suspend()
ctx = thread.context
x = self.remove_hxbp_in_context(ctx, addr)
if x is None:
raise ValueError("Could not setup HXBP in {0}".format(thread))
thread.set_context(ctx)
thread.resume()
@@ -0,0 +1,48 @@
import windows
import windows.generated_def as gdef
from windows.pycompat import int_types
from . import Debugger
from . import symbols
class SymbolDebugger(Debugger):
"""A debugger using the symbol API (hence PDB) for name resolution.
To use PDB, a correct version of dbghelp should be configured as well as ``_NT_SYMBOL_PATH``.
(See :ref:`debug_symbols_module`)
This debugger add a ``current_resolver`` variable (A :class:`~windows.debug.symbols.ProcessSymbolHandler`) for the ``current_process``.
"""
def __init__(self, *args, **kwargs):
super(SymbolDebugger, self).__init__(*args, **kwargs)
self._resolvers = {}
def _internal_on_load_dll(self, load_dll):
path = self._get_loaded_dll(load_dll)
# Path is used instead of name for naming the module (and can be set to whatever if using file handle)
x = self.current_resolver.load_module(load_dll.hFile, path=path, addr=load_dll.lpBaseOfDll)
def _internal_on_create_process(self, create_process):
# Create and setup a symbol resolver for the new process
resolver = symbols.ProcessSymbolHandler(self.current_process)
self._resolvers[self.current_process.pid] = resolver
self.current_resolver = resolver
def _update_debugger_state(self, debug_event):
super(SymbolDebugger, self)._update_debugger_state(debug_event)
self.current_resolver = self._resolvers[debug_event.dwProcessId]
def _resolve(self, addr, target):
if isinstance(addr, int_types):
return addr
if "+" in addr:
symbol, deplacement = addr.split("+", 1)
deplacement = int(deplacement, 0)
else:
symbol = addr
deplacement = 0
try:
return self.current_resolver[symbol].addr + deplacement
except WindowsError as e:
if not e.winerror in (gdef.ERROR_NOT_FOUND, gdef.ERROR_MOD_NOT_FOUND):
raise
return None
+744
View File
@@ -0,0 +1,744 @@
import os.path
import ctypes
import copy
import itertools
from collections import namedtuple
import windows
import windows.generated_def as gdef
from windows import winproxy
from windows.pycompat import basestring
DEFAULT_DBG_OPTION = gdef.SYMOPT_DEFERRED_LOADS + gdef.SYMOPT_UNDNAME
def set_dbghelp_path(path):
"""Set the path of the ``dbghelp.dll`` file to use. It allow to configure a different version of the DLL handling PDB downloading.
If ``path`` is a directory, the final ``dbghelp.dll`` will be computed as
``path\<current_process_bitness>\dbghelp.dll``.
This allow to use the same script transparently in both 32b & 64b python interpreters.
"""
loaded_modules = [m.name.lower() for m in windows.current_process.peb.modules]
if os.path.isdir(path):
path = os.path.join(path, str(windows.current_process.bitness), "dbghelp.dll")
if "dbghelp.dll" in loaded_modules:
raise ValueError("setup_dbghelp_path should be called before any dbghelp function")
# Change the DLL used by DbgHelpProxy
winproxy.DbgHelpProxy.APIDLL = path
return
# Load symbol config from ENV if present
try:
env_dbghelp_path = os.environ["PFW_DBGHELP_PATH"]
# Setup the dbghelp path used by PFW
set_dbghelp_path(env_dbghelp_path)
except KeyError as e:
pass
class SymbolInfoBase(object):
"""Represent a Symbol.
This class in based on the class `SYMBOL_INFO <https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-symbol_info>`_
with the handling on displacement embeded into it.
"""
# Init on ctypes struct is not always called
# resolver & displacement should be set manually
CHAR_TYPE = None
def __init__(self, *args, **kwargs):
self.resolver = kwargs.get("resolver", None)
#: POUET POUET
self.displacement = kwargs.get("displacement", 0) #: POUET POUET
def as_type(self):
# assert self.Address == 0 ?
return SymbolType(self.Index, self.ModBase, self.resolver)
@property
def name(self):
"""The name of the symbol"""
if not self.NameLen:
return None
size = self.NameLen
addr = ctypes.addressof(self) + type(self).Name.offset
return (self.CHAR_TYPE * size).from_address(addr)[:]
@property
def fullname(self):
"""The fullname of the symbol in the windbg format ``mod!sym+displacement``"""
return str(self)
@property
def addr(self):
"""The address of the symbol"""
return self.Address + self.displacement
@property
def start(self):
"""The address of the start of the symbol
If the symbol include a displacement, it is not taken into account
"""
return self.Address
@property # Fixed ?
def module(self):
"""The module containing the symbol
:type: :class:`SymbolModule`
"""
return self.resolver.get_module(self.ModBase)
@property
def tag(self):
"""The Tag of the module
:type: :class:`~windows.generated_def.winstructs.SymTagEnum`
"""
return gdef.SymTagEnum.mapper[self.Tag]
def __int__(self):
"""An alias for ``addr``"""
return self.addr
def __str__(self):
"""The fullname of the symbol in the windbg format ``mod!sym+displacement``"""
if self.displacement:
return "{self.module.name}!{self.name}+{self.displacement:#x}".format(self=self)
return "{self.module.name}!{self.name}".format(self=self)
def __repr__(self):
if self.displacement:
return '<{0} name="{1}" start={2:#x} displacement={3:#x} tag={4}>'.format(type(self).__name__, self.name, self.start, self.displacement, self.tag.name)
return '<{0} name="{1}" start={2:#x} tag={3}>'.format(type(self).__name__, self.name, self.start, self.tag.name)
class SymbolInfoA(gdef.SYMBOL_INFO, SymbolInfoBase):
"""Represent a Symbol.
This class in based on the class `SYMBOL_INFO <https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-symbol_info>`_
with the handling on displacement embeded into it.s
Exemple:
>>> sh = windows.debug.symbols.VirtualSymbolHandler()
>>> mod = sh.load_file(r"c:\windows\system32\kernelbase.dll")
>>> sym1 = sh["kernelbase!CreateFileW"]
>>> sym2 = sh[int(sym1) + 3]
>>> sym2
<SymbolInfoA name="CreateFileW" start=0x100f20b0 displacement=0x3 tag=SymTagPublicSymbol>
>>> hex(sym2.start)
'0x100f20b0L'
>>> hex(sym2.addr)
'0x100f20b3L'
>>> hex(sym2.displacement)
'0x3L'
>>> str(sym2)
'kernelbase!CreateFileW+0x3'
"""
CHAR_TYPE = gdef.CHAR
class SymbolInfoW(gdef.SYMBOL_INFOW, SymbolInfoBase):
CHAR_TYPE = gdef.WCHAR
# We use the A Api in our code (for now)
SymbolInfo = SymbolInfoA
class SymbolType(object):
def __init__(self, typeid, modbase, resolver):
# Inheritance ?
self.resolver = resolver
self._typeid = typeid # Kind of a handle. Different of typeid property.
self.modbase = modbase
def _get_type_info(self, typeinfo, ires=None):
res = ires
if res is None:
res = TST_TYPE_RES_TYPE.get(typeinfo, gdef.DWORD)()
windows.winproxy.SymGetTypeInfo(self.resolver.handle, self.modbase, self._typeid, typeinfo, ctypes.byref(res))
if ires is not None:
return ires
newres = res.value
if isinstance(res, gdef.LPWSTR):
windows.winproxy.LocalFree(res)
return newres
@property
def name(self):
return self._get_type_info(gdef.TI_GET_SYMNAME)
@property
def size(self):
return self._get_type_info(gdef.TI_GET_LENGTH)
@property
def tag(self):
return self._get_type_info(gdef.TI_GET_SYMTAG)
# Diff type/typeid ?
@property
def type(self):
return self.new_typeid(self._get_type_info(gdef.TI_GET_TYPE))
@property
def typeid(self):
return self.new_typeid(self._get_type_info(gdef.TI_GET_TYPEID))
@property
def basetype(self):
return gdef.BasicType.mapper[self._get_type_info(gdef.TI_GET_BASETYPE)]
@property
def parent(self):
return self.new_typeid(self._get_type_info(gdef.TI_GET_CLASSPARENTID))
@property
def datakind(self):
return gdef.DataKind.mapper[self._get_type_info(gdef.TI_GET_DATAKIND)]
@property
def udtkind(self):
return gdef.UdtKind.mapper[self._get_type_info(gdef.TI_GET_UDTKIND)]
@property
def offset(self):
return self._get_type_info(gdef.TI_GET_OFFSET)
@property
def nb_children(self):
return self._get_type_info(gdef.TI_GET_CHILDRENCOUNT)
@property
def value(self):
return self._get_type_info(gdef.TI_GET_VALUE)
@property
def children(self):
count = self.nb_children
class res_struct(ctypes.Structure):
_fields_ = [("Count", gdef.ULONG), ("Start", gdef.ULONG), ("Types", (gdef.ULONG * count))]
x = res_struct()
x.Count = count
x.Start = 0
self._get_type_info(gdef.TI_FINDCHILDREN, x)
return [self.new_typeid(ch) for ch in x.Types]
# Constructor
@classmethod
def from_symbol_info(cls, syminfo, resolver):
return cls(syminfo.TypeIndex, syminfo.ModBase, resolver)
# Constructor
def new_typeid(self, newtypeid):
return type(self)(newtypeid, self.modbase, self.resolver)
def __repr__(self):
if self.tag == gdef.SymTagBaseType:
return '<{0} <basetype> {1!r}>'.format(type(self).__name__, self.basetype)
elif self.tag == gdef.SymTagPointerType:
target_type = self.type.name
return '<{0} PTR TO "{1}" tag={2}>'.format(type(self).__name__, target_type, self.tag)
return '<{0} name="{1}" tag={2}>'.format(type(self).__name__, self.name, self.tag)
class SymbolModule(gdef.IMAGEHLP_MODULE64):
"""Represent a loaded symbol module
(see `MSDN IMAGEHLP_MODULE64 <https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-imagehlp_module64>`_)
.. note::
This represent a module in the ``symbol space`` for symbol resolution.
This can be completly virtual (particularly in the case of :class:`VirtualSymbolHandler`
"""
# Init on ctypes struct is not always called
# resolver should be set manually
def __init__(self, resolver):
self.resolver = resolver
@property
def addr(self):
"""The load address of the module"""
return self.BaseOfImage
@property
def name(self):
"""The name of the module"""
return self.ModuleName
@property
def path(self):
"""The full path and file name of the file from which symbols were loaded."""
return self.LoadedImageName
@property
def type(self):
"""The type of module (:class:`~windows.generated_def.winstructs.SYM_TYPE`),
which can be one of:
=========== =========================
SymCoff COFF symbols.
SymCv CodeView symbols.
SymDeferred Symbol loading deferred.
SymDia DIA symbols.
SymExport Symbols generated from a DLL export table.
SymNone No symbols are loaded.
SymPdb PDB symbols.
SymSym .sym file.
SymVirtual The virtual module created by SymLoadModuleEx with SLMFLAG_VIRTUAL.
=========== =========================
"""
return self.SymType
@property
def pdb(self):
"""The local path of the loaded PDB if present
Exemple:
>>> sh = windows.debug.symbols.VirtualSymbolHandler()
>>> mod = sh.load_file(r"c:\windows\system32\kernelbase.dll")
>>> mod.pdb
'd:\\symbols\\wkernelbase.pdb\\017FA9C5278235B7E6BFBA74A9A5AAD91\\wkernelbase.pdb'
"""
LoadedPdbName = self.LoadedPdbName
if not LoadedPdbName:
return None
return LoadedPdbName
def __repr__(self):
pdb_basename = self.LoadedPdbName.split(b"\\")[-1]
return '<{0} name="{1}" type={2} pdb="{3}" addr={4:#x}>'.format(type(self).__name__, self.name, self.type.value.name, pdb_basename, self.addr)
# https://docs.microsoft.com/en-us/windows/win32/debug/symbol-handler-initialization
class SymbolHandler(object):
"""Base class of symbol handler"""
def __init__(self, handle, search_path=None, invade_process=False):
# https://docs.microsoft.com/en-us/windows/desktop/api/dbghelp/nf-dbghelp-syminitialize
# This value should be unique and nonzero, but need not be a process handle.
# be sure to use the correct handle.
self.handle = handle #: The handle of the symbol handler
if not engine.options_already_setup:
engine.set_options(DEFAULT_DBG_OPTION)
winproxy.SymInitialize(handle, search_path, invade_process)
def load_module(self, file_handle=None, path=None, name=None, addr=0, size=0, data=None, flags=0):
"""Load a module at a given ``addr``. The module to load can be pass via a ``file_handle``
or the direct ``path`` of the file to load.
:return: :class:`SymbolModule` -- The loaded module
.. note::
The logic of ``SymLoadModuleEx`` seems somewhat strange about the naming of the loaded module.
A custom module ``name`` is only taken into account if the file is passed via a File handle.
To make it more intuitive, if this function is call with a ``path`` and ``name`` and no ``file_handle``,
it will open the path and directly call ``SymLoadModuleEx`` with a file handle and a name.
"""
# Is that a bug in SymLoadModuleEx ?
# To get a custom name for a module it use "path"
# So we need to use file_handle and set a custom path
# ! BUT it means we cannot get a custom name for a module where the path is not explicit and need to be searched
if name is not None and file_handle is None and os.path.exists(path):
try:
f = open(path)
file_handle = windows.utils.get_handle_from_file(f)
path = name
except Exception as e:
pass
# Expect a-string
path = windows.pycompat.raw_encode(path)
try:
load_addr = winproxy.SymLoadModuleEx(self.handle, file_handle, path, name, addr, size, data, flags)
except WindowsError as e:
# if e.winerror == 0:
# Already loaded ?
# What if someone try to load another PE at the same BaseOfDll ?
# return BaseOfDll
raise
return self.get_module(load_addr)
def load_file(self, path, name=None, addr=0, size=0, data=None, flags=0):
"""Load the module ``path`` at ``addr``
:return: :class:`SymbolModule` -- The loaded module
"""
return self.load_module(path=path, name=name, addr=addr, size=size, data=data, flags=flags)
def unload(self, addr):
"""Unload the module at ``addr``"""
return winproxy.SymUnloadModule64(self.handle, addr)
@staticmethod
@ctypes.WINFUNCTYPE(gdef.BOOL, gdef.PCSTR, gdef.DWORD64, ctypes.py_object)
def modules_aggregator(modname, modaddr, ctx):
ctx.append(modaddr)
return True
@property
def modules(self):
"""The list of loaded modules
:return: [:class:`SymbolModule`] -- A list of modules
"""
res = []
windows.winproxy.SymEnumerateModules64(self.handle, self.modules_aggregator, res)
return [self.get_module(addr) for addr in res]
def get_module(self, base):
modinfo = SymbolModule(self)
modinfo.SizeOfStruct = ctypes.sizeof(modinfo)
winproxy.SymGetModuleInfo64(self.handle, base, modinfo)
return modinfo
def symbol_and_displacement_from_address(self, addr):
displacement = gdef.DWORD64()
max_len_size = 0x1000
full_size = ctypes.sizeof(SymbolInfo) + (max_len_size - 1)
buff = windows.utils.BUFFER(SymbolInfo)(size=full_size)
sym = buff[0]
sym.SizeOfStruct = ctypes.sizeof(SymbolInfo)
sym.MaxNameLen = max_len_size
winproxy.SymFromAddr(self.handle, addr, displacement, buff) # SymFromAddrW ?
sym.resolver = self
sym.displacement = displacement.value
return sym
def symbol_from_name(self, name):
max_len_size = 0x1000
full_size = ctypes.sizeof(SymbolInfo) + (max_len_size - 1)
buff = windows.utils.BUFFER(SymbolInfo)(size=full_size)
sym = buff[0]
sym.SizeOfStruct = ctypes.sizeof(SymbolInfo)
sym.MaxNameLen = max_len_size
# Expect a-string
name = windows.pycompat.raw_encode(name)
windows.winproxy.SymFromName(self.handle, name, buff)
sym.resolver = self
sym.displacement = 0
return sym
def resolve(self, name_or_addr):
"""Resolve ``name_or_addr``.
If its an int -> Return the :class:`SymbolInfo` at the address.
If its a string -> Return the :class:`SymbolInfo` corresponding to the symbol name
:return: :class:`SymbolInfo`
.. note::
``__getitem__`` is an alias for ``resolve()``
Exemple:
>>> sh = windows.debug.symbols.VirtualSymbolHandler()
>>> mod = sh.load_file(r"c:\windows\system32\kernelbase.dll")
>>> mod
<SymbolModule name="kernelbase" type=SymPdb pdb="wkernelbase.pdb" addr=0x10000000>
>>> sh.resolve("kernelbase!CreateFileInternal")
<SymbolInfoA name="CreateFileInternal" addr=0x100f2120 tag=SymTagFunction>
>>> sh[0x100f2042]
<SymbolInfoA name="ReadFile" addr=0x100f1ee0 displacement=0x162 tag=SymTagFunction>
>>> str(sh[0x100f2042])
'kernelbase!ReadFile+0x162'
"""
# Only returns None if symbol is not Found ?
if isinstance(name_or_addr, windows.pycompat.anybuff):
return self.symbol_from_name(name_or_addr)
try:
return self.symbol_and_displacement_from_address(name_or_addr)
except WindowsError as e:
if e.winerror != gdef.ERROR_MOD_NOT_FOUND:
raise
# We could not resolve and address -> return None
return None
__getitem__ = resolve
"""Alias to resolve for simpler use"""
@staticmethod
@ctypes.WINFUNCTYPE(gdef.BOOL, ctypes.POINTER(SymbolInfo), gdef.ULONG , ctypes.py_object)
def simple_aggregator(info, size, ctx):
sym = info[0]
fullsize = sym.SizeOfStruct + sym.NameLen
cpy = windows.utils.BUFFER(SymbolInfo)(size=fullsize)
ctypes.memmove(cpy, info, fullsize)
ctx.append(cpy[0])
return True
def search(self, mask, mod=0, tag=0, options=gdef.SYMSEARCH_ALLITEMS, callback=None):
"""Search the symbols matching ``mask`` (``Windbg`` like).
:return: [:class:`SymbolInfo`] -- A list of :class:`SymbolInfo`
>>> sh = windows.debug.symbols.VirtualSymbolHandler()
>>> mod = sh.load_file(r"c:\windows\system32\kernelbase.dll")
>>> sh.search("kernelbase!CreateFile*")
[<SymbolInfoA name="CreateFileInternal" addr=0x100f2120 tag=SymTagFunction>,
<SymbolInfoA name="CreateFileMoniker" addr=0x10117d80 tag=SymTagFunction>,
<SymbolInfoA name="CreateFile2" addr=0x1011e690 tag=SymTagFunction>,
...]
"""
res = []
if callback is None:
callback = self.simple_aggregator
else:
callback = ctypes.WINFUNCTYPE(gdef.BOOL, ctypes.POINTER(SymbolInfo), gdef.ULONG , ctypes.py_object)(callback)
addr = getattr(mod, "addr", mod) # Retrieve mod.addr, else us the value directly
# Expect A-string
mask = windows.pycompat.raw_encode(mask)
windows.winproxy.SymSearch(self.handle, gdef.DWORD64(addr), 0, tag, mask, 0, callback, res, options)
for sym in res:
sym.resolver = self
sym.displacement = 0
return res
def get_symbols(self, addr, callback=None):
res = []
if callback is None:
callback = self.simple_aggregator
else:
callback = ctypes.WINFUNCTYPE(gdef.BOOL, ctypes.POINTER(SymbolInfo), gdef.ULONG , ctypes.py_object)(callback)
try:
windows.winproxy.SymEnumSymbolsForAddr(self.handle, addr, callback, res)
except WindowsError as e:
if e.winerror == gdef.ERROR_MOD_NOT_FOUND:
return []
raise
for sym in res:
sym.resolver = self
sym.displacement = 0
return res
# Type stuff
def get_type(self, name, mod=0):
max_len_size = 0x1000
full_size = ctypes.sizeof(SymbolInfo) + (max_len_size - 1)
buff = windows.utils.BUFFER(SymbolInfo)(size=full_size)
buff[0].SizeOfStruct = ctypes.sizeof(SymbolInfo)
buff[0].MaxNameLen = max_len_size
windows.winproxy.SymGetTypeFromName(self.handle, mod, name, buff)
return SymbolType.from_symbol_info(buff[0], resolver=self)
# TODO: mets de l'huile pour w4kfu
class StackWalker(object):
def __init__(self, resolver, process=None, thread=None, context=None):
self.resolver = resolver
if process is None and thread is None:
raise ValueError("At least a process or thread must be provided")
if process is None:
process = thread.owner
self.process = process
self.thread = thread
self.context = context
if windows.current_process.bitness == 32 and process.bitness == 64:
raise NotImplementedError("StackWalking 64b does not seems to works from 32b process")
def _stack_frame_generator(self):
ctx, machine = self._get_effective_context_and_machine()
frame = self._setup_initial_frame_from_context(ctx, machine)
thread_handle = self.thread.handle if self.thread else None
while True:
try:
windows.winproxy.StackWalkEx(machine,
# dbg.current_process.handle,
self.resolver.handle,
thread_handle,
# 0,
frame,
ctypes.byref(ctx),
None,
winproxy.resolve(winproxy.SymFunctionTableAccess64),
winproxy.resolve(winproxy.SymGetModuleBase64),
None,
0)
except WindowsError as e:
if not e.winerror:
return # No_ERROR -> end of stack walking
raise
yield type(frame).from_buffer_copy(frame) # Make a copy ?
def __iter__(self):
return self._stack_frame_generator()
# Autorise to force the retrieving of 32b stack when code is currently on 64b code ?
def _get_effective_context_and_machine(self):
ctx = self.context or self.thread.context
if self.process.bitness == 32:
# Process is 32b, so the context is inevitably x86
return (ctx, gdef.IMAGE_FILE_MACHINE_I386)
if windows.current_process.bitness == 32:
# If we are 32b, we will only be able to handle x86 stack
# ctx is obligatory a 32b one, as the case us32/target64 is handled
# in __init__ with a NotImplementedError
return (ctx, gdef.IMAGE_FILE_MACHINE_I386)
if self.process.bitness == 64:
# Process is 64b, so the context is inevitably x64
return (ctx, gdef.IMAGE_FILE_MACHINE_AMD64)
# Thing get a little more complicated here :)
# We are a 64b process and target is 32b.
# So we must find-out if we are in 32 or 64b world at the moment.
# The context_syswow.SegCS give us the information
# The context32.SegCs would be always 32
ctxsyswow = dbg.current_thread.context_syswow
if ctxsyswow.SegCs == gdef.CS_USER_32B:
return (ctx, gdef.IMAGE_FILE_MACHINE_I386)
return (ctxsyswow, gdef.IMAGE_FILE_MACHINE_AMD64)
def _setup_initial_frame_from_context(self, ctx, machine):
frame = gdef.STACKFRAME_EX()
frame.AddrPC.Mode = gdef.AddrModeFlat
frame.AddrFrame.Mode = gdef.AddrModeFlat
frame.AddrStack.Mode = gdef.AddrModeFlat
frame.AddrPC.Offset = ctx.pc
frame.AddrStack.Offset = ctx.sp
if machine == gdef.IMAGE_FILE_MACHINE_I386:
frame.AddrFrame.Offset = ctx.Ebp
# Need RBP on 64b ?
return frame
class VirtualSymbolHandler(SymbolHandler):
"""A SymbolHandler where its handle is not a valid process handle
Allow to create/resolve symbol in a 'virtual' process
But all API needing a real process handle will fail
"""
VIRTUAL_HANDLER_COUNTER = itertools.count(0x11223344)
def __init__(self, search_path=None):
handle = next(self.VIRTUAL_HANDLER_COUNTER)
super(VirtualSymbolHandler, self).__init__(handle, search_path, False)
# The VirtualSymbolHandler is not based on an existing process
# So load() in its simplest for should just take the path of the file to load
load = SymbolHandler.load_file
"""An alias for :func:`VirtualSymbolHandler.load_file`"""
def refresh(self):
"""Do nothing for a :class:`VirtualSymbolHandler`"""
return False
class ProcessSymbolHandler(SymbolHandler):
def __init__(self, process, search_path=None, invade_process=False):
super(ProcessSymbolHandler, self).__init__(process.handle, search_path, invade_process)
self.target = process
# The ProcessSymbolHandler is based on an existing process
# So load() in its simplest form should be able to load the symbol for an existing
# module that is already loaded
# Question: should be able to load other module at other address ?
def load(self, name):
"""Load the :class:`SymbolModule` associated with the loaded module ``name`` (as found in the PEB)
:return: :class:`SymbolModule`
Exemple:
>>> sh = windows.debug.symbols.ProcessSymbolHandler(windows.test.pop_proc_64())
<windows.debug.symbols.ProcessSymbolHandler object at 0x033A2C30>
>>> sh
<windows.debug.symbols.ProcessSymbolHandler object at 0x033A2C30>
>>> sh.load("kernelbase.dll")
<SymbolModule name="kernelbase" type=SymDeferred pdb="" addr=0x7ffb5b090000>
>>> sh["kernelbase!CreateProcessA"]
<SymbolInfoA name="CreateProcessA" start=0x7ffb5b2371f0 tag=SymTagPublicSymbol>
"""
mods = [x for x in self.target.peb.modules if x.name == name]
if not mods:
raise ValueError("Could not find module <{0}>".format(name))
assert len(mods) == 1 # Load all if multiple match ?
mod = mods[0]
return self.load_module(addr=mod.baseaddr, path=mod.fullname)
def refresh(self):
"""Update the list of loaded modules to match the modules present in the target process
.. note::
This function only call `SymRefreshModuleList <https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symrefreshmodulelist>`_ for now.
It seems that this function do not handle refreshing a 64b target from a 32b python
Also, on a 32b target from a 64b python it seems to only load symbols for the 64b modules (ntdll + syswow dll)
Exemple:
>>> sh = windows.debug.symbols.ProcessSymbolHandler(windows.test.pop_proc_64())
>>> sh.modules
[]
>>> sh.refresh()
44
>>> sh.modules
[<SymbolModule name="notepad" type=SymDeferred pdb="" addr=0x7ff772b80000>,
<SymbolModule name="ntdll" type=SymDeferred pdb="" addr=0x7ffb5d860000>,
<SymbolModule name="KERNEL32" type=SymDeferred pdb="" addr=0x7ffb5bb90000>,
<SymbolModule name="KERNELBASE" type=SymDeferred pdb="" addr=0x7ffb5b090000>,
...]
"""
return windows.winproxy.SymRefreshModuleList(self.handle)
def stackwalk(self, ctx):
pass
class SymbolEngine(object):
"""Represent the global symbol engine. Just a proxy to get/set global engine options
Its instance can be accessed using ``windows.debug.symbols.engine``
Exemple:
>>> windows.debug.symbols.engine.options
6L
>>> windows.debug.symbols.engine.options = gdef.SYMOPT_UNDNAME
>>> windows.debug.symbols.engine.options
2L
"""
def __init__(self):
# use to now if we need to call the setup of options
# At the first DbgHelp call
self.options_already_setup = False
def set_options(self, options):
self.options_already_setup = True
return windows.winproxy.SymSetOptions(options)
def get_options(self):
return windows.winproxy.SymGetOptions()
options = property(get_options, set_options)
"""The options of the Symbol engine
(`see options <https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symsetoptions#parameters>`_)
.. note::
Default options are: ``gdef.SYMOPT_DEFERRED_LOADS + gdef.SYMOPT_UNDNAME``
"""
engine = SymbolEngine()
"""The instance of the :class:`SymbolEngine`"""
TST_TYPE_RES_TYPE = {
gdef.TI_GET_SYMNAME: gdef.LPWSTR,
gdef.TI_GET_LENGTH: gdef.ULONG64,
gdef.TI_GET_ADDRESS: gdef.ULONG64,
gdef.TI_GTIEX_REQS_VALID: gdef.ULONG64,
gdef.TI_GET_SYMTAG: gdef.SymTagEnum,
gdef.TI_GET_VALUE: windows.com.Variant,
}