Most competent web developers have learned a thing or two about how to handle cross-site scripting and cross-site request forgery, the two main attack vectors for compromising the front end of a web application. These attacks take advantage of the complexity of modern web browsers and some of the idiosyncracies of how they function in the presence of content and scripts from a multitude of mutually mistrustful origins. Likewise, smartphone platforms are complex and must handle the safe coexistence of many apps with varying levels of trust and permissions, and they have some quirks of their own that present challenges for security. But since smartphones are a newer and younger technology, the kinds of security vulnerabilities present on them are much less well understood than the analogous web vulnerabilities. In this post we’ll take a look at an attack vector that often comes up on Android, namely intent spoofing, and discuss how to write apps that are secure from it.
Android Components and Intents
An Android application consists of a set of components of the following types:
- An activity represents a single screen. Everything that appears in the user interface is part of an activity, and this is the only type of component that is directly visible to the user.
- Services run in the background and often perform long-running operations. A service provides functionality by allowing other components to bind to it.
- Broadcast receivers register to receive notifications of particular events, which can either be system-wide or application-specific. For example, broadcast receivers can register to receive an
ACTION_BATTERY_LOWmessage from the system when the battery level drops below a certain threshold.
Intents come into play here because they are the main mechanism for communication between components. (There’s actually a fourth type of component, namely content providers, but we’ll leave those aside because they do not use intents for communication.) Intents are used to start activities and services, bind to services, and convey notifications to broadcast receivers. By default, a component can only receive intents from other components in the same application, but it can be configured to accept intents from outside applications by setting the
android:exported attribute in the manifest.
An intent can be classified as one of two types based on how it is addressed. Explicit intents name the exact recipient of the message. Implicit intents do not name a recipient, and instead rely on the message being routed to an appropriate component based on the nature of the intent. A component registers to receive implicit intents by declaring an intent filter specifying the kinds of intents it can handle. Implicit intents are useful when you want to request a particular piece of functionality without having to specify exactly which component should provide that functionality. For example, the easiest way to allow the user to take a picture is to send an implicit intent with the
MediaStore.ACTION_IMAGE_CAPTURE intent action. A camera app capable of handling that action will receive it, allow the user to take a picture, and make the result available to the caller. If a camera app with extended features happens to be installed at run time, the user can opt to use that one instead of the standard Android camera app.
There are two main ways that the security of intents can be compromised:
- Intent interception involves a malicious app receiving an intent that was not intended for it. This can cause a leak of sensitive information, but more importantly it can result in the malicious component being activated instead of the legitimate component. For example, if a malicious activity intercepted an intent then it would appear on the screen instead of the legitimate activity.
- Intent spoofing is an attack where a malicious application induces undesired behavior by forging an intent.
For this post we’ll focus on intent spoofing attacks. A typical scenario is that the vulnerable application has a component which only expects to receive intents from other components of the same application. However, if the component is exported then any application can send intents to it. Often it isn’t even necessary to be particularly clever about crafting a malicious intent—since the vulnerable component doesn’t expect intents from other applications, it blindly trusts whatever it receives.
This is where a few disadvantages of implicit intents come into play. Although they offer a useful level of flexibility in run-time binding of components, they are frequently overused or used inappropriately, with negative consequences for security. A major problem with respect to intent spoofing is that registering an intent filter to receive implicit intents makes that component exported by default. This allows all applications to send intents to that component. Moreover, they don’t have to be implicit intents, and if they’re explicit then they don’t even have to match the intent filter. Ironically, creating an intent filter for a component greatly widens the scope of intents that Android will allow to be sent to it. Developers must not rely on intent filters for security, because these filters place no restrictions whatsoever on explicit intents.
Last year I worked with a team on an Android security research project, and as part of that we downloaded about 500 of the most popular Android apps on Google Play. In addition to performing some automated static analysis on them, we had a closer look at a random sample of twenty applications. We were able to crash fourteen of them by sending malformed intents and found that four of them appeared to expose functionality that was intended for internal use only. The most interesting one we looked at is what is now an old version of the PayPal app for Android.
Among other things, the app allows users to send money electronically to other PayPal users. With some investigation we found that one way this is done is by starting a component called
SendMoneyActivity in the PayPal app. The component appears to be intended solely for the internal use of the app, but nonetheless it declares an intent filter and is therefore exported by default.
Conveniently, Android has a utility called
am that makes it easy to construct and send intents interactively when you log into a device or emulator with
adb shell. With a bit of experimentation we were able to figure out how to send forged but valid intents to
SendMoneyActivity. Here’s an example:
am start \ -a android.intent.action.SENDTO \ -d mailto:email@example.com \ --es com.paypal.android.p2pmobile.Amount 9.99 \ --ei com.paypal.android.p2pmobile.ParamType 42 \ -n com.paypal.android.p2pmobile/.activity.SendMoneyActivity
Here we specify:
- The type of action (
- A data field with an email URI of the person who is to receive the money (
- A string field with the amount of money
- An integer
ParamTypefield that has to be non-null to satisfy the activity’s expectations of the input
The result is that even though we have no special permissions, the activity opens with the recipient and dollar amount filled out and with the default payment method pre-selected. The only thing blocking this exploit is that the user must hit the “Send” button before the funds are transferred.
Although this flaw is not easily exploitable (and fortunately no longer appears to exist in current versions of the PayPal app), it is still remarkable that a finance-related application had an undocumented backdoor such as this one exposed to any unprivileged app on the device.
Writing Secure Code
Here are a few recommendations on how to write Android apps that are safe from intent spoofing:
- Avoid the use of implicit intents for communication between the components of a single app. Explicit intents are a much better choice for this situation, because they are simpler and they leave no doubt about the recipient of an intent.
- Explicitly set the value of
android:exportedfor each component in your manifest. This eliminates the risk of having a component exported inadvertently if it declares an intent filter.
- If you do want to accept intents from untrusted applications, always validate your input carefully, and do not assume that incoming intents are well-formed. Many applications can be made to crash simply by sending an intent that omits an expected field, usually causing a null-pointer exception.
- Make use of permissions when appropriate. If you write a component that you would like to be accessible only to another application you’ve written, you can export the component but protect it with a signature permission that allows access only to other applications signed by your developer key.
Above all, it’s a good idea to remain vigilant about security and to always be aware that applications on a phone or tablet must co-exist with other apps that might not be trustworthy.