#!~/.wine/drive_c/Python25/python.exe # -*- coding: utf-8 -*- # Crash logger # Copyright (c) 2009-2014, Mario Vilas # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice,this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from __future__ import with_statement __revision__ = "$Id: crash_logger.py 1299 2013-12-20 09:30:55Z qvasimodo $" __all__ = [ 'LoggingEventHandler', ] import winappdbg from winappdbg import * import re import os import sys import time import traceback try: import cerealizer cerealizer.freeze_configuration() except ImportError: pass # XXX TODO # Use the "signal" module to avoid having to deal with unexpected # KeyboardInterrupt exceptions everywhere. Ideally there should be a way to # implement some form of "critical sections" (I'm using the term loosely here, # meaning "sections that can't be interrupted by the user"), something like # this: a global flag to enable and disable raising KeyboardInterrupt, and a # couple functions to set it. The function that enables back KeyboardInterrupt # should check for a queued interruption request. Some experimenting is needed # to see how well this would behave on a Windows environment. #============================================================================== # XXX TODO # * Capture stderr from the debugees? # * Unless the full memory snapshot was requested, the debugger could return # DEBUG_CONTINUE and store the crash info in the database in background, # while the debugee tries to handle the exception. class LoggingEventHandler(EventHandler): """ Event handler that logs all events to standard output. It also remembers crashes, bugs or otherwise interesting events. @type crashCollector: class @cvar crashCollector: Crash collector class. Tipically L{Crash} or a custom subclass of it. Most users don't ever need to change this. See: U{http://winappdbg.sourceforge.net/Signature.html} """ # Default crash collector is our good old Crash class. crashCollector = Crash def __init__(self, options, currentConfig = None): # Copy the user-defined options. self.options = options # Copy the configuration used in this fuzzing session. self.currentConfig = currentConfig # Create the logger object. self.logger = Logger(options.logfile, options.verbose) # Create the crash container. self.knownCrashes = self._new_crash_container() # Create the cache of resolved labels. self.labelsCache = dict() # pid -> label -> address # Create the map of target services and their process IDs. self.pidToServices = dict() # pid -> set(service...) # Create the set of services marked for restart. self.srvToRestart = set() # Call the base class constructor. super(LoggingEventHandler, self).__init__() def _new_crash_container(self): url = self.options.database if not url: return DummyCrashContainer( allowRepeatedKeys = self.options.duplicates) if url.startswith('dbm://'): url = url[6:] return CrashContainer(url, allowRepeatedKeys = self.options.duplicates) return CrashDictionary(url, allowRepeatedKeys = self.options.duplicates) # Add the crash to the database. def _add_crash(self, event, bFullReport = None, bLogEvent = True): # Unless forced either way, full reports are generated for exceptions. if bFullReport is None: bFullReport = event.get_event_code() == win32.EXCEPTION_DEBUG_EVENT # Generate a crash object. crash = self.crashCollector(event) crash.addNote('Config: %s' % self.currentConfig) # Determine if the crash was previously known. # If we're allowing duplicates, treat all crashes as new. bNew = self.options.duplicates or crash not in self.knownCrashes # Add the crash object to the container. if bNew: crash.fetch_extra_data(event, self.options.memory) self.knownCrashes.add(crash) # Log the crash event. if bLogEvent and self.logger.is_enabled(): if bFullReport and bNew: msg = crash.fullReport(bShowNotes = False) else: msg = crash.briefReport() self.logger.log_event(event, msg) # The first element of the tuple is the Crash object. # The second element is True if the crash is new, False otherwise. return crash, bNew # Determine if this is an event we must take action on. def _is_action_event(self, event): return self.options.action and \ self._is_event_in_list( event, self.options.action_events ) # Determine if this is a crash event. def _is_crash_event(self, event): return self._is_event_in_list( event, self.options.crash_events ) # Common implementation of _is_action_event() and _is_crash_event(). def _is_event_in_list(self, event, event_list): return \ ('event' in event_list) or \ ('exception' in event_list and \ (event.get_event_code() == win32.EXCEPTION_DEBUG_EVENT and \ (event.is_last_chance() or self.options.firstchance))) or \ (event.eventMethod in event_list) # Actions to take for events. def _action(self, event, crash = None): # Pause if requested. if self.options.pause: raw_input("Press enter to continue...") try: # Run the configured commands after finding a crash if requested. self._run_action_commands(event, crash) finally: # Enter interactive mode if requested. if self.options.interactive: event.debug.interactive(bConfirmQuit = False) # Most events are processed here. Others have specific quirks on when to # consider them actionable or crashes. def _default_event_processing(self, event, bFullReport = None, bLogEvent = False): crash = None bNew = True try: if self._is_crash_event(event): crash, bNew = self._add_crash(event, bFullReport, bLogEvent) finally: if bNew and self._is_action_event(event): self._action(event, crash) # Run the configured commands after finding a crash. # Wait until each command completes before executing the next. # To avoid waiting, use the "start" command. def _run_action_commands(self, event, crash = None): for action in self.options.action: if '%' in action: if not crash: crash = self.crashCollector(event) action = self._replace_action_variables(action, crash) action = "cmd.exe /c " + action system = System() process = system.start_process(action, bConsole = True) process.wait() # Make the variable replacements in an action command line string. def _replace_action_variables(self, action, crash): # %COUNT% - Number of crashes currently stored in the database if '%COUNT%' in action: action = action.replace('%COUNT%', str(len(self.knownCrashes)) ) # %EXCEPTIONCODE% - Exception code in hexa if '%EXCEPTIONCODE%' in action: if crash.exceptionCode: exceptionCode = HexDump.address(crash.exceptionCode) else: exceptionCode = HexDump.address(0) action = action.replace('%EXCEPTIONCODE%', exceptionCode) # %EVENTCODE% - Event code in hexa if '%EVENTCODE%' in action: action = action.replace('%EVENTCODE%', HexDump.address(crash.eventCode) ) # %EXCEPTION% - Exception name, human readable if '%EXCEPTION%' in action: if crash.exceptionName: exceptionName = crash.exceptionName else: exceptionName = 'Not an exception' action = action.replace('%EXCEPTION%', exceptionName) # %EVENT% - Event name, human readable if '%EVENT%' in action: action = action.replace('%EVENT%', crash.eventName) # %PC% - Contents of EIP, in hexa if '%PC%' in action: action = action.replace('%PC%', HexDump.address(crash.pc) ) # %SP% - Contents of ESP, in hexa if '%SP%' in action: action = action.replace('%SP%', HexDump.address(crash.sp) ) # %FP% - Contents of EBP, in hexa if '%FP%' in action: action = action.replace('%FP%', HexDump.address(crash.fp) ) # %WHERE% - Location of the event (a label or address) if '%WHERE%' in action: if crash.labelPC: try: labelPC = str(crash.labelPC) except UnicodeError: labelPC = HexDump.address(crash.pc) else: labelPC = HexDump.address(crash.pc) action = action.replace('%WHERE%', labelPC) return action # Get the location of the code that triggered the event. def _get_location(self, event, address): label = event.get_process().get_label_at_address(address) if label: return label return HexDump.address(address) # Log an exception as a single line of text. def _log_exception(self, event): what = event.get_exception_description() address = event.get_exception_address() where = self._get_location(event, address) if event.is_first_chance(): chance = 'first' else: chance = 'second' msg = "%s (%s chance) at %s" % (what, chance, where) self.logger.log_event(event, msg) # Set all breakpoints that can be set # at each create process or load dll event. def _set_breakpoints(self, event): method = event.debug.break_at bplist = self.options.break_at self._set_breakpoints_from_list(event, bplist, method) method = event.debug.stalk_at bplist = self.options.stalk_at self._set_breakpoints_from_list(event, bplist, method) # Set a list of breakppoints using the given method. def _set_breakpoints_from_list(self, event, bplist, method): dwProcessId = event.get_pid() aModule = event.get_module() for label in bplist: if dwProcessId not in self.labelsCache: self.labelsCache[dwProcessId] = dict() # XXX FIXME # We may have a problem here for some ambiguous labels... if label not in self.labelsCache[dwProcessId]: try: address = aModule.resolve_label(label) except ValueError, e: address = None except RuntimeError, e: address = None except WindowsError, e: address = None if address is not None: self.labelsCache[dwProcessId][label] = address try: method(dwProcessId, address) except RuntimeError: pass except WindowsError: pass #-- Events -------------------------------------------------------------------- # Handle all events not handled by the following methods. def event(self, event): self._default_event_processing(event, bLogEvent = True) # Handle the create process events. def create_process(self, event): try: try: # Log the event. if self.logger.is_enabled(): lpStartAddress = event.get_start_address() szFilename = event.get_filename() if not szFilename: szFilename = Module.unknown if lpStartAddress: where = HexDump.address(lpStartAddress) msg = "Process %s started, entry point at %s" msg = msg % (szFilename, where) else: msg = "Attached to process %s" % szFilename self.logger.log_event(event, msg) finally: # Process the event. self._default_event_processing(event) finally: # Set user-defined breakpoints for this process. self._set_breakpoints(event) # Handle the create thread events. def create_thread(self, event): try: # Log the event. if self.logger.is_enabled(): lpStartAddress = event.get_start_address() if lpStartAddress: where = self._get_location(event, lpStartAddress) msg = "Thread started, entry point at %s" % where else: msg = "Attached to thread" self.logger.log_event(event, msg) finally: # Process the event. self._default_event_processing(event) # Handle the load dll events. def load_dll(self, event): try: try: # Log the event. if self.logger.is_enabled(): aModule = event.get_module() lpBaseOfDll = aModule.get_base() fileName = aModule.get_filename() if not fileName: fileName = "a new module" msg = "Loaded %s at %s" msg = msg % (fileName, HexDump.address(lpBaseOfDll)) self.logger.log_event(event, msg) finally: # Process the event. self._default_event_processing(event) finally: # Set user-defined breakpoints for this module. self._set_breakpoints(event) # Handle the exit process events. def exit_process(self, event): try: # Log the event. msg = "Process terminated, exit code %x" % event.get_exit_code() self.logger.log_event(event, msg) finally: try: # Process the event. self._default_event_processing(event) finally: try: # Clear the labels cache for this process. dwProcessId = event.get_pid() if dwProcessId in self.labelsCache: del self.labelsCache[dwProcessId] finally: # Restart if requested. if self.options.restart: dwProcessId = event.get_pid() aProcess = event.get_process() # Find out which services were running here. # FIXME: make this more efficient! currentServices = set([ d.ServiceName.lower() for d in aProcess.get_services() ]) debuggedServices = set(self.options.service) debuggedServices.intersection_update(currentServices) # We have services dying here, mark them for restart. # They are restarted later at the debug loop. if debuggedServices: self.srvToRestart.update(currentServices) # Now check if this process had hosted any of our # target services before. If the service is stopped # externally we won't know it here, so we need to # keep this information beforehand. targetServices = self.pidToServices.pop(dwProcessId, set()) if targetServices: self.srvToRestart.update(targetServices) # No services here, restart the process directly. if not debuggedServices and not targetServices: cmdline = aProcess.get_command_line() event.debug.execl(cmdline) # Handle the exit thread events. def exit_thread(self, event): try: # Log the event. msg = "Thread terminated, exit code %x" % event.get_exit_code() self.logger.log_event(event, msg) finally: # Process the event. self._default_event_processing(event, bLogEvent = False) # Handle the unload dll events. def unload_dll(self, event): # XXX FIXME # We should be updating the labels cache here, # otherwise we might lose the breakpoints if # the dll gets unloaded and then loaded again. try: # Log the event. if self.logger.is_enabled(): aModule = event.get_module() lpBaseOfDll = aModule.get_base() fileName = aModule.get_filename() if not fileName: fileName = 'a module' msg = "Unloaded %s at %s" msg = msg % (fileName, HexDump.address(lpBaseOfDll)) self.logger.log_event(event, msg) finally: # Process the event. self._default_event_processing(event) # Handle the debug output string events. def output_string(self, event): try: # Echo the debug strings. if self.options.echo: win32.OutputDebugString( event.get_debug_string() ) finally: # Process the event. self._default_event_processing(event, bLogEvent = True) # Handle the RIP events. def rip(self, event): try: # Log the event. if self.logger.is_enabled(): errorCode = event.get_rip_error() errorType = event.get_rip_type() if errorType == 0: msg = "RIP error at thread %d, code %x" elif errorType == SLE_ERROR: msg = "RIP fatal error at thread %d, code %x" elif errorType == SLE_MINORERROR: msg = "RIP minor error at thread %d, code %x" elif errorType == SLE_WARNING: msg = "RIP warning at thread %d, code %x" else: msg = "RIP error type %d, code %%x" % errorType self.logger.log_event(event, msg % errorCode) finally: # Process the event. self._default_event_processing(event) #-- Exceptions ---------------------------------------------------------------- # Kill the process if it's a second chance exception. # Otherwise we'd get stuck in an infinite loop. def _post_exception(self, event): if hasattr(event, 'is_last_chance') and event.is_last_chance(): ## try: ## event.get_thread().set_pc( ## event.get_process().resolve_symbol('kernel32!ExitProcess') ## ) ## except Exception: event.get_process().kill() # Handle all exceptions not handled by the following methods. def exception(self, event): try: # This is almost identical to the default processing. # The difference is how logging is handled. crash = None bNew = True try: if self._is_crash_event(event): crash, bNew = self._add_crash(event, bLogEvent = True) elif self.logger.is_enabled(): self._log_exception(event) finally: if bNew and self._is_action_event(event): self._action(event, crash) finally: # Postprocessing of exceptions. self._post_exception(event.debug.lastEvent) # Some exceptions are ignored by default. # You can explicitly enable them again in the config file. def _exception_ignored_by_default(self, event, exc_name): try: # This is almost identical to the exception() method. # The difference is the logic to determine if it's a crash. crash = None bNew = True try: if self._is_crash_event(event) and \ exc_name in self.options.events: crash, bNew = self._add_crash(event, bLogEvent = True) elif self.logger.is_enabled(): self._log_exception(event) finally: if bNew and self._is_action_event(event): self._action(event, crash) finally: # Postprocessing of exceptions. self._post_exception(event.debug.lastEvent) # Unknown (most likely C++) exceptions are not crashes, # unless explicitly overriden in the config file. def unknown_exception(self, event): self._exception_ignored_by_default(event, 'unknown_exception') # Microsoft Visual C exceptions are not crashes, # unless explicitly overriden in the config file. def ms_vc_exception(self, event): self._exception_ignored_by_default(event, 'ms_vc_exception') # Breakpoint events. def breakpoint(self, event): try: # Determine if it's the first chance exception event. bFirstChance = event.is_first_chance() # Step over breakpoints. # This includes both user-defined and hardcoded in the binary. if bFirstChance: event.continueStatus = win32.DBG_EXCEPTION_HANDLED # Determine if the breakpoint is ours. bOurs = hasattr(event, 'breakpoint') and event.breakpoint # If it's not ours, determine if it's a system breakpoint. # If it's ours we don't care. bSystem = False if not bOurs: # WOW64 breakpoints. bWow64 = event.get_exception_code() == \ win32.EXCEPTION_WX86_BREAKPOINT # Other system breakpoints. bSystem = bWow64 or \ event.get_process().is_system_defined_breakpoint( event.get_exception_address()) # Our breakpoints are always actionable, but only crashes if # explicitly stated. System breakpoints are not actionable nor # crashes unless explicitly stated, or overriden by the 'break_at' # option (in that case they become "our" breakpoints). Otherwise # use the same criteria as for all debug events. crash = None bNew = True try: # Determine if it's a crash event. bIsCrash = False if bOurs or bSystem: if bWow64: bIsCrash = 'wow64_breakpoint' in \ self.options.crash_events else: bIsCrash = 'breakpoint' in self.options.crash_events else: bIsCrash = self._is_crash_event(event) # Add it as a crash if so. Always log the brief report. if bIsCrash: crash, bNew = self._add_crash(event, bFullReport = False) finally: # Must the crash be treated as new? if bNew: # Determine if we must take action. bAction = False if bOurs: bAction = True elif bSystem: if bWow64: bAction = 'wow64_breakpoint' in \ self.options.crash_events else: bAction = 'breakpoint' in self.options.crash_events else: bAction = self._is_action_event(event) # If so, take action. if bAction: self._action(event, crash) finally: # Postprocessing of exceptions. self._post_exception(event.debug.lastEvent) # WOW64 breakpoints handled by the same code as normal breakpoints. def wow64_breakpoint(self, event): self.breakpoint(event) #============================================================================== class CrashLogger (object): # Options object with its default settings. class Options (object): def __init__(self): # Targets self.attach = list() self.console = list() self.windowed = list() self.service = list() # List options self.action = list() self.break_at = list() self.stalk_at = list() # Tracing options self.pause = False self.interactive = False self.time_limit = 0 self.echo = False self.action_events = ['exception', 'output_string'] self.crash_events = ['exception', 'output_string'] # Debugging options self.autodetach = True self.hostile = False self.follow = True self.restart = False # Output options self.verbose = True self.ignore_errors = False self.logfile = None self.database = None self.duplicates = True self.firstchance = False self.memory = 0 # Read the configuration file def read_config_file(self, config): # Initial options object with default values options = self.Options() # Keep track of duplicated options opt_history = set() # Regular expression to split the command and the arguments regexp = re.compile(r'(\S+)\s+(.*)') # Open the config file with open(config, 'rU') as fd: number = 0 while 1: # Read a line line = fd.readline() if not line: break number += 1 # Strip the extra whitespace line = line.strip() # If it's a comment line or a blank line, discard it if not line or line.startswith('#'): continue # Split the option and its arguments match = regexp.match(line) if not match: msg = "cannot parse line %d of config file %s" msg = msg % (number, config) raise RuntimeError(msg) key, value = match.groups() # Targets if key == 'attach': if value: options.attach.append(value) elif key == 'console': if value: options.console.append(value) elif key == 'windowed': if value: options.windowed.append(value) elif key == 'service': if value: options.service.append(value) # List options elif key == 'break_at': options.break_at.extend(self._parse_list(value)) elif key == 'stalk_at': options.stalk_at.extend(self._parse_list(value)) elif key == 'action': options.action.append(value) # Switch options else: # Warn about duplicated options if key in opt_history: print "Warning: duplicated option %s in line %d" \ " of config file %s" % (key, number, config) print else: opt_history.add(key) # Output options if key == 'verbose': options.verbose = self._parse_boolean(value) elif key == 'logfile': options.logfile = value elif key == 'database': options.database = value elif key == 'duplicates': options.duplicates = self._parse_boolean(value) elif key == 'firstchance': options.firstchance = self._parse_boolean(value) elif key == 'memory': options.memory = int(value) elif key == 'ignore_python_errors': options.ignore_errors = self._parse_boolean(value) # Debugging options elif key == 'hostile': options.hostile = self._parse_boolean(value) elif key == 'follow': options.follow = self._parse_boolean(value) elif key == 'autodetach': options.autodetach = self._parse_boolean(value) elif key == 'restart': options.restart = self._parse_boolean(value) # Tracing options elif key == 'pause': options.pause = self._parse_boolean(value) elif key == 'interactive': options.interactive = self._parse_boolean(value) elif key == 'time_limit': options.time_limit = int(value) elif key == 'echo': options.echo = self._parse_boolean(value) elif key == 'action_events': options.action_events = self._parse_list(value) elif key == 'crash_events': options.crash_events = self._parse_list(value) # Unknown option else: msg = ("unknown option %s in line %d" " of config file %s") % (key, number, config) raise RuntimeError(msg) # Return the options object return options def parse_targets(self, options): # Get the list of attach targets system = System() system.request_debug_privileges() system.scan_processes() attach_targets = list() for token in options.attach: if not token: continue try: dwProcessId = HexInput.integer(token) except ValueError: dwProcessId = None if dwProcessId is not None: if not system.has_process(dwProcessId): raise ValueError("can't find process %d" % dwProcessId) try: process = Process(dwProcessId) process.open_handle() process.close_handle() except WindowsError, e: raise ValueError("can't open process %d: %s" % (dwProcessId, e)) attach_targets.append(dwProcessId) else: matched = system.find_processes_by_filename(token) if not matched: raise ValueError("can't find process %s" % token) for process, name in matched: dwProcessId = process.get_pid() try: process = Process(dwProcessId) process.open_handle() process.close_handle() except WindowsError, e: raise ValueError("can't open process %d: %s" % (dwProcessId, e)) attach_targets.append(dwProcessId) options.attach = attach_targets # Get the list of console programs to execute console_targets = list() for token in options.console: if not token: continue vector = System.cmdline_to_argv(token) filename = vector[0] if not os.path.exists(filename): try: filename = win32.SearchPath(None, filename, '.exe')[0] except WindowsError, e: raise ValueError("error searching for %s: %s" % (filename, str(e))) vector[0] = filename token = System.argv_to_cmdline(vector) console_targets.append(token) options.console = console_targets # Get the list of windowed programs to execute windowed_targets = list() for token in options.windowed: if not token: continue vector = System.cmdline_to_argv(token) filename = vector[0] if not os.path.exists(filename): try: filename = win32.SearchPath(None, filename, '.exe')[0] except WindowsError, e: raise ValueError("error searching for %s: %s" % (filename, str(e))) vector[0] = filename token = System.argv_to_cmdline(vector) windowed_targets.append(token) options.windowed = windowed_targets # Get the list of services to attach to service_targets = list() for token in options.service: if not token: continue try: status = System.get_service(token) except WindowsError: try: token = System.get_service_from_display_name(token) status = System.get_service(token) except WindowsError, e: raise ValueError("error searching for service %s: %s" % (token, str(e))) if not hasattr(status, 'ProcessId'): raise ValueError("service targets not supported by the current platform") service_targets.append(token.lower()) options.service = service_targets # If no targets were set at all, show an error message if not options.attach and not options.console and not options.windowed and not options.service: raise ValueError("no targets found!") def parse_options(self, options): # Warn or fail about inconsistent use of DBM databases if options.database and options.database.startswith('dbm://'): if options.memory and options.memory > 1: print "Warning: using options 'dbm' and 'memory' in combination can have a severe" print " performance penalty." print if options.duplicates: if options.verbose: print "Warning: inconsistent use of 'duplicates'" print " DBM databases do not allow duplicate entries with the same key." print " This means that when the same crash is found more than once it will be logged" print " to standard output each time, but will only be saved once into the database." print else: msg = "inconsistent use of 'duplicates': " msg += "DBM databases do not allow duplicate entries with the same key" raise ValueError(msg) # Warn about inconsistent use of time_limit if options.time_limit and options.autodetach \ and (options.windowed or options.console): count = len(options.windowed) + len(options.console) print print "Warning: inconsistent use of 'time_limit'" if count == 1: print " An execution time limit was set, but the launched process won't be killed." else: print " An execution time limit was set, but %d launched processes won't be killed." % count print " Set 'autodetach' to false to make sure debugees are killed on exit." print " Alternatively use 'attach' instead of launching new processes." print # Warn about inconsistent use of pause and interactive if options.pause and options.interactive: print "Warning: the 'pause' option is ignored when 'interactive' is set." print def _parse_list(self, value): tokens = set() for token in value.lower().split(','): token = token.strip() tokens.add(token) return tokens def _parse_boolean(self, value): value = value.strip().lower() if value == 'true' or value == 'yes' or value == 'y': return True if value == 'false' or value == 'no' or value == 'n': return False return bool(int(value)) # Run from the command line def run_from_cmdline(self, args): # Show the banner print "WinAppDbg crash logger" print "by Mario Vilas (mvilas at gmail.com)" print winappdbg.version print # TODO: use optparse for this! # TODO: move crash_report.py here # TODO: implement a GUI try: # Help message if len(args) >= 2 and args[1].strip().lower() in ('-h', '--help', '/?'): self.show_help_banner() # Debugger mode elif len(args) == 2: config = args[1] options = self.read_config_file(config) self.parse_targets(options) self.parse_options(options) self.run(config, options) # JIT debugger mode elif len(args) == 4 and args[1].strip().lower() == '--jit': config = args[2] options = self.read_config_file(config) self.parse_options(options) options.attach.append(args[3]) self.parse_targets(options) self.run(config, options) # Install as JIT debugger elif len(args) == 3 and args[1].strip().lower() == '--install': self.install_as_jit(args[2]) # Uninstall as JIT debugger elif len(args) == 2 and args[1].strip().lower() == '--uninstall': self.uninstall_as_jit() # On error show the help message else: self.show_help_banner() # Catch errors and show them on screen except Exception, e: print "Runtime error: %s" % str(e) traceback.print_exc() return def install_as_jit(self, config): # Calculate the command line to run in JIT mode # TODO maybe fix this so it works with py2exe? interpreter = os.path.abspath(sys.executable) script = os.path.abspath(__file__) config = os.path.abspath(config) argv = [interpreter, script, '--jit', config, '%ld'] cmdline = System.argv_to_cmdline(argv) # Test the config file options = self.read_config_file(config) self.parse_options(options) # Show the previous JIT debugger # TODO check if it's us already # TODO maybe keep a backup? previous = System.get_postmortem_debugger() print "Previous JIT debugger was: %s" % previous # Install as JIT debugger System.set_postmortem_debugger(cmdline) def uninstall_as_jit(self): # Remove the current JIT debugger # TODO check if it's us or some other debugger # TODO maybe restore the previous one? System.remove_postmortem_debugger() def show_help_banner(self): script = os.path.split(__file__)[1] print "Usage:" print "\t%s " % script print print "See example.cfg for details on the config file format." # Run the crash logger def run(self, config, options): # Create the event handler oldCrashCount = 0 eventHandler = LoggingEventHandler(options, config) logger = eventHandler.logger # Log the time we begin this run if options.verbose: logger.log_text("Crash logger started, %s" % time.ctime()) logger.log_text("Configuration: %s" % config) # Run try: self._run(eventHandler, options) # Log the time we finish this run finally: if options.verbose: logger.log_text("Crash logger stopped, %s" % time.ctime()) def _run(self, eventHandler, options): # Create the debug object with Debug(eventHandler, bHostileCode = options.hostile) as debug: # Run the crash logger using this debug object try: self._start_or_attach(debug, options, eventHandler) try: self._debugging_loop(debug, options, eventHandler) except Exception: if not options.verbose: raise return # Kill all debugees on exit if requested finally: if not options.autodetach: debug.kill_all(bIgnoreExceptions = True) def _start_or_attach(self, debug, options, eventHandler): logger = eventHandler.logger # Start or attach to the targets try: for pid in options.attach: debug.attach(pid) for cmdline in options.console: debug.execl(cmdline, bConsole = True, bFollow = options.follow) for cmdline in options.windowed: debug.execl(cmdline, bConsole = False, bFollow = options.follow) for service in options.service: status = System.get_service(service) if not status.ProcessId: status = self._start_service(service, logger) debug.attach(status.ProcessId) try: eventHandler.pidToServices[status.ProcessId].add(service) except KeyError: srvSet = set() srvSet.add(service) eventHandler.pidToServices[status.ProcessId] = srvSet # If the 'autodetach' was set to False, # make sure the debugees die if the debugger dies unexpectedly finally: if not options.autodetach: debug.system.set_kill_on_exit_mode(True) @staticmethod def _start_service(service, logger): # Start the service. status = System.get_service(service) try: name = System.get_service_display_name(service) except WindowsError: name = service print "Starting service \"%s\"..." % name # TODO: maybe add support for starting services with arguments? System.start_service(service) # Wait for it to start. timeout = 20 status = System.get_service(service) while status.CurrentState == win32.SERVICE_START_PENDING: timeout -= 1 if timeout <= 0: logger.log_text("Error: timed out.") msg = "Timed out waiting for service \"%s\" to start" raise Exception(msg % name) time.sleep(0.5) status = System.get_service(service) # Done. logger.log_text("Service \"%s\" started successfully." % name) return status # Main debugging loop def _debugging_loop(self, debug, options, eventHandler): # Get the logger. logger = eventHandler.logger # If there's a time limit, calculate how much is it. timedOut = False if options.time_limit: maxTime = time.time() + options.time_limit # Loop until there are no more debuggees. while debug.get_debugee_count() > 0: # Wait for debug events, with an optional timeout. while 1: if options.time_limit: timedOut = time.time() > maxTime if timedOut: break try: debug.wait(100) break except WindowsError, e: if e.winerror not in (win32.ERROR_SEM_TIMEOUT, win32.WAIT_TIMEOUT): logger.log_exc() raise # don't ignore this error except Exception: logger.log_exc() raise # don't ignore this error if timedOut: logger.log_text("Execution time limit reached") break # Dispatch the debug event and continue execution. try: try: debug.dispatch() finally: debug.cont() except Exception: logger.log_exc() if not options.ignore_errors: raise # Restart services marked for restart by the event handler. # Also attach to those services we want to debug. try: while eventHandler.srvToRestart: service = eventHandler.srvToRestart.pop() try: descriptor = self._start_service(service, logger) if service in options.service: try: debug.attach(descriptor.ProcessId) try: eventHandler.pidToServices[descriptor.ProcessId].add(service) except KeyError: srvSet = set() srvSet.add(service) eventHandler.pidToServices[descriptor.ProcessId] = srvSet except Exception: logger.log_exc() if not options.ignore_errors: raise except Exception: logger.log_exc() if not options.ignore_errors: raise except Exception: logger.log_exc() if not options.ignore_errors: raise def main(argv): try: cl = CrashLogger() return cl.run_from_cmdline(argv) except KeyboardInterrupt: print "Interrupted by the user!" if __name__ == '__main__': try: import psyco psyco.bind(main) except ImportError: pass main(sys.argv)