TLDR; There’s a bug in Adobe Flash. [It got allocated with CVE-2018-15981]

The interpreter code of the Action Script Virtual Machine (AVM)

does not reset a with-scope pointer when an exception is caught,

leading later to a type confusion bug, and eventually to a remote code execution.

First, we will start with general information on how the Flash engine, AVM, works,

because I find it fascinating and important for understanding the matter at hand.

Flash runs code by either JITting it or by interpreting it. The design of the engine is to verify the ActionScript3 (AS in short) code and the meta data only once upon loading a (SWF) file and then to run the code while trusting it (without re-validation). This improves significantly the performance of running Flash applications. This affects instructions, they are also designed to have operands as part of their (fixed) byte code. For example, the pushstring instruction will take an index number into the strings array that is defined in the SWF file and it can only point to a valid entry in that array (if it won’t, the verifier component will reject this file from being run). Later, in the JITted code or interpreter, it will assume that the verifier already confirmed this instruction with its operand and just load the requested entry and push it to the stack, as opposed to reading the entry from a register, which can be dynamically set in runtime and thus require a runtime check.

The AS code also supports a stack and registers, and since it is a dynamic language that lets one define members and properties (key/value pairs) on the fly, the engine will have to search for them every time, and to coerce their types too to the expected types; of a function argument for example. This coercion is done all over the place and is the heart of making sure types are safe all over their use. The coercion will even go further and change a type to another expected type automatically to be valid for that function call.

When the AVM runs code, the relevant part in the verifier is doing its important job by following and analyzing statically all instructions by simulating them and it verifies the types of all registers, variables on the stack and more, in order to see how they are about to be used, and if it sees they are used as invalid/wrong types, it will fail loading the file in advance. Otherwise it will just let the operation continue normally with assuming the type is correct and expected. Imagine there’s a function prototype that receives an integer argument:

function foo(bar:int):void {}

but you pass a string to it instead:

foo("imahacker")

then the verifier won’t like it and will fail this code from running. In other times, if you pass an object which has a default-value method, it might invoke it in run time to hopefully get an integer, but it will make sure in runtime that it really got an integer… so some of the checks also happen in runtime when it can’t know in static time (so yes, it’s okay to pass object as an integer).

Another trait of AS is what make properties dynamic, or technically how they are searched for every time. Starting with local scope and going to higher scopes in the hierarchy up to the globals’ scope. This is all documented. It hurts to know that much searching is done in order to find the right property in the scope chain every time code accessed it.

Perhaps Flash gives full OOP abilities on top of a dynamic language but when we take a look at the open source code of the AVM, it’s really horrifying, code dups all over instead of encapsulation, or the same part of the SWF will be parsed in multiple places and each time different functionality will be applied to it and other horrors…that we, security researchers, love.

Anyway, Flash will prefer to use the JIT as much as possible to run actual code, because it’s faster. It will emit real code of the target hosting architecture while doing the same pass for verifying the code. And it can even JIT a function while it’s already running inside the interpreter and continue from that state, impressive. However, remember that the interpreter is always used to run constructors of a user class defined in the SWF. And this is how we make sure our code will get to run inside the vulnerable interpreter. The engine can also handle exceptions on its own (try-catch and throw), employing setbuf and jmpbuf, so if an exception is raised inside an AS function, the interpreter implementation or the JIT infrastructure itself will catch it and pass it along the

next handler in the chain until a correct AS catch-handler with the right type will be found and execution will resume inside the interpreter, in our case, from that spot of the catch handler.

Basically, if you try to make an infinite call-recursion to itself, you won’t see a native (Windows/Linux) exception thrown, but rather the interpreter checks the size of its own stack artificially in some spots and will emulate its own stack overflow/underflow exceptions. If you will try to do an integer division by zero, then they already handle it themselves in the div instruction handler, etc. It’s pretty much robust and a closed system. There are many other interesting Flash internals topics, like atoms, the garbage collector, JIT, etc, but that won’t be covered today.

Now that you have some idea of how the AVM works.

Let’s talk business.

In AS you can use the keyword “with” to be lazy and omit the name of the instance whose members you want to access.

That’s normal in many scripting languages, but I will show it nevertheless.

For example, instead of doing:

point.x = 0x90;

point.y = 0xc3;

point.z = 0xcd;

It can be written as following using the “with” keyword:

with (point)

{

x = 0x90;

y = 0xc3;

z = 0xcd;

}

For each assignment the interpreter will first use the with-scope if it’s set to avoid searching the full chain, once it got the property it will make the assignment itself. So, there’s an actual function that looks up the property in the scopes array of a given function. The origin of the bug occurs once an exception is thrown in the code, the catch handler infrastructure inside the interpreter will NOT reset its with-scope variable, so actually it will keep looking for properties in that with-scope in the next times. But (a big BUTT) the verifier, while simulating the exception catching,

will reset its own state for the with-scope. And this, ladies and gentlemen, leads to a discrepancy between the verifier and the interpreter. Thus, opens a type confusion attack. Pow pow pow. In other words, we managed to make the interpreter do one thing, while the verifier thinks it does another (legit) thing.

