Android Exported Service MITM Attacks
We are used to a device having multiple activities that can respond to the same
<intent-filter>. In that case, by default, the user will see a chooser if we
try to start one of those activities.
We are used to a device having multiple
BroadcastReceiver components that can
respond to the same
IntentFilter). In that case, in a
regular broadcast, all eligible receivers will receive it.
We are used to it being impossible to have multiple
with the same authority, as the second one fails on install with an
Services, though, follow none of these patterns, and the pattern they do follow raises the possibility of man-in-the-middle (MITM) attacks if you are not careful.
Services that are private to your application (no
android:exported="false") are safe, as explicit
Intents are safe with services,
as they stipulate the specific component that you are working with.
Services that are exported but are secured with a
are also safe, as only another app signed by the same signing key will be able
to work with the service.
But what happens if you want to have a exported service for which a
permission is impossible? For example, what happens if you want to have a service
expose an API for third party apps to consume? Those third party apps will be signed
by their own signing keys, not yours, and so
signature-level permissions will
signature-level permission, there is no automatic validation employed
by Android to ensure that the service you are trying to work with is, indeed, the
actual service you want to work with, instead of an imposter. Such an imposter
might be a totally different service implementation, simply advertising the same
<intent-filter> as the real service. Or, the imposter might be a cracked version
of the original service app, with additional code injected that makes use of the
data flowing between client and service, in addition to doing the normal service
In the latter case, the cracked version of the app might retain the same package name as your original service implementation. In that case, only one edition of the service can exist on the device, where the first one in “wins”. However, the user will be informed of a problem when they try to install the second such app, and so hopefully they will be able to determine which app is the valid implementation and which is the malware. If, however, they only install the cracked version of the component, the user is in trouble.
In addition, what happens if there are two (or more) services installed on
the device that claim to support the same
<intent-filter>, but have different
package names? You might think that this would fail on install, as happens
with providers with duplicate authorities. Alas, it does not. Instead, once again,
the first one in “wins”.
So, if we have
GoodService, both responding to the same
<intent-filter>, and a client app tries to communicate to
via the explicit
Intent matching that
might actually be communicating with
BadService, simply because
was installed first. The user is oblivious to this.
If you want to make sure that you are talking to the right third-party app, by
any IPC mechanism, you will need to compare the public keys. The public key that
signed an app is part of the APK; only apps signed by the same private key should
contain the same public key. Hence,
BadService should have a different public
key than the expected one, while
GoodService would have the expected public key.
You can obtain a binary-encoded edition of the public key from
getPackageInfo() to retrieve the
PackageInfo for an app, given its package
PackageManager.GET_SIGNATURES as the second parameter to
getPackageInfo() to ensure that signature data is retrieved and is part of
PackageInfo structure. Then, the
signatures data member of the
will contain an array of
Signature objects, which, despite their name, are actually
binary-encoded versions of the public keys. Calling
toByteArray() on a
will give you a value that you can compare against a known good binary-encoded
version of the public key (e.g., packaged as a raw resource).
This is somewhat tedious, and so over time somebody (possibly me) will need to package this up into something easier for developers to employ. However, you can see some sample code in this area:
An app that will list all installed packages and let you inspect the public key of that package, plus dump a copy of the binary-encoded public key for you to perhaps incorporate into a client app
An app that validates another package given such a binary-encoded public key
A set of three apps demonstrating the above scenario, where we want to ensure that we work with
GoodServicedespite the possibility of a
BadServicethat also advertises support for the same
(and, yes, a blow-by-blow explanation of this stuff is forthcoming in a future book update)
This code is clunky, and it does not handle apps signed with multiple keys, but it demonstrates some basic defenses that you can employ to avoid rogue service implementations.
Again, this is mostly a concern for IPC where
are not employed for securing that IPC. Non-exported services, or services with
signature-level permissions, should not need this sort of work. Conversely, you might
consider using this sort of defense for other components as well, such as a
to ensure that you are working with the expected provider, and not a cracked version
that does something nefarious with the data being passed back and forth.
Want an expert opinion on your Android app architecture decisions? Perhaps Mark Murphy can help!