Example #1
0
 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;
Example #2
0
 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";
Example #5
0
File: BugId.py Project: ea/BugId
   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;
Example #6
0
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";
Example #8
0
File: BugId.py Project: wflk/BugId
     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:
Example #9
0
     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";