Developers: It’s super easy to bypass Android’s hidden API restrictions
Flashback to over a year ago, and we’re all excited about seeing what’s to come in the Android P betas. Users are looking forward to new features, and developers are looking forward to some new tools to make their apps better. Unfortunately for some of those developers, the first Android P beta came with a bit of a Nasty Surprise: hidden API restrictions. Before I dive into what exactly that means, let me explain a bit about its context.
What’s This All About?
Android app developers don’t have to start from scratch when they make an app. Google provides tools to make app development easier and less repetitive. One of these tools is the Android SDK. The SDK is essentially a reference to all the functions developers can safely use in their apps. These functions come standard on all variants of Android that Google approves. The SDK isn’t exhaustive, though. There are quite a few “hidden” parts of Android’s framework that aren’t part of the SDK.
These “hidden” parts can be incredibly useful for more hacky or low-level stuff. For instance, my Widget Drawer app makes use of a non-SDK function to detect when a user launches an app from a widget so the drawer can automatically close. You might be thinking: “Why not just make these non-SDK functions part of the SDK?” Well, the problem is that their operation isn’t fully predictable. Google can’t make sure every single part of the framework works on every single device it approves, so more important methods are selected to be verified. Google doesn’t guarantee that the rest of the framework will stay consistent. Manufacturers can change or completely remove these hidden functions. Even in different versions of AOSP, you never know if a hidden function will still exist or work how it used to.
If you’re wondering why I’ve been using the word “hidden,” it’s because these functions aren’t even part of the SDK. Normally, if you try to use a hidden method or class in an app, it’ll fail to compile. Using them requires reflection or a modified SDK.
With Android P, Google decided that just hiding them wasn’t enough. When the first beta was released, it was announced that most (not all) hidden functions were no longer available for use to app developers. The first beta would warn you when your app used a blacklisted function. The next betas simply crashed your app. At least for me, this blacklist was pretty annoying. Not only did it break quite a bit of Navigation Gestures, but since hidden functions are blacklisted by default (Google has to manually whitelist some per-release), I had a lot of trouble getting Widget Drawer to work.
Now, there were a few ways to work around the blacklist. The first was to simply keep your app targeting API 27 (Android 8.1), since the blacklist only applied to apps targeting the latest API. Unfortunately, with Google’s minimum API requirements for publishing on the Play Store, it would only be possible to target API 27 for so long. As of November 1, 2019, all app updates to the Play Store must target API 28 or later.
The second workaround is actually something Google built into Android. It’s possible to run an ADB command to disable the blacklist entirely. That’s great for personal use and testing, but I can tell you firsthand that trying to get end-users to set up and run ADB is a nightmare.
So where does that leave us? We can’t target API 27 anymore if we want to upload to the Play Store, and the ADB method just isn’t viable. That doesn’t mean we’re out of options, though.
The Hidden API “Fix”
The hidden API blacklist only applies to non-whitelisted user applications. System applications, applications signed with the platform signature, and applications specified in a configuration file are all exempt from the blacklist. Funnily enough, the Google Play Services suite are all specified in that configuration file. I guess Google is better than the rest of us.
Anyway, let’s keep talking about the blacklist. The part we’re interested in today is that system applications are exempt. That means, for instance, that the System UI app can use all the hidden functions it wants since it’s on the system partition. Obviously, we can’t just push an app to the system partition. That needs root, a good file manager, knowledge of permissions…. It would be easier to use ADB. That’s not the only way we can be a system app, though, at least as far as the hidden API blacklist is concerned.
Cast your mind back to seven paragraphs ago when I mentioned reflection. If you don’t know what reflection is, I recommend reading this page, but here’s a quick summary. In Java, reflection is a way to access normally inaccessible classes, methods, and fields. It’s an incredibly powerful tool. Like I said in that paragraph, reflection used to be a way to access non-SDK functions. The API blacklist blocks the use of reflection, but it doesn’t block the use of double-reflection.
Now, here’s where it gets a little weird. Normally, if you wanted to call a hidden method, you’d do something like this (this is in Kotlin, but Java is similar):
val someHiddenClass = Class.forName("android.some.hidden.Class")\nval someHiddenMethod = someHiddenClass.getMethod("someHiddenMethod", String::class.java)\nsomeHiddenMethod.invoke(null, "some important string")\n
Thanks to the API blacklist, though, you’d just get a ClassNotFoundException. However, if you reflect twice, it works fine:
val forName = Class::class.java.getMethod("forName", String::class.java)\nval getMethod = Class::class.java.getMethod("getMethod", String::class.java, arrayOf<Class<*>>()::class.java)\nval someHiddenClass = forName.invoke(null, "android.some.hidden.Class") as Class<*>\nval someHiddenMethod = getMethod.invoke(someHiddenClass, "someHiddenMethod", String::class.java)\nsomeHiddenMethod.invoke(null, "some important string")\n
Weird right? Well, yes, but also no. The API blacklist tracks who’s calling a function. If the source isn’t exempt, it crashes. In the first example, the source is the app. However, in the second example, the source is the system itself. Instead of using reflection to get what we want directly, we’re using it to tell the system to get what we want. Since the source of the call to the hidden function is the system, the blacklist doesn’t affect us anymore.
So we’re done. We’ve got a way to bypass the API blacklist now. It’s a little clunky, but we could write a wrapper function so we don’t have to double-reflect every time. It’s not great, but it’s better than nothing. But actually, we’re not done. There’s a better way to do this that’ll let us use normal reflection or a modified SDK, like in the good old days.
Since the blacklist’s enforcement is evaluated per-process (which is the same as per-app in most cases), there might be some way for the system to record if the app in question is exempt or not. Luckily, there is, and it’s accessible to us. Using that newfound double-reflection hack, we’ve got a one-time-use code block:
val forName = Class::class.java.getDeclaredMethod("forName", String::class.java)\nval getDeclaredMethod = Class::class.java.getDeclaredMethod("getDeclaredMethod", String::class.java, arrayOf<Class<*>>()::class.java)\nval vmRuntimeClass = forName.invoke(null, "dalvik.system.VMRuntime") as Class<*>\nval getRuntime = getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null) as Method\nval setHiddenApiExemptions = getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", arrayOf(arrayOf<String>()::class.java)) as Method\nval vmRuntime = getRuntime.invoke(null)\nsetHiddenApiExemptions.invoke(vmRuntime, arrayOf("L"))\n
Okay, so technically, this isn’t telling the system that our app is exempt from the API blacklist. There’s actually another ADB command you can run to specify functions that shouldn’t be blacklisted. That’s what we’re taking advantage of above. The code basically overrides whatever the system thinks is exempt for our app. Passing “L” at the end means all methods are exempt. If you want to exempt specific methods, use the Smali syntax:
Now we’re actually done. Make a custom Application class, put that code in the
onCreate() method, and bam, no more restrictions.
Thanks to XDA Member weishu, the developer of VirtualXposed and Taichi, for originally discovering this method. We would also like to thank XDA Recognized Developer topjohnwu for pointing this workaround out to me. Here’s a bit more about how it works, although it’s in Chinese. I also wrote about this on Stack Overflow, with an example in JNI as well.