def cBugReport_foAnalyzeException_STATUS_ACCESS_VIOLATION(oBugReport, oCdbWrapper, oException): # Parameter[0] = access type (0 = read, 1 = write, 8 = execute) # Parameter[1] = address assert len(oException.auParameters) == 2, \ "Unexpected number of access violation exception parameters (%d vs 2)" % len(oException.auParameters); # Access violation: add the type of operation and the location to the exception id. sViolationTypeId = {0:"R", 1:"W", 8:"E"}.get(oException.auParameters[0], "?"); sViolationTypeDescription = {0:"reading", 1:"writing", 8:"executing"}.get(oException.auParameters[0], "accessing"); sViolationTypeNotes = sViolationTypeId == "_" and " (the type-of-accesss code was 0x%X)" % oException.auParameters[0] or ""; uAddress = oException.auParameters[1]; if uAddress == 0xFFFFFFFFFFFFFFFF and sViolationTypeId == "R": # In x64 mode, current processors will thrown an exception when you use an address larger than 0x7FFFFFFFFFFF and # smaller than 0xFFFF800000000000. In such cases cdb reports incorrect information in the exception parameters, # e.g. the address is always reported as 0xFFFFFFFFFFFFFFFF and the access type is always "read". # A partial work-around is to get the address from the last instruction output, which can be retrieved by asking # cdb to output disassembly and address after each command. This may also tell us if the access type was "execute". oCdbWrapper.fasSendCommandAndReadOutput( \ ".prompt_allow +dis +ea; $$ Enable disassembly and address in cdb prompt"); # Do this twice in case the first time requires loading symbols, which can output junk that makes parsing ouput difficult. if not oCdbWrapper.bCdbRunning: return None; oCdbWrapper.fasSendCommandAndReadOutput( \ "~s; $$ Show disassembly and optional symbol loading stuff"); if not oCdbWrapper.bCdbRunning: return None; asLastInstructionAndAddress = oCdbWrapper.fasSendCommandAndReadOutput( "~s; $$ Show disassembly", bOutputIsInformative = True, ); if not oCdbWrapper.bCdbRunning: return None; # Revert to not showing disassembly and address: oCdbWrapper.fasSendCommandAndReadOutput( \ ".prompt_allow -dis -ea; $$ Revert to clean cdb prompt"); if not oCdbWrapper.bCdbRunning: return None; # Sample output: # |00007ffd`420b213e 488b14c2 mov rdx,qword ptr [rdx+rax*8] ds:00007df5`ffb60000=???????????????? # or # |60053594 ff7008 push dword ptr [eax+8] ds:002b:00000008=???????? # or # |00007ff6`e7ab1204 ffe1 jmp rcx {c0c0c0c0`c0c0c0c0} # or # |00000000`7fffffff ?? ??? # or # |00007ff9`b6f1a904 488b8d500d0000 mov rcx,qword ptr [rbp+0D50h] ss:00000244`4124f590=0000024441210240 assert len(asLastInstructionAndAddress) == 1, \ "Unexpected last instruction output:\r\n%r" % "\r\n".join(asLastInstructionAndAddress); oEIPOutsideAllocatedMemoryMatch = re.match("^%s$" % "".join([ r"([0-9a-f`]+)", r"\s+", r"\?\?", r"\s+", r"\?\?\?" # address spaces "??" spaces "???" ]), asLastInstructionAndAddress[0]); if oEIPOutsideAllocatedMemoryMatch: sAddress = oEIPOutsideAllocatedMemoryMatch.group(1); sViolationTypeId = "E"; sViolationTypeDescription = "executing"; else: oLastInstructionMatch = re.match("^%s$" % "".join([ r"[0-9a-f`]+", r"\s+", # address spaces r"[0-9a-f`]+", r"\s+", # opcode spaces r"\w+", r"\s+", # instruction spaces r"(?:", # either{ r"([^\[,]+,.+)", # (destination operand that does not reference memory "," source operand ) r"|", # }or{ ".*" # any other combination of operands r")", # } r"(?:", # either{ r"\ws:", # segment register ":" r"(?:[0-9a-f`]{4}:)?", # optional { segment value ":" } r"([0-9a-f`]+)", # (address) r"=(\?+|[0-9a-f`]+)", # "=" (either{ "???????" }or{ value }) r"|", # }or{ r"\{([0-9a-f`]+)\}", # "{" (address) "}" r")", # } ]), asLastInstructionAndAddress[0]); assert oLastInstructionMatch, \ "Unexpected last instruction output:\r\n%s" % "\r\n".join(asLastInstructionAndAddress); sDestinationOperandThatDoesNotReferenceMemory, sAddress1, sValue, sAddress2 = oLastInstructionMatch.groups(); sAddress = sAddress1 or sAddress2; if sAddress1: if sDestinationOperandThatDoesNotReferenceMemory: # The destination operand does not reference memory, so this must be a read AV sViolationTypeId = "R"; sViolationTypeDescription = "reading"; elif sValue[0] != "?": # The adress referenced can be read, so it must be write AV sViolationTypeId = "W"; sViolationTypeDescription = "writing"; else: sViolationTypeId = "_"; sViolationTypeDescription = "accessing"; sViolationTypeNotes = " (the type of accesss must be read or write, but cannot be determined)"; else: sViolationTypeId = "E"; sViolationTypeDescription = "executing"; uAddress = long(sAddress.replace("`", ""), 16); if sViolationTypeId == "E": # Hide the stack frame for the address at which the execute access violation happened: (e.g. 0x0 for a NULL pointer). asHiddenTopFrames = ["0x%X" % uAddress]; else: # Hide common asHiddenTopFrames = asHiddenTopFramesForReadAndWriteAVs; dtsDetails_uSpecialAddress = ddtsDetails_uSpecialAddress_sISA[oCdbWrapper.sCurrentISA]; for (uSpecialAddress, (sAddressId, sAddressDescription, sSecurityImpact)) in dtsDetails_uSpecialAddress.items(): sBugDescription = "Access violation while %s memory at 0x%X using %s." % \ (sViolationTypeDescription, uAddress, sAddressDescription); iOffset = uAddress - uSpecialAddress; if iOffset != 0: uOverflow = {"x86": 1 << 32, "x64": 1 << 64}[oCdbWrapper.sCurrentISA]; if iOffset > dxBugIdConfig["uMaxAddressOffset"]: # Maybe this is wrapping: iOffset -= uOverflow; elif iOffset < -dxBugIdConfig["uMaxAddressOffset"]: # Maybe this is wrapping: iOffset += uOverflow; uOffset = abs(iOffset); if uOffset <= dxBugIdConfig["uMaxAddressOffset"]: oBugReport.sBugTypeId = "AV%s:%s%s" % (sViolationTypeId, sAddressId, fsGetOffsetDescription(iOffset)); oBugReport.atxMemoryRemarks.append(("Access violation", uAddress, None)); # TODO Find out size of access break; else: if uAddress >= 0x800000000000: oBugReport.sBugTypeId = "AV%s:Invalid" % sViolationTypeId; sBugDescription = "Access violation while %s memory at the invalid address 0x%X." % (sViolationTypeDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue."; else: # This is not a special marker or NULL, so it must be an invalid pointer # See is page heap has more details on the address at which the access violation happened: asPageHeapReport = oCdbWrapper.fasSendCommandAndReadOutput( "!heap -p -a 0x%X; $$ Get page heap information" % uAddress, bOutputIsInformative = True, ); if not oCdbWrapper.bCdbRunning: return None; # Sample output: # | address 0e948ffc found in # | _DPH_HEAP_ROOT @ 48b1000 # | in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize) # | e9f08bc: e948000 2000 # | 6d009cd2 verifier!AVrfDebugPageHeapFree+0x000000c2 # | 77d42e20 ntdll!RtlDebugFreeHeap+0x0000003c # | 77cfe0da ntdll!RtlpFreeHeap+0x0006c97a # | 77cf5d2c ntdll!RtlpFreeHeapInternal+0x0000027e # | 77c90a3c ntdll!RtlFreeHeap+0x0000002c # <<<snip>>> no 0-day information for you! # | address 07fd1000 found in # | _DPH_HEAP_ROOT @ 4fd1000 # | in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize) # | 7f51d9c: 7fd0fc0 40 - 7fd0000 2000 # | 6c469abc verifier!AVrfDebugPageHeapAllocate+0x0000023c # <<<snip>>> no 0-day information for you! # There may be errors, sample output: # |ReadMemory error for address 5b59c3d0 # |Use `!address 5b59c3d0' to check validity of the address. # <<<snip>>> # |************************************************************************* # |*** *** # |*** *** # |*** Either you specified an unqualified symbol, or your debugger *** # |*** doesn't have full symbol information. Unqualified symbol *** # |*** resolution is turned off by default. Please either specify a *** # |*** fully qualified symbol module!symbolname, or enable resolution *** # <<<snip>>> # unable to resolve ntdll!RtlpStackTraceDataBase asPageHeapReport = [ x for x in asPageHeapReport if not re.match(r"^(%s)\s*$" % "|".join([ "ReadMemory error for address [0-9`a-f]+", "Use `!address [0-9`a-f]+' to check validity of the address.", "\*\*\*.*\*\*\*", "unable to resolve ntdll!RtlpStackTraceDataBase", ]), x) ]; # TODO: error resolving symbol should be handled by attempting to reload them, similar to cCdbWrapper_fasGetStack if len(asPageHeapReport) >= 4 and not asPageHeapReport[0].startswith("unable to resolve ntdll!"): assert re.match(r"^\s+address [0-9`a-f]+ found in\s*$", asPageHeapReport[0]), \ "Unrecognized page heap report first line:\r\n%s" % "\r\n".join(asPageHeapReport); assert re.match(r"^\s+\w+ @ [0-9`a-f]+\s*$", asPageHeapReport[1]), \ "Unrecognized page heap report second line:\r\n%s" % "\r\n".join(asPageHeapReport); oBlockTypeMatch = re.match( # line #3 r"^\s+in (free-ed|busy) allocation \(" # space "in" space ("free-ed" | "busy") space "allocation (" r"\s*\w+:" # [space] DPH_HEAP_BLOCK ":" r"(?:\s+UserAddr\s+UserSize\s+\-)?" # optional{ space "UserAddr" space "UserSize" space "-" } r"\s+VirtAddr\s+VirtSize" # space "VirtAddr" space "VirtSize" r"\)\s*$", # ")" [space] asPageHeapReport[2]); assert oBlockTypeMatch, \ "Unrecognized page heap report third line:\r\n%s" % "\r\n".join(asPageHeapReport); oBlockAdressAndSizeMatch = re.match( # line #4 r"^\s+[0-9`a-f]+:" # space heap_header_address ":" r"(?:\s+([0-9`a-f]+)\s+([0-9`a-f]+)\s+\-)?" # optional{ space (heap_block_address) space (heap_block_size) space "-" } r"\s+[0-9`a-f]+\s+[0-9`a-f]+" # space heap_pages_address space heap_pages_size r"\s*$", # [space] asPageHeapReport[3]); assert oBlockAdressAndSizeMatch, \ "Unrecognized page heap report fourth line:\r\n%s" % "\r\n".join(asPageHeapReport); sBlockType = oBlockTypeMatch.group(1); sBlockAddress, sBlockSize = oBlockAdressAndSizeMatch.groups(); uBlockAddress = sBlockAddress and long(sBlockAddress.replace("`", ""), 16); uBlockSize = sBlockSize and long(sBlockSize.replace("`", ""), 16); if uBlockAddress: uMemoryDumpAddress = uBlockAddress; uMemoryDumpSize = uBlockSize; if uAddress < uBlockAddress: uPrefix = uBlockAddress - uAddress; uMemoryDumpAddress -= uPrefix; uMemoryDumpSize += uPrefix; elif uAddress >= uBlockAddress + uBlockSize: uPostFix = uAddress - (uBlockAddress + uBlockSize) + 1; uMemoryDumpSize += uPostFix; oBugReport.atxMemoryDumps.append(("Memory block in which access violation happened", uMemoryDumpAddress, uMemoryDumpSize)); oBugReport.atxMemoryRemarks.append(("Memory block start", uBlockAddress, None)); oBugReport.atxMemoryRemarks.append(("Memory block end", uBlockAddress + uBlockSize, None)); oBugReport.atxMemoryRemarks.append(("Access violation", uAddress, None)); # TODO Find out size of access if sBlockType == "free-ed": # Page heap says the memory was freed: oBugReport.sBugTypeId = "UAF%s" % sViolationTypeId; sAddressDescription = "freed memory"; sBugDescription = "Access violation while %s %s at 0x%X indicates a use-after-free." % \ (sViolationTypeDescription, sAddressDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue."; elif sBlockType == "busy": # Page heap says the region is allocated, so the heap block must be inaccessible or the access must have been # beyond the end of the heap block, in the next memory page: uPageEndAddress = (uBlockAddress | 0xFFF) + 1; # Follows the page in which the block is located. bAccessIsBeyondBlock = uAddress >= uBlockAddress + uBlockSize; # The same type of block may have different sizes for 32-bit and 64-bit versions of an application, so the size # cannot be used in the id. The same is true for the offset, but the fact that there is an offset is unique to # the bug, so that can be added. if bAccessIsBeyondBlock: uOffsetPastEndOfBlock = uAddress - uBlockAddress - uBlockSize; sOffsetDescription = "%d/0x%X bytes beyond" % (uOffsetPastEndOfBlock, uOffsetPastEndOfBlock); sBugDescription = "Out-of-bounds access violation while %s memory at 0x%X; %s a %d/0x%X byte heap block at 0x%X." % \ (sViolationTypeDescription, uAddress, sOffsetDescription, uBlockSize, uBlockSize, uBlockAddress); asCorruptedBytes= None; # Increase size of memory dump beyond end of block if uOffsetPastEndOfBlock != 0: if sViolationTypeDescription == "writing": # Page heap stores the heap as close as possible to the edge of a page, taking into account that the start # of the heap block must be properly aligned. Bytes between the heap block and the end of the page are # initialized to 0xD0 and may have been modified before the program wrote beyond the end of the page. # We can use this to get a better idea of where to OOB write started: uHeapBlockEndAddress = uBlockAddress + uBlockSize; uPaddingSize = uPageEndAddress - uHeapBlockEndAddress; oCorruptionDetector = cCorruptionDetector(oCdbWrapper); oCorruptionDetector.fDetectCorruption(uHeapBlockEndAddress, *[0xD0 for x in xrange(uPaddingSize)]); if oCorruptionDetector.bCorruptionDetected: # We detected a modified byte; there was an OOB write before the one that caused this access # violation. Use it's offset instead and add this fact to the description. uStartAddress = oCorruptionDetector.uCorruptionStartAddress; oBugReport.atxMemoryRemarks.append(("Memory corruption", uStartAddress, uAddress - uStartAddress)); uOffsetPastEndOfBlock = uStartAddress - uHeapBlockEndAddress; sBugDescription += (" An earlier out-of-bounds write was detected at 0x%X, %d/0x%X bytes " \ "beyond that block because it modified the page heap suffix pattern.") % \ (uStartAddress, uOffsetPastEndOfBlock, uOffsetPastEndOfBlock); sMemoryDumpDescription = "memory corruption at 0x%X" % uStartAddress; asCorruptedBytes = oCorruptionDetector.fasCorruptedBytes(); elif uAddress == uPageEndAddress and uAddress > uBlockAddress + uBlockSize: sBugDescription += " An earlier out-of-bounds access before this address may have happened without " \ "having triggered an access violation."; # The access was beyond the end of the block (out-of-bounds, OOB) oBugReport.sBugTypeId = "OOB%s[%s]%s" % (sViolationTypeId, fsGetNumberDescription(uBlockSize), \ fsGetOffsetDescription(uOffsetPastEndOfBlock)); if asCorruptedBytes: sBugDescription += " The following byte values were written to the corrupted area: %s." % \ ", ".join(asCorruptedBytes); oBugReport.sBugTypeId += oCorruptionDetector.fsCorruptionId() or ""; else: # The access was inside the block but apparently the kind of access attempted is not allowed (e.g. write to # read-only memory). oBugReport.sBugTypeId = "AV%s[%s]@%s" % (sViolationTypeId, \ fsGetNumberDescription(uBlockSize), fsGetNumberDescription(uOffsetFromStartOfBlock)); sOffsetDescription = "%d/0x%X bytes into" % (uOffsetFromStartOfBlock, uOffsetFromStartOfBlock); sBugDescription = "Access violation while %s memory at 0x%X; %s a %d/0x%X byte heap block at 0x%X." % \ (sViolationTypeDescription, uAddress, sOffsetDescription, uBlockSize, uBlockSize, uBlockAddress); sSecurityImpact = "Potentially exploitable security issue."; else: raise NotImplemented("NOT REACHED"); sPageHeapOutputHTML = sBlockHTMLTemplate % { "sName": "Page heap report for address 0x%X" % uAddress, "sContent": "<pre>%s</pre>" % "\r\n".join([oCdbWrapper.fsHTMLEncode(s) for s in asPageHeapReport]) }; oBugReport.asExceptionSpecificBlocksHTML.append(sPageHeapOutputHTML); else: asMemoryProtectionInformation = oCdbWrapper.fasSendCommandAndReadOutput( "!vprot 0x%X; $$ Get memory protection information" % uAddress, bOutputIsInformative = True, ); if not oCdbWrapper.bCdbRunning: return None; # BaseAddress: 00007df5ff5f0000 # AllocationBase: 00007df5ff5f0000 # AllocationProtect: 00000001 PAGE_NOACCESS # RegionSize: 0000000001d34000 # State: 00002000 MEM_RESERVE # Type: 00040000 MEM_MAPPED # BaseAddress: 0000000000000000 # AllocationBase: 0000000000000000 # RegionSize: 0000000022f60000 # State: 00010000 MEM_FREE # Protect: 00000001 PAGE_NOACCESS # BaseAddress: 00007ffffffe0000 # AllocationBase: 00007ffffffe0000 # AllocationProtect: 00000002 PAGE_READONLY # RegionSize: 0000000000010000 # State: 00002000 MEM_RESERVE # Protect: 00000001 PAGE_NOACCESS # Type: 00020000 MEM_PRIVATE # !vprot: extension exception 0x80004002 # "QueryVirtual failed" assert len(asMemoryProtectionInformation) > 0, \ "!vprot did not return any results."; if re.match(r"^(%s)$" % "|".join([ "ERROR: !vprot: extension exception 0x80004002\.", "!vprot: No containing memory region found", "No export vprot found", ]), asMemoryProtectionInformation[0]): oBugReport.sBugTypeId = "AV%s:Unallocated" % sViolationTypeId; sBugDescription = "Access violation while %s unallocated memory at 0x%X." % (sViolationTypeDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue, if the attacker can control the address or the memory at the address."; else: uAllocationStartAddress = None; uAllocationProtectionFlags = None; uAllocationSize = None; uStateFlags = None; uProtectionFlags = None; uTypeFlags = None; for sLine in asMemoryProtectionInformation: oLineMatch = re.match(r"^(\w+):\s+([0-9a-f]+)(?:\s+\w+)?$", sLine); assert oLineMatch, \ "Unrecognized memory protection information line: %s\r\n%s" % (sLine, "\r\n".join(asMemoryProtectionInformation)); sInfoType, sValue = oLineMatch.groups(); uValue = long(sValue, 16); if sInfoType == "BaseAddress": pass; # Appear to be the address rounded down to the nearest start of a page, i.e. not useful information. elif sInfoType == "AllocationBase": uAllocationStartAddress = uValue; elif sInfoType == "AllocationProtect": uAllocationProtectionFlags = uValue; elif sInfoType == "RegionSize": uAllocationSize = uValue; elif sInfoType == "State": uStateFlags = uValue; elif sInfoType == "Protect": uProtectionFlags = uValue; elif sInfoType == "Type": uTypeFlags = uValue; oBugReport.atxMemoryRemarks.append(("Memory allocation start", uAllocationStartAddress, None)); oBugReport.atxMemoryRemarks.append(("Memory allocation end", uAllocationStartAddress + uAllocationSize, None)); if uStateFlags == 0x10000: oBugReport.sBugTypeId = "AV%s:Unallocated" % sViolationTypeId; sBugDescription = "Access violation while %s unallocated memory at 0x%X." % (sViolationTypeDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue, if the attacker can control the address or the memory at the address."; elif uStateFlags == 0x2000: # MEM_RESERVE # These checks were added to make sure I understood exactly what was going on. However, it turns out that I don't # because these checks fail without me being able to understand why. So, I've decided to disable them and see what # happens. If you have more information that can help me make sense of this and improve it, let me know! # assert uTypeFlags in [0x20000, 0x40000], \ # "Expected MEM_RESERVE memory to have type MEM_PRIVATE or MEM_MAPPED\r\n%s" % "\r\n".join(asMemoryProtectionInformation); # # PAGE_READONLY !? Apparently... # assert uProtectionFlags == 0x1 or uAllocationProtectionFlags in [0x1, 02], \ # "Expected MEM_RESERVE memory to have protection PAGE_NOACCESS or PAGE_READONLY\r\n%s" % "\r\n".join(asMemoryProtectionInformation); oBugReport.sBugTypeId = "AV%s:Reserved" % sViolationTypeId; sBugDescription = "Access violation while %s reserved but unallocated memory at 0x%X." % (sViolationTypeDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue, if the address is attacker controlled."; elif uStateFlags == 0x1000: # MEM_COMMIT dsMemoryProtectionsDescription_by_uFlags = { 0x01: "inaccessible", 0x02: "read-only", 0x04: "read- and writable", 0x08: "read- and writable", 0x10: "executable", 0x20: "read- and executable", 0x40: "full-access", 0x80: "full-access" }; sMemoryProtectionsDescription = dsMemoryProtectionsDescription_by_uFlags.get(uAllocationProtectionFlags); assert sMemoryProtectionsDescription, \ "Unexpected MEM_COMMIT memory to have protection value 0x%X\r\n%s" % (uAllocationProtectionFlags, "\r\n".join(asMemoryProtectionInformation)); oBugReport.sBugTypeId = "AV%s:Arbitrary" % sViolationTypeId; sBugDescription = "Access violation while %s %s memory at 0x%X." % (sViolationTypeDescription, sMemoryProtectionsDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue, if the address is attacker controlled."; oBugReport.atxMemoryDumps.append(("Memory block in which access violation happened", uAllocationStartAddress, uAllocationSize)); oBugReport.atxMemoryRemarks.append(("Access violation", uAddress, None)); # TODO Find out size of access else: raise AssertionError("Unexpected memory state 0x%X\r\n%s" % (uStateFlags, "\r\n".join(asMemoryProtectionInformation))); oBugReport.sBugDescription = sBugDescription + sViolationTypeNotes; oBugReport.sSecurityImpact = sSecurityImpact; dtxBugTranslations = ddtxBugTranslations.get(oBugReport.sBugTypeId, None); if dtxBugTranslations: oBugReport = oBugReport.foTranslate(dtxBugTranslations); if oBugReport: oBugReport.oStack.fHideTopFrames(asHiddenTopFrames); return oBugReport;
def cCdbWrapper_fbDetectAndReportVerifierErrors(oCdbWrapper, asCdbOutput): uErrorNumber = None uProcessId = None sMessage = None uVerifierStopHeapBlockStartAddress = None uVerifierStopHeapBlockSize = None uCorruptedStamp = None uCorruptionAddress = None asRelevantLines = [] for sLine in asCdbOutput: # Ignore exmpty lines if not sLine: continue # Look for the first VERIFIER STOP message and gather information if uErrorNumber is None: oErrorMessageMatch = re.match( r"^VERIFIER STOP ([0-9A-F]+): pid 0x([0-9A-F]+): (.*?)\s*$", sLine) if oErrorMessageMatch: sErrorNumber, sProcessId, sMessage = oErrorMessageMatch.groups( ) uErrorNumber = long(sErrorNumber, 16) uProcessId = long(sProcessId, 16) asRelevantLines.append(sLine) continue # A VERIFIER STOP message has been detected, gather what information verifier provides: oInformationMatch = re.match(r"^\t([0-9A-F]+) : (.*?)\s*$", sLine) if oInformationMatch: asRelevantLines.append(sLine) sValue, sDescription = oInformationMatch.groups() uValue = long(sValue, 16) sDescription = sDescription.lower() # Both "Corruption address" and "corruption address" are used :( if sDescription == "heap block": uVerifierStopHeapBlockStartAddress = uValue elif sDescription == "block size": uVerifierStopHeapBlockSize = uValue elif sDescription == "corrupted stamp": uCorruptedStamp = uValue elif sDescription == "corruption address": uCorruptionAddress = uValue else: assert sLine.strip().replace("=", "") == "", \ "Unknown VERIFIER STOP message line: %s\r\n%s" % (repr(sLine), "\r\n".join(asCdbOutput)) break else: assert uErrorNumber is None, \ "Detected the start of a VERIFIER STOP message but not the end\r\n%s" % "\r\n".join(asCdbOutput) return False if uErrorNumber == 0x303: # ======================================= # VERIFIER STOP 0000000000000303: pid 0xB2C: NULL handle passed as parameter. A valid handle must be used. # # 0000000000000000 : Not used. # 0000000000000000 : Not used. # 0000000000000000 : Not used. # 0000000000000000 : Not used. # # ======================================= # This is not interesting; do not report an error. return True assert uVerifierStopHeapBlockStartAddress is not None, \ "The heap block start address was not found in the verifier stop message.\r\n%s" % "\r\n".join(asRelevantLines) assert uVerifierStopHeapBlockSize is not None, \ "The heap block size was not found in the verifier stop message.\r\n%s" % "\r\n".join(asRelevantLines) oCorruptionDetector = cCorruptionDetector(oCdbWrapper) oPageHeapReport = cPageHeapReport.foCreate( oCdbWrapper, uVerifierStopHeapBlockStartAddress) if not oCdbWrapper.bCdbRunning: return None if oPageHeapReport: # Prefer page heap information over VERIFIER STOP info - the later has been known to be incorrect sometimes, for # instance when the application frees (heap pointer + offset), the VERIFIER STOP info will use that as the heap # block base, whereas the page heap report will correctly report (heap pointer) as the heap block base. uAllocationStartAddress = oPageHeapReport.uAllocationStartAddress uAllocationEndAddress = oPageHeapReport.uAllocationEndAddress # Check the page heap data near the heap block for signs of corruption: oPageHeapReport.fbCheckForCorruption(oCorruptionDetector) if not oCdbWrapper.bCdbRunning: return None if oPageHeapReport and oPageHeapReport.uBlockStartAddress: uHeapBlockStartAddress = oPageHeapReport.uBlockStartAddress uHeapBlockSize = oPageHeapReport.uBlockSize uHeapBlockEndAddress = uHeapBlockStartAddress + uHeapBlockSize else: uHeapBlockStartAddress = uVerifierStopHeapBlockStartAddress uHeapBlockSize = uVerifierStopHeapBlockSize uHeapBlockEndAddress = uVerifierStopHeapBlockStartAddress + uVerifierStopHeapBlockSize atxMemoryRemarks = [] if oCdbWrapper.bGenerateReportHTML: uMemoryDumpStartAddress = uHeapBlockStartAddress uMemoryDumpSize = uVerifierStopHeapBlockSize if oPageHeapReport: atxMemoryRemarks.extend(oPageHeapReport.fatxMemoryRemarks()) # Handle various VERIFIER STOP messages. sBugDescription = None if sMessage in ["corrupted start stamp", "corrupted end stamp"]: assert uCorruptionAddress is None, \ "We do not expect the corruption address to be provided in this VERIFIER STOP message\r\n%s" % \ "\r\n".join(asRelevantLines) if not oCorruptionDetector.bCorruptionDetected and uHeapBlockStartAddress != uVerifierStopHeapBlockStartAddress: # When the application attempts to free (heap pointer + offset), Verifier does not detect this and will assume # the application provided pointer is correct. This causes it to look for the start and end stamp in the wrong # location and report this bug as memory corruption. When the page heap data shows no signs of corruption, we # can special case it. iFreeAtOffset = uVerifierStopHeapBlockStartAddress - uHeapBlockStartAddress sBugTypeId = "MisalignedFree[%s]%s" % (fsGetNumberDescription( uHeapBlockSize), fsGetOffsetDescription(iFreeAtOffset)) sOffsetBeforeOrAfter = iFreeAtOffset < 0 and "before" or "after" sBugDescription = "The application attempted to free memory using a pointer that was %d/0x%X bytes %s a " \ "%d/0x%X byte heap block at address 0x%X" % (abs(iFreeAtOffset), abs(iFreeAtOffset), \ sOffsetBeforeOrAfter, uHeapBlockSize, uHeapBlockSize, uHeapBlockStartAddress) sSecurityImpact = "Unknown: this type of bug has not been analyzed before" else: sBugTypeId = "OOBW[%s]" % (fsGetNumberDescription(uHeapBlockSize)) assert oCorruptionDetector.bCorruptionDetected, \ "Cannot find any sign of corruption" elif sMessage == "corrupted suffix pattern": assert uCorruptionAddress is not None, \ "The corruption address is expected to be provided in this VERIFIER STOP message:\r\n%s" % \ "\r\n".join(asRelevantLines) # Page heap stores the heap as close as possible to the edge of a page, taking into account that the start of the # heap block must be properly aligned. Bytes between the heap block and the end of the page are initialized to # 0xD0. Verifier has detected that one of the bytes changed value, which indicates an out-of-bounds write. BugId # will try to find all bytes that were changed: sBugTypeId = "OOBW[%s]" % (fsGetNumberDescription(uHeapBlockSize)) assert oCorruptionDetector.bCorruptionDetected, \ "Cannot find any sign of corruption" elif sMessage == "corrupted infix pattern": assert uCorruptionAddress is not None, \ "The corruption address is expected to be provided in the VERIFIER STOP message:\r\n%s" % \ "\r\n".join(asRelevantLines) # Page heap sometimes does not free a heap block immediately, but overwrites the bytes with 0xF0. Verifier has # detected that one of the bytes changed value, which indicates a write-after-free. BugId will try to find all # bytes that were changed: sBugTypeId = "UAFW[%s]" % (fsGetNumberDescription(uHeapBlockSize)) # TODO add these checks to cPaheHeapReport if possible. oCorruptionDetector.fbDetectCorruption( uHeapBlockStartAddress, [0xF0 for x in xrange(uHeapBlockSize)]) assert oCorruptionDetector.bCorruptionDetected, \ "Cannot find any sign of corruption" else: sBugTypeId = "HeapCorrupt[%s]" % ( fsGetNumberDescription(uHeapBlockSize)) # sBugDescription is not set if this is a memory corruption # See if we have a better idea of where the corruption started and ended: if oCorruptionDetector.bCorruptionDetected: uCorruptionStartAddress = oCorruptionDetector.uCorruptionStartAddress uCorruptionEndAddress = oCorruptionDetector.uCorruptionEndAddress uCorruptionSize = uCorruptionEndAddress - uCorruptionStartAddress if oCdbWrapper.bGenerateReportHTML: atxMemoryRemarks.extend(oCorruptionDetector.fatxMemoryRemarks()) # I believe verifier may not check various memory areas in order from lowest to highest address. So it may not report # the byte that got corrupted with the lowest address; therefore it may not make sense to compare what verifier # reported with what our corruption detector found: # assert uCorruptionAddress is None or uCorruptionAddress == oCorruptionDetector.uCorruptionStartAddress, \ # "Verifier reported corruption at address 0x%X but BugId detected corruption at address 0x%X\r\n%s" % \ # (uCorruptionAddress, oCorruptionDetector.uCorruptionStartAddress, "\r\n".join(asRelevantLines)); bCorruptionDetected = True elif uCorruptionAddress: if oCdbWrapper.bGenerateReportHTML: atxMemoryRemarks.append( ("Corrupted memory", uCorruptionAddress, None)) uCorruptionStartAddress = uCorruptionAddress uCorruptionEndAddress = uCorruptionAddress bCorruptionDetected = True else: bCorruptionDetected = False if bCorruptionDetected: # If the corruption starts before or ends after the heap block, expand the memory dump to include the entire # corrupted region. if oCdbWrapper.bGenerateReportHTML: if uCorruptionStartAddress < uMemoryDumpStartAddress: uMemoryDumpSize += uMemoryDumpStartAddress - uCorruptionStartAddress uMemoryDumpStartAddress = uCorruptionStartAddress if uCorruptionEndAddress < uMemoryDumpStartAddress + uMemoryDumpSize: uMemoryDumpSize += uCorruptionEndAddress - ( uMemoryDumpStartAddress + uMemoryDumpSize) # Get a human readable description of the start offset of corruption relative to the heap block, where corruption # starting before or inside the heap block will be relative to the start, and corruption after it to the end. uCorruptionStartOffset = uCorruptionStartAddress - uHeapBlockStartAddress if uCorruptionStartOffset >= uHeapBlockSize: uCorruptionStartOffset -= uHeapBlockSize sCorruptionStartOffsetDescription = "%d/0x%X bytes beyond" % ( uCorruptionStartOffset, uCorruptionStartOffset) sBugTypeId += fsGetOffsetDescription(uCorruptionStartOffset) elif uCorruptionStartOffset > 0: sCorruptionStartOffsetDescription = "%d/0x%X bytes into" % ( uCorruptionStartOffset, uCorruptionStartOffset) sBugTypeId += "@%s" % fsGetNumberDescription( uCorruptionStartOffset) else: sCorruptionStartOffsetDescription = "%d/0x%X bytes before" % ( -uCorruptionStartOffset, -uCorruptionStartOffset) sBugTypeId += fsGetOffsetDescription(uCorruptionStartOffset) sBugDescription = "Page heap detected heap corruption at 0x%X; %s a %d/0x%X byte heap block at address 0x%X" % \ (uCorruptionStartAddress, sCorruptionStartOffsetDescription, uHeapBlockSize, uHeapBlockSize, uHeapBlockStartAddress) # If we detected corruption by scanning certain bytes in the applications memory, make sure this matches what # verifier reported and save all bytes that were affected: so far, we only saved the bytes that had an unexpected # value, but there is a chance that a byte was overwritten with the same value it has before, in which case it was # not saved. This can be detect if it is surrounded by bytes that did change value. This code reads the value of all # bytes between the first and last byte that we detected was corrupted: if oCorruptionDetector.bCorruptionDetected: asCorruptedBytes = oCorruptionDetector.fasCorruptedBytes() sBugDescription += " The following byte values were written to the corrupted area: %s." % ", ".join( asCorruptedBytes) sBugTypeId += oCorruptionDetector.fsCorruptionId() or "" sSecurityImpact = "Potentially exploitable security issue, if the corruption is attacker controlled" else: assert sBugDescription, \ "sBugDescription should have been set" oBugReport = cBugReport.foCreate(oCdbWrapper, sBugTypeId, sBugDescription, sSecurityImpact) if not oCdbWrapper.bCdbRunning: return None if oCdbWrapper.bGenerateReportHTML: oBugReport.atxMemoryDumps.append(("Memory near heap block at 0x%X" % uMemoryDumpStartAddress, \ uMemoryDumpStartAddress, uMemoryDumpSize)) oBugReport.atxMemoryRemarks.extend(atxMemoryRemarks) # Output the VERIFIER STOP message for reference if oCdbWrapper.bGenerateReportHTML: sVerifierStopMessageHTML = sBlockHTMLTemplate % { "sName": "VERIFIER STOP message", "sCollapsed": "Collapsed", "sContent": "<pre>%s</pre>" % "\r\n".join([ oCdbWrapper.fsHTMLEncode(s, uTabStop=8) for s in asRelevantLines ]) } oBugReport.asExceptionSpecificBlocksHTML.append( sVerifierStopMessageHTML) # Output the page heap information for reference if oPageHeapReport: sPageHeapOutputHTML = sBlockHTMLTemplate % { "sName": "Page heap output for heap block at 0x%X" % uHeapBlockStartAddress, "sCollapsed": "Collapsed", "sContent": "<pre>%s</pre>" % "\r\n".join([ oCdbWrapper.fsHTMLEncode(s, uTabStop=8) for s in oPageHeapReport.asPageHeapOutput ]) } oBugReport.asExceptionSpecificBlocksHTML.append( sPageHeapOutputHTML) oBugReport.bRegistersRelevant = False oCdbWrapper.oBugReport = oBugReport return True
def cBugReport_foAnalyzeException_STATUS_ACCESS_VIOLATION(oBugReport, oCdbWrapper, oException): # Parameter[0] = access type (0 = read, 1 = write, 8 = execute) # Parameter[1] = address assert len(oException.auParameters) == 2, \ "Unexpected number of access violation exception parameters (%d vs 2)" % len(oException.auParameters); # Access violation: add the type of operation and the location to the exception id. sViolationTypeId = {0:"R", 1:"W", 8:"E"}.get(oException.auParameters[0], "?"); sViolationTypeDescription = {0:"reading", 1:"writing", 8:"executing"}.get(oException.auParameters[0], "accessing"); sViolationTypeNotes = sViolationTypeId == "_" and " (the type-of-accesss code was 0x%X)" % oException.auParameters[0] or ""; uAddress = oException.auParameters[1]; if uAddress == 0xFFFFFFFFFFFFFFFF and sViolationTypeId == "R": # In x64 mode, current processors will thrown an exception when you use an address larger than 0x7FFFFFFFFFFF and # smaller than 0xFFFF800000000000. In such cases cdb reports incorrect information in the exception parameters, # e.g. the address is always reported as 0xFFFFFFFFFFFFFFFF and the access type is always "read". # A partial work-around is to get the address from the last instruction output, which can be retrieved by asking # cdb to output disassembly and address after each command. This may also tell us if the access type was "execute". oCdbWrapper.fasSendCommandAndReadOutput( \ ".prompt_allow +dis +ea; $$ Enable disassembly and address in cdb prompt"); # Do this twice in case the first time requires loading symbols, which can output junk that makes parsing ouput difficult. if not oCdbWrapper.bCdbRunning: return None; oCdbWrapper.fasSendCommandAndReadOutput( \ "~s; $$ Show disassembly and optional symbol loading stuff"); if not oCdbWrapper.bCdbRunning: return None; asLastInstructionAndAddress = oCdbWrapper.fasSendCommandAndReadOutput( "~s; $$ Show disassembly", bOutputIsInformative = True, ); if not oCdbWrapper.bCdbRunning: return None; # Revert to not showing disassembly and address: oCdbWrapper.fasSendCommandAndReadOutput( \ ".prompt_allow -dis -ea; $$ Revert to clean cdb prompt"); if not oCdbWrapper.bCdbRunning: return None; # Sample output: # |00007ffd`420b213e 488b14c2 mov rdx,qword ptr [rdx+rax*8] ds:00007df5`ffb60000=???????????????? # or # |60053594 ff7008 push dword ptr [eax+8] ds:002b:00000008=???????? # or # |00007ff6`e7ab1204 ffe1 jmp rcx {c0c0c0c0`c0c0c0c0} # or # |00000000`7fffffff ?? ??? # or # |00007ff9`b6f1a904 488b8d500d0000 mov rcx,qword ptr [rbp+0D50h] ss:00000244`4124f590=0000024441210240 assert len(asLastInstructionAndAddress) == 1, \ "Unexpected last instruction output:\r\n%r" % "\r\n".join(asLastInstructionAndAddress); oEIPOutsideAllocatedMemoryMatch = re.match("^%s$" % "".join([ r"([0-9a-f`]+)", r"\s+", r"\?\?", r"\s+", r"\?\?\?" # address spaces "??" spaces "???" ]), asLastInstructionAndAddress[0]); if oEIPOutsideAllocatedMemoryMatch: sAddress = oEIPOutsideAllocatedMemoryMatch.group(1); sViolationTypeId = "E"; sViolationTypeDescription = "executing"; else: oLastInstructionMatch = re.match("^%s$" % "".join([ r"[0-9a-f`]+", r"\s+", # address spaces r"[0-9a-f`]+", r"\s+", # opcode spaces r"\w+", r"\s+", # instruction spaces r"(?:", # either{ r"([^\[,]+,.+)", # (destination operand that does not reference memory "," source operand ) r"|", # }or{ ".*" # any other combination of operands r")", # } r"(?:", # either{ r"\ws:", # segment register ":" r"(?:[0-9a-f`]{4}:)?", # optional { segment value ":" } r"([0-9a-f`]+)", # (address) r"=(\?+|[0-9a-f`]+)", # "=" (either{ "???????" }or{ value }) r"|", # }or{ r"\{([0-9a-f`]+)\}", # "{" (address) "}" r")", # } ]), asLastInstructionAndAddress[0]); assert oLastInstructionMatch, \ "Unexpected last instruction output:\r\n%s" % "\r\n".join(asLastInstructionAndAddress); sDestinationOperandThatDoesNotReferenceMemory, sAddress1, sValue, sAddress2 = oLastInstructionMatch.groups(); sAddress = sAddress1 or sAddress2; if sAddress1: if sDestinationOperandThatDoesNotReferenceMemory: # The destination operand does not reference memory, so this must be a read AV sViolationTypeId = "R"; sViolationTypeDescription = "reading"; elif sValue[0] != "?": # The adress referenced can be read, so it must be write AV sViolationTypeId = "W"; sViolationTypeDescription = "writing"; else: sViolationTypeId = "_"; sViolationTypeDescription = "accessing"; sViolationTypeNotes = " (the type of accesss must be read or write, but cannot be determined)"; else: sViolationTypeId = "E"; sViolationTypeDescription = "executing"; uAddress = long(sAddress.replace("`", ""), 16); oBugReport.atxMemoryRemarks.append(("Access violation", uAddress, None)); # TODO Find out size of access if sViolationTypeId == "E": # Hide the top stack frame if it is for the address at which the execute access violation happened: if oBugReport and oBugReport.oStack and oBugReport.oStack.aoFrames and oBugReport.oStack.aoFrames[0].uInstructionPointer == uAddress: oBugReport.oStack.aoFrames[0].bIsHidden = True; uPointerSize = oCdbWrapper.fuGetValue("@$ptrsize"); if not oCdbWrapper.bCdbRunning: return None; uPageSize = oCdbWrapper.fuGetValue("@$pagesize"); if not oCdbWrapper.bCdbRunning: return; dtsDetails_uSpecialAddress = ddtsDetails_uSpecialAddress_sISA[oCdbWrapper.sCurrentISA]; for (uSpecialAddress, (sAddressId, sAddressDescription, sSecurityImpact)) in dtsDetails_uSpecialAddress.items(): sBugDescription = "Access violation while %s memory at 0x%X using %s." % \ (sViolationTypeDescription, uAddress, sAddressDescription); iOffset = uAddress - uSpecialAddress; if iOffset != 0: uOverflow = {"x86": 1 << 32, "x64": 1 << 64}[oCdbWrapper.sCurrentISA]; if iOffset > dxBugIdConfig["uMaxAddressOffset"]: # Maybe this is wrapping: iOffset -= uOverflow; elif iOffset < -dxBugIdConfig["uMaxAddressOffset"]: # Maybe this is wrapping: iOffset += uOverflow; uOffset = abs(iOffset); if uOffset <= dxBugIdConfig["uMaxAddressOffset"]: oBugReport.sBugTypeId = "AV%s:%s%s" % (sViolationTypeId, sAddressId, fsGetOffsetDescription(iOffset)); break; else: if uAddress >= 0x800000000000: oBugReport.sBugTypeId = "AV%s:Invalid" % sViolationTypeId; sBugDescription = "Access violation while %s memory at the invalid address 0x%X." % (sViolationTypeDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue."; else: # This is not a special marker or NULL, so it must be an invalid pointer # See is page heap has more details on the address at which the access violation happened: oPageHeapReport = cPageHeapReport.foCreate(oCdbWrapper, uAddress); if not oCdbWrapper.bCdbRunning: return None; if oPageHeapReport: oBugReport.atxMemoryRemarks.extend(oPageHeapReport.fatxMemoryRemarks()); if oPageHeapReport.uBlockStartAddress: if oCdbWrapper.bGenerateReportHTML: uMemoryDumpStartAddress = oPageHeapReport.uBlockStartAddress; uMemoryDumpSize = oPageHeapReport.uBlockSize; if oCdbWrapper.bGenerateReportHTML: if uAddress < oPageHeapReport.uBlockStartAddress: uPrefix = oPageHeapReport.uBlockStartAddress - uAddress; uMemoryDumpStartAddress -= uPrefix; uMemoryDumpSize += uPrefix; elif uAddress >= oPageHeapReport.uBlockEndAddress: uPostFix = uAddress - oPageHeapReport.uBlockEndAddress + 1; uMemoryDumpSize += uPostFix; # Check if we're not trying to dump a rediculous amount of memory: # Clamp start and end address uMemoryDumpStartAddress, uMemoryDumpSize = ftuLimitedAndAlignedMemoryDumpStartAddressAndSize( uAddress, uPointerSize, uMemoryDumpStartAddress, uMemoryDumpSize ); oBugReport.atxMemoryDumps.append(("Memory near access violation at 0x%X" % uAddress, \ uMemoryDumpStartAddress, uMemoryDumpSize)); if oPageHeapReport.sBlockType == "free-ed": # Page heap says the memory was freed: unfortunately, it does not tell us how big the block was, or exactly # where in the allocated memory it was stored, but we know it must have been allocated as close to the end # as possible, so the offset from the end of the allocated memory should be static unless the size of the # block changes and/or the offset at which the code tries to read. uOffsetFromEndOfAllocation = uAddress - oPageHeapReport.uAllocationEndAddress; if uOffsetFromEndOfAllocation < 0: oBugReport.sBugTypeId = "UAF%s[]~%s" % (sViolationTypeId, fsGetNumberDescription(-uOffsetFromEndOfAllocation)); else: # The code tried to access data outside the bounds of the freed memory: double face-palm! oBugReport.sBugTypeId = "OOBUAF%s[]%s" % (sViolationTypeId, fsGetOffsetDescription(uOffsetFromEndOfAllocation)); sAddressDescription = "freed memory"; sBugDescription = "Access violation while %s %s at 0x%X indicates a use-after-free." % \ (sViolationTypeDescription, sAddressDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue."; elif oPageHeapReport.sBlockType == "busy": # Page heap says the region is allocated, so the heap block must be inaccessible or the access must have been # beyond the end of the heap block, in the next memory page: bAccessIsBeyondBlock = uAddress >= oPageHeapReport.uBlockEndAddress; # The same type of block may have different sizes for 32-bit and 64-bit versions of an application, so the size # cannot be used in the id. The same is true for the offset, but the fact that there is an offset is unique to # the bug, so that can be added. if bAccessIsBeyondBlock: uOffsetPastEndOfBlock = uAddress - oPageHeapReport.uBlockEndAddress; sOffsetDescription = "%d/0x%X bytes beyond" % (uOffsetPastEndOfBlock, uOffsetPastEndOfBlock); sBugDescription = "Out-of-bounds access violation while %s memory at 0x%X; %s a %d/0x%X byte heap block at 0x%X." % \ (sViolationTypeDescription, uAddress, sOffsetDescription, oPageHeapReport.uBlockSize, \ oPageHeapReport.uBlockSize, oPageHeapReport.uBlockStartAddress); asCorruptedBytes= None; # Increase size of memory dump beyond end of block if uOffsetPastEndOfBlock != 0: if sViolationTypeDescription == "writing": # Page heap stores the heap as close as possible to the edge of a page, taking into account that the start # of the heap block must be properly aligned. Bytes between the heap block and the end of the page are # initialized to 0xD0 and may have been modified before the program wrote beyond the end of the page. # We can use this to get a better idea of where to OOB write started: oCorruptionDetector = cCorruptionDetector(oCdbWrapper); if oPageHeapReport.fbCheckForCorruption(oCorruptionDetector): # We detected a modified byte; there was an OOB write before the one that caused this access # violation. Use it's offset instead and add this fact to the description. if oCdbWrapper.bGenerateReportHTML: oBugReport.atxMemoryRemarks.extend(oCorruptionDetector.fatxMemoryRemarks()); uStartAddress = oCorruptionDetector.uCorruptionStartAddress; uOffsetPastEndOfBlock = uStartAddress - oPageHeapReport.uBlockEndAddress; sBugDescription += (" An earlier out-of-bounds write was detected at 0x%X, %d/0x%X bytes " \ "beyond that block because it modified the page heap suffix pattern.") % \ (uStartAddress, uOffsetPastEndOfBlock, uOffsetPastEndOfBlock); asCorruptedBytes = oCorruptionDetector.fasCorruptedBytes(); elif uAddress == oPageHeapReport.uAllocationEndAddress and uAddress > oPageHeapReport.uBlockEndAddress: sBugDescription += " An earlier out-of-bounds access before this address may have happened without " \ "having triggered an access violation."; # The access was beyond the end of the block (out-of-bounds, OOB) oBugReport.sBugTypeId = "OOB%s[%s]%s" % (sViolationTypeId, \ fsGetNumberDescription(oPageHeapReport.uBlockSize), fsGetOffsetDescription(uOffsetPastEndOfBlock)); if asCorruptedBytes: sBugDescription += " The following byte values were written to the corrupted area: %s." % \ ", ".join(asCorruptedBytes); oBugReport.sBugTypeId += oCorruptionDetector.fsCorruptionId() or ""; else: # The access was inside the block but apparently the kind of access attempted is not allowed (e.g. write to # read-only memory). oBugReport.sBugTypeId = "AV%s[%s]@%s" % (sViolationTypeId, \ fsGetNumberDescription(oPageHeapReport.uBlockSize), fsGetNumberDescription(uOffsetFromStartOfBlock)); sOffsetDescription = "%d/0x%X bytes into" % (uOffsetFromStartOfBlock, uOffsetFromStartOfBlock); sBugDescription = "Access violation while %s memory at 0x%X; %s a %d/0x%X byte heap block at 0x%X." % \ (sViolationTypeDescription, uAddress, sOffsetDescription, oPageHeapReport.uBlockSize, \ oPageHeapReport.uBlockSize, oPageHeapReport.uBlockStartAddress); sSecurityImpact = "Potentially exploitable security issue."; else: raise NotImplemented("NOT REACHED"); if oCdbWrapper.bGenerateReportHTML: sPageHeapOutputHTML = sBlockHTMLTemplate % { "sName": "Page heap output for address 0x%X" % uAddress, "sCollapsed": "Collapsed", "sContent": "<pre>%s</pre>" % "\r\n".join([ oCdbWrapper.fsHTMLEncode(s, uTabStop = 8) for s in oPageHeapReport.asPageHeapOutput ]) }; oBugReport.asExceptionSpecificBlocksHTML.append(sPageHeapOutputHTML); else: oThreadEnvironmentBlock = cThreadEnvironmentBlock.foCreate(oCdbWrapper); uOffsetFromTopOfStack = uAddress - oThreadEnvironmentBlock.uStackTopAddress; uOffsetFromBottomOfStack = oThreadEnvironmentBlock.uStackBottomAddress - uAddress; if uOffsetFromTopOfStack >= 0 and uOffsetFromTopOfStack <= uPageSize: oBugReport.sBugTypeId = "AV%s[Stack]+%s" % (sViolationTypeId, fsGetOffsetDescription(uOffsetFromTopOfStack)); sBugDescription = "Access violation while %s memory at 0x%X; %d/0x%X bytes passed the top of the stack at 0x%X." % \ (sViolationTypeDescription, uAddress, uOffsetFromTopOfStack, uOffsetFromTopOfStack, oThreadEnvironmentBlock.uStackTopAddress); sSecurityImpact = "Potentially exploitable security issue."; elif uOffsetFromBottomOfStack >= 0 and uOffsetFromBottomOfStack <= uPageSize: oBugReport.sBugTypeId = "AV%s[Stack]-%s" % (sViolationTypeId, fsGetOffsetDescription(-uOffsetFromBottomOfStack)); sBugDescription = "Access violation while %s memory at 0x%X; %d/0x%X bytes before the bottom of the stack at 0x%X." % \ (sViolationTypeDescription, uAddress, uOffsetFromBottomOfStack, uOffsetFromBottomOfStack, oThreadEnvironmentBlock.uStackTopAddress); sSecurityImpact = "Potentially exploitable security issue."; else: asMemoryProtectionInformation = oCdbWrapper.fasSendCommandAndReadOutput( "!vprot 0x%X; $$ Get memory protection information" % uAddress, bOutputIsInformative = True, ); if not oCdbWrapper.bCdbRunning: return None; # BaseAddress: 00007df5ff5f0000 # AllocationBase: 00007df5ff5f0000 # AllocationProtect: 00000001 PAGE_NOACCESS # RegionSize: 0000000001d34000 # State: 00002000 MEM_RESERVE # Type: 00040000 MEM_MAPPED # BaseAddress: 0000000000000000 # AllocationBase: 0000000000000000 # RegionSize: 0000000022f60000 # State: 00010000 MEM_FREE # Protect: 00000001 PAGE_NOACCESS # BaseAddress: 00007ffffffe0000 # AllocationBase: 00007ffffffe0000 # AllocationProtect: 00000002 PAGE_READONLY # RegionSize: 0000000000010000 # State: 00002000 MEM_RESERVE # Protect: 00000001 PAGE_NOACCESS # Type: 00020000 MEM_PRIVATE # !vprot: extension exception 0x80004002 # "QueryVirtual failed" assert len(asMemoryProtectionInformation) > 0, \ "!vprot did not return any results."; if re.match(r"^(%s)$" % "|".join([ "ERROR: !vprot: extension exception 0x80004002\.", "!vprot: No containing memory region found", "No export vprot found", ]), asMemoryProtectionInformation[0]): oBugReport.sBugTypeId = "AV%s:Unallocated" % sViolationTypeId; sBugDescription = "Access violation while %s unallocated memory at 0x%X." % (sViolationTypeDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue, if the attacker can control the address or the memory at the address."; else: uAllocationStartAddress = None; uAllocationProtectionFlags = None; uAllocationSize = None; uStateFlags = None; uProtectionFlags = None; uTypeFlags = None; for sLine in asMemoryProtectionInformation: oLineMatch = re.match(r"^(\w+):\s+([0-9a-f]+)(?:\s+\w+)?$", sLine); assert oLineMatch, \ "Unrecognized memory protection information line: %s\r\n%s" % (sLine, "\r\n".join(asMemoryProtectionInformation)); sInfoType, sValue = oLineMatch.groups(); uValue = long(sValue, 16); if sInfoType == "BaseAddress": pass; # Appear to be the address rounded down to the nearest start of a page, i.e. not useful information. elif sInfoType == "AllocationBase": uAllocationStartAddress = uValue; elif sInfoType == "AllocationProtect": uAllocationProtectionFlags = uValue; elif sInfoType == "RegionSize": uAllocationSize = uValue; elif sInfoType == "State": uStateFlags = uValue; elif sInfoType == "Protect": uProtectionFlags = uValue; elif sInfoType == "Type": uTypeFlags = uValue; if oCdbWrapper.bGenerateReportHTML: oBugReport.atxMemoryRemarks.append(("Memory allocation start", uAllocationStartAddress, None)); oBugReport.atxMemoryRemarks.append(("Memory allocation end", uAllocationStartAddress + uAllocationSize, None)); if uStateFlags == 0x10000: oBugReport.sBugTypeId = "AV%s:Unallocated" % sViolationTypeId; sBugDescription = "Access violation while %s unallocated memory at 0x%X." % \ (sViolationTypeDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue, if the attacker can control the address or the memory at the address."; elif uStateFlags == 0x2000: # MEM_RESERVE # These checks were added to make sure I understood exactly what was going on. However, it turns out that I don't # because these checks fail without me being able to understand why. So, I've decided to disable them and see what # happens. If you have more information that can help me make sense of this and improve it, let me know! # assert uTypeFlags in [0x20000, 0x40000], \ # "Expected MEM_RESERVE memory to have type MEM_PRIVATE or MEM_MAPPED\r\n%s" % "\r\n".join(asMemoryProtectionInformation); # # PAGE_READONLY !? Apparently... # assert uProtectionFlags == 0x1 or uAllocationProtectionFlags in [0x1, 02], \ # "Expected MEM_RESERVE memory to have protection PAGE_NOACCESS or PAGE_READONLY\r\n%s" % "\r\n".join(asMemoryProtectionInformation); oBugReport.sBugTypeId = "AV%s:Reserved" % sViolationTypeId; sBugDescription = "Access violation while %s reserved but unallocated memory at 0x%X." % \ (sViolationTypeDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue, if the address is attacker controlled."; elif uStateFlags == 0x1000: # MEM_COMMIT dsMemoryProtectionsDescription_by_uFlags = { 0x01: "inaccessible", 0x02: "read-only", 0x04: "read- and writable", 0x08: "read- and writable", 0x10: "executable", 0x20: "read- and executable", 0x40: "full-access", 0x80: "full-access" }; sMemoryProtectionsDescription = dsMemoryProtectionsDescription_by_uFlags.get(uAllocationProtectionFlags); assert sMemoryProtectionsDescription, \ "Unexpected MEM_COMMIT memory to have protection value 0x%X\r\n%s" % \ (uAllocationProtectionFlags, "\r\n".join(asMemoryProtectionInformation)); oBugReport.sBugTypeId = "AV%s:Arbitrary" % sViolationTypeId; sBugDescription = "Access violation while %s %s memory at 0x%X." % \ (sViolationTypeDescription, sMemoryProtectionsDescription, uAddress); sSecurityImpact = "Potentially exploitable security issue, if the address is attacker controlled."; if oCdbWrapper.bGenerateReportHTML: # Clamp size, potentially update start if size needs to shrink but end is not changed. uMemoryDumpStartAddress, uMemoryDumpSize = ftuLimitedAndAlignedMemoryDumpStartAddressAndSize( uAddress, uPointerSize, uAllocationStartAddress, uAllocationSize ); oBugReport.atxMemoryDumps.append(("Memory near access violation at 0x%X" % uAddress, \ uMemoryDumpStartAddress, uMemoryDumpSize)); else: raise AssertionError("Unexpected memory state 0x%X\r\n%s" % \ (uStateFlags, "\r\n".join(asMemoryProtectionInformation))); oBugReport.sBugDescription = sBugDescription + sViolationTypeNotes; oBugReport.sSecurityImpact = sSecurityImpact; dtxBugTranslations = ddtxBugTranslations.get(oBugReport.sBugTypeId, None); if dtxBugTranslations: oBugReport = oBugReport.foTranslate(dtxBugTranslations); return oBugReport;
def cCdbWrapper_fbDetectAndReportVerifierErrors(oCdbWrapper, asCdbOutput): uErrorNumber = None uProcessId = None sMessage = None uHeapBlockAddress = None uHeapBlockSize = None uCorruptedStamp = None uCorruptionAddress = None asRelevantLines = [] for sLine in asCdbOutput: # Ignore exmpty lines if not sLine: continue # Look for the first VERIFIER STOP message and gather information if uErrorNumber is None: oErrorMessageMatch = re.match( r"^VERIFIER STOP ([0-9A-F]+): pid 0x([0-9A-F]+): (.*?)\s*$", sLine) if oErrorMessageMatch: sErrorNumber, sProcessId, sMessage = oErrorMessageMatch.groups( ) uErrorNumber = long(sErrorNumber, 16) uProcessId = long(sProcessId, 16) asRelevantLines.append(sLine) continue asRelevantLines.append(sLine) # A VERIFIER STOP message has been detected, gather what information verifier provides: oInformationMatch = re.match(r"\t([0-9A-F]+) : (.*?)\s*$", sLine) if oInformationMatch: sValue, sDescription = oInformationMatch.groups() uValue = long(sValue, 16) if sDescription == "Heap block": uHeapBlockAddress = uValue elif sDescription == "Block size": uHeapBlockSize = uValue elif sDescription == "Corrupted stamp": uCorruptedStamp = uValue elif sDescription == "corruption address": uCorruptionAddress = uValue else: assert sLine.strip().replace("=", "") == "", \ "Unknown VERIFIER STOP message line: %s" % repr(sLine) break else: assert uErrorNumber is None, \ "Detected the start of a VERIFIER STOP message but not the end\r\n%s" % "\r\n".join(asLines) return False uHeapBlockEndAddress = uHeapBlockAddress + uHeapBlockSize uHeapPageEndAddress = (uHeapBlockEndAddress | 0xFFF) + 1 assert uHeapPageEndAddress >= uHeapBlockEndAddress, \ "The heap block at 0x%X is expected to end at 0x%X, but the page is expected to end at 0x%X, which is impossible." % \ (uHeapBlockAddress, uHeapBlockEndAddress, uHeapPageEndAddress) oCorruptionDetector = cCorruptionDetector(oCdbWrapper) # End of VERIFIER STOP message; report a bug. if sMessage in ["corrupted start stamp", "corrupted end stamp"]: assert uCorruptionAddress is None, \ "We do not expect the corruption address to be provided in the VERIFIER STOP message" sBugTypeId = "OOBW" # Both the start and end stamp may have been corrupted and it appears that a bug in verifier causes a corruption # of the end stamp to be reported as a corruption of the start stamp, so we'll check both for unexpected values: uPointerSize = oCdbWrapper.fuGetValue("$ptrsize") if not oCdbWrapper.bCdbRunning: return # https://msdn.microsoft.com/en-us/library/ms220938(v=vs.90).aspx uEndStampAddress = uHeapBlockAddress - uPointerSize # ULONG with optional padding to pointer size if uPointerSize == 8: # End stamp comes immediately after other header values oCorruptionDetector.fDetectCorruption(uEndStampAddress, 0, 0, 0, 0, [0xBA, 0xBB], 0xBB, 0xBA, 0xDC) else: oCorruptionDetector.fDetectCorruption(uEndStampAddress, [0xBA, 0xBB], 0xBB, 0xBA, 0xDC) uStackTraceAddress = uEndStampAddress - uPointerSize # PVOID uFreeQueueAddress = uStackTraceAddress - 2 * uPointerSize # LIST_ENTRY uActualSizeAddress = uFreeQueueAddress - uPointerSize # size_t uRequestedSizeAddress = uActualSizeAddress - uPointerSize # size_t uHeapAddressAddress = uRequestedSizeAddress - uPointerSize # PVOID uStartStampAddress = uHeapAddressAddress - uPointerSize # ULONG with optional padding to pointer size if uPointerSize == 8: # End stamp comes immediately before other header values oCorruptionDetector.fDetectCorruption(uStartStampAddress, [0xBA, 0xBB], 0xBB, 0xCD, 0xAB, 0, 0, 0, 0) else: oCorruptionDetector.fDetectCorruption(uStartStampAddress, [0xBA, 0xBB], 0xBB, 0xCD, 0xAB) assert oCorruptionDetector.bCorruptionDetected, \ "Cannot find any sign of corruption" elif sMessage == "corrupted suffix pattern": assert uCorruptionAddress is not None, \ "The corruption address is expected to be provided in the VERIFIER STOP message:\r\n%s" % \ "\r\n" % (asRelevantLines) # Page heap stores the heap as close as possible to the edge of a page, taking into account that the start of the # heap block must be properly aligned. Bytes between the heap block and the end of the page are initialized to # 0xD0. Verifier has detected that one of the bytes changed value, which indicates an out-of-bounds write. BugId # will try to find all bytes that were changed: sBugTypeId = "OOBW" uPaddingSize = uHeapPageEndAddress - uHeapBlockEndAddress oCorruptionDetector.fDetectCorruption( uHeapBlockEndAddress, *[0xD0 for x in xrange(uPaddingSize)]) assert oCorruptionDetector.bCorruptionDetected, \ "Cannot find any sign of corruption" elif sMessage == "corrupted infix pattern": assert uCorruptionAddress is not None, \ "The corruption address is expected to be provided in the VERIFIER STOP message:\r\n%s" % \ "\r\n" % (asRelevantLines) # Page heap sometimes does not free a heap block immediately, but overwrites the bytes with 0xF0. Verifier has # detected that one of the bytes changed value, which indicates a write-after-free. BugId will try to find all # bytes that were changed: sBugTypeId = "UAFW" oCorruptionDetector.fDetectCorruption( uHeapBlockAddress, *[0xF0 for x in xrange(uHeapBlockSize)]) assert oCorruptionDetector.bCorruptionDetected, \ "Cannot find any sign of corruption" else: sBugTypeId = "HeapCorrupt" if uHeapBlockSize is not None: sBugTypeId += "[%s]" % (fsGetNumberDescription(uHeapBlockSize)) if oCorruptionDetector.bCorruptionDetected: if uCorruptionAddress is None: uCorruptionAddress = oCorruptionDetector.uCorruptionStartAddress else: assert uCorruptionAddress == oCorruptionDetector.uCorruptionStartAddress, \ "Verifier reported corruption at address 0x%X but BugId detected corruption at address 0x%X" % \ (uCorruptionAddress, oCorruptionDetector.uCorruptionStartAddress) if uCorruptionAddress is not None: sMessage = "heap corruption" uCorruptionOffset = uCorruptionAddress - uHeapBlockAddress if uCorruptionOffset >= uHeapBlockSize: uCorruptionOffset -= uHeapBlockSize sOffsetDescription = "%d/0x%X bytes beyond" % (uCorruptionOffset, uCorruptionOffset) sBugTypeId += fsGetOffsetDescription(uCorruptionOffset) elif uCorruptionOffset > 0: sOffsetDescription = "%d/0x%X bytes into" % (uCorruptionOffset, uCorruptionOffset) sBugTypeId += "@%s" % fsGetNumberDescription(uCorruptionOffset) else: sOffsetDescription = "%d/0x%X bytes before" % (-uCorruptionOffset, -uCorruptionOffset) sBugTypeId += fsGetOffsetDescription(uCorruptionOffset) sBugDescription = "Page heap detected %s at 0x%X; %s a %d/0x%X byte heap block at address 0x%X" % \ (sMessage, uCorruptionAddress, sOffsetDescription, uHeapBlockSize, uHeapBlockSize, uHeapBlockAddress) uRelevantAddress = uCorruptionAddress else: sBugDescription = "Page heap detected %s in a %d/0x%X byte heap block at address 0x%X." % \ (sMessage, uHeapBlockSize, uHeapBlockSize, uHeapBlockAddress) uRelevantAddress = uHeapBlockAddress # If we detected corruption by scanning certain bytes in the applications memory, make sure this matches what # verifier reported and save all bytes that were affected: so far, we only saved the bytes that had an unexpected # value, but there is a chance that a byte was overwritten with the same value it has before, in which case it was # not saved. This can be detect if it is surrounded by bytes that did change value. This code reads the value of all # bytes between the first and last byte that we detected was corrupted: asCorruptedBytes = oCorruptionDetector.fasCorruptedBytes() if asCorruptedBytes: sBugDescription += " The following byte values were written to the corrupted area: %s." % ", ".join( asCorruptedBytes) sBugTypeId += oCorruptionDetector.fsCorruptionId() or "" sSecurityImpact = "Potentially exploitable security issue, if the corruption is attacker controlled" oCdbWrapper.oBugReport = cBugReport.foCreate(oCdbWrapper, sBugTypeId, sBugDescription, sSecurityImpact) oCdbWrapper.oBugReport.duRelevantAddress_by_sDescription \ ["memory corruption at 0x%X" % uRelevantAddress] = uRelevantAddress oCdbWrapper.oBugReport.bRegistersRelevant = False return True