9

I've been reading up-on DoS (denial-of-service) attacks within android and although I understand attacks like the below, which make use of regular programming functions etc.

  • (Android Web Browser) When an Android Device visits a particular webpage, that webpage can retain JavaScript which can using a large loop push Android devices to open linked applications e.g. the Android Market Place, Email, Messaging etc. many times proving a crash http://packetstormsecurity.com/files/118539/Android-4.0.3-Browser-Crash.html

Vulnerability I can't understand:

Vulnerability Details :

CVE-2015-1474

Multiple integer overflows in the GraphicBuffer::unflatten function in platform/frameworks/native/libs/ui/GraphicBuffer.cpp in Android through 5.0 allow attackers to gain privileges or cause a denial of service (memory corruption) via vectors that trigger a large number of (1) file descriptors or (2) integer values.

http://www.cvedetails.com/cve/CVE-2015-1474/

a

Google Android Integer Overflows in GraphicBuffer::unflatten() Let Remote Users Execute Arbitrary Code

SecurityTracker Alert ID: 1031875

SecurityTracker URL: http://securitytracker.com/id/1031875

CVE Reference: CVE-2015-1474 (Links to External Site)

Date: Mar 10 2015

Impact: Execution of arbitrary code via network, User access via network

Fix Available: Yes Vendor Confirmed: Yes

Description: A vulnerability was reported in Google Android. A remote user can execute arbitrary code on the target system.

A remote user can send specially crafted data to trigger an integer overflow in GraphicBuffer::unflatten() and potentially execute arbitrary code on the target system. The code will run with the privileges of the target application.

Impact: A remote user can execute arbitrary code on the target system.

Solution: The vendor has issued a source code fix, available at: https://android.googlesource.com/platform/frameworks/native/+/38803268570f90e97452cd9a30ac831661829091

Vendor URL: android.googlesource.com/ (Links to External Site)

Cause: Boundary error

Underlying OS:

Message History: None.

http://www.securitytracker.com/id/1031875

a

References:

    http://www.cvedetails.com/cve/CVE-2015-1474/
    http://www.securitytracker.com/id/1031875

I find issues like the above (making use of specific O.S frameworks etc.) a lot more difficult to understand.

What I can gather from the above is:

The attack allows attackers to:

  • gain privileges
  • cause denial of service (memory corruption)

But I can't understand what exactly is opening the O.S to these problems? e.g. What exactly does this mean: A remote user can send specially crafted data to trigger an integer overflow in GraphicBuffer::unflatten()

How can an attacker do something like this remotely? What does GraphicBuffer::unflatten() mean?

Or

How would a user would go about executing this arbitrary code and what sort of code would this be?

What am I missing?


Small queries about Answer:

Firstly, where is the function native_handle_create and variable native_handle declared (as I can see their use, but neither local or global declaration).

Secondly how did you determine what the actual size of the buffer should be(so to determine, whether the current size of numFds or numInts was incorrect i.e. shouldn't be -1)?

Thirdly what does h->data mean, as I can't find declaration for a variable named data and I don't know what -> means? also what is sizeof(int)?

Finally I thought numFds was unsigned const size_t numFds = buf[8];, and if numFds was of type size_t I thought it shouldn't be able to hold negative numbers to begin with (unless I've missed something)? Also, I don't understand how we move from creating Heap Corruption or Array Index Out of Bounds Exception etc. to arbitrary code execution?

I apologize if these further queries are irritating, I'm just trying to properly get my head around it. Thanks again :-)

8

The easiest way to understand the vulnerability is to look at the diff, dig through the code, and work out how you might exploit it.

The vulnerable method's signature looks like this:

status_t GraphicBuffer::unflatten(
    void const*& buffer, size_t& size, int const*& fds, size_t& count) {

The important arguments here are void const*& buffer, which is a buffer of data for unflattening, and size_t& size, which specifies the size of the buffer.

Keep in mind that size_t is typedef unsigned int, which means it's an unsigned integer. This means it cannot represent negative values.

The method starts as follows:

// if the size of the buffer is less than 8 integers, return because it's too small
if (size < 8*sizeof(int)) return NO_MEMORY;

// cast the buffer to an integer pointer type (think "array of ints")
// remember that the size argument is NOT an int, but a size_t - therefore unsigned int.
int const* buf = static_cast<int const*>(buffer);

// check that the buffer contains the right magic number at the start
if (buf[0] != 'GBFR') return BAD_TYPE;

// populate two size_t variables - these are the ones which cause the integer overflow later!
// note that this data is supplied by the calling application, so it can control it.
const size_t numFds  = buf[8];
const size_t numInts = buf[9];

So, as you can see, there's some cursory checking to see if the buffer is the right size. It then goes on to read more data from the buffer and populate two counts (Fds and Ints). These numbers specify how many values come after the header data.

Let's carry on...

// check that the buffer is large enough to contain the number of ints specified.
// notice that 10 is the number of ints read so far (buf[9] was our last access)
// so in order to store numInts ints, the buffer needs to be 10 + numInts ints long.
const size_t sizeNeeded = (10 + numInts) * sizeof(int);
// if the buffer isn't big enough, fail
if (size < sizeNeeded) return NO_MEMORY;

// if the count argument is less than zero, fail
// this is meaningless because count is size_t, so can't be negative
size_t fdCountNeeded = 0;
if (count < fdCountNeeded) return NO_MEMORY;

if (handle) {
    // free previous handle if any
    free_handle();
}

So far so good, mostly. It's checked to see that the buffer is big enough to have the number of "Ints" that the header claims. There's a pointless check for negative counts, but that's impossible anyway. Finally it cleans up any old handle just in case, to prevent handle leaks.

Now comes the vulnerability...

// if either numFds or numInts is nonzero...
if (numFds || numInts) {
    // read a bunch of fields...
    width  = buf[1];
    height = buf[2];
    stride = buf[3];
    format = buf[4];
    usage  = buf[5];

    // native_handle_create is just a wrapper for malloc'ing a struct which
    // contains a few metadata fields and the fds/ints data.
    // the function takes two ints as its parameters, so it can take negatives.
    // the underlying code does malloc(...+((numFds+numInts)*sizeof(int)))
    // and since nothing has checked if numFds < 0, this will make a buffer
    // which is smaller than expected!
    //
    // example: if numFds is -1 and numInts is 20, the allocated data buffer
    // will be 19*sizeof(int) bytes long.
    //
    // this is potentially bad already, but it gets worse.
    native_handle* h = native_handle_create(numFds, numInts);

    // this will copy the list of file descriptors (fds) from the fds pointer
    // to the h->data pointer. the number of bytes to copy is numFds*sizeof(int).
    // it is important to note that the third argument of memcpy takes a size_t,
    // which is unsigned. numFds is a signed int, but when you multiply it with
    // sizeof(int), which is type size_t, you get a size_t, which is interpreted
    // as an UNsigned integer. as such, -1 (0xFFFFFFFF) in signed becomes
    // 4294967295 when translated over to size_t. so, it attempts to copy way
    // too much data from fds to h->data, leading to heap corruption.
    memcpy(h->data,          fds,     numFds*sizeof(int));

    // <-- CRASH HERE, HEAP IS CORRUPTED

    memcpy(h->data + numFds, &buf[10], numInts*sizeof(int));

    handle = h;
} else {
    width = height = stride = format = usage = 0;
    handle = NULL;
}

Essentially the problem is that the number of file descriptors (numFds) is read as a signed integer, but then gets converted to an unsigned integer for the memcpy, which turns a small negative into a large positive and overruns the heap buffer. This is all caused by missing checks on the values of numInts and numFds.

According to the FullDisclosure post, you can exploit it by crafting a class which derives from BufferQueue, which has access to the data that ultimately gets passed to unflatten's buffer argument.

Now, since all this vulnerable code sits inside the system_server process, which runs under the system user, corrupting its heap from a user application (i.e. an app) is a big deal. If you could overwrite a function pointer (e.g. from a callback or event) inside a struct or class that is on the heap, you could gain arbitrary code execution within system_server, thus gaining you privileges.


To answer your clarification requests...

Firstly, where is the function native_handle_create and variable native_handle declared (as I can see their use, but neither local or global declaration).

Android Cross Reference is useful for finding this kind of thing. The function is declared in native_handle.c and the native_handle struct (not a variable - h is the pointer variable of type native_handle) is declared in native_handle.h.

Secondly how did you determine what the actual size of the buffer should be(so to determine, whether the current size of numFds or numInts was incorrect i.e. shouldn't be -1)?

Well we know that there can't be -1 items in a list, so that's invalid. If you look at the patch diff, they've added some checks:

// compute the maximum possible number of ints that you can store in a buffer
// whose size is represented by a size_t.
const size_t maxNumber = UINT_MAX / sizeof(int);
// if numFds exceeds the max number of possible ints (e.g. if 0xFFFFFFFF is passed)
// this check will fail. the second check is the same, except it accounts for the
// fact that 10 ints in the buffer are already used for other purposes.
if (numFds >= maxNumber || numInts >= (maxNumber - 10)) {
    // clear data and fail
    width = height = stride = format = usage = 0;
    handle = NULL;
    ALOGE("unflatten: numFds or numInts is too large: %d, %d",
            numFds, numInts);
    return BAD_VALUE;
}

Later we can see even more checks...

 // originally this was set to 0, which essentially did nothing.
 size_t fdCountNeeded = numFds;
 if (count < fdCountNeeded) return NO_MEMORY;

The final check after the native_handle_create call prevents a failed allocation from causing problems - before it would just do the memcpy to h->data even if h was null, leading to a null dereference bug (DoS condition).

So these checks ensure that the input values aren't outside of bounds and therefore can't be exploited as before.

Thirdly what does h->data mean, as I can't find declaration for a variable named data and I don't know what -> means? also what is sizeof(int)?

The -> operator is a dereference. The operand on the left is a pointer to a struct. So h->data dereferences h, which is a pointer to a struct of type native_handle (we mentioned this earlier) and accesses the data field of that struct. By comparison, if h was not a pointer but was just a plain struct, you'd do h.data instead.

The sizeof macro gives you the size of a type in bytes. So sizeof(int) will return the native int size for your compiler implementation and target platform. In most cases it should return 4 or 8 to specify a 32-bit or 64-bit integer.

Finally I thought numFds was unsigned const size_t numFds = buf[8];, and if numFds was of type size_t I thought it shouldn't be able to hold negative numbers to begin with (unless I've missed something)?

Yes, numFds is size_t, but that just means that the raw bytes within that value are interpreted as an unsigned integer. If it contains 0xFFFFFFFF, its directly interpreted decimal value would be 4294967295. However, the problem comes when you cast a size_t to an int, at which point the raw bytes within are interpreted as a signed integer, in which case that 0xFFFFFFFF suddenly turns into -1.

Also, I don't understand how we move from creating Heap Corruption or Array Index Out of Bounds Exception etc. to arbitrary code execution?

That's a complicated topic, but I'll give you a summary. The heap is where most of your application data goes. This means that when you're making buffers and instances of classes and various other things, most of that data is in the heap. Now, imagine you've got a class with an event - let's call it Dog, and have the event called Bark. When certain conditions arise, an instance of that class will raise the Bark event, so that other objects can react to that bark. An event is just a pointer. Other objects handle that event by assigning it a pointer to a method, i.e. a pointer to some code. So, when the Dog instance wants to raise Bark, it looks at the Bark pointer and starts executing the code there. In assembly this would look something like call [eax+0x0...].

Since the Dog instance exists in memory, on the heap, so does its Bark pointer. If you could overwrite the Bark pointer to point to some other executable memory, then caused the Dog instance to raise the event, it'd run the code at that other location. If you control the code at that location (e.g. by filling a buffer with code and using that address for your pointer) then you get arbitrary code execution.

You can exploit this by simply flooding the heap with data structures that are likely to overwrite your target data in a meaningful way. It's not always 100% reliable, but that's just how it goes sometimes.

Things get even more complicated with NX and ASLR, where you have to use pointer leaks and ret2libc/ROP to exploit it, but that's a much bigger topic.

If you want to get into exploit writing, I highly recommend Corelan's Exploit Writing Tutorials. They're a little out of date - you'll want a WinXP VM with hardware NX disabled - but it's still a strong resource.

  • First of all thank you, your answer was fantastic and has has explained a lot :-) I feel cheecky asking and for that I apologize but, I really hope you don't mind, if I ask one or two small queries about it (just as, this is very complex). - I've added these to my actual question above (so easier to read) – user76779 May 19 '15 at 1:20
  • @SureCoat I added some clarifications which should help. – Polynomial May 19 '15 at 9:07
