CVE-2015-5122: Exploitation Using COOP

0xshlomil · June 4, 2018

In this post we discuss advanced code reuse technique “COOP,” which uses an old vulnerability to conform to the theoretical boundaries of CFI.

Originally posted at https://perception-point.io/blog/cve-2015-5122-exploitation-using-coop/

Special thanks to my colleague Oshri Sela for collaborating with me on this piece.

Introduction

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

CVE-2015-5122 is a use-after-free vulnerability that was utilized by Hacking Team to exploit Adobe Flash Player (<= 18.0.0.203). 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 = 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 the magic function returns to a different address than the address that was pushed on the stack by the original call instruction.

CFI Constraints

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”

COOP

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 th… 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 the 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 li… We demonstrate below the combination of two vfgadgets: ATL::CComControl::CreateControlWindow and ML-G CLibrariesFolderBase::v_AreAllLibraries.

vfgadget #1

Active Template Library (ATL) is a C++ template library that simplifies the programming of COM objects in Windows. These templates are found in some 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 … As every window, it needs to have a WndProc function – a callback function that processes all the messages sent to the window. This function places 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 (the supposed HANDLE of the window) with a C++ this pointer. The thunk data and its disassembly are as follows:

Thunk data:
c7 44 24 04 [DWORD thisPointer] e9 [DWORD WndProc]

Disassembly:
mov DWORD PTR [esp+0x4], thisPointer
jmp WndProc

More information about the ATLThunk can be found here. Note: This method applies to Windows 7 Build 7601 SP1 as mentioned here and was changed in later ATL versions.

A notable aspect of this vfgadget is that the thunk is written to a page allocated by VirtualAlloc with EXECUTE_READWRITE permissions. The VirtualAlloc call is performed by ATL::_stdcallthunk::Init, as seen in the following screenshot (0x40 is the flProtect parameter, meaning EXECUTE_READWRITE):

ATL::_stdcallthunk::Init

Other considerations for this vfgadget:

  • The created thunk pointer is saved later inside our CComControl fake object.
  • One check can prevent the vfgadget from calling ATL::_stdcallthunk::Init, but we have full control over this check because it validates that one of the object’s members is NULL.

To summarize, we create our counterfeit CComControl object, call the ATL::CComControl::CreateControlWindow vfgadget, and use our read/write primitive (ExploitByteArray) to read the created thunk address. This gives us a READWRITE_EXECUTE page to store our shellcode. However, the Magic method we use to execute the vfgadget passes three arguments to the virtual function. But ATL::CComControl::CreateControlWindow accepts only two arguments, which results in stack corruption and a process 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.

vfgadget #2

To find such a vfgadget, we searched shell32.dll with an IDA script using the following constraints:

  1. A virtual function (it is xref’d from a vtable).
  2. The function contains at least one indirect call that involves registers.
  3. The function receives 3 arguments like our Magic method (detected by the “retn X” opcode).
  4. The function passes 2 arguments to the indirect called function (detected by subtracting the pops count from the pushes count).
  5. The function’s memory size is smaller than 0x30 bytes – after all, we don’t want a long and complex vfgadget.

We reviewed the script results and chose CLibrariesFolderBase::v_AreAllLibraries.

This vfgadget is 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).

vfgadget Execution


Combining it all together

To build our counterfeit objects:

  1. The magic method pointer [vtable+0x18] will point to our ML-ARG-G.
  2. The ML-ARG-G inner call vtable’s offset [vtable+0x4c] will point to the CreateControlWindow vfgadget.
// First, we save the current this pointer, to recover it later
original_this = eba.read(magic_object);

var magic_vtable = magic_object + 0x40;

// Now, let's 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 CreateControlWindow 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 = new Array(0x41);
Magic.call.apply(null, args);
eba.write(magic_object, original_this);

Counterfeit objects in memory

When our COOP flow finishes, we can read the created READWRITE_EXECUTE allocated page pointer. It will be stored at offset 0x5c from the this pointer:

// createWindow allocated a page with EXECUTE_READWRITE protection,
// and stored a pointer to it on magic_object + 5C
var allocated_address = eba.read(magic_object + 0x5c);

// Get page base address
allocated_address = allocated_address & 0xFFFFF000;

Memory Allocation

Finally, we can write our compiled shellcode to the allocated_address, update the Magic vtable offset with this address, and call the Magic method again to achieve code execution.

Additional Thoughts

There are several ways to implement exploits using the COOP technique. During the research, we also found 5 vfgadgets in Flash DLL (18.0.0.203) that do the following:

  1. Create 2 directories, under any path of our choice.
  2. Write a file to a path under the name “digest.s”.
  3. Call MoveFileEx API as we have full control over the source and destination parameters (this will rename the file “digest.s” to “atl.dll,” which is necessary for the final vfgadget).
  4. Call SetCurrentDirectory API with a controllable path parameter. We can use it to set the process’s current directory to the path containing our payload file.
  5. Call LoadLibrary to “atl.dll,” which will load our payload DLL.

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.


Conclusion

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 the “magic” method’s original function would not have been detected by Microsoft CFG since 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.

Twitter, Facebook