mirror of
https://github.com/vivisect/vivisect
synced 2026-06-08 18:04:23 +00:00
4979ea079f
* 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>
434 lines
15 KiB
Python
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()
|