This is how we do it:

In the beginning we load the with-scope with a legit object. We later raise a dummy exception and immediately catch it ourselves. Now, the interpreter will still use the with-object we loaded, although the verifier thinks we don’t use a with-scope anymore, we will query for a member with a certain controlled type from the with-scope again and now use it as an argument for a function or an operand for an instruction that expects something else, and voila we got a type confusion. Remember, the verifier will think we use a different property that matches the expected type we want to forge and thus won’t fail loading our file.

Let’s see the bug in the interpreter code first, and then an example on how to trigger it.

At line 796, you can see:

register Atom* volatile withBase = NULL;

That’s the local variable of the interpreter function that we’re messing up with (pun intended)! Note that technically the withBase is used as a pointer-offset to the scopeBase array

(that’s all the scopes that the function loaded on its scopes stack), but that doesn’t change the logic or nature of the bug, just how we tailor the bits and bytes to trigger the bug, if you are interested to understand this confusing description, you will have to read findproperty implementation. And at line 2347 which is the handler of the findproperty instruction:

*(++sp) = env->findproperty(scope, scopeBase, scopeDepth, multiname, b1, withBase);

See they pass the withBase to the findproperty, that’s the one to look up a property in the scope chain. This is where we will make it return a different property,

while the verifier will think it returned a valid typed property from our object. Now, we can use throw keyword to raise an exception, and the catch handler infrastructure at line 3540, will handle it.

CATCH (Exception *exception)

You can see that it will reset many variables of its own state machine, and set the interpreter’s PC (program counter, or next instruction’s address) to start with the target handler’s address, etc.

But they forgot to reset the withBase. I bet they didn’t forget, and they did it for performance sake, their code has tons of micro and obsessive optimizations, that today a good engineer wouldn’t just do. You can also note that they clear scopeBase array only in debugger mode (line 3573), and that used to be another bug, until they realized they better do it always.

They used to have many bugs around the scope arrays in the interpreter, but they’re all fixed now in the binaries, since we look at an old source code you can still find them.

Finally, let’s see how we would maneuver this altogether.

I used the great rabcdasm tool to assemble this.

This code is partial and only the relevant guts of the triggering exploit.

; All functions start with some default scope, we keep it too. getlocal0 pushscope ; We create a property with the name "myvar" that is really an object itself of type NewClass2. ; This property which is inside the object/dictionary will be the one the verifier sees. getlocal0 findpropstrict QName(PackageNamespace(""), "NewClass2") constructprop QName(PackageNamespace(""), "NewClass2"), 0 initproperty QName(PackageInternalNs(""), "myvar") ; We push a second item on the scope array, ; to make the withBase point to its location in the scopes array. ; Note this array contains both normal scope objects and with scope objects. getlocal0 pushwith ; Now we throw an exception, just to make the interpreter keeping a stale withBase. L10: pushbyte 1 throw L12: nop L16: ; This is our catch handler, we continue to do our thing. ; Now note that the withBase points to the second item on the scopes array, ; which currently won't be used until we put something there (because the scopes array is empty). ; Put back the first scope on the scope stack, so we can work normally. getlocal0 pushscope ; Now we're going to create an object which has one property of an integer type and its name is "myvar". ; This property will be returned instead of the correct one from the object we created above. pushstring "myvar" ; Next is the attacking type to be really used instead!!!1@ :) ; This is the actual value that will be fetched by getproperty instruction below. pushint 534568 ; Create an object with 1 property (name, value pair on the stack) newobject 1 ; Mark it as an object type. coerce QName(PackageNamespace(""), "Object") ; And now push it to the scope, note we don't use pushwith this time! ; Because we want to keep old value, otherwise verifier will be on to us, ; This makes our object that we just created as the new with-scope object. pushscope ; Now, findproperty et al will actually scan for our property using the withBase in practice, ; which has our fake object that we recently created, ; containing a property with the "myvar" name, with a different type from what the verifier sees ; (Remember - it sees the object above, in the beginning of this function). findproperty Multiname("myvar", [PackageInternalNs(""), PackageNamespace("")]) getproperty Multiname("myvar", [PackageInternalNs(""), PackageNamespace("")]) ; Now practically on the stack we have an integer, ; instead of an object, and next executing getslot which assumes an object (NewClass2) is in the stack, ; will crash miserably! getslot 1 returnvoid

The triggering code can be done with many different variations. Instructions like nextvlaue can be targeted too, because it doesn’t verify its operands in runtime and can leak pointers etc.

When I found this bug at first, I thought there’s small chance it’s a real bug. Particularly, I had my doubts, because the chances to have a forgotten/dangling with-scope is high in a normal Flash application. So how come nobody encountered this bug before as a misbehavior of their app? E.G. by getting a wrong variable, etc. Apparently, the combination to cause this scenario accurately is not that high after all.

Good bye Flash, you’ve been kind…