CVE-2014-7911 – A Deep Dive Analysis of Android System Service Vulnerability and Exploitation

Jan 06, 2015
13 minutes
... views

In this post we discuss CVE-2014-7911 and the various techniques that can be used to achieve privilege escalation. We also examine how some of these techniques can be blocked using several security mechanisms.

The Vulnerability

CVE-2014-7911 was presented here along with a very descriptive POC that was written by Jann Horn. Described briefly, the ObjectInputStream doesn't validate that the serialized object's class type, as described in the serialized object, is actually serializable. It creates an instance of the wanted class anyway with the deserialized values of the object. Therefore, one can create object of any class, and control its private variables, by serializing objects from another class, that would be deserialized as data members of the wanted class.

Let's look at the example below:

The following snippet (copied from the original POC) shows a spoofed BinderProxy instance:

In the POC code that was provided above, an attacker serializes a class named AAdroid.os.BinderProxy and changes its name to android.os.BinderProxy after marshalling it, and before sending it to the system server.

android.os.BinderProxy class isn't serializable, and it involves native code that handles mObject and mOrgue as pointers. 

If it was serializable, then the pointers valued wouldn't be deserialized, but their dereferenced values would.

The deserialization code in ObjectInputStream deserializes the sent object as an android.os.BinderProxy instance, leading to type confusion.

As mentioned earlier, this type confusion results in the native code reading pointer values from the attacker's spoofed android.os.BinderProxy, supposedly private fields, which the attacker modified.

Specifically, the field of interest is mOrgue.

The android.os.BinderProxy contains a finalize method that will result in native code invocation. This native code uses mOrgue as a pointer.

This is the finalize method:

And this is the declaration of destroy:

The native destroy function:

Eventually, the native code invokes decStrong

(i.e., in drl->decStrong((void*)javaObjectForIBinder);)

Note that at this point, drl is controlled by an attacker, as evident by the line

So decStrong is going to be called with us controlling 'this' pointer.

Let's take a look on decStrong code from RefBase class source:

Note the line refs->mBase->onLastStrongRef(id); These lines will eventually lead to arbitrary code execution.

In the following screenshot of RefBase::decStrong assembly, the attacker controls r0('this pointer')

cve 1 6 15 1

Exploitation

The first use of the controlled register r0, which contains the 'this' pointer (drl) is in these lines:

These lines are translated to the following assembly:

First, r4 is loaded with the mRefs variable.

Note that r0 is the 'this' pointer of the drl, and mRefs is the first private variable following the virtual function table, hence it is 4 bytes after 'this' pointer.

Then, android_atomic_dec is being called with &refs->mStrong

This is translated to:

r0 now contains &refs->mStrong.

Note that the mStrong variable is the first data member of refs (in the class weakref_impl), and that this class contains no virtual functions, hence it does not contain a vtable, so the mStrong variable is at offset 0 of r4.

As one can tell -  the line refs->removeStrongRef(id); is not present in the assembly  simply because the compiler optimized and omitted it, since it has an empty implementation, as one can see from the following code:

Following the call to android_atomic_dec are these lines of code:

These are translated to the following assembly lines:

Note that android_atomic_dec returns the value of the specified memory address before the decrement took place. So in order to invoke refs->mBase->onLastStrongRef(id) (blx r2), we must get refs->mStrong to get the value of 1.

As we can see up to now, an attacker has several constraints that he must adhere to if he wishes to gain code execution:

  1. drl (our first controlled pointer, i.e. r0 when entering decStrong) must point to a readable memory location.
  2. refs->mStrong must have the value of 1
  3. The dereference chain at the line refs->mBase->onLastStrongRef(id) must succeed and eventually point to an executable address.

In addition, an attacker must overcome the usual obstacles of exploitation - ASLR and DEP.

One can employ basic techniques to fulfill these requirements and overcome the mentioned security mechanisms, including heap spraying, stack pivoting and ROP. Let’s look at these in detail.

Heap spray

An attacker's first step will be to get a reliable readable address with arbitrary data - most commonly achieved by a heap spray.

The system server provides several core functionalities for the android device, many of which are exposed to applications via various service interfaces.

A common paradigm to invoke a service in the context of the system server is like the following:

The acquired manager allows us to invoke functionality in the system server on behalf of us, via IPC.

Several services can be used by us for a heap spray, but for the purpose of this blog, we decided to use a heap spray that requires special app permissions, to prevent normal applications from utilizing this technique.

The location manager allows us to register test providers via the function addTestProvider - allowing us to pass an arbitrary name that contains arbitrary data. As we mentioned, one should enable developer options and enable the mock locations option in order to utilize this.

Note that this heap spray does have its limitations – the data is sprayed using the name field, which is Unicode. This imposes a limitation – we are limited to byte sequences which correspond to valid Unicode code points.

Spray addresses manipulation

After spraying the system server process memory address space, we encountered another issue - our chosen static address indeed pointed to readable data on each run, but not to the exact same offset in the spray chunk each time.

We decided to solve this problem by crafting a special spray that contains decreasing pointer values.

Here is an illustration of the sprayed buffer, followed by an explanation of its structure:

cve 1 6 15 2

STATIC_ADDRESS is the arbitrarily chosen static pointer in mOrgue.
GADGET_BUFFER_OFFSET is the offset of GADGET_BUFFER from the beginning of the spray.

In each run of system server process, the static address we chose points to our controlled data, but with different offsets.

