def fFinishedHandler(oTest, oBugReport): global bFailed, oOutputLock; if not bFailed: oOutputLock and oOutputLock.acquire(); oTest.bHasOutputLock = True; if oTest.sExpectedBugTypeId: if not oBugReport: print "- Failed test: %s" " ".join([dsBinaries_by_sISA[oTest.sISA]] + oTest.asCommandLineArguments); print " Expected: %s" % oTest.sExpectedBugTypeId; print " Got nothing"; bFailed = True; elif not oTest.sExpectedBugTypeId == oBugReport.sBugTypeId: print "- Failed test: %s" " ".join([dsBinaries_by_sISA[oTest.sISA]] + oTest.asCommandLineArguments); print " Expected: %s" % oTest.sExpectedBugTypeId; print " Reported: %s @ %s" % (oBugReport.sId, oBugReport.sBugLocation); print " %s" % (oBugReport.sBugDescription); bFailed = True; else: print "+ %s" % oTest; elif oBugReport: print "- Failed test: %s" " ".join([dsBinaries_by_sISA[oTest.sISA]] + oTest.asCommandLineArguments); print " Expected no report"; print " Reported: %s @ %s" % (oBugReport.sId, oBugReport.sBugLocation); print " %s" % (oBugReport.sBugDescription); bFailed = True; else: print "+ %s" % oTest; oOutputLock and oOutputLock.release(); oTest.bHasOutputLock = False; if dxConfig["bSaveTestReports"] and oBugReport: sFileNameBase = fsCreateFileName("%s = %s" % (oTest, oBugReport.sId)); # File name may be too long, keep trying to save it with a shorter name or output an error if that's not possible. while len(sFileNameBase) > 0: sFilePath = os.path.join(os.path.dirname(__file__), "Test reports", "%s.html" % sFileNameBase); try: oFile = open(sFilePath, "wb"); except IOError: sFileNameBase = sFileNameBase[:-1]; continue; try: oFile.write(oBugReport.sDetailsHTML); finally: oFile.close(); break; else: oOutputLock and oOutputLock.acquire(); oTest.bHasOutputLock = True; print " - Bug report cannot be saved"; bFailed = True; oOutputLock and oOutputLock.release(); oTest.bHasOutputLock = False; oTest.fFinished(); oTest.bHandlingResult = False;
def fFinishedHandler(oTest, oErrorReport): global bFailed, oOutputLock; bThisTestFailed = False; if not bFailed: oOutputLock.acquire(); if oTest.srBugId: if not oErrorReport: print "- %s" % oTest; print " => got no error"; bThisTestFailed = bFailed = True; elif not re.match("^([0-9A-F_#]{2})+ (%s) .+\.exe!.*$" % re.escape(oTest.srBugId), oErrorReport.sId): print "- %s" % oTest; print " => %s (%s)" % (oErrorReport.sId, oErrorReport.sErrorDescription); bThisTestFailed = bFailed = True; else: print "+ %s" % oTest; elif oErrorReport: print "- %s" % oTest; print " => %s (%s)" % (oErrorReport.sId, oErrorReport.sErrorDescription); bThisTestFailed = bFailed = True; else: print "+ %s" % oTest; if bThisTestFailed: print " Command line: %s" % " ".join([dsBinaries_by_sISA[oTest.sISA]] + oTest.asCommandLineArguments); oOutputLock.release(); if dxConfig["bSaveTestReports"]: sFileNameBase = fsCreateFileName("%s = %s" % (oTest, oErrorReport.sId)); # File name may be too long, keep trying to save it with a shorter name or output an error if that's not possible. while len(sFileNameBase) > 0: sFilePath = os.path.join(os.path.dirname(__file__), "Test reports", "%s.html" % sFileNameBase); try: oFile = open(sFilePath, "wb"); except IOError: sFileNameBase = sFileNameBase[:-1]; continue; try: oFile.write(oErrorReport.sHTMLDetails); finally: oFile.close(); break; else: oOutputLock.acquire(); print " - Error report cannot be saved"; bFailed = True; oOutputLock.release(); oTest.fFinished();
def cCdbWrapper_fCdbStdInOutThread(oCdbWrapper): # Create a list of commands to set up event handling. The default for any exception not explicitly mentioned is to be # handled as a second chance exception. asExceptionHandlingCommands = ["sxd *"]; # request second chance debugger break for certain exceptions that indicate the application has a bug. for sCommand, axExceptions in daxExceptionHandling.items(): for xException in axExceptions: sException = isinstance(xException, str) and xException or ("0x%08X" % xException); asExceptionHandlingCommands.append("%s %s" % (sCommand, sException)); sExceptionHandlingCommands = ";".join(asExceptionHandlingCommands); # Read the initial cdb output related to starting/attaching to the first process. asIntialCdbOutput = oCdbWrapper.fasReadOutput(); if not oCdbWrapper.bCdbRunning: return; # Turn off prompt information - it's not parsed anyway and clutters output. # Not that +dis and +ea are needed in cErrorReport_foSpecialErrorReport_STATUS_ACCESS_VIOLATION as this causes # an exmpty command to output the oCdbWrapper.fasSendCommandAndReadOutput(".prompt_allow +dis +ea -reg -src -sym"); if not oCdbWrapper.bCdbRunning: return; oCdbWrapper.asHTMLCdbStdIOBlocks.pop(-1); # This command is not relevant, remove it from the log. # Exception handlers need to be set up. oCdbWrapper.bExceptionHandlersHaveBeenSet = False; # Only fire _fApplicationRunningCallback if the application was started for the first time or resumed after it was # paused to analyze an exception. bInitialApplicationRunningCallbackFired = False; bDebuggerNeedsToResumeAttachedProcesses = len(oCdbWrapper.auProcessIdsPendingAttach) > 0; bApplicationWasPausedToAnalyzeAnException = False; # An error report will be created when needed; it is returned at the end oErrorReport = None; while asIntialCdbOutput or len(oCdbWrapper.auProcessIdsPendingAttach) + len(oCdbWrapper.auProcessIds) > 0: if asIntialCdbOutput: # First parse the intial output asCdbOutput = asIntialCdbOutput; asIntialCdbOutput = None; else: # Then attach to a process, or start or resume the application if not bInitialApplicationRunningCallbackFired or bApplicationWasPausedToAnalyzeAnException: # Application was started or resumed after an exception oCdbWrapper.fApplicationRunningCallback and oCdbWrapper.fApplicationRunningCallback(); bInitialApplicationRunningCallbackFired = True; asCdbOutput = oCdbWrapper.fasSendCommandAndReadOutput("g"); if not oCdbWrapper.bCdbRunning: return; # Save the current number of blocks of StdIO; if this exception is not relevant it can be used to remove all blocks # added while analyzing it. These blocks are not considered to contain useful information and removing them can # reduce the risk of OOM when irrelevant exceptions happens very often. The last block contains a prompt, which # will become the first analysis command's block, so it is not saved. uOriginalHTMLCdbStdIOBlocks = len(oCdbWrapper.asHTMLCdbStdIOBlocks) - 1; # If cdb is attaching to a process, make sure it worked. for sLine in asCdbOutput: oFailedAttachMatch = re.match(r"^Cannot debug pid \d+, Win32 error 0n\d+\s*$", sLine); assert not oFailedAttachMatch, "Failed to attach to process!\r\n%s" % "\r\n".join(asCdbOutput); # Find out what event caused the debugger break asLastEventOutput = oCdbWrapper.fasSendCommandAndReadOutput(".lastevent"); if not oCdbWrapper.bCdbRunning: return; # Sample output: # |Last event: 3d8.1348: Create process 3:3d8 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) # - or - # |Last event: c74.10e8: Exit process 4:c74, code 0 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) bValidLastEventOutput = len(asLastEventOutput) == 2 and re.match(r"^\s*debugger time: .*$", asLastEventOutput[1]); oEventMatch = bValidLastEventOutput and re.match( "".join([ r"^Last event: ([0-9a-f]+)\.[0-9a-f]+: ", r"(?:", r"(Create|Exit) process [0-9a-f]+\:([0-9a-f]+)(?:, code [0-9a-f]+)?", r"|", r"(.*?) \- code ([0-9a-f]+) \(!*\s*(?:first|second) chance\s*!*\)", r")\s*$", ]), asLastEventOutput[0], re.I ); assert oEventMatch, "Invalid .lastevent output:\r\n%s" % "\r\n".join(asLastEventOutput); ( sProcessIdHex, sCreateExitProcess, sCreateExitProcessIdHex, sExceptionDescription, sExceptionCode ) = oEventMatch.groups(); uProcessId = long(sProcessIdHex, 16); uExceptionCode = sExceptionCode and int(sExceptionCode, 16); if uExceptionCode in (STATUS_BREAKPOINT, STATUS_WAKE_SYSTEM_DEBUGGER) and uProcessId not in oCdbWrapper.auProcessIds: # This is assumed to be the initial breakpoint after starting/attaching to the first process or after a new # process was created by the application. This assumption may not be correct, in which case the code needs to # be modifed to check the stack to determine if this really is the initial breakpoint. But that comes at a # performance cost, so until proven otherwise, the code is based on this assumption. sCreateExitProcess = "Create"; sCreateExitProcessIdHex = sProcessIdHex; if sCreateExitProcess: # Make sure the created/exited process is the current process. assert sProcessIdHex == sCreateExitProcessIdHex, "%s vs %s" % (sProcessIdHex, sCreateExitProcessIdHex); oCdbWrapper.fHandleCreateExitProcess(sCreateExitProcess, uProcessId); # If there are more processes to attach to, do so: if len(oCdbWrapper.auProcessIdsPendingAttach) > 0: asAttachToProcess = oCdbWrapper.fasSendCommandAndReadOutput(".attach 0n%d" % oCdbWrapper.auProcessIdsPendingAttach[0]); if not oCdbWrapper.bCdbRunning: return; else: # Set up exception handling if this has not been done yet. if not oCdbWrapper.bExceptionHandlersHaveBeenSet: # Note to self: when rewriting the code, make sure not to set up exception handling before the debugger has # attached to all processes. But do so before resuming the threads. Otherwise one or more of the processes can # end up having only one thread that has a suspend count of 2 and no amount of resuming will cause the process # to run. The reason for this is unknown, but if things are done in the correct order, this problem is avoided. oCdbWrapper.bExceptionHandlersHaveBeenSet = True; oCdbWrapper.fasSendCommandAndReadOutput(sExceptionHandlingCommands); if not oCdbWrapper.bCdbRunning: return; # If the debugger attached to processes, mark that as done and resume threads in all processes. if bDebuggerNeedsToResumeAttachedProcesses: bDebuggerNeedsToResumeAttachedProcesses = False; for uProcessId in oCdbWrapper.auProcessIds: oCdbWrapper.fasSendCommandAndReadOutput("|~[0n%d]s;~*m" % uProcessId); if not oCdbWrapper.bCdbRunning: return; # This exception and the commands executed to analyze it are not relevant to the analysis of the bug. As mentioned # above, the commands and their output will be removed from the StdIO array to reduce the risk of OOM. oCdbWrapper.asHTMLCdbStdIOBlocks = ( oCdbWrapper.asHTMLCdbStdIOBlocks[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands ["<span class=\"CDBIgnoredException\">%s process %d exception.</span>" % (sCreateExitProcess, uProcessId)] + # Replacement for analysis commands oCdbWrapper.asHTMLCdbStdIOBlocks[-1:] # Last block contains prompt and must be conserved. ); else: # Report that the application is paused for analysis... oCdbWrapper.fExceptionDetectedCallback and oCdbWrapper.fExceptionDetectedCallback(uExceptionCode, sExceptionDescription); # And potentially report that the application is resumed later... bApplicationWasPausedToAnalyzeAnException = True; # Create an error report, if the exception is fatal. oCdbWrapper.oErrorReport = cErrorReport.foCreate(oCdbWrapper, uExceptionCode, sExceptionDescription); if not oCdbWrapper.bCdbRunning: return; if oCdbWrapper.oErrorReport is not None: if dxBugIdConfig["bSaveDump"]: sDumpFileName = fsCreateFileName(oCdbWrapper.oErrorReport.sId); sOverwrite = dxBugIdConfig["bOverwriteDump"] and "/o" or ""; oCdbWrapper.fasSendCommandAndReadOutput(".dump %s /ma \"%s.dmp\"" % (sOverwrite, sDumpFileName)); if not oCdbWrapper.bCdbRunning: return; break; oCdbWrapper.asHTMLCdbStdIOBlocks = ( oCdbWrapper.asHTMLCdbStdIOBlocks[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands ["<span class=\"CDBIgnoredException\">Exception 0x%08X in process %d.</span>" % (uExceptionCode, uProcessId)] + oCdbWrapper.asHTMLCdbStdIOBlocks[-1:] # Last block contains prompt and must be conserved. ); # Terminate cdb. oCdbWrapper.bCdbWasTerminatedOnPurpose = True; oCdbWrapper.fasSendCommandAndReadOutput("q"); assert not oCdbWrapper.bCdbRunning, "Debugger did not terminate when requested";
def cCdbWrapper_fCdbStdInOutThread(oCdbWrapper): # cCdbWrapper initialization code already acquire a lock on cdb on behalf of this thread, so the "interrupt on # timeout" thread does not attempt to interrupt cdb while this thread is getting started. try: # Create a list of commands to set up event handling. The default for any exception not explicitly mentioned is to # be handled as a second chance exception. asExceptionHandlingCommands = ["sxd *"]; # request second chance debugger break for certain exceptions that indicate the application has a bug. for sCommand, axExceptions in daxExceptionHandling.items(): for xException in axExceptions: sException = isinstance(xException, str) and xException or ("0x%08X" % xException); asExceptionHandlingCommands.append("%s %s" % (sCommand, sException)); sExceptionHandlingCommands = ";".join(asExceptionHandlingCommands); # Read the initial cdb output related to starting/attaching to the first process. asIntialCdbOutput = oCdbWrapper.fasReadOutput(); if not oCdbWrapper.bCdbRunning: return; # Turn off prompt information as it is not useful most of the time, but can clutter output. oCdbWrapper.fasSendCommandAndReadOutput(".prompt_allow -dis -ea -reg -src -sym", bIsRelevantIO = False); if not oCdbWrapper.bCdbRunning: return; # Exception handlers need to be set up. oCdbWrapper.bExceptionHandlersHaveBeenSet = False; # Only fire fApplicationRunningCallback if the application was started for the first time or resumed after it was # paused to analyze an exception. bInitialApplicationRunningCallbackFired = False; bDebuggerNeedsToResumeAttachedProcesses = len(oCdbWrapper.auProcessIdsPendingAttach) > 0; bApplicationWasPausedToAnalyzeAnException = False; # An bug report will be created when needed; it is returned at the end oBugReport = None; # Memory can be allocated to be freed later in case the system has run low on memory when an analysis needs to be # performed. This is done only if dxBugIdConfig["uReserveRAM"] > 0. The memory is allocated at the start of # debugging, freed right before an analysis is performed and reallocated if the exception was not fatal. bReserveRAMAllocated = False; while asIntialCdbOutput or len(oCdbWrapper.auProcessIdsPendingAttach) + len(oCdbWrapper.auProcessIds) > 0 and oCdbWrapper.bCdbRunning: # If requested, reserve some memory in cdb that can be released later to make analysis under low memory conditions # more likely to succeed. if dxBugIdConfig["uReserveRAM"] and not bReserveRAMAllocated: uBitMask = 2 ** 31; while uBitMask >= 1: sBit = dxBugIdConfig["uReserveRAM"] & uBitMask and "A" or ""; if bReserveRAMAllocated: oCdbWrapper.fasSendCommandAndReadOutput("aS /c RAM .printf \"${RAM}{$RAM}%s\";" % sBit, bIsRelevantIO = False); elif sBit: oCdbWrapper.fasSendCommandAndReadOutput("aS RAM \"%s\";" % sBit, bIsRelevantIO = False); bReserveRAMAllocated = True; if not oCdbWrapper.bCdbRunning: return; uBitMask /= 2; # Discard any cached information about modules loaded in the current process, as this may be about to change # during execution of the application. oCdbWrapper.doModules_by_sCdbId = None; if asIntialCdbOutput: # First parse the intial output asCdbOutput = asIntialCdbOutput; asIntialCdbOutput = None; else: # Then attach to a process, or start or resume the application if ( # If we've just started the application or we've attached to all processes and are about to resume them... not bInitialApplicationRunningCallbackFired and len(oCdbWrapper.auProcessIdsPendingAttach) == 0 ) or ( # ... or if we've paused the application and are about to resume it ... bApplicationWasPausedToAnalyzeAnException ): # ...report that the application is about to start running. oCdbWrapper.fApplicationRunningCallback and oCdbWrapper.fApplicationRunningCallback(); bInitialApplicationRunningCallbackFired = True; # Mark the time when the application was resumed. asCdbOutput = oCdbWrapper.fasSendCommandAndReadOutput(".time", bIsRelevantIO = False); if not oCdbWrapper.bCdbRunning: return; oTimeMatch = len(asCdbOutput) > 0 and re.match(r"^Debug session time: (.*?)\s*$", asCdbOutput[0]); assert oTimeMatch, "Failed to get debugger time!\r\n%s" % "\r\n".join(asCdbOutput); oCdbWrapper.oApplicationTimeLock.acquire(); try: oCdbWrapper.nApplicationResumeDebuggerTime = fnGetDebuggerTime(oTimeMatch.group(1)); oCdbWrapper.nApplicationResumeTime = time.clock(); finally: oCdbWrapper.oApplicationTimeLock.release(); # Release the lock on cdb so the "interrupt on timeout" thread can attempt to interrupt cdb while the # application is running. if len(oCdbWrapper.auProcessIdsPendingAttach) == 0: oCdbWrapper.oCdbLock.release(); try: asCdbOutput = oCdbWrapper.fasSendCommandAndReadOutput("g", bMayContainApplicationOutput = True); if not oCdbWrapper.bCdbRunning: return; finally: # Get a lock on cdb so the "interrupt on timeout" thread does not attempt to interrupt cdb while we execute # commands. if len(oCdbWrapper.auProcessIdsPendingAttach) == 0: oCdbWrapper.oCdbLock.acquire(); # Let the interrupt-on-timeout thread know that the application has been interrupted, so that when it detects # another timeout should be fired, it will try to interrupt the application again. oCdbWrapper.bInterruptPending = False; if oCdbWrapper.bGetDetailsHTML: # Save the current number of blocks of StdIO; if this exception is not relevant it can be used to remove all # blocks added while analyzing it. These blocks are not considered to contain useful information and removing # them can reduce the risk of OOM when irrelevant exceptions happens very often. The last block contains a # prompt, which will become the first analysis command's block, so it is not saved. uOriginalHTMLCdbStdIOBlocks = len(oCdbWrapper.asCdbStdIOBlocksHTML) - 1; # Find out what event caused the debugger break asLastEventOutput = oCdbWrapper.fasSendCommandAndReadOutput(".lastevent"); if not oCdbWrapper.bCdbRunning: return; # Sample output: # |Last event: 3d8.1348: Create process 3:3d8 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) # - or - # |Last event: c74.10e8: Exit process 4:c74, code 0 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) assert len(asLastEventOutput) == 2, "Invalid .lastevent output:\r\n%s" % "\r\n".join(asLastEventOutput); oEventMatch = re.match( "^%s\s*$" % "".join([ r"Last event: ([0-9a-f]+)\.[0-9a-f]+: ", r"(?:", r"(Create|Exit) process [0-9a-f]+\:([0-9a-f]+)(?:, code [0-9a-f]+)?", r"|", r"(.*?) \- code ([0-9a-f]+) \(!*\s*(first|second) chance\s*!*\)", r"|", r"Hit breakpoint (\d+)", r")", ]), asLastEventOutput[0], re.I ); assert oEventMatch, "Invalid .lastevent output on line #1:\r\n%s" % "\r\n".join(asLastEventOutput); oEventTimeMatch = re.match(r"^\s*debugger time: (.*?)\s*$", asLastEventOutput[1]); assert oEventTimeMatch, "Invalid .lastevent output on line #2:\r\n%s" % "\r\n".join(asLastEventOutput); oCdbWrapper.oApplicationTimeLock.acquire(); try: if oCdbWrapper.nApplicationResumeDebuggerTime: # Add the time between when the application was resumed and when the event happened to the total application # run time. oCdbWrapper.nApplicationRunTime += fnGetDebuggerTime(oEventTimeMatch.group(1)) - oCdbWrapper.nApplicationResumeDebuggerTime; # Mark the application as suspended by setting nApplicationResumeDebuggerTime to None. oCdbWrapper.nApplicationResumeDebuggerTime = None; oCdbWrapper.nApplicationResumeTime = None; finally: oCdbWrapper.oApplicationTimeLock.release(); ( sProcessIdHex, sCreateExitProcess, sCreateExitProcessIdHex, sExceptionDescription, sExceptionCode, sChance, sBreakpointId, ) = oEventMatch.groups(); uProcessId = long(sProcessIdHex, 16); uExceptionCode = sExceptionCode and long(sExceptionCode, 16); uBreakpointId = sBreakpointId and long(sBreakpointId); if ( uExceptionCode in (STATUS_BREAKPOINT, STATUS_WAKE_SYSTEM_DEBUGGER) and uProcessId not in oCdbWrapper.auProcessIds ): # This is assumed to be the initial breakpoint after starting/attaching to the first process or after a new # process was created by the application. This assumption may not be correct, in which case the code needs to # be modifed to check the stack to determine if this really is the initial breakpoint. But that comes at a # performance cost, so until proven otherwise, the code is based on this assumption. sCreateExitProcess = "Create"; sCreateExitProcessIdHex = sProcessIdHex; bGetBugReportForException = True; if sCreateExitProcess: # Make sure the created/exited process is the current process. assert sProcessIdHex == sCreateExitProcessIdHex, "%s vs %s" % (sProcessIdHex, sCreateExitProcessIdHex); oCdbWrapper.fHandleCreateExitProcess(sCreateExitProcess, uProcessId); # If there are more processes to attach to, do so: if len(oCdbWrapper.auProcessIdsPendingAttach) > 0: asAttachToProcess = oCdbWrapper.fasSendCommandAndReadOutput(".attach 0n%d" % oCdbWrapper.auProcessIdsPendingAttach[0]); if not oCdbWrapper.bCdbRunning: return; else: # Set up exception handling if this has not been done yet. if not oCdbWrapper.bExceptionHandlersHaveBeenSet: # Note to self: when rewriting the code, make sure not to set up exception handling before the debugger has # attached to all processes. But do so before resuming the threads. Otherwise one or more of the processes # can end up having only one thread that has a suspend count of 2 and no amount of resuming will cause the # process to run. The reason for this is unknown, but if things are done in the correct order, this problem # is avoided. oCdbWrapper.bExceptionHandlersHaveBeenSet = True; oCdbWrapper.fasSendCommandAndReadOutput(sExceptionHandlingCommands, bIsRelevantIO = False); if not oCdbWrapper.bCdbRunning: return; # If the debugger attached to processes, mark that as done and resume threads in all processes. if bDebuggerNeedsToResumeAttachedProcesses: bDebuggerNeedsToResumeAttachedProcesses = False; for uProcessId in oCdbWrapper.auProcessIds: oCdbWrapper.fSelectProcess(uProcessId); if not oCdbWrapper.bCdbRunning: return; oCdbWrapper.fasSendCommandAndReadOutput("~*m", bIsRelevantIO = False); if not oCdbWrapper.bCdbRunning: return; if oCdbWrapper.bGetDetailsHTML: # As this is not relevant to the bug, remove the commands and their output from cdb IO and replace with a # description of the exception to remove clutter and reduce memory usage. oCdbWrapper.asCdbStdIOBlocksHTML = ( oCdbWrapper.asCdbStdIOBlocksHTML[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands ["<span class=\"CDBIgnoredException\">%s process %d breakpoint.</span><br/>" % (sCreateExitProcess, uProcessId)] + # Replacement for analysis commands oCdbWrapper.asCdbStdIOBlocksHTML[-1:] # Last block contains prompt and must be conserved. ); bGetBugReportForException = False; elif uExceptionCode == DBG_CONTROL_BREAK: # Debugging the application was interrupted and the application suspended to fire some timeouts. This exception # is not a bug and should be ignored. bGetBugReportForException = False; elif uExceptionCode == STATUS_SINGLE_STEP: # A bug in cdb causes single step exceptions at locations where a breakpoint is set. Since I have never seen a # single step exception caused by a bug in an application, I am assuming these are all caused by this bug and # ignore them: bGetBugReportForException = False; elif uExceptionCode in [STATUS_BREAKPOINT, STATUS_WX86_BREAKPOINT]: if dxBugIdConfig["bIgnoreFirstChanceBreakpoints"] and sChance == "first": bGetBugReportForException = False; else: sCurrentFunctionSymbol = oCdbWrapper.fsGetSymbol("@$ip"); if not oCdbWrapper.bCdbRunning: return; sCallerFunctionSymbol = oCdbWrapper.fsGetSymbol("@$ra"); if not oCdbWrapper.bCdbRunning: return; if ( # When BugId interrupts the application, a CDB_CONTROL_BREAK exception is generated first and a # STATUS_BREAKPOINT second. Since only one exception is needed, the second one is ignored. # The two top stack frames can be used to detect certain breakpoints that should be ignored: sCurrentFunctionSymbol == "ntdll.dll!DbgBreakPoint" and sCallerFunctionSymbol == "ntdll.dll!DbgUiRemoteBreakin" ) or ( # When a 32-bit application is running on a 64-bit OS, creating a new processes can generate two exceptions; # first a STATUS_BREAKPOINT, then a STATUS_WX86_BREAKPOINT. Only the first exception is needed, so the # second is ignored. sCurrentFunctionSymbol == "ntdll.dll!LdrpDoDebuggerBreak" and sCallerFunctionSymbol == "ntdll.dll!LdrpInitializeProcess" ): bGetBugReportForException = False; if bGetBugReportForException: # If available, free previously allocated memory to allow analysis in low memory conditions. if bReserveRAMAllocated: # This command is not relevant to the bug, so it is hidden in the cdb IO to prevent OOM. oCdbWrapper.fasSendCommandAndReadOutput("ad RAM;", bIsRelevantIO = False); bReserveRAMAllocated = False; if uBreakpointId is not None: if oCdbWrapper.bGetDetailsHTML: # Remove the breakpoint event from the cdb IO to remove clutter and reduce memory usage. oCdbWrapper.asCdbStdIOBlocksHTML = ( oCdbWrapper.asCdbStdIOBlocksHTML[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands oCdbWrapper.asCdbStdIOBlocksHTML[-1:] # Last block contains prompt and must be conserved. ); fBreakpointCallback = oCdbWrapper.dfCallback_by_uBreakpointId[uBreakpointId]; fBreakpointCallback(); else: # Report that the application is paused for analysis... if oCdbWrapper.fExceptionDetectedCallback: if sBreakpointId: oCdbWrapper.fExceptionDetectedCallback(STATUS_BREAKPOINT, "A BugId breakpoint"); else: oCdbWrapper.fExceptionDetectedCallback(uExceptionCode, sExceptionDescription); # And potentially report that the application is resumed later... bApplicationWasPausedToAnalyzeAnException = True; # Create a bug report, if the exception is fatal. oCdbWrapper.oBugReport = cBugReport.foCreateForException(oCdbWrapper, uExceptionCode, sExceptionDescription); if not oCdbWrapper.bCdbRunning: return; if oCdbWrapper.oBugReport is None and oCdbWrapper.bGetDetailsHTML: # Otherwise remove the analysis commands from the cdb IO and replace with a description of the exception to # remove clutter and reduce memory usage. oCdbWrapper.asCdbStdIOBlocksHTML = ( oCdbWrapper.asCdbStdIOBlocksHTML[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands ["<span class=\"CDBIgnoredException\">Exception 0x%08X in process %d ignored.</span><br/>" % (uExceptionCode, uProcessId)] + oCdbWrapper.asCdbStdIOBlocksHTML[-1:] # Last block contains prompt and must be conserved. ); # See if a bug needs to be reported if oCdbWrapper.oBugReport is not None: # See if a dump should be saved if dxBugIdConfig["bSaveDump"]: sDumpFileName = fsCreateFileName(oCdbWrapper.oBugReport.sId); sOverwrite = dxBugIdConfig["bOverwriteDump"] and "/o" or ""; oCdbWrapper.fasSendCommandAndReadOutput(".dump %s /ma \"%s.dmp\"" % (sOverwrite, sDumpFileName)); if not oCdbWrapper.bCdbRunning: return; # Stop to report the bug. break; # Execute any pending timeout callbacks oCdbWrapper.oTimeoutsLock.acquire(); try: axTimeoutsToFire = []; for xTimeout in oCdbWrapper.axTimeouts: (nTimeoutTime, fTimeoutCallback, axTimeoutCallbackArguments) = xTimeout; if nTimeoutTime <= oCdbWrapper.nApplicationRunTime: # This timeout should be fired. oCdbWrapper.axTimeouts.remove(xTimeout); axTimeoutsToFire.append((fTimeoutCallback, axTimeoutCallbackArguments)); # print "@@@ firing timeout %.1f seconds late: %s" % (oCdbWrapper.nApplicationRunTime - nTimeoutTime, repr(fTimeoutCallback)); finally: oCdbWrapper.oTimeoutsLock.release(); for (fTimeoutCallback, axTimeoutCallbackArguments) in axTimeoutsToFire: fTimeoutCallback(*axTimeoutCallbackArguments); # Terminate cdb. oCdbWrapper.bCdbWasTerminatedOnPurpose = True; oCdbWrapper.fasSendCommandAndReadOutput("q"); finally: oCdbWrapper.bCdbStdInOutThreadRunning = False; # Release the lock on cdb so the "interrupt on timeout" thread can notice cdb has terminated oCdbWrapper.oCdbLock.release(); assert not oCdbWrapper.bCdbRunning, "Debugger did not terminate when requested";
bGetDetailsHTML = dxConfig["bSaveReport"], fApplicationRunningCallback = fApplicationRunningHandler, fExceptionDetectedCallback = fExceptionDetectedHandler, ); oBugId.fWait(); if oBugId.oBugReport: print "* A bug was detected in the application."; print; print " Id: %s" % oBugId.oBugReport.sId; print " Description: %s" % oBugId.oBugReport.sBugDescription; print " Location: %s" % oBugId.oBugReport.sBugLocation; if oBugId.oBugReport.sBugSourceLocation: print " Source: %s" % oBugId.oBugReport.sBugSourceLocation; print " Security impact: %s" % oBugId.oBugReport.sSecurityImpact; if dxConfig["bSaveReport"]: sFileNameBase = fsCreateFileName("%s %s" % (oBugId.oBugReport.sId, oBugId.oBugReport.sBugLocation)); # File name may be too long, keep trying to save it with a shorter name or output an error if that's not posible. while len(sFileNameBase) > 0: sFileName = sFileNameBase + ".html"; try: oFile = open(sFileName, "wb"); except IOError: sFileNameBase = sFileNameBase[:-1]; continue; sDetailsHTML = oBugId.oBugReport.sDetailsHTML; try: oFile.write(sDetailsHTML); finally: oFile.close(); print " Bug report: %s (%d bytes)" % (sFileName, len(sDetailsHTML)); break;
def cCdbWrapper_fCdbStdInOutThread(oCdbWrapper): # Create a list of commands to set up event handling. The default for any exception not explicitly mentioned is to be # handled as a second chance exception. asExceptionHandlingCommands = ["sxd *"] # request second chance debugger break for certain exceptions that indicate the application has a bug. for sCommand, axExceptions in daxExceptionHandling.items(): for xException in axExceptions: sException = isinstance( xException, str) and xException or ("0x%08X" % xException) asExceptionHandlingCommands.append("%s %s" % (sCommand, sException)) sExceptionHandlingCommands = ";".join(asExceptionHandlingCommands) # Read the initial cdb output related to starting/attaching to the first process. asIntialCdbOutput = oCdbWrapper.fasReadOutput() if not oCdbWrapper.bCdbRunning: return # Turn off prompt information - it's not parsed anyway and clutters output. # Not that +dis and +ea are needed in cErrorReport_foSpecialErrorReport_STATUS_ACCESS_VIOLATION as this causes # an exmpty command to output the oCdbWrapper.fasSendCommandAndReadOutput( ".prompt_allow +dis +ea -reg -src -sym") if not oCdbWrapper.bCdbRunning: return oCdbWrapper.asHTMLCdbStdIOBlocks.pop(-1) # This command is not relevant, remove it from the log. # Exception handlers need to be set up. oCdbWrapper.bExceptionHandlersHaveBeenSet = False # Only fire _fApplicationRunningCallback if the application was started for the first time or resumed after it was # paused to analyze an exception. bInitialApplicationRunningCallbackFired = False bDebuggerNeedsToResumeAttachedProcesses = len( oCdbWrapper.auProcessIdsPendingAttach) > 0 bApplicationWasPausedToAnalyzeAnException = False # An error report will be created when needed; it is returned at the end oErrorReport = None while asIntialCdbOutput or len(oCdbWrapper.auProcessIdsPendingAttach ) + len(oCdbWrapper.auProcessIds) > 0: if asIntialCdbOutput: # First parse the intial output asCdbOutput = asIntialCdbOutput asIntialCdbOutput = None else: # Then attach to a process, or start or resume the application if not bInitialApplicationRunningCallbackFired or bApplicationWasPausedToAnalyzeAnException: # Application was started or resumed after an exception oCdbWrapper.fApplicationRunningCallback and oCdbWrapper.fApplicationRunningCallback( ) bInitialApplicationRunningCallbackFired = True asCdbOutput = oCdbWrapper.fasSendCommandAndReadOutput("g") if not oCdbWrapper.bCdbRunning: return # Save the current number of blocks of StdIO; if this exception is not relevant it can be used to remove all blocks # added while analyzing it. These blocks are not considered to contain useful information and removing them can # reduce the risk of OOM when irrelevant exceptions happens very often. The last block contains a prompt, which # will become the first analysis command's block, so it is not saved. uOriginalHTMLCdbStdIOBlocks = len(oCdbWrapper.asHTMLCdbStdIOBlocks) - 1 # If cdb is attaching to a process, make sure it worked. for sLine in asCdbOutput: oFailedAttachMatch = re.match( r"^Cannot debug pid \d+, Win32 error 0n\d+\s*$", sLine) assert not oFailedAttachMatch, "Failed to attach to process!\r\n%s" % "\r\n".join( asCdbOutput) # Find out what event caused the debugger break asLastEventOutput = oCdbWrapper.fasSendCommandAndReadOutput( ".lastevent") if not oCdbWrapper.bCdbRunning: return # Sample output: # |Last event: 3d8.1348: Create process 3:3d8 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) # - or - # |Last event: c74.10e8: Exit process 4:c74, code 0 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) bValidLastEventOutput = len(asLastEventOutput) == 2 and re.match( r"^\s*debugger time: .*$", asLastEventOutput[1]) oEventMatch = bValidLastEventOutput and re.match( "".join([ r"^Last event: ([0-9a-f]+)\.[0-9a-f]+: ", r"(?:", r"(Create|Exit) process [0-9a-f]+\:([0-9a-f]+)(?:, code [0-9a-f]+)?", r"|", r"(.*?) \- code ([0-9a-f]+) \(!*\s*(?:first|second) chance\s*!*\)", r")\s*$", ]), asLastEventOutput[0], re.I) assert oEventMatch, "Invalid .lastevent output:\r\n%s" % "\r\n".join( asLastEventOutput) (sProcessIdHex, sCreateExitProcess, sCreateExitProcessIdHex, sExceptionDescription, sExceptionCode) = oEventMatch.groups() uProcessId = long(sProcessIdHex, 16) uExceptionCode = sExceptionCode and int(sExceptionCode, 16) if uExceptionCode in (STATUS_BREAKPOINT, STATUS_WAKE_SYSTEM_DEBUGGER ) and uProcessId not in oCdbWrapper.auProcessIds: # This is assumed to be the initial breakpoint after starting/attaching to the first process or after a new # process was created by the application. This assumption may not be correct, in which case the code needs to # be modifed to check the stack to determine if this really is the initial breakpoint. But that comes at a # performance cost, so until proven otherwise, the code is based on this assumption. sCreateExitProcess = "Create" sCreateExitProcessIdHex = sProcessIdHex if sCreateExitProcess: # Make sure the created/exited process is the current process. assert sProcessIdHex == sCreateExitProcessIdHex, "%s vs %s" % ( sProcessIdHex, sCreateExitProcessIdHex) oCdbWrapper.fHandleCreateExitProcess(sCreateExitProcess, uProcessId) # If there are more processes to attach to, do so: if len(oCdbWrapper.auProcessIdsPendingAttach) > 0: asAttachToProcess = oCdbWrapper.fasSendCommandAndReadOutput( ".attach 0n%d" % oCdbWrapper.auProcessIdsPendingAttach[0]) if not oCdbWrapper.bCdbRunning: return else: # Set up exception handling if this has not been done yet. if not oCdbWrapper.bExceptionHandlersHaveBeenSet: # Note to self: when rewriting the code, make sure not to set up exception handling before the debugger has # attached to all processes. But do so before resuming the threads. Otherwise one or more of the processes can # end up having only one thread that has a suspend count of 2 and no amount of resuming will cause the process # to run. The reason for this is unknown, but if things are done in the correct order, this problem is avoided. oCdbWrapper.bExceptionHandlersHaveBeenSet = True oCdbWrapper.fasSendCommandAndReadOutput( sExceptionHandlingCommands) if not oCdbWrapper.bCdbRunning: return # If the debugger attached to processes, mark that as done and resume threads in all processes. if bDebuggerNeedsToResumeAttachedProcesses: bDebuggerNeedsToResumeAttachedProcesses = False for uProcessId in oCdbWrapper.auProcessIds: oCdbWrapper.fasSendCommandAndReadOutput( "|~[0n%d]s;~*m" % uProcessId) if not oCdbWrapper.bCdbRunning: return # This exception and the commands executed to analyze it are not relevant to the analysis of the bug. As mentioned # above, the commands and their output will be removed from the StdIO array to reduce the risk of OOM. oCdbWrapper.asHTMLCdbStdIOBlocks = ( oCdbWrapper.asHTMLCdbStdIOBlocks[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands [ "<span class=\"CDBIgnoredException\">%s process %d exception.</span>" % (sCreateExitProcess, uProcessId) ] + # Replacement for analysis commands oCdbWrapper.asHTMLCdbStdIOBlocks[ -1:] # Last block contains prompt and must be conserved. ) else: # Report that the application is paused for analysis... oCdbWrapper.fExceptionDetectedCallback and oCdbWrapper.fExceptionDetectedCallback( uExceptionCode, sExceptionDescription) # And potentially report that the application is resumed later... bApplicationWasPausedToAnalyzeAnException = True # Create an error report, if the exception is fatal. oCdbWrapper.oErrorReport = cErrorReport.foCreate( oCdbWrapper, uExceptionCode, sExceptionDescription) if not oCdbWrapper.bCdbRunning: return if oCdbWrapper.oErrorReport is not None: if dxBugIdConfig["bSaveDump"]: sDumpFileName = fsCreateFileName( oCdbWrapper.oErrorReport.sId) sOverwrite = dxBugIdConfig["bOverwriteDump"] and "/o" or "" oCdbWrapper.fasSendCommandAndReadOutput( ".dump %s /ma \"%s.dmp\"" % (sOverwrite, sDumpFileName)) if not oCdbWrapper.bCdbRunning: return break oCdbWrapper.asHTMLCdbStdIOBlocks = ( oCdbWrapper.asHTMLCdbStdIOBlocks[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands [ "<span class=\"CDBIgnoredException\">Exception 0x%08X in process %d.</span>" % (uExceptionCode, uProcessId) ] + oCdbWrapper.asHTMLCdbStdIOBlocks[ -1:] # Last block contains prompt and must be conserved. ) # Terminate cdb. oCdbWrapper.bCdbWasTerminatedOnPurpose = True oCdbWrapper.fasSendCommandAndReadOutput("q") assert not oCdbWrapper.bCdbRunning, "Debugger did not terminate when requested"
def cCdbWrapper_fCdbDebuggerThread(oCdbWrapper): # Read the initial cdb output related to starting/attaching to the first process. asIntialCdbOutput = oCdbWrapper.fasReadOutput(); if not oCdbWrapper.bCdbRunning: return; # Exception handlers need to be set up. oCdbWrapper.bExceptionHandlersHaveBeenSet = False; # Only fire _fApplicationRunningCallback if the application was started for the first time or resumed after it was # paused to analyze an exception. bInitialApplicationRunningCallbackFired = False; bDebuggerNeedsToResumeAttachedProcesses = len(oCdbWrapper.auProcessIdsPendingAttach) > 0; bApplicationWasPausedToAnalyzeAnException = False; # An error report will be created when needed; it is returned at the end oErrorReport = None; while asIntialCdbOutput or len(oCdbWrapper.auProcessIdsPendingAttach) + len(oCdbWrapper.auProcessIds) > 0: if asIntialCdbOutput: # First parse the intial output asCdbOutput = asIntialCdbOutput; asIntialCdbOutput = None; else: # Then attach to a process, or start or resume the application if not bInitialApplicationRunningCallbackFired or bApplicationWasPausedToAnalyzeAnException: # Application was started or resumed after an exception oCdbWrapper.fApplicationRunningCallback and oCdbWrapper.fApplicationRunningCallback(); bInitialApplicationRunningCallbackFired = True; asCdbOutput = oCdbWrapper.fasSendCommandAndReadOutput("g"); if not oCdbWrapper.bCdbRunning: return; # If cdb is attaching to a process, make sure it worked. for sLine in asCdbOutput: oFailedAttachMatch = re.match(r"^Cannot debug pid \d+, Win32 error 0n\d+\s*$", sLine); assert not oFailedAttachMatch, "Failed to attach to process!\r\n%s" % "\r\n".join(asCdbOutput); # Find out what event caused the debugger break asLastEventOutput = oCdbWrapper.fasSendCommandAndReadOutput(".lastevent"); if not oCdbWrapper.bCdbRunning: return; # Sample output: # |Last event: 3d8.1348: Create process 3:3d8 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) # - or - # |Last event: c74.10e8: Exit process 4:c74, code 0 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) bValidLastEventOutput = len(asLastEventOutput) == 2 and re.match(r"^\s*debugger time: .*$", asLastEventOutput[1]); oEventMatch = bValidLastEventOutput and re.match( "".join([ r"^Last event: ([0-9a-f]+)\.[0-9a-f]+: ", r"(?:", r"(Create|Exit) process [0-9a-f]+\:([0-9a-f]+)(?:, code [0-9a-f]+)?", r"|", r"(.*?) \- code ([0-9a-f]+) \(!*\s*(?:first|second) chance\s*!*\)", r")\s*$", ]), asLastEventOutput[0], re.I ); assert oEventMatch, "Invalid .lastevent output:\r\n%s" % "\r\n".join(asLastEventOutput); ( sProcessIdHex, sCreateExitProcess, sCreateExitProcessIdHex, sExceptionDescription, sExceptionCode ) = oEventMatch.groups(); uProcessId = long(sProcessIdHex, 16); uExceptionCode = sExceptionCode and int(sExceptionCode, 16); if uExceptionCode in (STATUS_BREAKPOINT, STATUS_WAKE_SYSTEM_DEBUGGER) and uProcessId not in oCdbWrapper.auProcessIds: # This is assumed to be the initial breakpoint after starting/attaching to the first process or after a new # process was created by the application. This assumption may not be correct, in which case the code needs to # be modifed to check the stack to determine if this really is the initial breakpoint. But that comes at a # performance cost, so until proven otherwise, the code is based on this assumption. sCreateExitProcess = "Create"; sCreateExitProcessIdHex = sProcessIdHex; if sCreateExitProcess: # Make sure the created/exited process is the current process. assert sProcessIdHex == sCreateExitProcessIdHex, "%s vs %s" % (sProcessIdHex, sCreateExitProcessIdHex); oCdbWrapper.fHandleCreateExitProcess(sCreateExitProcess, uProcessId); # If there are more processes to attach to, do so: if len(oCdbWrapper.auProcessIdsPendingAttach) > 0: asAttachToProcess = oCdbWrapper.fasSendCommandAndReadOutput(".attach 0n%d" % oCdbWrapper.auProcessIdsPendingAttach[0]); if not oCdbWrapper.bCdbRunning: return; else: # Set up exception handling if this has not been done yet. if not oCdbWrapper.bExceptionHandlersHaveBeenSet: # Note to self: when rewriting the code, make sure not to set up exception handling before the debugger has # attached to all processes. But do so before resuming the threads. Otherwise one or more of the processes can # end up having only one thread that has a suspend count of 2 and no amount of resuming will cause the process # to run. The reason for this is unknown, but if things are done in the correct order, this problem is avoided. oCdbWrapper.bExceptionHandlersHaveBeenSet = True; oCdbWrapper.fasSendCommandAndReadOutput(sExceptionHandlingCommands); if not oCdbWrapper.bCdbRunning: return; # If the debugger attached to processes, mark that as done and resume threads in all processes. if bDebuggerNeedsToResumeAttachedProcesses: bDebuggerNeedsToResumeAttachedProcesses = False; for uProcessId in oCdbWrapper.auProcessIds: oCdbWrapper.fasSendCommandAndReadOutput("|~[0n%d]s;~*m" % uProcessId); if not oCdbWrapper.bCdbRunning: return; continue; # Report that the application is paused for analysis... oCdbWrapper.fExceptionDetectedCallback and oCdbWrapper.fExceptionDetectedCallback(uExceptionCode, sExceptionDescription); # And potentially report that the application is resumed later... bApplicationWasPausedToAnalyzeAnException = True; # Optionally perform enhanced symbol reload oCdbWrapper.fEnhancedSymbolReload(); if not oCdbWrapper.bCdbRunning: return; # Create an error report, if the exception is fatal. oCdbWrapper.oErrorReport = cErrorReport.foCreate(oCdbWrapper, uExceptionCode, sExceptionDescription); if not oCdbWrapper.bCdbRunning: return; if oCdbWrapper.oErrorReport is not None: if dxBugIdConfig["bSaveDump"]: sDumpFileName = fsCreateFileName(oCdbWrapper.oErrorReport.sId); sOverwrite = dxBugIdConfig["bOverwriteDump"] and "/o" or ""; oCdbWrapper.fasSendCommandAndReadOutput(".dump %s /ma \"%s.dmp\"" % (sOverwrite, sDumpFileName)); if not oCdbWrapper.bCdbRunning: return; break; # Terminate cdb. oCdbWrapper.bCdbWasTerminatedOnPurpose = True; oCdbWrapper.fasSendCommandAndReadOutput("q"); assert not oCdbWrapper.bCdbRunning, "Debugger did not terminate when requested";
auApplicationProcessIds=auApplicationProcessIds, asSymbolServerURLs=[], fApplicationRunningCallback=fApplicationRunningHandler, fExceptionDetectedCallback=fExceptionDetectedHandler, ) oBugId.fWait() if oBugId.oErrorReport: print "* A bug was detected in the application." print print " Id: %s" % oBugId.oErrorReport.sId print " Description: %s" % oBugId.oErrorReport.sErrorDescription print " Process binary: %s" % oBugId.oErrorReport.sProcessBinaryName print " Code: %s" % oBugId.oErrorReport.sCodeDescription print " Security impact: %s" % oBugId.oErrorReport.sSecurityImpact if dxConfig["bSaveReport"]: sFileNameBase = fsCreateFileName(oBugId.oErrorReport.sId) # File name may be too long, keep trying to while len(sFileNameBase) > 0: sFileName = sFileNameBase + ".html" try: oFile = open(sFileName, "wb") except IOError: sFileNameBase = sFileNameBase[:-1] continue try: oFile.write(oBugId.oErrorReport.sHTMLDetails) finally: oFile.close() print " Error report: %s (%d bytes)" % (sFileName, len(oBugId.oErrorReport.sHTMLDetails)) break else:
auApplicationProcessIds=auApplicationProcessIds, asSymbolServerURLs=[], fApplicationRunningCallback=fApplicationRunningHandler, fExceptionDetectedCallback=fExceptionDetectedHandler, ) oBugId.fWait() if oBugId.oErrorReport: print "* A bug was detected in the application." print print " Id: %s" % oBugId.oErrorReport.sId print " Description: %s" % oBugId.oErrorReport.sErrorDescription print " Process binary: %s" % oBugId.oErrorReport.sProcessBinaryName print " Code: %s" % oBugId.oErrorReport.sCodeDescription print " Security impact: %s" % oBugId.oErrorReport.sSecurityImpact if dxConfig["bSaveReport"]: sFileNameBase = fsCreateFileName(oBugId.oErrorReport.sId) # File name may be too long, keep trying to while len(sFileNameBase) > 0: sFileName = sFileNameBase + ".html" try: oFile = open(sFileName, "wb") except IOError: sFileNameBase = sFileNameBase[:-1] continue try: oFile.write(oBugId.oErrorReport.sHTMLDetails) finally: oFile.close() print " Error report: %s (%d bytes)" % ( sFileName, len(oBugId.oErrorReport.sHTMLDetails)) break
def cCdbWrapper_fCdbStdInOutThread(oCdbWrapper): # Create a list of commands to set up event handling. The default for any exception not explicitly mentioned is to be # handled as a second chance exception. asExceptionHandlingCommands = ["sxd *"]; # request second chance debugger break for certain exceptions that indicate the application has a bug. for sCommand, axExceptions in daxExceptionHandling.items(): for xException in axExceptions: sException = isinstance(xException, str) and xException or ("0x%08X" % xException); asExceptionHandlingCommands.append("%s %s" % (sCommand, sException)); sExceptionHandlingCommands = ";".join(asExceptionHandlingCommands); # Read the initial cdb output related to starting/attaching to the first process. asIntialCdbOutput = oCdbWrapper.fasReadOutput(); if not oCdbWrapper.bCdbRunning: return; # Turn off prompt information as it is not useful most of the time, but can clutter output. oCdbWrapper.fasSendCommandAndReadOutput(".prompt_allow -dis -ea -reg -src -sym", bIsRelevantIO = False); if not oCdbWrapper.bCdbRunning: return; # Exception handlers need to be set up. oCdbWrapper.bExceptionHandlersHaveBeenSet = False; # Only fire _fApplicationRunningCallback if the application was started for the first time or resumed after it was # paused to analyze an exception. bInitialApplicationRunningCallbackFired = False; bDebuggerNeedsToResumeAttachedProcesses = len(oCdbWrapper.auProcessIdsPendingAttach) > 0; bApplicationWasPausedToAnalyzeAnException = False; # An bug report will be created when needed; it is returned at the end oBugReport = None; # Memory can be allocated to be freed later in case the system has run low on memory when an analysis needs to be # performed. This is done only if dxBugIdConfig["uReserveRAM"] > 0. The memory is allocated at the start of debugging, # freed right before an analysis is performed and reallocated if the exception was not fatal. bReserveRAMAllocated = False; # Keep track of created processes so an x86 create process breakpoint following an x64 create process breakpoint for # the same process can be hidden. uHideX86BreakpointForProcessId = None; while asIntialCdbOutput or len(oCdbWrapper.auProcessIdsPendingAttach) + len(oCdbWrapper.auProcessIds) > 0: # If requested, reserve some memory in cdb that can be released later to make analysis under low memory conditions # more likely to succeed. if dxBugIdConfig["uReserveRAM"] and not bReserveRAMAllocated: uBitMask = 2 ** 31; while uBitMask >= 1: sBit = dxBugIdConfig["uReserveRAM"] & uBitMask and "A" or ""; if bReserveRAMAllocated: oCdbWrapper.fasSendCommandAndReadOutput("aS /c RAM .printf \"${RAM}{$RAM}%s\";" % sBit, bIsRelevantIO = False); elif sBit: oCdbWrapper.fasSendCommandAndReadOutput("aS RAM \"%s\";" % sBit, bIsRelevantIO = False); bReserveRAMAllocated = True; if not oCdbWrapper.bCdbRunning: return; uBitMask /= 2; # Discard any cached information about modules loaded in the current process, as this may be about to change during # execution of the application. oCdbWrapper.doModules_by_sCdbId = None; if asIntialCdbOutput: # First parse the intial output asCdbOutput = asIntialCdbOutput; asIntialCdbOutput = None; else: # Then attach to a process, or start or resume the application if not bInitialApplicationRunningCallbackFired or bApplicationWasPausedToAnalyzeAnException: # Application was started or resumed after an exception oCdbWrapper.fApplicationRunningCallback and oCdbWrapper.fApplicationRunningCallback(); bInitialApplicationRunningCallbackFired = True; asCdbOutput = oCdbWrapper.fasSendCommandAndReadOutput("g", bMayContainApplicationOutput = True); if not oCdbWrapper.bCdbRunning: return; if oCdbWrapper.bGetDetailsHTML: # Save the current number of blocks of StdIO; if this exception is not relevant it can be used to remove all blocks # added while analyzing it. These blocks are not considered to contain useful information and removing them can # reduce the risk of OOM when irrelevant exceptions happens very often. The last block contains a prompt, which # will become the first analysis command's block, so it is not saved. uOriginalHTMLCdbStdIOBlocks = len(oCdbWrapper.asCdbStdIOBlocksHTML) - 1; # If cdb is attaching to a process, make sure it worked. for sLine in asCdbOutput: oFailedAttachMatch = re.match(r"^Cannot debug pid \d+, Win32 error 0n\d+\s*$", sLine); assert not oFailedAttachMatch, "Failed to attach to process!\r\n%s" % "\r\n".join(asCdbOutput); # Find out what event caused the debugger break asLastEventOutput = oCdbWrapper.fasSendCommandAndReadOutput(".lastevent"); if not oCdbWrapper.bCdbRunning: return; # Sample output: # |Last event: 3d8.1348: Create process 3:3d8 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) # - or - # |Last event: c74.10e8: Exit process 4:c74, code 0 # | debugger time: Tue Aug 25 00:06:07.311 2015 (UTC + 2:00) bValidLastEventOutput = len(asLastEventOutput) == 2 and re.match(r"^\s*debugger time: .*$", asLastEventOutput[1]); oEventMatch = bValidLastEventOutput and re.match( "".join([ r"^Last event: ([0-9a-f]+)\.[0-9a-f]+: ", r"(?:", r"(Create|Exit) process [0-9a-f]+\:([0-9a-f]+)(?:, code [0-9a-f]+)?", r"|", r"(.*?) \- code ([0-9a-f]+) \(!*\s*(first|second) chance\s*!*\)", r")\s*$", ]), asLastEventOutput[0], re.I ); assert oEventMatch, "Invalid .lastevent output:\r\n%s" % "\r\n".join(asLastEventOutput); ( sProcessIdHex, sCreateExitProcess, sCreateExitProcessIdHex, sExceptionDescription, sExceptionCode, sChance ) = oEventMatch.groups(); uProcessId = long(sProcessIdHex, 16); uExceptionCode = sExceptionCode and int(sExceptionCode, 16); if uExceptionCode in (STATUS_BREAKPOINT, STATUS_WAKE_SYSTEM_DEBUGGER) and uProcessId not in oCdbWrapper.auProcessIds: # This is assumed to be the initial breakpoint after starting/attaching to the first process or after a new # process was created by the application. This assumption may not be correct, in which case the code needs to # be modifed to check the stack to determine if this really is the initial breakpoint. But that comes at a # performance cost, so until proven otherwise, the code is based on this assumption. sCreateExitProcess = "Create"; sCreateExitProcessIdHex = sProcessIdHex; if uExceptionCode == STATUS_WX86_BREAKPOINT and uHideX86BreakpointForProcessId == uProcessId: # An x86 breakpoint may follow an x64 breakpoint when a new 32-bit process is created. Ignore it. uHideX86BreakpointForProcessId = None; if oCdbWrapper.bGetDetailsHTML: # This exception and the commands executed to analyze it are not relevant to the analysis of the bug. As mentioned # above, the commands and their output will be removed from the StdIO array to reduce the risk of OOM. oCdbWrapper.asCdbStdIOBlocksHTML = ( oCdbWrapper.asCdbStdIOBlocksHTML[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands ["<span class=\"CDBIgnoredException\">Create process %d x86 breakpoint.</span>" % uProcessId] + # Replacement for analysis commands oCdbWrapper.asCdbStdIOBlocksHTML[-1:] # Last block contains prompt and must be conserved. ); elif sCreateExitProcess: if sCreateExitProcess == "Create": # An x86 breakpoint may follow an x64 breakpoint when a new 32-bit process is created. The latter should be # recognized and reported as such: uHideX86BreakpointForProcessId = uProcessId; else: uHideX86BreakpointForProcessId = None; # Make sure the created/exited process is the current process. assert sProcessIdHex == sCreateExitProcessIdHex, "%s vs %s" % (sProcessIdHex, sCreateExitProcessIdHex); oCdbWrapper.fHandleCreateExitProcess(sCreateExitProcess, uProcessId); # If there are more processes to attach to, do so: if len(oCdbWrapper.auProcessIdsPendingAttach) > 0: asAttachToProcess = oCdbWrapper.fasSendCommandAndReadOutput(".attach 0n%d" % oCdbWrapper.auProcessIdsPendingAttach[0]); if not oCdbWrapper.bCdbRunning: return; else: # Set up exception handling if this has not been done yet. if not oCdbWrapper.bExceptionHandlersHaveBeenSet: # Note to self: when rewriting the code, make sure not to set up exception handling before the debugger has # attached to all processes. But do so before resuming the threads. Otherwise one or more of the processes can # end up having only one thread that has a suspend count of 2 and no amount of resuming will cause the process # to run. The reason for this is unknown, but if things are done in the correct order, this problem is avoided. oCdbWrapper.bExceptionHandlersHaveBeenSet = True; oCdbWrapper.fasSendCommandAndReadOutput(sExceptionHandlingCommands, bIsRelevantIO = False); if not oCdbWrapper.bCdbRunning: return; # If the debugger attached to processes, mark that as done and resume threads in all processes. if bDebuggerNeedsToResumeAttachedProcesses: bDebuggerNeedsToResumeAttachedProcesses = False; for uProcessId in oCdbWrapper.auProcessIds: oCdbWrapper.fasSendCommandAndReadOutput("|~[0n%d]s;~*m" % uProcessId, bIsRelevantIO = False); if not oCdbWrapper.bCdbRunning: return; if oCdbWrapper.bGetDetailsHTML: # As this is not relevant to the bug, remove the commands and their output from cdb IO and replace with a # description of the exception to remove clutter and reduce memory usage. oCdbWrapper.asCdbStdIOBlocksHTML = ( oCdbWrapper.asCdbStdIOBlocksHTML[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands ["<span class=\"CDBIgnoredException\">%s process %d breakpoint.</span>" % (sCreateExitProcess, uProcessId)] + # Replacement for analysis commands oCdbWrapper.asCdbStdIOBlocksHTML[-1:] # Last block contains prompt and must be conserved. ); elif sChance == "first" and oCdbWrapper.bIgnoreFirstChanceBreakpoints and uExceptionCode in [STATUS_WX86_BREAKPOINT, STATUS_BREAKPOINT]: pass; else: uHideX86BreakpointForProcessId = None; # Report that the application is paused for analysis... oCdbWrapper.fExceptionDetectedCallback and oCdbWrapper.fExceptionDetectedCallback(uExceptionCode, sExceptionDescription); # And potentially report that the application is resumed later... bApplicationWasPausedToAnalyzeAnException = True; # If available, free previously allocated memory to allow analysis in low memory conditions. if bReserveRAMAllocated: # This command is not relevant to the bug, so it is hidden in the cdb IO to prevent OOM. oCdbWrapper.fasSendCommandAndReadOutput("ad RAM;", bIsRelevantIO = False); bReserveRAMAllocated = False; # Create a bug report, if the exception is fatal. oCdbWrapper.oBugReport = cBugReport.foCreate(oCdbWrapper, uExceptionCode, sExceptionDescription); if not oCdbWrapper.bCdbRunning: return; if oCdbWrapper.oBugReport is not None: if dxBugIdConfig["bSaveDump"]: sDumpFileName = fsCreateFileName(oCdbWrapper.oBugReport.sId); sOverwrite = dxBugIdConfig["bOverwriteDump"] and "/o" or ""; oCdbWrapper.fasSendCommandAndReadOutput(".dump %s /ma \"%s.dmp\"" % (sOverwrite, sDumpFileName)); if not oCdbWrapper.bCdbRunning: return; break; if oCdbWrapper.bGetDetailsHTML: # Otherwise remove the analysis commands from the cdb IO and replace with a description of the exception to # remove clutter and reduce memory usage. oCdbWrapper.asCdbStdIOBlocksHTML = ( oCdbWrapper.asCdbStdIOBlocksHTML[0:uOriginalHTMLCdbStdIOBlocks] + # IO before analysis commands ["<span class=\"CDBIgnoredException\">Exception 0x%08X in process %d.</span>" % (uExceptionCode, uProcessId)] + oCdbWrapper.asCdbStdIOBlocksHTML[-1:] # Last block contains prompt and must be conserved. ); # Terminate cdb. oCdbWrapper.bCdbWasTerminatedOnPurpose = True; oCdbWrapper.fasSendCommandAndReadOutput("q"); assert not oCdbWrapper.bCdbRunning, "Debugger did not terminate when requested";