MasterKey ZipBug9950697

Yet Another Android Master Key Bug

http://www.saurik.com/id/19

Earlier this year,
Bluebox Security announced they had found a bug in the way Android verifies that application packages have not been tampered with by third-parties. While details were to be disclosed by Jeff Forristal (CTO of Bluebox)at
Black Hat 2013, due to the attention they caused, the bug was quickly found.

This bug was disclosed to Google in February, where it became bug #8219321. Bluebox felt thatresponsible disclosure was critical
for a bug this serious, so they gave Google many months to allow their hardware partners to be able to get everything fixed. After many months, however, only a few devices were fixed.

At a moment when the most attention was being given to this bug, a patch for a different issue hit the Android Open Source Project, a fix for bug #9695860. This bug had very similar ramifications, and a group called Android Security Squaddocumented
an exploit technique (albeit one weaker than the previous bug).

In previous articles, I first documented
how bug #8219321 works, as well as how to exploit it on any device to run code as the system user (previous techniques relied on finding existing valid packages with specific properties). In asecond
article, I showed how bug #9695860 was actually more powerful than the first.

Now, last night, the source code for Android 4.4 was released to AOSP, which includeda
patch for yet another bug, #9950697, in the signature verification of Android application packages. This bug is somewhat weaker than the previous ones, but is still sufficient to support the general exploit techniques I have described.

In this article, I describe this third bug and show how it can be used, providing both a proof-of-concept implementation in Python and a new version ofImpactor
that adds support for this signature bug. Finally, I show how Substrate can be used to patch this bug, and I release a new version of Backport with the fix.

(I was able to get this article out so quickly as I had actually found this bug back in June; I sadly did not think to post a hash to Twitter until July, however, a week after this patch was committed internally at Google. Regardless, if
you run the hash command from
this post from today you get
this hash I posted in July.)

Incorrect Assumptions

The bug that underlies this exploit is very similar to the one I analyzed inmy previous article, bug #9695860. Specifically, this will look very similar
to the original technique for that bug by Android Security Squad, with a few aspects of the first improvement I descibed in the section "Extreme Offset".

As I have already described how the zip file format fits together in previous articles, as well as how the Android signature verification code generally works (and the high-level problems it is subject to), I will refrain from repeating
myself here. Please read
my article on bug #8219321 and
my article on bug #9695860 for these details.

So, looking back at the calculation performed by the C++ code to find the data block after a local header, we see that that it is based on four things: the offset of the start of the header, the length of the header (a fixed size), the length
of the extra data field (which bug #9695860 exploited), and the length of the name of the file.

By process of elimination, this article will focus on the name length. The local header name is interesting as it is not used by any of the key implementations of unzip on Android: the central directory is instead read fully and indexed
(into some form of hashtable), storing only pointers to the local file headers.

If we look at the implementation of this code in Java, we see something interesting: while it reads the length of the extra data from the local header, it somehow already has the length of the name stored in a nameLength field of the ZipEntry
object. This field is a cache stored while reading the central directory headers.

// We don't know the entry data's start position.// All we have is the position of the entry's local// header. At position 28 we find the length of the// extra data. In some cases this length differs// from the
one coming in the central header.RAFStream rafstrm = new RAFStream(raf, entry.mLocalHeaderRelOffset + 28);DataInputStream is = new DataInputStream(rafstrm);int localExtraLenOrWhatever = Short.reverseBytes(is.readShort());is.close();// Skip the name and this
"extra" data or whatever it is:rafstrm.skip(entry.nameLength + localExtraLenOrWhatever);

What makes this bug exceptionally hilarious is the large comment sitting above it carefully pointing out that the length of the extra data might differ from the value stored in the central directory. Sadly, this failed to make an impression
while writing the code below that skips the name field (and "whatever it is" ;P).

The way we can exploit this is to set the length of the name in the local file header to a size large enough to skip the length of the real name (as defined in the central directory) and the data that will be used by Java. We then put the
modified data we want used by C++ after the data that will be verified by Java.

Proof of Concept

As some people have been describing these local file header exploits as complex to pull off (Android Police hadsaid
in an article that bug #9695860 "is more precise and relies on a fairly complete knowledge about the structure of the files"), I am going to provide a proof of concept for this exploit in Python.

The key to pulling off these exploits is that the flexibility of the zip file format is often tolerated even by simpler libraries. With the Python zipfile library (and zlib's minizip), each time you add a file the local header and data block
are written immediately, and the central directory is written when you are done.

When writing these files to the stream, the position of the stream is not moved; further, the position is read from the stream, for later use in the central directory. This means that if you write extra data to the file or move the file
pointer around, the zip library will just work around the changes without complaint.

In the case of Python, you can get access to the underlying file stream using the "fp" member of the ZipFile object. Writing multiple copies of the data or adding padding to the file is thereby quite simple. Finally, we can seek backwards
in the file to make changes to the headers that have been written.

Fixing the Bug

As in my previous articles, I will now point out that I feel like it is my responsibility to provide people the ability to protect themselves from the issues that I describe and implement. I will once again do so usingSubstrate.
At this time, there are no alternatives I can point to or comment on.

Clearly, it would be ideal to also disclose bugs earlier, but frankly: Google has known about this bug since July... this is simply not a priority to them, and earlier "responsible disclosure" thereby does not benefit the public. (As many
Android devices are also locked down from their owners, I thereby feel morally obligated on the other side to hold bugs until they are needed--or, of course, burned.)

The fix for this issue is very similar to the fix I published for bug #9695860, so the code for this extension will look very similar; the only new code is a few lines that check that the name length from the local header matches the one
from the central directory. You can see
the patch to Backport for reference. (Note: there was a mistake in that patch that I foundand fixed
before publishing this article.)

For the complete source code to this extension, you can clone its git repository from git://git.saurik.com/backport.git orview its repository
online using the
Gitweb instance I use. (Here is a direct link to
Hook.java.) Users who just want to install an APK can get it from the Cydia Gallery (inside of Substrate).

Updated Impactor

For users hoping for an end-to-end implementation to this bug, I have released an updated build ofCydia Impactor that will autodetect its usability
and use it as a system exploit when available (it actually checks this bug first, as it is more likely to be present than either of the other two bugs).

To use Impactor to get access to the system user of your device, run Impactor, select "start telnetd as system on port 2222", and click Start. Then, if you telnet to port 2222 on your device and run "id" you will find that you are running
as the system user. At this point you might be able to find ways to upgrade to full root access.

What will be interesting, of course, is to see the adoption curve of fixes for these three bugs. I mainly do work on iOS, where bugs are fixed centrally and quickly. In comparison, even though Google had the first of these bugs carefully
disclosed to them by Bluebox in February, their Nexus device line did not see a fix until July (as part of 4.3), and many devices even today have yet to be patched. The story for the second bug is even worse: here's hoping the third bug causes more updates.