Android Bug Superior to Master Key

Earlier this year, Bluebox Security announced a vulnerability in the way Android application packages are signed, allowing the contents of a package (such as its code) to be modified (potentially to introduce malware) without invalidating the signatures that had been placed on the package by the original developer.

This bug was to be disclosed at Blackhat 2013, but due to a large amount of attention that it was given by the community, it was rapidly found, both internally in security circles, as well as on blogs and forums. At this point, the bug is fully public, with a patch deployed to CyanogenMod, an open distribution of Android.

This bug became known in the press as "Master Key", due to how it lets you effectively sign your code using the keys of other developers. This bug has been covered in everything from TechCrunch to the LA Times.

In my previous article, I documented this bug in detail, and provided a method to exploit the vulnerability that would work across any device, without need for either luck or skill to satisfy particular requirements in the original application package (such as that it contain files that are optional to include).

While attention was focussed on Master Key, a patch hit the Android Open Source Project to the same area, and it became clear to some that it could be used in a similar way. An article was posted by the Android Security Squad (安卓安全小分队) detailing an exploit, which was covered in English by H-Online.

Later articles were written about this bug, each examining the same exploit technique, coming to the conclusion that this vulnerability offered less abilities than the Master Key exploit, due to very narrow and unlikely requirements that the original signed application package must satisfy.

In this article, I document different exploit techniques for this bug that do not have these limits; in fact, the second exploit described here provides much more capability than the Master Key exploit, allowing arbitrarily complex packages to take on the signatures from arbitrary signed packages. Finally, I look at the history of this bug in Android, showing when it was introduced.

Negative Extra Data

Before explaining this new technique, I will quickly explain the existing one to make its limitations more clear. In so doing, I assume familiarity with the previous Master Key exploit, which is based on the idea that Android has multiple implementations of unzip: one in Java that is used to verify signatures, and one in C++ that is used to extract files; my previous article documents that bug and exploit in detail.

In the case of this newer exploit, we look at the "local headers" placed on files: these contain metadata about the file, information on how it is stored, the name of the file, and the (possibly compressed) contents of the file. Additionally, there is a variable-length "extra" field for allowing file format extensions.

+---------+ | Header | | Name | +---------+ | Extra | +---------+ | Data | +---------+

The underlying mistake in the code is that many values from the zip file are being read as signed 16-bit numbers instead of unsigned 16-bit numbers. Due to the way two's complement numbers are formatted, this causes field values greater than 32,767 to "wrap" and become negative. 65,535 would be -1.

The reason this happened is that the developer working on this code used a DataInputStream to parse the file, calling its readShort method to get 16-bit numbers. This returns a short in Java, a language in which all integer types are signed. Here is the code that pulls the length of the extra data.

DataInputStream is = new DataInputStream(rafstrm); int localExtraLenOrWhatever = Short.reverseBytes(is.readShort());

(By the way, while I always make some changes to the code snippets I post--removing irrelevant logic, eliding obvious type casts, and adjusting the formatting-- I never change the actual identifiers or tokens: that "localExtraLenOrWhatever" is seriously the name of this variable given by someone at Google.)

Overlapping Content

To calculate where in the file the compressed data begins, the code takes the location of the header and adds the length of both the filename and the extra area; if the extra area length is negative, that is not checked, and the resulting value of the addition (now, subtraction) calculation is where the data begins to be read.

With this offset being negative, this causes an offset back into the header, conflicting with other data. The exploit as documented by Android Security Squad is actually quite ingenious and is based on a very lucky coincidence: that the format of a key file in most Android packages, "classes.dex" begins with "dex".

The exploit then sets the length of the extra field to 65,533, which Java reads as -3. This moves the beginning of the file data three characters back into the filename, which happens to still be valid for a dex file, as "classes.dex" ends with the beginning of the file, "dex". If the file is "stored" (uncompressed) it is then valid.

A modified version of the file can then be placed 65,533 bytes after the end of the filename. As long as the original file is not larger than 64k, it can fit in the space between the header and the modified payload. In the following ASCII diagram, I show both the (correct) view from C++ at the top and Java's overlapping view.

C++ Header Name 64k Extra Data +------>+--------->+--------->+--------> size=10|classes.dex\035\A* ...dex\035\B* +------>+---------> Java Header Name <-+ /+--------> (-3) Extra Data