-1

While I'm unfamiliar with this specific vulnerability, I can answer this question in general.

What exactly does this mean: A remote user can send specially crafted data to trigger an integer overflow in GraphicBuffer::unflatten()

This does not mean that a remote attacker can somehow make a remote procedure call on GraphicBuffer::unflatten. It means that there is (or even "may be") a path for a remote user to send data to your Android device such that the data is eventually passed into a call to GraphicBuffer::unflatten and that, if they designed that data carefully and maliciously, they can cause an integer overflow. For example, assuming that GraphicBuffer::unflatten has something to do with rendering images, the attacker could create a misformatted image and insert that into an email or a webpage that they hope will be opened on your Android device.

How would a user would go about executing this arbitrary code and what sort of code would this be?

Unless a researcher found a specific exploit for this integer overflow vulnerability, it is unlikely that it can lead to arbitrary arbitrary code execution. That said, it is conceivable that there are code paths that exist that could lead to arbitrary code execution.

I know that there are lots of 'if's and 'maybe's in the above text but, in an attempt to be responsible with regard to vulnerability disclosures, companies try to document the worst imaginable case. A good example of this is Adobe's Acrobat Reader vulnerabilities list. While they classify most vulnerabilities as having remote access, I suspect that is primarily due to downloading and running reader on a local PDF.

Unfortunately, as vulnerability disclosures are authored to try to hide details from attackers, they frequently leave customers confused and wanting more information.

Note: Whether it is an actual remote execution threat or just a theoretical one, you should respond by patching as quickly as possible.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy