What follows is a detailed analysis of the root cause of a vulnerability we call CVE-2015-X, as well as a step-by-step explanation of how to trigger it. For more on Flash vulnerabilities, we also invite you to read “The Latest UAF Vulnerabilities in Exploit Kits,” published May 28 by Tao Yan.
Not too long ago we came across a sample from the Angler Exploit kit (MD5: 049ff69bc23f36a78d86bbf1356c2f63c), which allegedly exploits CVE-2015-0359. The obfuscated SWF contains an encoded SWF (MD5: d45808cfa6f3cbfb343fdea269fdc375), which is later decoded and loaded into Flash, without getting saved on disk. Here’s a somewhat beautified example of this process:
The embedded SWF is heavily obfuscated, but the code has much in common with the source code for Angler EK’s CVE-2015-0313 exploit.
The first thing to do is determine whether the exploit is indeed for CVE-2015-0359. Some researchers think that it’s not. The embedded SWF contains a function we named CheckEnvironment, which eventually does this:
We note that playerVerOk is true if the Flash player’s version is at least 188.8.131.52, but older than 184.108.40.2069, or if the player’s version is at least 220.127.116.11, but older than 18.104.22.1685. However, the function returns true for any of the aforementioned versions, plus versions 22.214.171.1247 and 126.96.36.199.
We can see that if none of those versions is present, no exploitation occurs:
What’s more interesting, though, is that the exact exploit (vulnerability triggering) is chosen based on the version of Flash, like so:
For Flash versions 188.8.131.52 and above, but older than 184.108.40.2069, and for Flash versions 220.127.116.11 and above, but older than 18.104.22.1685, CVE-2015-0313 is used. The description of CVE-2015-0313 states the following:
“Use-after-free vulnerability in Adobe Flash Player before 22.214.171.1249 and 14.x through 16.x before 126.96.36.1995 on Windows …”
If the Flash version is 188.8.131.527 or 184.108.40.206, a different exploit (trigger) is used. This is the only line where the exploitation paths differ from each other, so if it had indeed been CVE-2015-0359, we would expect its description to look like the one for CVE-2015-0313. However, this is the description that appears instead:
“Double free vulnerability in Adobe Flash Player before 220.127.116.111 and 14.x through 17.x before 18.104.22.168 on Windows …”
We can say with confidence that this not CVE-2015-0359. This is probably a use-after-free (UAF) vulnerability, and not a double-free vulnerability (see the Technical Analysis section for actual proof). We therefore call this vulnerability CVE-2015-X.
This is the timeline of events for CVE-2015-X:
- February 2: CVE-2015-0313 is discovered as a 0-day exploited in the wild.
- Feburary 4/5: Adobe releases patched versions for CVE-2015-0313 for automatic/manual download. The patched version is 22.214.171.1245, and it also stops CVE-2015-X.
- February 25: A fully working exploit code for CVE-2015-0313 is published. A change of only a handful of code lines would create CVE-2015-X.
- March 12: Flash Player 126.96.36.199 is released. It is vulnerable to CVE-2015-X.
- April 14: Flash Player 188.8.131.52 is released, fixing both CVE-2015-0359 and CVE-2015-X.
This timeline suggests that attackers had a whole month to easily create and use CVE-2015-X against Flash Player 184.108.40.206, as a 0-day exploit. Both the attacks on CVE-2015-0313 and on CVE-2015-X were stopped by Palo Alto Networks Traps at the early exploitation stages, without causing any harm to the machine or to the organization. Prevention occurred without having any prior knowledge on the vulnerabilities or the attacks.
Having understood the gravity of the situation, and since there are at least a couple of almost-identical exploits that trigger “different” vulnerabilities, we would now like to understand the root cause of these vulnerabilities. Read on for a deep technical analysis, showing the root cause of these vulnerabilities and the vulnerability triggering flow.
The exploits for CVE-2015-0313 and CVE-2015-X both use a Flash ByteArray object, coupled with domainMemory, which performs the actual use of the dangling pointer. We would like to detail how achieving such a use-after-free situation is possible, and show a major flaw in the design of the ByteArray class.
We first need to make an important distinction between the class names exposed by Flash, and the class names that appear in the AVM. For example, the Flash ByteArray class is represented by the ByteArrayObject class in the AVM source code, and is not to be confused with the AVM ByteArray class, which is the internal representation of the byte array implementation. For the remainder of this research post, we’ll be using only the class names appearing in the AVM.
The AVM is written in C++. As opposed to how other languages are used, there are two ways to create an object: “statically” (where you would get back the actual object), and dynamically (where you would use the new operator and get back a pointer (reference) to the object). The classes concerning the representation of a byte array use both creation methods for their member variables.
Here are the member variables of ByteArrayObject:
m_byteArray is a static allocation of a ByteArray object. Its offset in the ByteArrayObject object is 0x18, for the tested version of Flash.
The ByteArray class inherits both from DataInput and DataOutput, and thus implicitly contains a representation of their objects. We note that the offset of the DataOutput object within the ByteArray object is 0x8.
Here are the member variables of ByteArray:
There are 3 interesting fields here:
- m_buffer: An object of a class with one field. The field points to an object of the ByteArray::Buffer class, which eventually leads to the actual array of bytes. In memory this field will be preceded by an additional field for the virtual function table pointer for the FixedHeapRef class. m_buffer is located at offset 0x24 in the ByteArray class, and so the interesting field is at offset 0x28.
- m_isShareable: This tells us whether the byte array may be shared between workers (Flash’s “threads”). Among other things, this has implications for the class’s copy constructor, because when the byte array is shared, there’s no need to copy the data, but rather to point to the exact same ByteArray::Buffer object m_buffer points to. The field can be found at offset 0x2C.
- m_subscribers: This is an object containing a pointer to a ListData object, which holds information about entities that should be notified when the byte array is reallocated/freed (i.e., its place in memory changes), or even simply when its size changes. Since byte arrays can be dynamically extended or cleared, this feature is important, though it is relevant only for entities holding a direct pointer to the actual byte array data, such as domainMemory. The field that has the pointer to the ListData object resides at offset 0x18 in the ByteArray object.
The ByteArray::Buffer object, reachable from m_buffer, has the following interesting fields:
- array (offset 0x8): A pointer to the actual array of bytes holding the data.
- capacity (offset 0xC): The maximum number of bytes the array can hold (actual allocation).
- length (offset 0x10): The number of array bytes currently “in use” by the program. If the program wants to use more bytes than are allocated (determined by the capacity field), the byte array must be reallocated, meaning that the array pointer will be changed to point to a different address in memory.
Finally, the ListData object reachable from m_subscribers has the following relevant field:
- len (offset 0x4): The number of subscribers that should be notified when the actual byte array buffer is reallocated (thereby changing its location in memory).
Here’s a graphical illustration of the objects and fields relevant to CVE-2015-X, for the examined Flash version:
The root cause of CVE-2015-X lies in the way the ByteArray’s equivalent of a copy constructor handles shareable byte arrays. If the byte array is shareable, the copy constructor uses the exact same reference to the ByteArray::Buffer object, but “forgets” about all of that byte array’s subscribers. This leads to an easy use-after-free situation, where the pointer to the actual byte array is changed (the old byte array is freed), but no subscribers are notified of that, and one of them later tries to use the dangling pointer.
There are actually two “copy constructors,” and both of them exhibit the same flaw in logic. Let’s look at the constructor that is used with workers:
We can see that m_subscribers is going to be initialized to an empty subscribers list, effectively “forgetting” that there may be subscribers that need to receive change notifications for the ByteaArray::Buffer object’s array member. It is important to note that although the figure above may make it seem as if this can only happen with workers, the other “copy constructor” also behaves in much of the same way, and may allow triggering of a similar bug in situations that don’t involve workers.
So, to trigger the bug, we must have a subscriber to a byte array, use a “copy constructor” to get a new byte array without subscribers, and make the “copied” byte array change its array pointer (e.g., add data to the byte array beyond its capacity) without notifying the original subscriber. Then, we can have the subscriber reference the freed buffer.
The in-the-wild exploits for both CVE-2015-0313 and CVE-2015-X used workers. The comments in the “copy constructor” shown above give us a hint as to why, but we still need to see how we can even get to running the vulnerable code. Before diving into the triggering code, let’s first examine carefully what happens when we create and run a new worker. Consider these two lines of ActionScript 3 (AS3) code:
We can find a hint of what’s to come in the following comment in the AVM source code:
We will later see how important this distinction between a worker and an actual light-weight thread is.
The documentation for the Worker class says that a worker is created from a byte array. This brings us here:
The initialization gets us here:
And we can see that a new Isolate object is created for the worker. Let’s drill down just one more time:
And we see that the isolate associated with the worker is of type ShellIsolate (which inherits from Isolate).
When we ask the worker to start running using the start method, it simply calls the following function:
This creates a new thread and starts it, like so:
We note that Isolate inherits from Runnable, and so the VMThread constructor saves the pointer to the ShellIsolate object (via Runnable*) in m_runnable:
Running the VMThread’s start method brings us here:
So a native (i.e., platform-specific) thread is created with startInternal as the function it should run. The function gets the VMThread object as a parameter, and invokes the run method from the associated isolate:
We get a run of yet another internal function:
And now we’re getting to the interesting part:
We can see here that a new TopLevel object is created for the worker’s isolate. The setup function contains these lines:
To understand why this is important, let’s see what is this TopLevel class. Quoting Adobe:
“The top level contains the core ActionScript classes and global functions.”
As such, we expect an object of the TopLevel class to exist only once, and this is indeed true, so long as you have just the main thread (“primordial worker”). However, when you create a new (“background”) worker, it is created using a new isolate, which creates a new TopLevel object. Recall the comment that we quoted earlier: When you create a worker, it creates a separate instance of a big part of the environment.
The fact that a new TopLevel object is created for the worker is key for exploitation. More specifically, the new TopLevel object contains an empty _isolateInternedObjects table, which will consequently allow us to reach the vulnerable code, as we will later see.
Having understood some of the mechanics of worker objects, we are now ready to show a detailed step-by-step example of triggering the CVE-2015-X vulnerability.
Step 1: Create a shareable Flash ByteArray object, and assign it to domainMemory.
Looking at the AVM sources, we see the effects of this assignment:
Where DomainEnv::globalMemorySubscribe generates a call to ByteArray::addSubscriber, which should add domainMemory (current domain) as a subscriber, to be notified of any changes in the underlying buffer holding the actual data for the object ba:
Debugging that with Windbg, and breaking on ByteArray::addSubscriber (offset 0x66B210), gives us the following:
The ecx register holds the this pointer, pointing to the ByteArray object (ba) that is adding a new subscriber (the current domain). Recalling the interesting fields in a ByteArray object, we get:
- The relevant ByteArray::Buffer object is on address 0x79D7230 (offset 0x28).
- The byte array is shareable (offset 0x2C).
- The subscriber information is on address 0x83050D0 (offset 0x18).
Examining the ByteArray::Buffer object we get:
So the actual array of bytes resides in memory on address 0x8317000 (offset 0x8), and its capacity (offset 0xC) and length (offset 0x10) are 0x1000 (4096 bytes).
Finally, looking at the subscribers ListData object, we get:
We can see that the length is 0 (offset 0x4), meaning there are no subscribers yet. We expect having a new subscriber at the end of the addSubscriber function, and indeed:
Step 2: Still in the main thread, create a worker, set the shareable byte array to be a property shared with the worker, and start the worker thread.
Let’s follow the code for setting the shared property:
As explained in Dion Blazakis’s seminal paper, atoms are actually tagged pointers. The channel item is created according to the object’s type, like so:
We get that the appropriate member function of the ByteArrayObject class is called:
Since our byte array is shareable, we get that the channel item that is stored as a shared property saves the pointer to the ByteArray::Buffer object (recall that this object holds the pointer to the actual array data, the capacity and the length, among other things), and the fact that the byte array is shareable.
One other important thing to note is the use of the intern function. Let’s see what it does:
m_value is the ByteArray::Buffer object we’ve just saved as part of the ChannelItem object. It is sent to the function internObject in the TopLevel object, along with the pointer to the ByteArrayObject object associated with (i.e., “holding”) that Buffer object. This association is then saved internally by the TopLevel object, like so:
So the key is the reference to the Buffer object, and the value is the reference to the ByteArrayObject object. This means that when you set a shared property of a byte array for a worker, the array’s ByteArray::Buffer reference can be used as a key to retrieve the corresponding ByteArrayObject reference via the TopLevel object. However, the only things that are saved in the channel item are the reference to the Buffer object, and the notion of whether this is a shareable byte array or not. Moreover, recall that the TopLevel object is unique per worker. Therefore, this association is only relevant to the current worker (which in this case is the primordial worker).
Step 3: In the worker thread, get the shared byte array property.
If we track the AVM source code at this point, we get to the root cause of the vulnerability. We start with getting the shared property:
Which leads us to Isolate::getSharedProperty, where we find this piece of code:
So the string “ba” is used as a key to fetch the corresponding reference to the ChannelItem object that we have saved on Step 2. Then, we use the function getAtom to convert the ChannelItem to an Atom, passing as an argument the TopLevel reference we got as a parameter ourselves (see previous code piece).
The function getAtom is the precursor to the vulnerability. This is how the function begins:
m_value is a reference to a ByteArray::Buffer object we got from the ChannelItem object, and we’re trying to find its associated ByteArrayObject object:
Although we put the relevant association in the _isolateInternedObjects table in Step 2, it was in the context of the primordial worker. Now, we’re in the context of a background worker, which has its own TopLevel object, with an empty _isolateInternedObjects table. This means that getInternedObject is going to return NULL. Had we issued the exact same call in the primordial worker, we would have gotten the exact same ByteArrayObject object that was put as a shared property, which also has a valid subscribers list.
Getting back to getAtom, we can clearly see the vulnerable “copy constructor” being called:
So we should get that the background worker has a reference to a new ByteArrayObject object, with no subscribers, that references the exact same ByteArray::Buffer object as the original ByteArrayObject object does in the primordial worker. We will see that this is indeed the case in the next step.
Step 4: In the worker thread, add bytes to the shared byte array, to force it to pass its capacity and reallocate the underlying buffer.
Execution of the writeBytes command will cause the freeing of the pointer (so as to allocate a larger array), without any notification to domainMemory. Before we examine the state of the byte array prior to executing the function, let’s remind ourselves of the values we saw in the primordial worker, after setting domainMemory to the byte array:
- The ByteArray object is at address 0x7A8F938.
- The ByteArray::Buffer object is at address 0x79D7230.
- The byte array is shareable.
- The object describing the subscribers is at address 0x83050D0.
- There is 1 subscriber (the current domain).
We now return to the writeBytes call in the background worker that will cause the “free” part of the UAF vulnerability. ByteArrayObject::writeBytes calls DataOutput::WriteByteArray, which later calls ByteArray::Write. We break on DataOutput::WriteByteArray (offset 0x6A1F10), though any other function we’ve just mentioned will do:
The ecx register holds the this pointer, which in this case points to a DataOutput object inside a ByteArray object (at offset 0x8). Comparing to the original values, we get:
- The ByteArray object is at address 0x8B0FF10 (0x8B0FF18 – 0x8).
- The ByteArray::Buffer object is at address 0x79D7230 (see offset 0x20 from DataOutput).
- The byte array is shareable (offset 0x24).
- The object describing the subscribers is at address 0x8BE2280 (offset 0x10).
So we get that this is a new ByteArray object, pointing to a new subscribers list, but sharing the same Buffer as the original ByteArray object in the primordial worker. This is what we anticipated during the analysis of Step 3.
Let’s see how many subscribers we have:
So there are no subscribers (offset 0x4) for the new ByteArray object, although domainMemory has raw access to the actual byte array pointed at by this object (through the Buffer object). This means that when we use the writeBytes method to write beyond the capacity of the buffer, forcing a reallocation and changing of position in memory, domainMemory will not be notified about that, and will keep on holding a pointer to an already-freed byte array.
Here is the code that actually gets called, but does not notify the relevant domain’s domainMemory due to the empty subscribers list (length returns 0):
The above is a detailed analysis of the root cause of the CVE-2015-X vulnerability, as well as a step-by-step explanation of how to trigger it. More importantly, we shared insights on workers, and suggested that there might be other paths leading to vulnerabilities of similar nature.
We note that the patches and code changes that prevented CVE-2015-0313 from being exploited did not actually address the root cause itself, which is exactly the same as in CVE-2015-X: deleting the subscribers’ information from the worker’s view of the byte array. This left the path open for exploitation of CVE-2015-X, a vulnerability based on the exact same bug. This exploitation indeed soon revealed itself.
Likewise, there may be similar vulnerabilities still lurking out there, perhaps even having to do with the exact same root cause. Deploying Palo Alto Networks Traps in your organization protects you from existing and future exploitation of applications, including in Flash.