Serious Limitations

As can be seen, this exploit technique has numerous requirements that make it difficult to use on random files. In a write-up by Sophos (a security company with a popular anti-virus product) called Anatomy of another Android hole - Chinese researchers claim new code verification bypass, they detailed these limitations.

As a result, the "extra field" flaw is not as generic as the "master key" bug. In particular, any APK hacked in this way must start with a classes.dex that fits into 65536 bytes when decompressed. That narrows things down a bit, which is a small mercy: out of 96 APK files extracted from my personal Android device, for example, 75 are ineligible for attack by this flaw.

A week earlier, a similar description was provided by Android Police in an article entitled Second "Master Key" Style APK Exploit Is Revealed Just Two Days After Original Goes Public, Already Patched By Google.

Fortunately, there are a few limitations to this attack. To begin with, unlike the "Master Key" exploit, this one can only replace a single file, classes.dex, and only if the original is smaller than 64k. Further, the process to construct the modified APK is more precise and relies on a fairly complete knowledge about the structure of the files.

Pau Oliva (the developer of a proof-of-concept for bug #8219321) said on Twitter "it was difficult to find APKs containing a classes.dex signed with a platform key" (as before my previous article people concentrated on classes.dex) "now go and find those with classes.dex <64Kb". Later, he indicated he did find one for Motorola.

These are not the only difficulties, however: one that no one seems to mention is that as the size of the resulting file is encoded in the local file header, the original file and the replacement file have to be the same size, putting irritating limits on the construction of the payload itself such as padding and internal structure.

Despite these limitations, this bug continues to be very tempting as it was found much later: while some devices have been patched against bug #8219321, almost none have been patched against bug #9695860. Both Android Police and Sophos mentioned the high likelihood of delays getting this patched.

There is some unfortunate news: since the fix appears to be pretty new, it's unlikely to have propagated to any device in the wild yet, including Nexus devices and the Google Play Edition variants of the Samsung Galaxy S4 and HTC One.

Of course, Google recently announced that seven days is an achievable timeframe for responding to vulnerability reports, down from the 60 days it accepted before. Although Google has indeed responded quickly by patching both holes, and should be commended for its efficiency, that doesn't get the fixes out into the wider world. It remains to be seen how hard Mountain View will lean on its many handset licensees to push out firmware updates for the "extra field" and "master key" flaws, since they go to the heart of application verification on the Android platform.

Extreme Offset

The first new technique I will present involves generalizing this exploit to work on files other than classes.dex. The way we will do this is by using a much longer negative offset, pushing the contents of the file past the beginning of the local file header. There is no requirement that local file headers not leave gaps.

C++ Header Name 32k Extra Data +------>+--------->+----------->+--------> dex\035\A* ...size=10|classes.dex PADDING ... dex\035\B* +------>+---------> Java Header Name <-------------------------------+ +--------> (-32k) Extra Data

Without the prefix overlap requirement, even at the cost of shrinking our size limit to 32k, this bug becomes much easier to use; in particular, it allows us to use the Dalvik debugger technique I documented in my previous Master Key article that replaces only AndroidManifest.xml, a file that is both small and contained in every package.

That said, the attack I described using the debugger is only useful for attacking a device from the outside via a USB cable: the debugger is not accessible from other applications on the device. The "scary malware" threat model involves replacing other files, especially code (whether classes.dex or JNI libraries).

In any event, this technique is still more limited than Master Key: while we can now replace any file, we still can only replace files that previously existed, and we've now gained the added limitations that the file cannot be larger than 32kB and that our replacement file be the same length as the original copy.

Central Directory

However, we can do better. Looking again at the original patch from Google, readers might note that there are two places where an extra length field is being read, and both made the mistake of doing so with a signed 16-bit integer. While people have been concentrating on one of them, the other is actually much more interesting.

To examine this second mistake, we need to turn our attention to a different part of the zip file format: the "central directory". This is an index that zip file parsers can use to rapidly find files stored in the zip archive, and is located at the end of the file. Each central directory entry points into the local file area.

+-> +---------+ ^ ^ | | Detail | | | | | Local | -/ | | | Name | | | | Comment | | | | Extra | | | +---------+ | | +---------+ / | | Detail | / | | Local | -/ | | Name | | | Comment | | | Extra | | +---------+ | ... | +---------+ | | Magic | magic | | Number | ^ +-- | Start | | find | Comment | | scan +---------+ | up EOF

This index also stores extra data. If we look at the C++ zip file library from Android, we see that to move to the next field it takes the offset of the current entry, adds the length of a directory entry, and then the length of the three variable length fields: name, comment, and extra. These are all unsigned 16-bit values.

unsigned int fileNameLen, extraLen, commentLen, hash; fileNameLen = get2LE(ptr + kCDENameLen); extraLen = get2LE(ptr + kCDEExtraLen); commentLen = get2LE(ptr + kCDECommentLen); hash = computeHash(ptr + kCDELen, fileNameLen); addToHash(ptr + kCDELen, fileNameLen, hash); ptr += kCDELen + fileNameLen + extraLen + commentLen;

Clamped Extra Data

Then, looking at the Java code, we again see that it is using readShort(), pulling a signed 16-bit value and storing it to a signed integer. However, unlike the earlier code that parsed the local file headers, while parsing the central directory negative extra data lengths are simply considered invalid and get skipped.

nameLength = it.readShort(); int extraLength = it.readShort(); int commentLength = it.readShort(); byte[] nameBytes = new byte[nameLength]; Streams.readFully(in, nameBytes, 0, nameBytes.length); name = new String(nameBytes, 0, nameBytes.length, Charsets.UTF_8); if (commentLength > 0) { byte[] commentBytes = new byte[commentLength]; Streams.readFully(in, commentBytes, 0, commentLength); comment = new String(commentBytes, 0, commentBytes.length, Charsets.UTF_8); } if (extraLength > 0) { extra = new byte[extraLength]; Streams.readFully(in, extra, 0, extraLength); }

This means that we no longer have to worry about the awkward behavior of moving backwards in the file: if you encode a large enough value, the C++ code will honor it, and the Java code will clamp it to 0. This gives us an opportunity to cause the Java and C parsers to view the file in two different ways without pain.

The only limitation is that the number of entries in the index itself is stored at the end of the index and is read by both of the parsers before processing the file: each one uses a for loop to pull exactly that many files, so the C++ and Java versions of the central directory must have the same number of entries.

Simple Techniques

To demonstrate these modifications to the file, I'm going to use ASCII art diagrams. Each of these diagrams will have boxes that represent entries in the central directory. The name field is presumed to be stored inside of the box. There will then be arrows showing the movements caused by the extra data length fields.

On the left is the correct interpretation by the C++ logic, and on the right how Java sees the file. Each entry also has either a forward slash, a backward slash, or an X, representing who can see the file: backward slash (leaning left) for C++, forward slash (leaning right) for Java, and X (two slashes) for both.

The first technique to look at is the simplest: taking the final entry in the list and splitting it into two separate records, one for C++ and the other for Java. We do this by setting the extra data length field to a large enough number for Java to ignore it: Java then parses a central directory inside of what C++ views as "extra".

C++ +-+ Java +-- |X| --+ | +-+ | (0) | +-+ <-+ >32k | |/| | +-+ | PAD +-> +-+ |\| +-+ EOF

This technique can be expanded to work with multiple entries: as 64kB is a large amount of space (a central directory entry is only 46 bytes plus the length of the filename), we can then just continue each chain normally, merging two entirely different zip files together, sharing only a single common file.

C++ +-+ Java +-- |X| --+ | +-+ | (0) | +-+ <-+ | |/| --+ | +-+ | 0 | +-+ <-+ >32k | |/| --+ | +-+ | 0 | +-+ <-+ | |/| | +-+ | PAD +-> +-+ +-- |\| 0 | +-+ +-> +-+ +-- |\| 0 | +-+ +-> +-+ |\| +-+ EOF

Advanced Interleaving

For completeness, I will now look at what we can do if you have a very large number of files, such that the total length of the filenames in the original zip file plus the 46 bytes of header for each file add up to greater than the 64kB of extra data that can be skipped by the C++ parser. The solution is to interleave the indexes.

One way to handle this is to re-synchronize back to shared files. As long as a record that Java sees is longer than a record that C++ sees, an offset of exactly 32kB (just at the boundary where it will get clamped to 0) will cause Java to use an extra data skip of slightly less than 32kB (by the difference in the filenames).

C++ +-+ Java +-- |X| --+ | +-+ | (0) | +-+ <-+ | |/| 32k | | | <- long name | | | --+ | +-+ | | PAD | +-> +-+ | 31.9k +-- |\| <-|-- short name | +-+ | +-> +-+ <-+ |X| ...

This is still somewhat irritating, though, as it puts horrible restrictions on the lengths of the filenames in each zip file. If we group items together into sets of four (two for Java and two for C++) this can be solved by pushing the items seen by Java sufficiently far apart that they divide the area skipped by C++.

C++ +-+ Java +-- |X| --+ | +-+ | (0) | +-+ <-+ | |/| --+ | +-+ | | PAD | | PAD | 25k 50k | PAD | | PAD | | +-+ <-+ | |/| --+ | +-+ | | PAD | | PAD | +-> +-+ | +-- |\| | 25k 0 | +-+ | +-> +-+ | +-- |\| | 0 | +-+ | +-> +-+ <-+ |X| ...

After then spending a lot of time building ASCII diagrams, one might realize (but not soon enough) that by also using the variable length comment field, we can accomplish the same effect as the previous example with a single pair of records: the space is divided between the comment and the extra data lengths.

C++ +-+ Java +-- |X| --+ | +-+ | 0 (0) | +-+ <-+ | |/| --+ 0 50k | +-+ | | PAD | | PAD | | PAD | 25k 25k +-> +-+ | +-- |\| | 0 0 | +-+ | +-> +-+ <-+ |X| ...

Using the comment field as well allows us to do a much simpler form of interleaving that does not require re-synchronization, where we use the first record to fork the two central directories, and then play leapfrog between the two lists, having each record skip the corresponding record seen by the other implementation.

C++ +-+ Java +-- |X| --+ | +-+ | | PAD | 50k 0 | PAD | 24.975k 24.975k | PAD | | PAD | | +-+ <-+ | |/| --+ | +-+ | +-> +-+ | size of +-- |\| <-|-- next | +-+ | size of | +-+ <-+ next --|-> |/| --+ | +-+ | +-> +-+ | size of +-- |\| <-|-- next | +-+ | size of | +-+ <-+ next --|-> | | --+ | ... |

Skipping Entries

We are still left with two limitations: we have to share the first file between the two lists, and we have to have the same number of files in both versions of the central directory. Additionally, of course, the Java version of the index has to have exactly the files that the original file had (as they were the ones signed).

It might be tempting to then attempt to rely on tricks based on the previous bug, but in addition to making this bug feel subordinate, that bug has been fixed on a number of devices for which this second bug is still viable. We thereby cannot use duplicate filenames to solve this problem: the zip parser will reject them.

Thankfully, it turns out that there is a kind of entry that can be added to a zip file that is ignored for the purposes of signature verification as they have no content to sign: directory entries (which also have the property that different implementations bother with them while others don't, so they can't be enforced).

Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { final JarEntry je = entries.nextElement(); if (je.isDirectory()) continue; final String name = je.getName(); if (name.startsWith("META-INF/")) continue; final Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer);

Creating a directory entry is really easy: all one has to do is add a slash to the end of the filename. (This same code also skips files in the META-INF folder, but those files are used as part of the signature verification system; as the directory entries are superior, I have not examined using the META-INF folder to hide files.)

public boolean isDirectory() { return name.charAt(name.length() - 1) == '/'; }

Historical Look

While testing my implementation of this exploit on Android 2.3, I found out that it did not work: that older version of Android correctly determined that the file's signatures had been compromised. Intrigued, I went into the history of libcore, and determined that this bug was introduced by Google in 2010.

One of the design decisions of Android was how they were going to obtain their standard library; rather than using GNU Classpath, they went with Apache Harmony (largely, as I understand, due to licensing differences). Apache Harmony has since been retired (in 2011): Google now maintains a fork for Android.

It turns out that, originally, Harmony's implementation of ZipFile was actually written in C. The C code constructed each ZipEntry via JNI. Passing the data between the layers apparently involved large buffer copies, which used too much memory for the Android mobile platform, so Google rewrote ZipFile in Java.

This original implementation of the Java version of ZipFile did not have this bug: it carefully dealt with sign extension, and in fact the developers who wrote it had written their own implementation of a Reader specifically for unsigned little-endian values, providing readShortLE (returning int) and readIntLE (returning long).

However, calling through a reader for every field of the central directory header (which must be read before even a single small file from the zip file can be extracted) was considered sufficiently slow that they replaced the logic by reading the header in a single pass and (correctly) doing the bit math while parsing.

/* * We're seeing performance issues when we call * readShortLE and readIntLE, so we're going to * read the entire header at once and then parse * the results out without using any function calls. * Uglier, but should be much faster. * * Note that some lines look a bit different, because * the corresponding fields or locals are long and * so we need to do & 0xffffffffl to avoid problems * induced by sign extension. */

nameLen = (hdrBuf[28] & 0xff) | ((hdrBuf[29] & 0xff) << 8); int extraLen = (hdrBuf[30] & 0xff) | ((hdrBuf[31] & 0xff) << 8); int commentLen = (hdrBuf[32] & 0xff) | ((hdrBuf[33] & 0xff) << 8);

The original contribution from Google to Harmony was made in 2009 before Harmony began shutting down and the project became hosted entirely by Google for purposes of Android. We thereby can find the patch in Harmony's bug tracker as an attachment to issue HARMONY-6346, and the resulting discussion.

A year later, in late 2010 after the project had transitioned, we can scour Google's repository to find the patch that introduces this bug. This patch was made to fix Android bug #3181430, apparently "a sign-extension bug on CRCs with the top bit set" (I got this description out of a comment in the associated test case).

This code's quite hairy in its use of int/long. I can't just change the fields to int because they seem to use -1L to mean "unset" while still allowing the whole int range of values (including -1). We'll have to look at the zip specification to see whether that's right, but for now, let's just avoid sign extension.

As we now know, this patch actually added more sign extension issues ;P. I also believe that there was a misunderstanding of why the code was using int and long in the first place: the fields that were int were actually storing 16-bit numbers (for long, 32-bit), so "the whole int range of values" was not used.

Fixing the Bug

How much of a threat any of these bugs are to normal users is still in question: most people avail themselves of larger application distribution channels, such as Google's Play Store, that can (and supposedly do) centrally scan for these vulnerabilities in their catalogs to make certain that they are not spreading malware.

However, in an article posted a few days ago, Symantec documents multiple findings in the wild of the original Master Key exploit being used by a developer in China to distribute malware in various alternative markets. They provide screenshots of the application, and more information on what the code does.

We have discovered four additional Android applications infected by the same attacker and being distributed on third-party app sites. The apps are a popular news app, an arcade game, a card game, and a betting and lottery app. All of these apps are designed for Chinese language users.

It is therefore understandable that some people would rather be protected with a local fix. Additionally, I consider it irresponsible to detail exploits like this without providing mitigations when possible (such as a hotfix for users to apply). Finally, I think showing how things are fixed further explains how everything works.

In my previous article, I recommended users turn to third-parties for production-level fixes; if nothing else, I had yet to provide a patch for this second set of bugs. However, in addition to some of my previous caveats, while researching this second article I have determined that ReKey (my primary recommendation) does not actually fix bug #9695860 (its website was not very clear on this).

As I am now providing a complete fix for both bugs, I thankfully can now point users who want a fix directly at Backport (the name of the project under which I've placed these fixes, ostensibly "backported" from later versions of Android). The real goal, however, of my article is just to describe how the fix works.

Substrate Extension

To go about fixing this bug for existing devices, we will use Substrate, the code modification platform I provide for both iOS and Android. Developers wanting to learn more about Substrate should go to its website, where there is fairly complete API documentation; the boilerplate, though, is quite simple.

public class Hook { public static void initialize() { ... } }

The first hook we need fixes the code that reads the local file header. This is contained in a method called getInputStream, which takes a ZipEntry and returns an InputStream that reads the contents. Our goal is to verify the fields that will be read and then to call through to the original implementation to allow it to read the file.

Note carefully that this approach does not replace the contents of the method entirely, as some alternative hook-based attempts to fix this bug have done: performing replacements instead of patches with hooks breaks behaviors that others may be layering onto this class; it is critical to avoid such hooks.

To get access to the file header data, we use the mRaf field of ZipFile. This is a RandomAccessFile, a class that already exports useful methods like readShort(). This access must be synchronized, as users first seek to their place of interest and then scan the file changing its state. We verify both of the patched fields.

To replace the contents of the method, we use Substrate's MS.hookMethod, which allows us to redirect the implementation of any method. This API takes a functor object called "invoked"; in the code we provide, we can use "invoke" to call through to the previous implementation. Arguments are boxed in an array.

For the actual verification, we will reject zip files where the signed bit is 1 for the incorrectly parsed fields (as previous versions of Android would have failed to parse this package correctly, it is little loss to remove them, and makes it more obvious if new zip files would fail on older devices). As the values we are reading are little endian, this bit must be checked using the value 0x0080 rather than 0x8000.

final Field raf = ZipFile.class. getDeclaredField("mRaf"); raf.setAccessible(true); final Field local = ZipEntry.class. getDeclaredField("mLocalHeaderRelOffset"); local.setAccessible(true); Method getInputStream = ZipFile.class. getDeclaredMethod("getInputStream", ZipEntry.class); MS.hookMethod(ZipFile.class, getInputStream, new MS.MethodAlteration<ZipFile, InputStream>() { public InputStream invoked( ZipFile thiz, Object... args ) throws Throwable { ZipEntry entry = (ZipEntry) args[0]; RandomAccessFile raf = (RandomAccessFile) raf.get(thiz); synchronized (raf) { raf.seek(local.getLong(entry)); raf.skipBytes(6); if ((raf.readShort() & 0x0080) != 0) throw new ZipException(); raf.skipBytes(20); if ((raf.readShort() & 0x0080) != 0) throw new ZipException(); } return invoke(thiz, args); } } );

Our second hook is to ZipEntry: its constructor reads from the central directory, moving the file pointer forward to the location of the next header in the process. This method takes two arguments: a byte array that is used to store the contents of the header (an old optimization) and an InputStream pointing at its location.

As this class's purpose is just to store information from the header, it doesn't matter that it is temporarily invalid; we therefore will verify that the file was safe after it has been executed. This allows us to use the contents of the header buffer to keep from messing with the position of the InputStream that we were passed.

It happens that the InputStream instance passed into this method is an instance of BufferedInputStream, which guarantees implementations of mark and reset; these could be used to scan forward to verify, and then after to rewind back to the top of the header. Doing this would let us verify before we call the method.

However, doing that would rely on the underlying type of the InputStream; that is not a sane assumption to be making, even if it happens to be true: other people might modify the behaviors of this class or the implementation might change; the type signature, however, cannot be changed and is our contract.

The fields we will verify come in chunks, so I use for loops to make the code slightly shorter. It should be explicitly noted that this code does not verify the values of the time and date fields: these fields almost always overflow; thankfully, they really are just data, and do not affect how the file is read or verified.

Constructor init = ZipEntry.class. getDeclaredConstructor(byte[].class, InputStream.class); MS.hookMethod(ZipEntry.class, init, new MS.MethodAlteration<ZipEntry, Void>() { public Void invoked(ZipEntry thiz, Object... args) throws Throwable { byte[] header = (byte[]) args[0]; invoke(thiz, args); DataInputStream in = new DataInputStream( new ByteArrayInputStream(header)); in.skip(8); for (int i = 0; i != 2; ++i) if ((in.readShort() & 0x0080) != 0) throw new ZipException(); in.skip(16); for (int i = 0; i != 3; ++i) if ((in.readShort() & 0x0080) != 0) throw new ZipException(); return null; } } );

For the complete source code to this extension, you can clone its git repository from git://git.saurik.com/backport.git or view 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).

Complete Signature Bypass

We now have the ability to construct a zip file with two central directories. Both central directories have to start with the same file, but that file can be a directory entry. Both zip files have to have the same number of files, but the shorter one can be filled up with directory entries to match the larger one.

Otherwise, these two central directories are entirely separate, and can have totally unrelated filenames and point to totally unrelated entries in the aforementioned local data area of the zip file. This means we can effectively "merge" two zip files together of arbitrary complexity and contents.

This is, as I hope is now clear, much more powerful than the now well-known "Master Key" vulnerability. In comparison, the previous bug only allowed us to replace the contents of files that were present in the original signed zip: we could not, for example, add a "classes.dex" where one previously didn't exist.

The Master Key bug, however, is both slightly easier to implement (as demonstrated using zip and sed in my previous article) and supports Android 2.3; it thereby is likely to continue to be used for some time to come. However, as applications normally contain many files in the form of resources, to truly masquerade packages with stolen signatures requires this new, more powerful exploit.