Achieving full R\W primitive with CVE-2019-0539
CFI has most certainly set the standard for exploit mitigations, and has inspired many implementations such as Microsoft CFG, Microsoft RFG, PaX Team’s RAP™ and Clang’s CFI.
In this series of posts we’re going to demonstrate how modern CFI implementations can be circumvented.
Specifically in this post we’ll be demonstrating an advanced code reuse technique, Counterfeit Object-Oriented Programming (COOP) utilizing an old vulnerability to conform to the theoretical boundaries of CFI.
CVE-2015-5122 is a use-after-free vulnerability that was utilized by Hacking Team to exploit Adobe Flash Player (<= 126.96.36.199). An analysis of the vulnerability itself can be found here. Note that by leveraging this vulnerability we are able to gain a full read-write primitive to the process memory.
We based our work on Metasploit’s implementation of CVE-2015-5122 which can be found here. In order to achieve a read/write primitive the vulnerability is used to overwrite the length member of a vector object. The vector object is wrapped with a class named ExploitByteArray that contains the methods write(addr, data) and read(addr) that provide the full read/write primitives. Code execution is gained by defining a fake “magic” method in the Exploiter class, and overriding its virtual function pointer with an address of choice.
The metasploit implementation first calls VirtualProtect in order to change a sprayed stack-pivot stub’s page protection to READWRITE_EXECUTE, and then uses the magic method a second time to call the executable stub and commence a ROP chain.
// VirtualProtect the stub with a *reliable* stack pivot eba.write(stack_address + 8 + 0x80 + 28, virtualprotect) eba.write(magic_object, stack_address + 8 + 0x80); // overwrite vtable (needs to be restored) eba.write(magic + 0x1c, stub_address) eba.write(magic + 0x20, 0x10) var args:Array = new Array(0x41) Magic.call.apply(null, args); // Call to our stack pivot and init the rop chain eba.write(stack_address + 8 + 0x80 + 28, stub_address + 8) eba.write(magic_object, stack_address + 8 + 0x80); // overwrite vtable (needs to be restored) eba.write(magic + 0x1c, stack_address + 0x18000) Magic.call.apply(null, null); eba.write(magic_object, magic_table); eba.write(magic + 0x1c, magic_arg0) eba.write(magic + 0x20, magic_arg1)
This exploit implementation is sufficient in bypassing ASLR and DEP protections, however it’s far from being undetectable by modern CFI implementations.
For example, Shadow Stack (backward-edge) based CFI implementations will easily detect the stack pivot and the ROP chain execution as magic function returns to a different address than the address that was pushed on the stack by the original call instruction.
In order to make this exploit undetectable by modern CFI implementations, we must conform to the following constraints as described by Schuster et al:
C-1 indirect calls/jumps to non address-taken locations
C-2 returns not in compliance with the call stack
C-3 excessive use of indirect branches
C-4 pivoting of the stack pointer (possibly temporarily)
C-5 injection of new code pointers or manipulation of existing ones
C-6* indirect calls/jumps to “critical functions” We add an additional constraint that applies a fine-grain policy for “critical functions”.
Counterfeit Object-Oriented Programming (COOP) is a new code-reuse technique for C++ applications. This technique relies on the assumption that CFI solutions do not consider C++ semantics, as they don’t check that every virtual function is called by the appropriate virtual callsite. By creating a counterfeit object with a fake vtable with vptr’s of our choosing, we can invoke any virtual function without triggering CFI because CFI solutions don’t validate that the called virtual-function has any connection to the caller object’s class.
First, we need to find virtual functions that perform the operations we plan to do. These functions are called “vfgadgets”.
Second, in order to combine them we need to find a special vfgadget called Main Loop Gadget (ML-G or ML-ARG-G if it passes arguments). This vfgadget contains a loop that iterates through a list of objects and calls a virtual function for each one of them. It’s common to find such a vfgadget in a large C++ application. By filling the counterfeit ML-G object with a fake member list of objects, each object will hold a fake vtable containing a different vfgadget, we can execute different parts of the application’s code without violating the constraints described above.
We demonstrate below, the combination of two vfgadgets, ATL::CComControl::CreateControlWindow and ML-G CLibrariesFolderBase::v_AreAllLibraries.
Active Template Library(ATL) is a C++ template library that is used to simplify the programming of COM object in Windows. You can find these templates in some of Windows libraries, including shell32.dll (version 6.1.7601.23403). One of shell32.dll objects is CComControl, a class that provides methods for creating and managing ATL controls. The important method for us is CreateControlWindow. According to Microsoft’s documentation the method initializes and creates a window object by calling CWindowImpl::Create and which calls additional internal objects as described below. As every window, it needs to have a WndProc function – a callback function that will process all the messages that will be sent to the window. We found out that this function puts a thunk as the WndProc procedure of the window. This thunk transforms the Windows C callback call into a virtual function call, by overwriting the first WndProc stack argument (that’s supposed to be the HANDLE of the window) with a C++ this pointer The thunk data and its disassembly:
Thunk data: c7 44 24 04 [DWORD thisPointer] e9 [DWORD WndProc] disas: mov DWORD PTR [esp+0x4], thisPointer jmp WndProc
More information about the ATLThunk can be found here. Note, this method is relevant to Windows 7 Build 7601 SP1 as mentioned here, and is supposed to be changed in a later ATL version. What’s really interesting with this vfgadget is that the thunk is written to a page that is allocated by VirtualAlloc with EXECUTE_READWRITE permissions. This VirtualAlloc call is performed by ATL::_stdcallthunk::Init as you can see in the following screenshot (0x40 is the flProtect parameter, meaning EXECUTE_READWRITE):
Other considerations regarding this vfgadget are:
To summarize, we will create our counterfeit CComControl object, call the ATL::CComControl::CreateControlWindow vfgadget, and then use our read/write primitive (ExploitByteArray) to read the created thunk address. Ultimately giving us a READWRITE_EXECUTE page to store our shellcode. Well, almost, the Magic method we use in order to execute the vfgadget passes three arguments to virtual function, however ATL:CComControl::CreateControlWindow receives only two arguments which lead to a stack corruption and crashes the process. In order to avoid a stack corruption we use another vfgadget that receives 3 arguments and use it to call ATL::CComControl::CreateControlWindow.
To find such a vfgadget we searched shell32.dll with an IDA script with the following constraints:
We looked over the script result and chose CLibrariesFolderBase::v_AreAllLibraries.
This vfgadget is actually a mainloop gadget (ML-ARG-G) that calls the same virtual function (offset 0x4c of the VTABLE) in every iteration and stops the iteration only when the function returns a successful error code (0).
Building our counterfeit objects, the magic method pointer [vtable+0x18] will point to our ML-ARG-G, and the ML-ARG-G inner call vtable’s offset [vtable+0x4c] will point to CreateControlWindow vfgadget:
// first, we save the current this pointer, to recover it later original_this = eba.read(magic_object); var magic_vtable:uint = magic_object+0x40; // now, lets put a fake vptr at [magic_object] eba.write(magic_object, magic_vtable); // [vtable+0x18] will hold the first vfgadget that will be invoked // it will be the ML-ARG-G we found in shell32. eba.write(magic_vtable + 0x18, ml_arg_g); // [vtable+0x4c] will hold the second vfgadget that will be invoked // It will be the createWindow vfgadget we found in shell32 eba.write(magic_vtable+0x4C, createWindow_g)
Now, invoking the Magic method will perform our COOP flow:
eba.write(magic + 0x1c, 0x0) eba.write(magic + 0x20, magic_object+0x100) var args:Array = new Array(0x41) Magic.call.apply(null, args); eba.write(magic_object, original_this);
// createWindow allocated a page with EXECUTE_READWRITE protection, and stored a pointer to it on magic_object+5C var allocated_address:uint = eba.read(magic_object+0x5c) // get page base address allocated_address = allocated_address&0xFFFFF000
Now, we can simply write our compiled shellcode to the allocated_address, put this address in Magic vtable offset, and call the magic method again to achieve code execution.
There are several ways to implement exploits using the COOP technique. During the research we also found 5 vfgadgets in flash DLL (188.8.131.52) that do the following:
Combining all these vfgadgets together is possible but very complicated and is left as an exercise for the reader. As mentioned before, we found one simple vfgadget that gives us a memory page with READWRITE_EXECUTE protection.
We have successfully demonstrated how COOP can be used to circumvent modern CFI solutions by conforming to the constraints mentioned above.
C-1 the redirection of “magic” method’s original function would not have been detected by Microsoft CFG for the destination was a legal function of the binary.
C-2 no backward-edge CFI policies were violated since no return addresses were overwritten
C-3 no ROP chain was utilized
C-4 no stack pointers were changed
C-5 COOP manipulates object pointers avoiding the need to manipulate pointers in code
C-6 VirtualAlloc was invoked from a legal offset circumventing EMET’s critical function protection.
In order to mitigate COOP attacks CFI implementations must take language semantics and contextual states into account. As mentioned earlier, in this particular case applying a fine-grained policy that matches virtual call sites to the relevant object’s virtual functions or any other kind of semantic validation should be enough.
We’ve spotted CVE-2017-8570, a.k.a the “Composite Moniker” in the wild alive and kicking.
Resume files can be dangerous, especially when they are encrypted word documents.