Category: misc/android Authors: orion, hpmv
- Android Studio is recommended
- challenge apps were tested on Pixel 3a AVD API 34
spellbound.zip
has the files that should be distributed in the challenge download:
- DictionaryApp-signed.apk
- DictionaryService-signed.apk
There's no obfuscation, we expect people to just go decompile the apks.
Both apps are signed so they can be installed on the emulator. The key used to sign both apps to create the hardcoded signature in the apps' identity check is included in the repo. Since this key is pretty related to the challenge the keystore is included in this repo for reproducibility reasons, but it goes without saying don't use this keystore for anything important etc.
This challenge required exploiting a behavior of Android bound services. Namely this part:
You can connect multiple clients to a service simultaneously. However, the system caches the IBinder service communication channel. In other words, the system calls the service's onBind() method to generate the IBinder only when the first client binds. The system then delivers that same IBinder to all additional clients that bind to that same service, without calling onBind() again.
This app exports two services:
- SignatureService
- DictionaryService
SignatureService is only accessible if you have the permission com.dicectf2024.permission.dictionary.BIND_SIGNATURE_SERVICE
declared in the manifest. This permission is only available to apps signed with the same signing key due to protectionLevel="signature"
. It's intended that this is only accessible from DictionaryApp.
DictionaryService is the interesting service that serves a bunch of words and their definitions. If the magic word is received (flag
), it returns the flag token, which is a 16-character random string stored in Encrypted Shared Preferences. This is to ensure it cannot be accessed by another app by normal means.
Even though this service is exported, this service is only intended to be bound to from DictionaryApp. To ensure this, it has a permission check in onBind
that is pretty restrictive:
- First, the incoming intent must have two signed extras. It must be signed with a key in DictionaryService's keystore. The only way to achieve this outside the app is through SignatureService, which is only accessible to apps signed with the same signing key.
- The signed extras contains a timestamp and a package name. The timestamp must not be expired and the package name must match DictionaryApp.
- Finally, it queries PackageManager to verify that only a single app on the entire system has the
BIND_SIGNATURE_SERVICE
permission, as well as that that single app matches DictionaryApp's package name and signature. Even if you somehow managed to sign an app with the same key, if there are more apps with this permission the check will fail.
If any of these checks fail, onBind
will just return null
as the binder interface and not the actual binder.
This app has an exported activity that other apps can launch. The activity will bind to DictionaryService in onCreate
, send a word to it, receive a defintion back, and display it onscreen.
Note it also unbinds the services upon navigating away, this is intended so you don't accidentally stumble on the solution if you had DictionaryApp open already while launching AttackerApp directly from Android Studio or something.
The solution is to write an attacker app that
- Launches DictionaryApp, triggering it to bind to DictionaryService
- Tries to bind to DictionaryService after DictionaryApp has successfully binded. Since
onBind
is only ever called once and the same interface is returned to all clients, this bypasses the permission check. Now you can callgetData
on the service with the wordflag
to get flag, and print it out in logcat (which the solve service will give you)
The easiest is just to sleep for a few seconds after launching DictionaryApp's DefinitionActivity and then try to call bindService
. Or you can start another service that binds to DictionaryService.
Note that if an untrusted app tries to bind to DictionaryService with an intent without the proper identity, DictionaryService will just return null
. The result of bindService
will still show as true
but you can't do anything with this interface.
The evaluation infra for this challenge (not included in the source code here) was hosted externally and heavily based on https://github.com/google/android-emulator-container-scripts . Contact hpmv or orion if you are interested in the infrastructure setup.