Stagefright: Mission Accomplished?

Update (2015-08-13 1:16pm CST): We’ve been in contact with Zimperium and are working with them to provide coverage for detection of this flaw through their Stagefright Detector app. They have been very responsive (more so than the affected vendor) and we plan to alert them of similar flaws we’ve recently discovered.

 

“Given enough eyeballs, all bugs are shallow”

 

That famous quote, from Eric S. Raymond’s book The Cathedral and the Bazaar, has inspired us to release new details on the recent Stagefright vulnerability affecting an estimated 950 million Android devices.


The Stagefright vulnerability was initially reported to Google in April 2015 and then publicly in July, just prior to the widely hyped talk at the Black Hat security conference in Las Vegas. News of the flaw was covered by major media outlets and touted as one of the single worst vulnerabilities to affect the platform.

Along with the initial bug report, a set of patches to stagefright flaws were supplied and accepted by Google. One of these patches, addressing CVE-2015-3824 (aka Google Stagefright ‘tx3g’ MP4 Atom Integer Overflow) was quite simple, consisting of merely 4 lines of changed code, as show below:

 

 
Fix integer overflow when handling MPEG4 tx3g atom
 
When the sum of the 'size' and 'chunk_size' variables is larger than 2^32,
an integer overflow occurs. Using the result value to allocate memory
leads to an undersized buffer allocation and later a potentially
exploitable heap corruption condition. Ensure that integer overflow does
not occur.

Bug: 20923261
Change-Id: Id050a36b33196864bdd98b5ea24241f95a0b5d1f
diff --git a/media/libstagefright/MPEG4Extractor.cpp b/media/libstagefright/MPEG4Extractor.cpp
index 8e47fda..ab1dade 100644
--- a/media/libstagefright/MPEG4Extractor.cpp
+++ b/media/libstagefright/MPEG4Extractor.cpp
@@ -1897,6 +1897,10 @@
                size = 0;
         	}
+        	if (SIZE_MAX - chunk_size <= size) {
+            	return ERROR_MALFORMED;
+        	}
+
         	uint8_t *buffer = new (std::nothrow) uint8_t[size + chunk_size];
         	if (buffer == NULL) {
             	return ERROR_MALFORMED;

 
According to the original discoverer of the vulnerability, “Basically, within 48 hours I had an email telling me that they had accepted all of the patches I sent them, which was great…You know, that’s a very good feeling.” 

Around July 31st, Exodus Intelligence security researcher Jordan Gruskovnjak noticed that there seemed to be a severe problem with the proposed patch. As the code was not yet shipped to Android devices, we had no ability to verify this authoritatively.

In the following week, hackers converged in Las Vegas for the annual Black Hat conference during which the Stagefright vulnerability received much attention, both during the talk and at the various parties and events.

After the festivities concluded and the supposedly patched firmware was released to the public, Jordan proceeded to investigate whether his assumptions regarding its fallibility were well founded. They were.

With the updated firmware flashed to a Nexus 5 device, Jordan crafted an MP4 to bypass the patch and was greeted with the following crash upon testing:

Build fingerprint: 'google/hammerhead/hammerhead:5.1.1/LMY48I/2074855:user/release-keys'
Revision: '11'
ABI: 'arm'
pid: 9614, tid: 9751, name: NuCachedSource2  >>> /system/bin/mediaserver <<<;
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'frameworks/av/media/libstagefright/NuCachedSource2.cpp:580 CHECK_LE( size,(size_t)mHighwaterThresholdBytes) failed: 4294967292 vs. 20971520'

backtrace:
    #00 pc 00039f4c  /system/lib/libc.so (tgkill+12)
    #01 pc 000173c1  /system/lib/libc.so (pthread_kill+52)
    #02 pc 00017fd3  /system/lib/libc.so (raise+10)
    #03 pc 00014795  /system/lib/libc.so (__libc_android_abort+36)
    #04 pc 00012f44  /system/lib/libc.so (abort+4)
    #05 pc 00007b51  /system/lib/libcutils.so (__android_log_assert+88)
    #06 pc 0008ac89  /system/lib/libstagefright.so (android::NuCachedSource2::readInternal(long long, void*, unsigned int)+80)
    #07 pc 0008ade3  /system/lib/libstagefright.so (android::NuCachedSource2::onRead(android::sp const&)+122)
...

 

 

Deadline exceeded – automatically derestricting

 

We notified Google of the issue on August 7th but have not had a reply to our query regarding their release of an updated fix. Due to this, as well as the following facts, we have decided to notify the public of our findings here on the Exodus Intelligence blog. 

 

  • The flaw was initially reported over 120 days ago to Google, which exceeds even their own 90-day disclosure deadline.
  • The patch is 4 lines of code and was (presumably) reviewed by Google engineers prior to shipping. The public at large believes the current patch protects them when it in fact does not.
  • The flaw affects an estimated 950 million Google customers.
  • Despite our notification (and their confirmation), Google is still currently distributing the faulty patch to Android devices via OTA updates.
  • There has been an inordinate amount of attention drawn to the bug–we believe we are likely not the only ones to have noticed it is flawed. Others may have malicious intentions.
  • Google has not given us any indication of a timeline for correcting the faulty patch, despite our queries.
  • The Stagefright Detector application released by Zimperium (the company behind the initial discovery) reports “Congratulations! Your device is not affected by vulnerabilities in Stagefright!” when in fact it is, leading to a false sense of security among users.

Without further preamble, the technical details follow:

As stated above, the fix is as follows:

index 8e47fda..ab1dade 100644
--- a/media/libstagefright/MPEG4Extractor.cpp
+++ b/media/libstagefright/MPEG4Extractor.cpp
@@ -1897,6 +1897,10 @@
                size = 0;
         	}