r0 (which always hold the same chosen STATIC_ADDRESS) can fall to any offset in the "Relative Address Chunk", therefore point to any of the STATIC_ADDRESS + GADGET_BUFFER_OFFSET - 4N addresses, on each time.

Note the following equation:

GADGET_BUFFER = Beginning_of_spray + GADGET_BUFFER_OFFSET

In the case that r0 (=STATIC_ADDRESS) points to the beginning of the spray : STATIC_ADDRESS = Beginning_of_spray.

Hence: GADGET_BUFFER = STATIC_ADDRESS + GADGET_BUFFER_OFFSET

On any other case – r0(=STATIC_ADDRESS) points to an offset inside the spray (the offset is dword aligned):

STATIC_ADDRESS = Beginning_of_spray + 4N

Beginning_of_spray = STATIC_ADDRESS – 4N.

Hence: GADGET_BUFFER = STATIC_ADDRESS + GADGET_BUFFER_OFFSET – 4N

The higher offset in the chunk that r0(=STATIC_ADDRESS) points to, the more we have to subtract to make the expression:

STATIC_ADDRESS + GADGET_BUFFER_OFFSET - 4N points to GADGET_BUFFER.

No matter to which offset in the chunk r0 points to, dereference it would give us the current address of GADGET_BUFFER.                         

But where do we get if we dereference the other addresses in the chunk?

As farther as we go above r0, the farther the dereference would bring us below GADGET_BUFFER.

cve 1 6 15 3

Now that we have a valid working spray, let’s go back to analyzing the assembly.

So to overcome the second constraint – in which refs->mStrong must contain 1

[r4] should contain 1, hence [GADGET_BUFFER - 4] should contains 1.

Now, after atomic_dec return value is indeed 1, we should overcome the other dereferences to get to the blx opcode.

Note in order to succeed with this dereference, [GADGET_BUFFER + 4] should contain a KNOWN valid address.

We arbitrarily chose the known address - STATIC_ADDRESS.

cve 1 6 15 4

So now we can build the GADGET_BUFFER as following:

cve 1 6 15 5

ROP CHAIN

We chose to run the "system" function with a predefined command line.

In order to control the r0 register, and make it point to the command line string, we should use some gadgets that would manipulate the registers.

We got only one function call, so to take control on the execution flow with our gadgets, we should use a stack pivot gadget.

Therefore, the first function pointer is the preparations for the stack pivot gadget:

cve 1 6 15 6

Where r5 equals to the original r0 (STATIC_ADDRESS) as one can see at the beginning of decStrong.

Call to the next gadget - which should be 21(=0x54 / 4) dwords from the beginning of GADGET_BUFFER

cve 1 6 15 7

This gadget does the Stack Pivoting.

SP register points to r7 – therefore the stack is under our control and points to GADGET_BUFFER.

Ret to the next gadget that should be kept 8 dwords from the beginning of GADGET_BUFFER

(Note the pop     {r4-r11,pc} instruction, which pops 8 registers off the stack before popping pc).

cve 1 6 15 8

Now r0 points to 56 (0x38) bytes before GADGET_BUFFER, so we have 52 command line chars, excluding the "1" for atomic_dec.

Ret to the next gadget that should be kept 10 dwords from the beginning of GADGET_BUFFER (2 dwords after the current gadget – pop     {r3,pc})

cve 1 6 15 9

That is the last gadget where we call system!

Here is an updated layout of the memory for this to happen:

cve 1 6 15 10

Android and ARM

There are two important issues we should keep in mind when choosing the gadgets addresses.

  • There is an ASLR mechanism on Android, and the addresses wouldn't be the same on each and every time. In order to know the correct address, we use the fact that both system server process, and our app are forked from the same process - ZYGOTE, meaning we share the same modules. So we get the address of the necessary modules in system server process, by parsing the maps file of our process. 'maps' is a file in /proc/<pid>/maps which contains the memory layout and the loaded modules addresses.
  • On ARM CPU, there are two modes of opcode parsing: ARM (4 bytes per opcode) and THUMB (variable bytes per opcode – 2 or 4). Meaning that the same address pointed by PC, could be parsed differently by the cpu when running in different modes. Parts of the gadgets we use are in the THUMB mode. In order to make the processor change its mode when parsing those gadgets, we change the pointer address from the actual address, to (address & 1) - turning on the LSB, which make the cpu jmp to the correct address with THUMB mode.

PAYLOAD

As described before, we use the "system" function to run our command line. The length of the command line is limited, and actually a command line can't be used for every purpose. So we decided to use a pre compiled elf, that being written to the file system, as an asset of our app. This elf can do anything with uid 1000 (the uid of system server). The command line we send as an argument to system is simply –

"sh -c " + file_path

CONCLUSION

Android has some common security mechanisms such as ASLR and DEP which should make an exploitation of vulnerability harder to achieve.

Moreover, every application runs in its own process, so the IPC communication could be validated, and making guesses on memory layout shouldn't be intuitive. On the other hand, the fact that every process is forked from the same process makes ASLR irrelevant for vulnerabilities within zygote's sons and the binder connection from every process to system server could lead to heap spray as seen on this post. Those issues appear to be inherent in the Android OS design.

Palo Alto Networks has been researching an Android security solution that based on our lab testing would have blocked this exploit (as well as other exploits) with multiple exploit mitigation modules. We hope to share more details in the coming months.


Subscribe to the Blog!

Sign up to receive must-read articles, Playbooks of the Week, new feature announcements, and more.