Files
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

1602 lines
51 KiB
Python

"""
Vtrace Debugger Framework
Vtrace is a *mostly* native python debugging framework which
can be used to quickly write programatic debuggers and research
tools.
I'm not known for writting great docs... but the code should
be pretty straight forward...
This has been in use for many years privately, but is nowhere
*near* free of bugs... idiosyncracies abound.
==== Werd =====================================================
Blah blah blah... many more docs to come.
Brought to you by kenshoto. e-mail invisigoth.
Greetz:
h1kari - eeeeeooorrrmmm CHKCHKCHKCHKCHKCHKCHK
Ghetto - wizoo... to the tizoot.
atlas - *whew* finally... no more teasing...
beatle/dnm - come out and play yo!
The Kenshoto Gophers.
Blackhats Everywhere.
"""
# Copyright (C) 2007 Invisigoth - See LICENSE file for details
import os
import re
import code
import time
import types
import logging
import platform
import envi
import envi.bits as e_bits
import envi.memory as e_mem
import envi.registers as e_reg
import envi.expression as e_expr
import envi.symstore.resolver as e_resolv
import envi.symstore.symcache as e_symcache
import vstruct
from vtrace.const import *
logger = logging.getLogger(__name__)
remote = None # If set, we're a vtrace client (set to serverhost)
cobra_daemon = None
port = 0x5656
verbose = False
# File Descriptor / Handle Types
FD_UNKNOWN = 0 # Unknown or we don't have a type for it
FD_FILE = 1
FD_SOCKET = 2
FD_PIPE = 3
FD_LOCK = 4 # Win32 Mutant/Lock/Semaphore
FD_EVENT = 5 # Win32 Event/KeyedEvent
FD_THREAD = 6 # Win32 Thread
FD_REGKEY = 7 # Win32 Registry Key
# Vtrace Symbol Types
SYM_MISC = -1
SYM_GLOBAL = 0 # Global (mostly vars)
SYM_LOCAL = 1 # Locals
SYM_FUNCTION = 2 # Functions
SYM_SECTION = 3 # Binary section
SYM_META = 4 # Info that we enumerate
# Vtrace Symbol Offsets
VSYM_NAME = 0
VSYM_ADDR = 1
VSYM_SIZE = 2
VSYM_TYPE = 3
VSYM_FILE = 4
from vtrace.rmi import *
from vtrace.notifiers import *
from vtrace.breakpoints import *
from vtrace.watchpoints import *
import vtrace.util as v_util
class PlatformException(Exception):
"""
A universal way to represent a failure in the
platform layer for this tracer. platformFoo methods
should raise this rather than allowing their platform
specific exception types (which don't likely pickle, or
are not cross platform)
"""
pass
class AccessViolation(Exception):
"""
An exception which is raised on bad-touch to memory
"""
def __init__(self, va, perm=0):
self.va = va
self.perm = perm
Exception.__init__(self, "AccessViolation at 0x%.8x (%d)" % (va, perm))
class Trace(e_mem.IMemory, e_reg.RegisterContext, e_resolv.SymbolResolver, object):
"""
The main tracer object. A trace instance is dynamically generated using
this and *many* potential mixin classes. However, API users should *not*
worry about the methods that come from the mixins... Everything that is
*meant* to be used from the API is contained and documented here.
"""
def __init__(self, archname=None):
# For the crazy thread-call-proxy-thing
# (must come first for __getattribute__
self.requires_thread = {}
self.proxymeth = None # FIXME hack for now...
self._released = False
# The universal place for all modes
# that might be platform dependant...
self.modes = {}
self.modedocs = {}
self.notifiers = {}
self.namecache = []
# For all transient data (if notifiers want
# to track stuff per-trace
self.metadata = {}
self.initMode("RunForever", False, "Run until RunForever = False")
self.initMode("NonBlocking", False, "A call to wait() fires a thread to wait *for* you")
self.initMode("ThreadProxy", True, "Proxy necessary requests through a single thread (can deadlock...)")
self.initMode("SingleStep", False, "All calls to run() actually just step. This allows RunForever + SingleStep to step forever ;)")
self.initMode("FastStep", False, "All stepi() will NOT generate a step event")
self.regcache = None
self.regcachedirty = False
self.sus_threads = {} # A dictionary of suspended threads
# Set if we're a server and this trace is proxied
self.proxy = None
# Set us up with an envi arch module
# FIXME eventually we should just inherit one...
if archname is None:
archname = envi.getCurrentArch()
arch = envi.getArchByName(archname)
self.setMeta('Architecture', archname)
self.arch = envi.getArchModule(name=archname)
e_resolv.SymbolResolver.__init__(self, width=self.arch.getPointerSize())
e_mem.IMemory.__init__(self, arch=arch)
e_reg.RegisterContext.__init__(self)
# Add event numbers to here for auto-continue
self.auto_continue = [NOTIFY_LOAD_LIBRARY, NOTIFY_CREATE_THREAD, NOTIFY_UNLOAD_LIBRARY, NOTIFY_EXIT_THREAD, NOTIFY_DEBUG_PRINT]
# Create a LoadLibrary hook to enable simple and consistent
# Break-On-Load/Init functionality. This is also necessary for
# resolving symbols on new library loads.
self.registerNotifier(NOTIFY_LOAD_LIBRARY, LibraryNotifier())
def setBreakOnLibraryLoad(self, setting=True):
'''
Cause execution to halt when a new library is loaded.
'''
logger.info("setting 'BreakOnLibraryLoad' to %r", setting)
self.setMeta('BreakOnLibraryLoad', setting)
def setBreakOnLibraryInit(self, setting=True):
'''
Set breakpoint on a newly loaded Library's init function
(aka. <Libname>.__entry)
'''
logger.info("setting 'BreakOnLibraryInit' to %r", setting)
self.setMeta('BreakOnLibraryInit', setting)
def execute(self, cmdline):
"""
Start a new process and debug it
"""
if self.isAttached():
raise Exception("ERROR - Tracer must first be detached before you can execute()")
pid = self.platformExec(cmdline)
self._justAttached(pid)
self.setMeta('ExecCommand', cmdline)
self.wait()
def parseOpcodes(self, num, va=None):
'''
Returns next num of linear disasm'd opcodes objects. Optionally pass
a va to start there instead of the current program counter.
'''
if num <= 0:
raise Exception('you must specify a positive number of opcodes')
if va is None:
va = self.getProgramCounter()
ops = []
for i in range(0, num):
op = self.parseOpcode(va)
ops.append(op)
va += op.size
return ops
def getCurrentSignal(self):
'''
Retrieve the current signal/exception posted to the process.
If there are no pending signals/exceptions the API will return
None. For POSIX systems, this will be a traditional POSIX signal.
For Windows systems it will be a current exception code (if any).
Example: sig = trace.getCurrentSignal()
'''
return self.platformGetSignal()
def setCurrentSignal(self, sig=None):
'''
Set the currently pending signal for delivery to the target process on
continue. This is intended for use by programs wishing the mask or
change the delivery of exceptions on a NOTIFY_SIGNAL event.
Example: trace.setCurrentSignal(None)
'''
return self.platformSetSignal(sig)
def addIgnoreSignal(self, code, address=0):
"""
By adding an IgnoreSignal you tell the tracer object to
supress the notification of a particular type of signal.
In POSIX, these are regular signals, in Win32, these
are exception codes. This is mostly useful in RunForever
mode because you still need the process to begin running again.
(these may be viewed/modified by the metadata key "IgnoredSignals")
FIXME: make address do something.
"""
self.getMeta("IgnoredSignals").append(code)
def delIgnoreSignal(self, code, address=0):
"""
See addIgnoreSignal for a description of signal ignoring.
This removes an ignored signal and re-enables it's delivery.
"""
self.getMeta("IgnoredSignals").remove(code)
def attach(self, pid):
"""
Attach to a new process ID.
"""
if self.isAttached():
self.detach()
try:
self.platformAttach(pid)
self._justAttached(pid)
self.wait()
except Exception as msg:
raise PlatformException(str(msg))
def stepi(self):
"""
Single step the target process ONE instruction (and do
NOT activate breakpoints for the one step). Also, we
don't deliver pending signals for the single step...
Use the mode FastStep to allow/suppress notifier callbacks on step
"""
self.requireNotRunning()
# Since we don't go through the normal run/wait
# code, we have a little house-keeping to do...
self.curbp = None
self._syncRegs()
self.platformStepi()
event = self.platformWait()
self.platformProcessEvent(event)
def run(self, until=None):
"""
Allow the traced target to continue execution. (Depending on the mode
"Blocking" this will either block until an event, or return immediately)
Additionally, the argument until may be used to cause execution to
continue until the specified address is reached (internally uses and
removes a breakpoint).
"""
self.requireAttached()
self.requireNotRunning()
self.requireNotExited()
if self.getMode("SingleStep", False):
self.steploop()
else:
if until is not None:
self.setMode("RunForever", True)
self.addBreakpoint(StopAndRemoveBreak(until))
self._doRun()
self.wait()
def runAgain(self, val=True):
"""
The runAgain() method may be used from inside a notifier
(Notifier, Breakpoint, Watchpoint, etc...) to inform the trace
that once event processing is complete, it should continue
running the trace.
"""
self.runagain = val
def kill(self):
"""
Kill the target process for this trace (will result in process
exit and fire appropriate notifiers)
"""
self.requireAttached()
self.requireNotExited()
self.platformKill()
self.attached = False
def detach(self):
'''
Detach from the currently attached process.
'''
self.requireNotRunning()
self.fireNotifiers(NOTIFY_DETACH)
self._syncRegs()
self.platformDetach()
self.attached = False
self.pid = 0
self.mapcache = None
def release(self):
'''
Release resources for this tracer. This API should be called
once you are done with the trace.
'''
if not self._released:
self._released = True
if self.attached:
self.detach()
self._cleanupResources()
self.platformRelease()
def getPid(self):
"""
Return the pid for this Trace
"""
return self.pid
def getNormalizedLibNames(self):
"""
Symbols are stored internally based off of
"normalized" library names. This method returns
the list of normalized names for the loaded libraries.
(probably only useful for writing symbol browsers...)
"""
return list(self.getMeta("LibraryBases").keys())
def getSymsForFile(self, libname):
"""
Return the entire symbol list for the specified
normalized library name. The list is returned as
"symtup" tuples of (va,size,name,type,fname).
"""
self._loadBinaryNorm(libname)
sym = self.getSymByName(libname)
if sym is None:
raise Exception('Invalid Library Name: %s' % libname)
return sym.getSymList()
def getNames(self):
if not self.namecache:
names = []
for lib in self.getNormalizedLibNames():
for sym in self.getSymsForFile(lib):
names.append((sym.value, str(sym)))
self.namecache = sorted(names, key=lambda n: len(n[1]), reverse=True)
return self.namecache
def getSymByAddr(self, addr, exact=True):
"""
Return an envi Symbol object for an address.
Use exact=False to get the nearest previous match.
"""
# NOTE: Override this from envi.SymbolResolver to do on-demand
# file parsing.
r = e_resolv.SymbolResolver.getSymByAddr(self, addr, exact=exact)
if r is not None:
return r
# See if we need to parse the file.
mmap = self.getMemoryMap(addr)
if mmap is None:
return None
va, size, perms, fname = mmap
if not self._loadBinary(fname):
return None
# Take a second shot after parsing
return e_resolv.SymbolResolver.getSymByAddr(self, addr, exact=exact)
def getSymByAddrThunkAware(self, va):
'''
TODO: DO NOT USE THIS FUNCTION, GOING AWAY.
getBestSymEtc? depth / aggressiveness?
for the given va:
1. attempt to get the sym by using getSymByAddr
2. if 1 fails, check the target of the branch for a sym.
returns a tuple (sym, is_thunk). sym is None if no sym is found.
'''
sym = self.getSymByAddr(va)
if sym is not None:
return str(sym), False
try:
op = self.parseOpcode(va)
for tva, tflags in op.getTargets(emu=self):
if tva is None:
continue
sym = self.getSymByAddr(tva)
if sym is not None:
return str(sym), True
except Exception as e:
# getTargets->readMemory error on bva
logger.warning('getSymByAddrThunkAware: %s', e)
return None, False
def getSymByName(self, name):
"""
Return an envi.Symbol object for the given name (or None)
"""
self._loadBinaryNorm(name)
return e_resolv.SymbolResolver.getSymByName(self, name)
def setSymCachePath(self, path):
'''
Set the symbol cache path for the tracer.
The "path" syntax is a ; seperated list of either directories
or cobra URIs which implement the SymbolCache interface.
Example:
trace.setSymCachePath('/home/invisigoth/.envi/symcache;cobra://symbols.com/SymbolCache')
NOTE: vdb automatically handles this with a config option
'''
self.symcache = e_symcache.SymbolCachePath(path)
def searchSymbols(self, regex, libname=None):
'''
Search for symbols which match the given regular expression. Specify
libname as the "normalized" library name to only search the specified
lib.
Example: for sym in trace.searchSymbols('.*CreateFile.*', 'kernel32'):
'''
reobj = re.compile(regex)
if libname is not None:
libs = [libname, ]
else:
libs = self.getNormalizedLibNames()
ret = []
for lname in libs:
for sym in self.getSymsForFile(lname):
symstr = str(sym)
if reobj.match(symstr):
ret.append(sym)
return ret
def getRegisterContext(self, threadid=None):
"""
Retrieve the envi.registers.RegisterContext object for the
specified thread. Use this API to iterate over threads
register values without setting the global tracer thread context.
"""
if threadid is None:
threadid = self.getMeta("ThreadId")
return self._cacheRegs(threadid)
#######################################################################
#
# We mirror the RegisterContext API using our own thread index based
# cache. These APIs must stay in sync with envi.registers.RegisterContext
# NOTE: for now we only need to over-ride get/setRegister because all the
# higher level APIs call them.
#
def getRegister(self, idx):
ctx = self.getRegisterContext()
return ctx.getRegister(idx)
def setRegister(self, idx, value):
ctx = self.getRegisterContext()
ctx.setRegister(idx, value)
#######################################################################
def allocateMemory(self, size, perms=e_mem.MM_RWX, suggestaddr=0):
"""
Allocate a chunk of memory inside the target process' address
space. Memory wil be mapped rwx unless otherwise specified with
perms=envi.memory.MM_FOO values. Optionally you may *suggest* an address
to the allocator, but there is no guarentee. Returns the mapped
memory address.
"""
self.requireNotRunning()
self.mapcache = None # We may have a new memory map
return self.platformAllocateMemory(size, perms=perms, suggestaddr=suggestaddr)
def protectMemory(self, va, size, perms):
"""
Change the page protections on the specified region of memory.
See envi.memory for perms values.
"""
self.requireNotRunning()
self.mapcache = None # We may have new memory protections
return self.platformProtectMemory(va, size, perms)
def readMemory(self, address, size):
"""
Read memory from address. Areas that are NOT valid memory will be read
back as \x00s (this probably goes in a mixin soon)
"""
self.requireNotRunning()
return self.platformReadMemory(int(address), int(size))
def writeMemory(self, address, bytez):
"""
Write the given bytes to the address in the current trace.
"""
self.requireNotRunning()
self.platformWriteMemory(int(address), bytez)
def searchMemory(self, needle, regex=False):
"""
Search all of process memory for a sequence of bytes.
"""
ret = e_mem.IMemory.searchMemory(self, needle, regex=regex)
self.setMeta('search', ret)
self.setVariable('search', ret)
return ret
def searchMemoryRange(self, needle, address, size, regex=False):
"""
Search a memory range for the specified sequence of bytes
"""
ret = e_mem.IMemory.searchMemoryRange(self, needle, address, size, regex=regex)
self.setMeta('search', ret)
self.setVariable('search', ret)
return ret
def setMeta(self, name, value):
"""
Set some metadata. Metadata is a clean way for
arbitrary trace consumers (and notifiers) to present
and track additional information in trace objects.
Any modules which use this *should* initialize them
on attach (so when they get re-used they're clean)
Some examples of metadata used:
ShouldBreak - We're expecting a non-signal related break
ExitCode - The int() exit code (if exited)
PendingSignal - The current signal
"""
self.metadata[name] = value
def getMeta(self, name, default=None):
"""
Get some metadata. Metadata is a clean way for
arbitrary trace consumers (and notifiers) to present
and track additional information in trace objects.
If you specify a default and the key doesn't exist, not
not only will the default be returned, but the key will
be set to the default specified.
"""
if default is not None:
if name not in self.metadata:
self.metadata[name] = default
return self.metadata.get(name, None)
def hasMeta(self, name):
"""
Check to see if a metadata key exists... Mostly un-necessary
as getMeta() with a default will set the key to the default
if non-existant.
"""
return name in self.metadata
def getMode(self, name, default=False):
"""
Get the value for a mode setting allowing
for a clean default...
"""
return self.modes.get(name, default)
def setMode(self, name, value):
"""
Set a mode setting... This is ONLY valid
if that mode has been iniitialized with
initMode(name, value). Otherwise, it's an
unsupported mode for this platform ;) cute huh?
This way, platform sections can cleanly setmodes
and such.
"""
if name not in self.modes:
raise Exception("Mode %s not supported on this platform" % name)
self.modes[name] = bool(value)
def injectso(self, filename):
"""
Inject a shared object into the target of the trace. So, on windows
this is easy with InjectDll and on *nix... it's.. fugly...
NOTE: This method will likely cause the trace to run. Do not call from
within a notifier!
"""
self.requireNotRunning()
self.platformInjectSo(filename)
def ps(self):
"""
Return a list of proccesses which are currently running on the
system.
(pid, name)
"""
return self.platformPs()
def addBreakByExpr(self, symname, fastbreak=False):
'''
Add a breakpoint by resolving an expression. This will create
the Breakpoint object for you and add it to the trace. It
returns the newly created breakpoint id.
Optionally, set fastbreak=True to have the breakpoint behave in
"fast break" mode which automatically continues execution and does
not fire notifiers for the breakpoint.
Example: trace.addBreakByExpr('kernel32.CreateFileA + ecx')
'''
bp = Breakpoint(None, expression=symname)
bp.fastbreak = fastbreak
return self.addBreakpoint(bp)
def addBreakByAddr(self, va, fastbreak=False):
'''
Add a breakpoint by address. This will create the Breakpoint
object for you and add it to the trace. It returns the newly
created breakpoint id.
Optionally, set fastbreak=True to have the breakpoint behave in
"fast break" mode which automatically continues execution and does
not fire notifiers for the breakpoint.
Example: trace.addBreakByAddr(0x7c770308)
'''
bp = Breakpoint(va)
bp.fastbreak = fastbreak
return self.addBreakpoint(bp)
def addBreakpoint(self, breakpoint):
"""
Add a breakpoint/watchpoint to the trace. The "breakpoint" argument
is a vtrace Breakpoint/Watchpoint object or something that extends it.
To add a basic breakpoint use:
trace.addBreakpoint(vtrace.Breakpoint(address))
NOTE: expression breakpoints do *not* get evaluated in fastbreak mode
This will return the internal ID given to the new breakpoint
"""
breakpoint.inittrace(self)
breakpoint.id = self.nextBpId()
addr = breakpoint.resolveAddress(self)
if addr is None:
self.bpbyid[breakpoint.id] = breakpoint
self.deferred.append(breakpoint)
return breakpoint.id
if addr in self.breakpoints:
raise Exception("ERROR: Duplicate break for address 0x%.8x" % addr)
self.bpbyid[breakpoint.id] = breakpoint
self.breakpoints[addr] = breakpoint
# fastbreaks are always active... (except when they're not...)
if breakpoint.fastbreak:
breakpoint.activate(self)
return breakpoint.id
def removeBreakpoint(self, id):
"""
Remove the breakpoint with the specified ID
"""
self.requireAttached()
bp = self.bpbyid.pop(id, None)
if bp is not None and not bp.stealthbreak:
bp.deactivate(self)
if bp in self.deferred:
self.deferred.remove(bp)
else:
self.breakpoints.pop(bp.address, None)
# If the bp is also curbp, set curbp to None
if self.curbp == bp:
self.curbp = None
# Remove cached breakpoint code
Breakpoint.bpcodeobj.pop(id, None)
def _updateBreakAddresses(self):
"""
Update breakpoint address resolution (if unresolved).
Intended to be run after events which change the namespace, such as
NOTIFY_LOAD_LIBRARY events
"""
for bp in self.deferred:
bp.resolveAddress(self)
if bp.address is not None:
self.breakpoints[bp.address] = bp
self.deferred.remove(bp)
bp.activate(self)
logger.warning("Resolved bp address: %r", bp)
def getCurrentBreakpoint(self):
"""
Return the current breakpoint otherwise None
"""
return self.curbp
def getBreakpoint(self, id):
"""
Return a reference to the breakpoint with the requested ID.
NOTE: NEVER set locals or use things like setBreakpointCode()
method on return'd breakpoint objects as they may be remote
and would then be *coppies* of the bp objects. (use the trace's
setBreakpointCode() instead).
"""
return self.bpbyid.get(id)
def getBreakpointByAddr(self, va):
'''
Return the breakpoint object (or None) for a given virtual address.
'''
return self.breakpoints.get(va)
def getBreakpoints(self):
"""
Return a list of the current breakpoints.
"""
return list(self.bpbyid.values())
def getBreakpointEnabled(self, bpid):
"""
An accessor method for returning if a breakpoint is
currently enabled.
NOTE: code which wants to be remote-safe should use this
"""
bp = self.getBreakpoint(bpid)
if bp is None:
raise Exception("Breakpoint %d Not Found" % bpid)
return bp.isEnabled()
def setBreakpointEnabled(self, bpid, enabled=True):
"""
An accessor method for setting a breakpoint enabled/disabled.
NOTE: code which wants to be remote-safe should use this
"""
bp = self.getBreakpoint(bpid)
if bp is None or bp.stealthbreak:
raise Exception("Breakpoint %d Not Found" % bpid)
if not enabled: # To catch the "disable" of fastbreaks...
bp.deactivate(self)
return bp.setEnabled(enabled)
def setBreakpointCode(self, bpid, pystr):
"""
Because breakpoints are potentially on the remote debugger
and code is not pickleable in python, special access methods
which takes strings of python code are necessary for the
vdb interface to quick script breakpoint code. Use this method
to set the python code for this breakpoint.
"""
bp = self.getBreakpoint(bpid)
if bp is None:
raise Exception("Breakpoint %d Not Found" % bpid)
bp.setBreakpointCode(pystr)
def getBreakpointCode(self, bpid):
"""
Return the python string of user specified code that will run
when this breakpoint is hit.
"""
bp = self.getBreakpoint(bpid)
if bp is not None:
return bp.getBreakpointCode()
return None
def call(self, address, args, convention=None):
"""
Setup the "stack" and call the target address with the following
arguments. If the argument is a string or a buffer, copy that into
memory and hand in the argument.
The current state of ALL registers are returned as a dictionary at the
end of the call...
Additionally, a "convention" string may be specified that the underlying
platform may be able to interpret...
"""
self.requireNotRunning()
return self.platformCall(address, args, convention)
def registerNotifier(self, event, notifier):
"""
Register a notifier who will be called for various
events. See NOTIFY_* constants for handler hooks.
"""
nlist = self.notifiers.get(event, None)
if nlist:
nlist.append(notifier)
else:
nlist = []
nlist.append(notifier)
self.notifiers[event] = nlist
def deregisterNotifier(self, event, notifier):
nlist = self.notifiers.get(event, [])
if notifier in nlist:
nlist.remove(notifier)
def getNotifiers(self, event):
return self.notifiers.get(event, [])
def requireNotExited(self):
'''
Call in a method that requires the trace to have not exited.
'''
if self.exited:
raise Exception('ERROR - Request invalid for trace which exited')
def requireNotRunning(self):
'''
Call in a method that requires the debugger the be attached and not
running.
'''
self.requireAttached()
if self.isRunning():
raise Exception('ERROR - trace is running; use "break" before running the specified command')
def requireAttached(self):
'''
Call in a method that requires the debugger to be attached.
'''
if not self.attached:
raise Exception('ERROR - attach to a process first')
def getFds(self):
"""
Get a list of (fd, type, bestname) pairs. This is MOSTLY useful
for HUMON consumtion... or giving HUMONs consumption...
"""
self.requireNotRunning()
if not self.fds:
self.fds = self.platformGetFds()
return self.fds
def getMemoryMaps(self):
"""
Return a list of the currently mapped memory for the target
process. This is acomplished by calling the platform's
platformGetMaps() mixin method. This will also cache the
results until CONTINUE. The format is (addr, len, perms, file).
"""
self.requireNotRunning()
if not self.mapcache:
self.mapcache = self.platformGetMaps()
return self.mapcache
def getMemoryFault(self):
'''
If the most receent event is a memory access error, this API will
return a tuple of (<addr>, <perm>) on supported platforms. Otherwise,
a (None, None) will result.
Example:
import envi.memory as e_mem
vaddr, vperm = trace.getMemoryFault()
if vaddr is not None:
print('Memory Fault At: 0x%.8x (perm: %d)' % (vaddr, vperm))
'''
return self.platformGetMemFault()
def isAttached(self):
'''
Return true or false if this trace's target processing is attached.
'''
return self.attached
def isRunning(self):
'''
Return true or false if this trace's target process is running.
'''
return self.running
def hasExited(self):
'''
Return true or false if this trace's target process has exited.
'''
return self.exited
def isRemote(self):
'''
Return true or false if this trace's target process is a CobraProxy
object to a trace on another system.
'''
return False
def enableAutoContinue(self, event):
"""
Put the tracer object in to AutoContinue mode
for the specified event. To make all events
continue running see RunForever mode in setMode().
"""
if event not in self.auto_continue:
self.auto_continue.append(event)
def disableAutoContinue(self, event):
"""
Disable Auto Continue for the specified
event.
"""
if event in self.auto_continue:
self.auto_continue.remove(event)
def getAutoContinueList(self):
"""
Retrieve the list of vtrace notification events
that will be auto-continued.
"""
return list(self.auto_continue)
def parseExpression(self, expression):
"""
Parse a python expression with many useful helpers mapped
into the execution namespace.
Example: trace.parseExpression("ispoi(ecx+ntdll.RtlAllocateHeap)")
"""
vlocs = VtraceExpressionLocals(self)
return int(e_expr.evaluate(expression, vlocs))
def sendBreak(self):
"""
Send an asynchronous break signal to the target process.
This is only valid if the target is actually running...
"""
self.requireAttached()
self.setMode("RunForever", False)
self.setMeta("ShouldBreak", True)
self.platformSendBreak()
time.sleep(0.01)
# If we're non-blocking, we gotta wait...
if self.getMode("NonBlocking", True):
while self.isRunning():
time.sleep(0.01)
def getStackTrace(self):
"""
Returns a list of (instruction pointer, stack frame) tuples.
If stack tracing results in an error, the error entry will
be (-1, -1). Otherwise most platforms end up with 0, 0 as
the top stack frame
"""
# FIXME thread id argument!
return self.archGetStackTrace()
def getThreads(self):
"""
Get a dictionary of <threadid>:<tinfo> pairs where
tinfo is platform dependant, but is typically either
the top of the stack for that thread, or the TEB on
win32
"""
if not self.threadcache:
self.threadcache = self.platformGetThreads()
return self.threadcache
def getCurrentThread(self):
'''
Return the thread id of the currently selected thread.
'''
return self.getMeta('ThreadId')
def selectThread(self, threadid):
"""
Set the "current thread" context to the given thread id.
(For example stack traces and register values will depend
on the current thread context). By default the thread
responsible for an "interesting event" is selected.
"""
if threadid not in self.getThreads():
raise Exception("ERROR: Invalid threadid chosen: %d" % threadid)
self.requireNotRunning()
self.platformSelectThread(threadid)
self.setMeta("ThreadId", threadid)
def isThreadSuspended(self, threadid):
"""
Used to determine if a thread is suspended.
"""
return self.sus_threads.get(threadid, False)
def suspendThread(self, threadid):
"""
Suspend a thread by ID. This will mean that on continuing
the trace, the suspended thread will not be scheduled.
"""
self.requireNotRunning()
if self.sus_threads.get(threadid):
raise Exception("The specified thread is already suspended")
if threadid not in self.getThreads().keys():
raise Exception("There is no thread %d!" % threadid)
self.platformSuspendThread(threadid)
self.sus_threads[threadid] = True
def resumeThread(self, threadid):
"""
Resume a suspended thread.
"""
self.requireNotRunning()
if not self.sus_threads.get(threadid):
raise Exception("The specified thread is not suspended")
self.platformResumeThread(threadid)
self.sus_threads.pop(threadid)
def injectThread(self, pc):
"""
Create a new thread inside the target process. This thread
will begin execution on the next process run().
"""
self.requireNotRunning()
#self.platformInjectThread(pc)
pass
def joinThread(self, threadid):
'''
Run the trace in a loop until the specified thread exits.
'''
self.setMode('RunForever', True)
self._join_thread = threadid
# Temporarily make run/wait blocking
nb = self.getMode('NonBlocking')
self.setMode('NonBlocking', False)
self.run()
self.setMode('NonBlocking', nb)
def getStructNames(self, namespace=None):
'''
This method returns either the structure names, or
the structure namespaces that the target tracer is aware
of. If "namespace" is specified, it is structures within
that namespace, otherwise it is "known namespaces"
Example: namespaces = trace.getStructNames()
ntdll_structs = trace.getStructNames(namespace='ntdll')
'''
if namespace:
return self.vsbuilder.getVStructNames(namespace=namespace)
return self.vsbuilder.getVStructNamespaceNames()
def getStruct(self, sname, va=None):
"""
Retrieve a vstruct structure optionally populated with memory from
the specified address. Returns a standard vstruct object.
"""
# Check if we need to parse symbols for a library
libbase = sname.split('.')[0]
self._loadBinaryNorm(libbase)
if self.vsbuilder.hasVStructNamespace(libbase):
vs = self.vsbuilder.buildVStruct(sname)
# FIXME this is deprecated and should die...
else:
vs = vstruct.getStructure(sname)
if vs is None:
return None
if va is None:
return vs
bytez = self.readMemory(va, len(vs))
vs.vsParse(bytez)
return vs
def setVariable(self, name, value):
"""
Set a named variable in the trace which may be used in
subsequent VtraceExpressions.
Example:
trace.setVariable("whereiam", trace.getProgramCounter())
"""
self.localvars[name] = value
def getVariable(self, name):
"""
Get the value of a previously set variable name.
(or None on not found)
"""
return self.localvars.get(name)
def getVariables(self):
"""
Get the dictionary of named variables.
"""
return dict(self.localvars)
def hex(self, value):
"""
Much like the python hex routine, except this will automatically
pad the value's string length out to pointer width.
"""
width = self.arch.getPointerSize()
return e_bits.hex(value, width)
def vprint(self, msg, addnl=True):
if addnl:
msg = msg + "\n"
return print(msg)
def buildNewTrace(self, **kwargs):
'''
Build a new/clean trace "like" this one. For platforms where a
special trace was handed in, this allows initialization of a new one.
For most implementations, this is very simple....
Example:
if need_another_trace:
newt = trace.buildNewTrace()
'''
return self.__class__(**kwargs)
class TraceGroup(Notifier, v_util.TraceManager):
"""
Encapsulate several traces, run them, and continue to
handle their event notifications.
"""
def __init__(self):
Notifier.__init__(self)
v_util.TraceManager.__init__(self)
self.traces = {}
self.go = True # A little ghetto switch for those who read the source
# We are a notify all notifier by default
self.registerNotifier(NOTIFY_ALL, self)
self.setMode("NonBlocking", True)
def setMeta(self, name, value):
"""
A trace group's setMeta function will set "persistant" metadata
which will be added again to any trace on attach. Additionally,
setting metadata on a tracegroup will cause all current traces
to get the update as well....
"""
v_util.TraceManager.setMeta(self, name, value)
for trace in self.traces.values():
trace.setMeta(name, value)
def setMode(self, name, value):
v_util.TraceManager.setMode(self, name, value)
for trace in self.getTraces():
trace.setMode(name, value)
def detachAll(self):
"""
Detach from ALL the currently targetd processes
"""
for trace in self.traces.values():
try:
if trace.isRunning():
trace.sendBreak()
trace.detach()
except:
pass
def run(self):
"""
Our run method is a little different than a traditional
trace. It will *never* block.
"""
if len(self.traces.keys()) == 0:
raise Exception("ERROR - can't run() with no traces!")
for trace in self.traces.values():
if trace.exited:
self.traces.pop(trace.pid)
trace.detach()
continue
if not trace.isRunning():
trace.run()
def execTrace(self, cmdline):
trace = getTrace()
self._initTrace(trace)
trace.execute(cmdline)
self.traces[trace.getPid()] = trace
return trace
def addTrace(self, proc):
"""
Add a new tracer to this group the "proc" argument
may be either an int for a pid (which we will attach
to) or an already attached (and broken) tracer object.
"""
if isinstance(proc, int):
trace = getTrace()
self._initTrace(trace)
self.traces[proc] = trace
try:
trace.attach(proc)
except:
self.delTrace(proc)
raise
else: # Hopefully a tracer object... if not.. you're dumb.
trace = proc
self._initTrace(trace)
self.traces[trace.getPid()] = trace
return trace
def getTrace(self):
'''
Similar to vtrace.getTrace(), but also init's
the trace for being managed by a TraceGroup.
Example:
tg = TraceGroup()
t = tg.getTrace()
t....
'''
t = getTrace()
self.addTrace(t)
return t
def _initTrace(self, trace):
"""
- INTERNAL -
Setup a tracer object to be ready for being in this
trace group (setup modes and notifiers). Only addTrace()
and execTrace() probably need to be aware of this.
"""
self.manageTrace(trace)
def delTrace(self, pid):
"""
Remove a trace from the current TraceGroup
"""
trace = self.traces.pop(pid, None)
self.unManageTrace(trace)
def getTraces(self):
"""
Return a list of the current traces
"""
return list(self.traces.values())
def getTraceByPid(self, pid):
"""
Return the the trace for process PID if we're
already attached. Return None if not.
"""
return self.traces.get(pid, None)
def notify(self, event, trace):
# Remove this trace, and free it
# on the server if present
if event == NOTIFY_EXIT:
self.delTrace(trace.getPid())
class VtraceExpressionLocals(e_expr.MemoryExpressionLocals):
"""
A class which serves as the namespace dictionary during the
evaluation of an expression on a tracer.
"""
def __init__(self, trace):
e_expr.MemoryExpressionLocals.__init__(self, trace, symobj=trace)
self.trace = trace
self.update({
'trace': trace,
'vtrace': vtrace
})
self.update({
'frame': self.frame,
'teb': self.teb,
'bp': self.bp,
'meta': self.meta,
'go': self.go,
})
def __getitem__(self, name):
# Check registers
if self.trace.isAttached() and not self.trace.isRunning():
regs = self.trace.getRegisters()
r = regs.get(name, None)
if r is not None:
return r
# Check local variables
locs = self.trace.getVariables()
r = locs.get(name, None)
if r is not None:
return r
# Check the loaded libraries
for lib in self.trace.getNormalizedLibNames():
for sym in self.trace.getSymsForFile(lib):
if str(sym) == name:
return sym
return e_expr.MemoryExpressionLocals.__getitem__(self, name)
def go(self):
'''
A shortcut for trace.runAgain() which may be used in
breakpoint code (or similar even processors) to begin
execution again after event processing...
'''
self.trace.runAgain()
def frame(self, index):
"""
Return the address of the saved base pointer for
the specified frame.
Usage: frame(<index>)
"""
stack = self.trace.getStackTrace()
return stack[index][1]
def teb(self, threadnum=None):
"""
The expression teb(threadid) will return whatever the
platform stores as the int for threadid. In the case
of windows, this is the TEB, others may be the thread
stack base or whatever. If threadid is left out, it
uses the threadid of the current thread context.
"""
if threadnum is None:
# Get the thread ID of the current Thread Context
threadnum = self.trace.getMeta("ThreadId")
teb = self.trace.getThreads().get(threadnum, None)
if teb is None:
raise Exception("ERROR - Unknown Thread Id %d" % threadnum)
return teb
def bp(self, bpid):
"""
The expression bp(0) returns the resolved address of the given
breakpoint
"""
bp = self.trace.getBreakpoint(bpid)
if bp is None:
raise Exception("Unknown Breakpoint ID: %d" % bpid)
return bp.resolveAddress(self.trace)
def meta(self, name):
"""
An expression friendly (terse) way to get trace metadata
(equiv to trace.getMeta(name))
Example: meta("foo")
"""
return self.trace.getMeta(name)
def reqTargOpt(opts, targ, opt, valstr='<value>'):
val = opts.get( opt )
if val is None:
raise Exception('Target "%s" requires option: %s=%s' % (targ, opt, valstr))
return val
def getTrace(target=None, **kwargs):
"""
Return a tracer object appropriate for this platform.
This is the function you will use to get a tracer object
with the appropriate ancestry for your host.
ex. mytrace = vtrace.getTrace()
NOTE: Use the release() method on the tracer once debugging
is complete. This releases the tracer thread and allows
garbage collection to function correctly.
Some specialized tracers may be constructed by specifying the "target"
name from one of the following list. Additionally, each "specialized"
tracer may require additional kwargs (which are listed).
Examples:
# A tracer for *this* os
t = vtrace.getTrace()
# A tracer for the gdbstub debugging a vmware 32bit hypervisor
t = vtrace.getTrace(target='vmware32', host='localhost', port=8832)
Targets:
Alpha Targets:
vmware32 -
host=<host> ( probably 'localhost' )
port=<port> ( probably 8832 )
"""
if target == 'gdbserver':
host = reqTargOpt(kwargs, 'gdbserver', 'host', '<host>')
port = reqTargOpt(kwargs, 'gdbserver', 'port', '<port>')
arch = reqTargOpt(kwargs, 'gdbserver', 'arch', '<i386|amd64|arm>')
plat = reqTargOpt(kwargs, 'gdbserver', 'plat', '<windows|linux>')
if arch not in ('i386', 'amd64', 'arm'):
raise Exception('Invalid arch specified for "gdbserver" target: %s' % arch)
if plat not in ('windows', 'linux'):
raise Exception('Invalid plat specified for "gdbserver" target: %s' % plat)
if target == 'vmware32':
import vtrace.platforms.vmware as vt_vmware
host = reqTargOpt(kwargs, 'vmware32', 'host', '<host>')
port = int( reqTargOpt(kwargs, 'vmware32', 'port', '<port>') )
plat = 'windows'
#plat = reqTargOpt(kwargs, 'vmware32', 'plat', '<windows|linux>')
#if plat not in ('windows', 'linux'):
#raise Exception('Invalid plat specified for "vmware32" target: %s' % plat)
return vt_vmware.VMWare32WindowsTrace( host=host, port=port )
if remote: #We have a remote server!
return getRemoteTrace()
# From here down, we're trying to build a trace for *this* platform!
os_name = platform.system().lower() # Like "linux", "darwin","windows"
arch = envi.getCurrentArch()
if os_name == "linux":
import vtrace.platforms.linux as v_linux
if arch == "amd64":
return v_linux.LinuxAmd64Trace()
elif arch == "i386":
return v_linux.Linuxi386Trace()
# Keep separate just in case
elif arch == "armv7l":
return v_linux.LinuxArmTrace()
elif arch == "armv6l":
return v_linux.LinuxArmTrace()
else:
raise Exception("Sorry, no linux support for %s" % arch)
elif os_name == "freebsd":
import vtrace.platforms.freebsd as v_freebsd
if arch == "i386":
return v_freebsd.FreeBSDi386Trace()
elif arch == "amd64":
return v_freebsd.FreeBSDAmd64Trace()
else:
raise Exception("Sorry, no FreeBSD support for %s" % arch)
elif os_name == "sunos5":
raise Exception("Solaris needs porting!")
#import vtrace.platforms.posix as v_posix
#import vtrace.platforms.solaris as v_solaris
#ilist.append(v_posix.PosixMixin)
#if arch == "i386":
#import vtrace.archs.intel as v_intel
#ilist.append(v_intel.i386Mixin)
#ilist.append(v_solaris.SolarisMixin)
#ilist.append(v_solaris.Solarisi386Mixin)
elif os_name == "darwin":
#if 9 not in os.getgroups():
#print 'You MUST be in the procmod group....'
#print 'Use: sudo dscl . append /Groups/procmod GroupMembership invisigoth'
#print '(put your username in there unless you want to put me in too... ;)'
#raise Exception('procmod group membership required')
if os.getuid() != 0:
logger.error('For NOW you *must* be root. There are some crazy MACH perms...')
raise Exception('You must be root for now (on OSX)....')
logger.warning('Also... the darwin port is not even REMOTELY working yet. Solid progress though...')
#'sudo dscl . append /Groups/procmod GroupMembership invisigoth'
#'sudo dscl . read /Groups/procmod GroupMembership'
import vtrace.platforms.darwin as v_darwin
if arch == 'i386':
return v_darwin.Darwini386Trace()
elif arch == 'amd64':
return v_darwin.DarwinAmd64Trace()
else:
raise Exception('Darwin not supported on %s (only i386...)' % arch)
elif os_name in ['microsoft', 'windows']:
import vtrace.platforms.win32 as v_win32
if arch == "i386":
return v_win32.Windowsi386Trace()
elif arch == "amd64":
return v_win32.WindowsAmd64Trace()
else:
raise Exception("Windows with arch %s is not supported!" % arch)
else:
raise Exception("ERROR - OS %s not supported yet" % os_name)
def interact(pid=0, server=None, trace=None):
"""
Just a cute and dirty way to get a tracer attached to a pid
and get a python interpreter instance out of it.
"""
global remote
remote = server
if trace is None:
trace = getTrace()
if pid:
trace.attach(pid)
mylocals = {}
mylocals["trace"] = trace
code.interact(local=mylocals)
def getEmu(trace, arch=envi.ARCH_DEFAULT):
'''
See comment for emulator from trace (in envitools); does not set any
registers or mem.
TODO: this really belongs in envitools, or somewhere else, but putting it
in envitools causes a circular import problem due to the TraceEmulator.
'''
if arch == envi.ARCH_DEFAULT:
arch_name = trace.getMeta('Architecture')
else:
arch_name = envi.getArchById(trace.arch)
arch_mod = envi.getArchModule(arch_name)
emu = arch_mod.getEmulator()
return emu