Files
vivisect-vivisect/vtrace/breakpoints.py
atlas0fd00m 4979ea079f Vtrace conversion and bugfixes (#406)
* lotsa work making it all work, with segments and maps and collapsing adjacent maps and bugfixes and...

* pagemap and memorymap deletion

* update unittests

* memory-related updates (and unittests for delMemoryMap)

* unittests and MapNotFoundException

* update unittest to test appropriately

* change in symstore fix

* more bugfixes for vdb/win32.  symbols are a bit of a mess and may need to be refactored in the future.

* make syms work (do_syms and do_bp) on Win32

* unittest mods per @rakuy0

* aaaaand this will make unittests break.
unittest discovered a bug in symstore about storing subresolvers... still need to fix the bug.

* lockstep emulator class and some improvements for Win32 PEB/TEB capture

* enhancements for envitools.LockstepEmulator class

* cleanup and import bugfix

* collapseMemoryMaps::strict

* bugfix:  deleting symbols and tests

* bugfix: Win32 detaching

* updates per @rakuy0

* touchups per @rakuy0

* tweaking the test to account for different versions of Linux and python

* updates per @rakuy0 and improvements from i386_emu_... PR.  we're housing the refugee code here while the cull takes place in PR#405

* update to identify FreeBSD ELF files (OS matters on some of these vtrace and emu changes)

* a few bugfixes for Elf and Vtrace handling of Elfs.

* Break On Library Init (and stub for Library Load)
and bugfix for clicking EFLAGS gui buttons.

* modified flaky unittest

* removed prints

* bugfix for event/threading for notify/breakpoints

* Finally!  LockStepper Class moving into vtrace.envitools.  Raw move here... updates to follow (for easy diffing between commits)

* bugfix

* revamped the LockStepper class (still have to remove LockStepEmulator class and revamp "lockStepEmulator" when we're done)

* oops

* bugfix

* change a few INS_SYSTEM x86/x64 instruction opcodes to be unique

* unify @rakuy0's and my lockstep emu classes and clean up

* de-x86ify

* bsd commented code removal
.gitignore to ignore docs build files

* bugfix: vwFromTrace() call to addFile was handing in the md5 object, not a serializable string (hexdigest)
bugfix: vdb snapshot wasn't capturing TEB's from a trace object, vw/emuFromTrace wasn't allowing for PEB/TEBs tracking from snapshot

* bugfix: str versus bytes

* cleanup 'Comparing' print/log message

* updates per @rakuy0

* finish the docstr (per rakuy0)

* finish the docstr (per rakuy0) (for realz this time, last commit was actually the @idlethread change)

* document user interface for InteractiveLSMon

* MM_* removed from envi.memory and left only in envi.const

* allow --LL and --LI settings to persist between different traces in the same session (instead of only for the first run and only if provided "-c /path/to/proggy" at the command line.  in the future, make this more cohesive by setting vdbbin to create the `db` and use `db.newTrace()` instead of punching low-level into vtrace.getTrace.  this will have to include handling of `platargs` which is beyond the scope of this PR currently and would unnecessarily delay review/merging

* cleanup per rakuy0

* cleanups per rakuy0

* bugfix (unittests have been screaming about a typo) and minor cleanup.

* kwargs to get new trace able to handle platform magic.
logging of --LI and --LL effects.

* yes, @rakuy0, that should do something ;)

* windows debugging privs (tested on Win7 and Win10)
debugging print statements that need to be cut/converted to logs

* Break on Library Load/Init bugfixes

* make POSIX (Linux) catch Library Loads and raise the correct notifiers.
this requires hooking a function in `ld` which causes libraries to be rediscovered and unresolved breakpoints to be resolved if possible.

* outdated code causing Python warnings.

* decouping Vtrace from VDB, where only the latter has a trace.db.config (or trace.db, for that matter)

* no prints!

* equaling out the unresolved breakpoint warnings.

* update vtrace unittest

* remove need for pywin32 (in README)

* remove dependency on pywin32 and winadmin

* vprint on both VdbTrace and Trace objects.
and string/bytes bugfixes.

* add to the BP execution context

* don't need to hand in **kwargs to `self.getTrace()` in fact, that's bad.

* REMOVE collapseMaps functionality.  no longer important since we can emulate/read across memory maps now.
there are many other bugfixes in the branch that should be merged, and this was always a little wonky.

* lost an import somewhere.

* test_privs was getting caught by unittests :)

* remove tests for the collapseMaps that i just yanked out of this PR.

* update checkprivs to use only ctypes instead of pywin32.
TODO: wrap this ability into indicating that Windows users don't have sufficient rights for debug.

* updates per @rakuy0

---------

Co-authored-by: James Gross <45212823+rakuy0@users.noreply.github.com>
2023-05-01 12:06:32 -04:00

434 lines
15 KiB
Python

"""
Breakpoint Objects
"""
# Copyright (C) 2007 Invisigoth - See LICENSE file for details
import time
import logging
from collections import defaultdict
import vtrace
logger = logging.getLogger(__name__)
class Breakpoint:
"""
Breakpoints in Vtrace are platform independant objects that
use the underlying trace objects to get things like the
program counter and the break instruction. As long as
platforms are completely implemented, all breakpoint
objects should be portable.
"""
bpcodeobj = {} # Cache compiled code objects on the class def
def __init__(self, address, expression=None):
self.resonce = False # has this addr expression been resolved yet?
self.address = address
self.enabled = True # should this BP be used/ignored
self.active = False # have we placed a BP in the code (eg. i386: \xCC)
self.silent = False # don't print "Hit Break" messages, still runs code/notifiers
self.fastbreak = False # no NOTIFY_BREAK, autocont, no NOTIFY_CONTINUE
self.stealthbreak = False # no NOTIFY_BREAK - used for hidden/system events
self._complained = False # only complain about not resolving this *once*
self.id = -1
self.vte = None
self.bpcode = None
if expression:
self.vte = expression
def getAddress(self):
"""
This will return the address for this breakpoint. If the return'd
address is None, this is a deferred breakpoint which needs to have
resolveAddress() called to attempt to set the address.
"""
return self.address
def getId(self):
return self.id
def getName(self):
if self.vte:
return str(self.vte)
return "0x%.8x" % self.address
def __repr__(self):
if self.address is None:
addr = "unresolved"
else:
addr = "0x%.8x" % self.address
return "[%d] %s %s: %s" % (self.id, addr, self.__class__.__name__, self.getName())
def inittrace(self, trace):
'''
A callback to do housekeeping at the time the breakpoint is
added to the tracer object. This should be used instead of activate
for initialization time infoz to save on time per activate call...
'''
pass
def activate(self, trace):
if not self.active:
trace.archActivBreakpoint(self.address)
self.active = True
def deactivate(self, trace):
if self.active:
trace.archClearBreakpoint(self.address)
self.active = False
def resolvedaddr(self, trace, addr):
'''
An initialization callback which will be executed when the
actual address for this breakpoint has been resolved.
'''
def resolveAddress(self, trace):
"""
Try to resolve the address for this break. If this is a statically
addressed break, just return the address. If it has an "expression"
use that to resolve the address...
"""
if self.address is None and self.vte:
try:
self.address = trace.parseExpression(self.vte)
except Exception as e:
# this will happen with unresolved breakpoints.
# depending on when resolution happens, the library may not have loaded yet.
if not self._complained:
logger.warning('Failed to resolve breakpoint address for expression: %s (delayed resolution?)', self.vte)
self._complained = True
self.address = None
# If we resolved, lets get our saved code...
if self.address is not None and not self.resonce:
self.resonce = True
self.resolvedaddr(trace, self.address)
return self.address
def isEnabled(self):
"""
Is this breakpoint "enabled"?
"""
return self.enabled
def setEnabled(self, enabled=True):
"""
Set this breakpoints "enabled" status
"""
self.enabled = enabled
def setBreakpointCode(self, pystr):
"""
Use this method to set custom python code to run when this
breakpoint gets hit. The code will have the following objects
mapped into it's namespace when run:
trace - the tracer
vtrace - the vtrace module
bp - the breakpoint
"""
self.bpcode = pystr
Breakpoint.bpcodeobj.pop(self.id, None)
def getBreakpointCode(self):
"""
Return the current python string that will be run when this break is hit.
"""
return self.bpcode
def notify(self, event, trace):
"""
Breakpoints may also extend and implement "notify" which will be
called whenever they are hit. If you want to continue the ability
for this breakpoint to have bpcode, you must call this method from
your override.
"""
if self.bpcode is not None:
cobj = Breakpoint.bpcodeobj.get(self.id, None)
if cobj is None:
fname = "BP:%d (0x%.8x)" % (self.id, self.address)
cobj = compile(self.bpcode, fname, "exec")
Breakpoint.bpcodeobj[self.id] = cobj
d = vtrace.VtraceExpressionLocals(trace)
d['bp'] = self
d['event'] = event
d['trace'] = trace
d['vprint'] = trace.vprint
if hasattr(trace, 'db'):
d['db'] = trace.db
exec(cobj, None, d)
class TrackerBreak(Breakpoint):
"""
A breakpoint which will record how many times it was hit
(by the address it was at) as metadata for the tracer.
"""
def notify(self, event, trace):
tb = trace.getMeta("TrackerBreak", None)
if tb is None:
tb = {}
trace.setMeta("TrackerBreak", tb)
tb[self.address] = (tb.get(self.address, 0) + 1)
Breakpoint.notify(self, event, trace)
class OneTimeBreak(Breakpoint):
"""
This type of breakpoint is exclusivly for marking
and code-coverage stuff. It removes itself.
(most frequently used with a continued trace)
"""
def notify(self, event, trace):
trace.removeBreakpoint(self.id)
Breakpoint.notify(self, event, trace)
class StopRunForeverBreak(Breakpoint):
"""
This breakpoint will turn off RunForever mode
on the tracer object when hit. it's a good way
to let things run on and on processing exceptions
but stop when you get to this one thing.
"""
def notify(self, event, trace):
trace.setMode("RunForever", False)
Breakpoint.notify(self, event, trace)
class StopAndRemoveBreak(Breakpoint):
"""
When hit, take the tracer out of run-forever mode and
remove this breakpoint.
"""
def notify(self, event, trace):
trace.setMode("RunForever", False)
trace.removeBreakpoint(self.id)
Breakpoint.notify(self, event, trace)
class CallBreak(Breakpoint):
"""
A special breakpoint which will restore process
state (registers in particular) when it gets hit.
This is primarily used by the call method inside
the trace object to restore original state
after a successful "call" method call.
Additionally, the endregs dict will be filled in
with the regs at the time it was hit and kept until
we get garbage collected...
"""
def __init__(self, address, saved_regs):
Breakpoint.__init__(self, address)
self.endregs = None # Filled in when we get hit
self.saved_regs = saved_regs
def notify(self, event, trace):
self.endregs = trace.getRegisters()
trace.removeBreakpoint(self.id)
trace.setRegisters(self.saved_regs)
trace.setMeta("PendingSignal", None)
class SnapshotBreak(Breakpoint):
"""
A special breakpoint type which will produce vtrace snapshots
for the target process when hit. The snapshots will be saved
to a default name of <exename>-<timestamp>.vsnap. This is not
recommended for use in heavily hit breakpoints as taking a
snapshot is processor intensive.
"""
def notify(self, event, trace):
exe = trace.getExe()
snap = trace.takeSnapshot()
snap.saveToFile("%s-%d.vsnap" % (exe, time.time()))
Breakpoint.notify(self, event, trace)
class NiceBreakpoint(Breakpoint):
'''
Calls the underlying breakpoint constructor with the correct constructor
automagically by checking the type you passed in (int vs other).
'''
def __init__(self, expr, *args, **kwargs):
if isinstance(expr, int):
vtrace.Breakpoint.__init__(self, expr, *args, **kwargs)
else:
vtrace.Breakpoint.__init__(self, None, expression=expr, *args, **kwargs)
def addHook(trace, expr, pre_callback, post_callback=None, cc=None, argc=None):
'''
Adds the specified pre and post callbacks to the specified expression.
'''
hbp = HookBreakpoint(expr, callingconv=cc, argc=argc)
addr = hbp.resolveAddress(trace)
# does a hook bp already exist in the deferred or active bplist?
ret_bp = None
if addr is None:
for dbp in trace.deferred:
if dbp.getName() == expr:
ret_bp = dbp
break
else:
ret_bp = trace.getBreakpointByAddr(addr)
if ret_bp is None:
# add a new bp, one does not exist at this location already
trace.addBreakpoint(hbp)
ret_bp = hbp
elif not isinstance(ret_bp, HookBreakpoint):
raise Exception('cannot add this hook, non-HookBreakpoint bp at this location')
ret_bp.addPreHook(pre_callback)
if post_callback is not None:
ret_bp.addPostHook(post_callback)
class HookBreakpoint(NiceBreakpoint):
'''
A special breakpoint that allows pre/post handlers to be registered on a
bp for a function. Pre-handlers are executed at the time of the break.
Post-handlers are implemented as a seperate breakpoint placed at the
return address. (as read at the time of entry to the function, this does
not handle things that manually mess with the return address within the
function)
Handlers are executed one at a time in an ordered sequence. Execution
continues without breaking to the user.
The prototype for pre hook callback handlers is:
def prehook(event, trace, ret_addr, args, callconv)
event - the event
trace - trace object
ret_addr - the return address (if calling convention is known)
args - the function arguments (if calling convention is known)
callconv - the calling convention object
The prototype for post hook callback handlers is the same.
def posthook(event, trace, saved_ret_addr, saved_args, callconv)
'''
def __init__(self, expr, callingconv=None, argc=None):
vtrace.NiceBreakpoint.__init__(self, expr)
self.fastbreak = True
self.prehooks = []
self.posthooks = []
self.cc = callingconv
self.argc = argc
# holds call information by thread id
# { tid : (ret addr, (arg0, arg1, ...) ), ... }
self.callinfo = defaultdict(list)
self.error_cb = self.defaultErrorHandler
def defaultErrorHandler(self, hook_cb_name, stre):
logger.error('Pre hook callback "%s" exception: %s', hook_cb_name, stre)
def resolvedaddr(self, trace, addr):
'''
When we get resolved, lookup in impapi the calling convention and other
details about the function. Do not do this if we were explicitly told
what to do.
'''
# told explicitly what to do, don't go look anything up
if self.cc is not None and self.argc is not None:
return
# TODO: move this out of here after we move impapi to a top-level
# package.
import vivisect.impapi as viv_impapi
# this code also exists in win32stealth, we should put this somewhere
# common
platform = trace.getMeta('Platform')
arch = trace.getMeta('Architecture')
self.impapi = viv_impapi.getImportApi(platform, arch)
cc = self.impapi.getImpApiCallConv(self.vte)
emu = vtrace.getEmu(trace)
self.cc = emu.getCallingConvention(cc)
apiargs = self.impapi.getImpApiArgs(self.vte)
if apiargs is not None:
self.argc = len(apiargs)
def addPreHook(self, callback):
self.prehooks.append(callback)
def addPostHook(self, callback):
self.posthooks.append(callback)
def runPreHookCallbacks(self, hook_cbs, event, trace, ret_addr, args):
for hook_cb in hook_cbs:
try:
hook_cb(event, trace, ret_addr, args, self.cc)
except Exception as e:
self.defaultErrorHandler(hook_cb, str(e))
def notify(self, event, trace):
ret_addr = None
args = None
if self.cc is not None:
ret_addr = self.cc.getReturnAddress(trace)
args = self.cc.getCallArgs(trace, self.argc)
self.callinfo[trace.getCurrentThread()] = (ret_addr, args)
# setup a PostHookBreakpoint on where we are headed to (if one is not
# already there) we can't do this if we don't know the calling conv
# information.
if ret_addr is not None:
ret_bp = trace.getBreakpointByAddr(ret_addr)
if ret_bp is None:
ret_bp = PostHookBreakpoint(ret_addr, self)
trace.addBreakpoint(ret_bp)
if not isinstance(ret_bp, PostHookBreakpoint):
raise Exception('cannot add PostHookBreakpoint, another type of bp exists at this location')
self.runPreHookCallbacks(self.prehooks, event, trace, ret_addr, args)
class PostHookBreakpoint(NiceBreakpoint):
def __init__(self, expr, parent_hook_bp):
vtrace.NiceBreakpoint.__init__(self, expr)
self.parent = parent_hook_bp
self.fastbreak = True
def runPostHookCallbacks(self, event, trace, saved_ret_addr, saved_args):
for hook_cb in self.parent.posthooks:
try:
hook_cb(event, trace, saved_ret_addr, saved_args, self.parent.cc)
except Exception as e:
logger.error('Post hook callback "%s" exception: %s', hook_cb, e)
def notify(self, event, trace):
tup = self.parent.callinfo.get(trace.getCurrentThread(), None)
if tup is None:
return
ret_addr, args = tup
self.runPostHookCallbacks(event, trace, ret_addr, args)
class PosixLibLoadHookBreakpoint(Breakpoint):
'''
POSIX systems need to hook DL to identfy when libraries are loaded.
'''
def __init__(self, expression):
Breakpoint.__init__(self, None, expression=expression)
self.stealthbreak = True
def notify(self, event, trace):
logger.debug("PosixLibLoadHookBreakpoint: reanalyze maps and resolve symbols")
if not trace._findLibraryMaps(b'\x7fELF', always=True):
# if we find new maps, we'll let the LOAD_LIBRARY Autoload config setting handle
# whether we continue or not. if we fire this and *don't* find a new map,
# let's just continue like nothing ever happened. "Nothing to see here."
trace.runAgain()