+        	if (SIZE_MAX - chunk_size <= size) {
+            	return ERROR_MALFORMED;
+        	}
+
         	uint8_t *buffer = new (std::nothrow) uint8_t[size + chunk_size];
         	if (buffer == NULL) {
             	return ERROR_MALFORMED;

 

The patch prevents the undersized allocation of the buffer variable due to the size + chunk_size integer overflow. Thus chunk_size is enforced against SIZE_MAX (0xFFFFFFFF) in order to prevent this behavior.

Even if everything seems right, the elegance of this bug is that it is hiding in plain sight. The one important aspect that is overlooked is the data types of chunk_size and size. Looking at the variable definitions, size is of size_t type which is an unsigned int. However, the flaw manifests due to the type of chunk_size, which is uint64_t.
As chunk_size is a 64 bit variable it may hold values > SIZE_MAX, which can be accomplished thanks to the following code:

status_t MPEG4Extractor::parseChunk(off64_t *offset, int depth) {
    ALOGV("entering parseChunk %lld/%d", *offset, depth);
    uint32_t hdr[2];
    if (mDataSource->readAt(*offset, hdr, 8) < 8) { 
        return ERROR_IO; 
    } 
    uint64_t chunk_size = ntohl(hdr[0]); 
    int32_t chunk_type = ntohl(hdr[1]); 
    off64_t data_offset = *offset + 8; 
    if (chunk_size == 1) { 
        if (mDataSource->readAt(*offset + 8, &chunk_size, 8) < 8) {
            return ERROR_IO;
        }
        chunk_size = ntoh64(chunk_size);

In the above, chunk_size is set to a 32-bit value by the ntohl() function, which is sourcing the input from the MP4 metadata. However, if chunk_size is set to 0x01, the if condition (on line 771 above) evaluates as true. Then, a 64-bit value is read from the input MP4 and stored as the chunk_size instead.

For example, if a malicious MP4 is crafted with a chunk_size of 0x1ffffffff (notice this is larger than a 32-bit value) the faulty overflow check will be bypassed because chunk_sizeSIZE_MAX. Next, chunk_size is added to size. If size is any value greater than 0, an integer overflow will occur. If, for instance, size is 1, the addition will result in a value of 0x200000000, which is larger than a 32-bit value. The following call to the new operator will truncate that value down to fit into a 32-bit integer, thus allocating an undersized buffer.

 uint8_t *buffer = new (std::nothrow) uint8_t[size + chunk_size]; 

Subsequently, chunk_size worth of data is read into this undersized buffer. Even if the value is truncated to 32-bits, the function will still read 0xFFFFFFFF bytes into the buffer, leading to a heap overflow:

 if ((size_t)(mDataSource->readAt(*offset, buffer + size, chunk_size))

As shown above, the issue is still exploitable, despite the patches currently being shipped to Android devices. As of this morning, Google has notified us they have allocated the CVE identifier CVE-2015-3864 to our report.

In summary, the Stagefright disclosure process was an interesting one to observe. The (un)surprising outcome being that given all the exposure this vulnerability received combined with essentially infinite resources on the vendor side, effective security mitigations were still not deployed. Google employs a tremendously large security staff, so much so that many members dedicate time to audit other vendor’s software and hold them accountable to provide a code fix within a deadline period. If Google cannot demonstrate the ability to successfully remedy a disclosed vulnerability affecting their own customers then what hope do the rest of us have?

 

Mission Accomplished

 
Technical details: Jordan Gruskovnjak / @jgrusko
Commentary: Aaron Portnoy / @aaronportnoy

 
Follow us on Twitter: @ExodusIntel