Accessibility Check Bypass Countermeasure

Most applications utilize Android APIs for verifying untrusted Android Accessibility Service binding with an app. They have to implement a whitelist of multiple applications based on their installing information to allow their applications to run while trusted Accessibility Service binding apps are working. In this post, McAiden Research covers some major mistakes which could allow a malware which misuses the Accessibility Service features to bypass those checks and still conduct financial fraud until now.

Note: To keep it short, we call the Android Accessibility Service, the “AAS”.

IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS

In the previous post (Analysis on Bumble Part 2) we demonstrate how to use the IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS flag for a View instance to hide information from being reached by AAS binding apps. Now we will introduce the possibility of circumvent that protection by using FLAG_INCLUDE_NOT_IMPORTANT_VIEWS which will ignore the importance of the View.

The following figures show how we use the FLAG_INCLUDE_NOT_IMPORTANT_VIEWS to retrieve information of an AAS event is fired

So if your application was created to prevent itself from misuse of AAS , the flag IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS won’t help much.

App Whitelisting Using Only Package Name

McAiden observe some applications on the market places and found some behavior interesting. These applications allow for specific package names to use the AAS. The intention of this behavior could allows a malware to bypass the accessibility check of the application more easier because the whitelist needs to be performed on the client-side app. The attacker can look for that logics and find valid package names which can be used in the malware.

In this case, we recommend re-implementing your whitelisting in more secure way such as verifying the application signing information instead of only package name (if the whitelist is still required).

Installer Whitelisting Using Only Package Name

Some applications strengthen their checks by allowing an application to use the AAS when it comes from a valid installer. For example, when an AAS binding app is installed from Google Play Store (Google Play Store has its package name “com.android.vending”). In the real world, we cannot avoid a large group of users who use Android phones with vendor-specific application store e.g. Huawei, Samsung, Oppo etc. The application needs to allow the AAS app when it comes from that store also. The easiest choice of checking the installer is by obtaining the AAS app installer package name. So the application needs to perform whitelisting based on the installer package name of the app that require AAS.

In this case, we found a problem. Because any malware developer can choose any package name for their application. Why don’t the attacker manipulate the installing package name of their malware ?

Before answer that question, we need to understand that there are at least 2 ways to install a malware on a device and manipulate the installing package name. One is to use setInstallerPackageName() (PackageManager#setInstallerPackageName) comes with SDK level 11.

There are some constrains that limit the usage of this API maliciously, so let talk about another approach. The second way is to create a dropper application. There are legitimate ways for an Android application to drops (install) another APK on a device (when unknown source is enabled for that dropper app). The ways are “File provider + Intent(ACTION_VIEW)" (basically it is “File provider + Intent(some_type)”) or “PackageInstaller” APIs. Both dropper techniques can be used to manipulate the installing package name of the dropped application especially PackageInstaller.

Sample of using File provider + Intent(ACTION_VIEW) to install another APK:

As you may have seen in the code comments, the installer package name of the dropped app can be obtained in numerous ways depend on SDK version.

In SDK 30+, by calling the getInstallingPackageName() can result in false negative when the actual installer is com.google.android.packageinstaller but the retrieved value is com.sec.android.app.samsungapps. We found this happen in some devices e.g. some models of OnePlus devices.

Sample of using PackageInstaller API:

By using the PackageInstaller API, the dropper package name will be used to set as installing package of the dropped app. Regardless of how the developer obtain the installing package name, the value can be controlled by the attacker who builds the dropper.

As of 03-April-2023 (06:30 GMT+7), we assessed on a group of applications from Play Store that have AAS checks and found that some of them can be bypassed by manipulating the installer package name information (the assessment was conducted with only on a non-rooted OnePlus 7T, Android 11).

Countermeasure

The AAS on Android platform is very useful when it is used in a good way for helping people with disabilities. But when this kind of attack breaks out, we have to think carefully if we will prohibit usage of its capability because there are numerous people who need it. Let’s say your application users require the AAS feature to be enabled and your application is in a target group of some threat actors, here we provide some countermeasures:

  • Avoid implementing only-package-name whitelisting. If you need to query the installing package name for a specific app, use getInitiatingPackageName() instead of getInstallingPackageName() in SDK30+.
  • If you need to implement a whitelist for some apps, whitelist its signing information instead of only package name.

Here is the example of checks:

  1. Query installed AAS apps.
  2. Check if the installed AAS app has AAS enabled.
  3. Check if AAS app is system app and do nothing if it is a system app.
  4. If it is NOT system app, check the installer of the AAS app
    • Check if the installer is in the whitelist installers
    • Check if the installer signing information match with information pinned in the whitelist
[...]
checkAASButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        checkForUntrustedAAS();
    }
});
[...]

public void checkForUntrustedAAS(){
    if (isUntrustedAccessibilityFound(this)) {
        Toast.makeText(LoginActivity.this, "[!] Untrusted AAS enabled", Toast.LENGTH_LONG).show();
    } else {
        Toast.makeText(LoginActivity.this, "[!] No untrusted AAS enabled", Toast.LENGTH_LONG).show();
    }
}

private void updateUiWithUser(LoggedInUserView model) {
    String welcome = getString(R.string.welcome) + model.getDisplayName();
    // TODO : initiate successful logged in experience
    Toast.makeText(getApplicationContext(), welcome, Toast.LENGTH_LONG).show();
}

private void showLoginFailed(@StringRes Integer errorString) {
    Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_SHORT).show();
}

public boolean isUntrustedAccessibilityFound(Context context) {
    Boolean result = false;
    AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);

    // List<AccessibilityServiceInfo> runningServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); // Avoid using due to CVE-2022-20495 (unproved yet)
    List<AccessibilityServiceInfo> installedServices = am.getInstalledAccessibilityServiceList();
    for (AccessibilityServiceInfo service : installedServices) {
        String packageName = service.getResolveInfo().serviceInfo.packageName;
        boolean isInstalledServiceEnabled = isAccessibilityServiceEnable(context, packageName);
        Log.d(TAG, "[!] Package: " + packageName + " uses AAS, enable?: " + isInstalledServiceEnabled);
        if (isInstalledServiceEnabled) {
            if (isSystemApp(packageName)) {
                // Good, do nothing, go to next AAS app
            } else {
                // The AAS app is not system app, need to validate its installer (installer package name + its signing info.)

                // This will return true (untrusted installer found) when
                // - installer is null
                // - installer is not in whitelist
                // - installer is in whitelist but its signing information is mismatch
                String installer = getInstallerPackageNameFromAppPackageName(context, packageName);
                if (installer == null) {
                    Log.d(TAG, "[!] Package: " + packageName + " has null installer");
                    textView.append("[!] Untrusted AAS: " + packageName + " has null installer\n");
                    return true;
                }
                if(!isTrustedInstaller(context, installer)) {
                    Log.d(TAG, "[!] Package: " + packageName + " its installer signing signature mismatched: " + installer);
                    textView.append("[!] Untrusted AAS: " + packageName + " installer signature mismatched\n");
                    return true;
                }
            }
        } else {
            // the installed service is not enabled, do nothing
        }
    }
    return result;
}

boolean isAccessibilityServiceEnable(Context context, String packageName) {
    int isAccessibilitySettingEnable = 0;
    try {
        isAccessibilitySettingEnable = Settings.Secure.getInt(context.getApplicationContext().getContentResolver(), Settings.Secure.ACCESSIBILITY_ENABLED);
        if (isAccessibilitySettingEnable == 1) {
            String settingValue = Settings.Secure.getString(context.getApplicationContext().getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
            TextUtils.SimpleStringSplitter splitter = mStringColonSplitter;
            splitter.setString(settingValue);
            while (splitter.hasNext()) {
                String accessibilityService = splitter.next().split("/")[0];
                if (accessibilityService.equalsIgnoreCase(packageName)) {
                    return true;
                }
            }
        }

    } catch (Settings.SettingNotFoundException e) {
        e.printStackTrace();
    }

    return false;
}

boolean isSystemApp(String packageName) {
    boolean isSystemApp = false;
    try {
        ApplicationInfo app = this.getPackageManager().getApplicationInfo(packageName, 0);
        isSystemApp = ((app.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
        Log.i(TAG,"[!] Package: " + packageName + ", is system app: " + isSystemApp);
    } catch (PackageManager.NameNotFoundException e) {
        Log.i(TAG, "[!] Package not found >>" + packageName +"<<");
    }
    return isSystemApp;
}

private String getDeviceBrand(){
    String brand = "";
    brand = Build.BRAND.toLowerCase();
    return brand;
}

// The following code is for checking if the app is installed from possible store based on device brand.
//    if (!hasPossibleInstaller(this, packageName)){
//        Log.d(TAG, "[!] Package: " + packageName + " has impossible installer: " + installer);
//        return true;
//    }
boolean hasPossibleInstaller(Context context, String packageName) {

    String installerPackageName = getInstallerPackageNameFromAppPackageName(context, packageName);
    String deviceBrand = getDeviceBrand();
    Log.d(TAG, "[!] device brand: " + deviceBrand);
    List<String> possibleInstallers = getPossibleInstallerFromDeviceBrand(deviceBrand);
    Boolean result = false;
    result = possibleInstallers.contains(installerPackageName);

    return result;
}

private String getInstallerPackageNameFromAppPackageName(Context context, String packageName) {
    String installerPackageName = null;
    // McAiden: getInstallSourceInfo() comes with SDK 30+, otherwise use getInstallerPackageName()
    // McAiden: Because the installing package name can be changed by calling PackageManager#setInstallerPackageName(String, String), getInitiatingPackageName()  return the initial installing package name
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        try {
            installerPackageName = context.getPackageManager().getInstallSourceInfo(packageName).getInitiatingPackageName();

        } catch (PackageManager.NameNotFoundException e) {
            Log.i(TAG, "[!] Package not found: " + packageName);
        } catch (Exception e) {
            Log.i(TAG, "[!] Error: " + e.getMessage());
        }
    } else {
        installerPackageName = context.getPackageManager().getInstallerPackageName(packageName);
        Log.i(TAG,"[!] Package: " + packageName + ", installer: " + installerPackageName);
        textView.setText("\nPackage: " + packageName + "\ninstaller: " + installerPackageName + textView.getText());
    }
    return installerPackageName;
}

private boolean isTrustedInstaller(Context context, String installerPackageName) {
    Boolean trust = false;
    HashMap<String,String[]> installerSigningInfoMap = new HashMap<String,String[]>();

    // There are possibilities that an official store has more than one signature. That's why we put the signature in String array
    // So we can pin more than one signature per store.
    // Signature from getSha256Hash(sig.toByteArray()))
    // Use Signature.toByteArray() because it is used in APKMirror and APKSigner
    installerSigningInfoMap.put("com.android.vending",new String[]{"f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83"});
    installerSigningInfoMap.put("com.google.android.feedback",new String[]{"f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83"});
    installerSigningInfoMap.put("com.xiaomi.mipicks",new String[]{"c9009d01ebf9f5d0302bc71b2fe9aa9a47a432bba17308a3111b75d7b2149025"});
    installerSigningInfoMap.put("com.huawei.appmarket",new String[]{"3baf59a2e5331c30675fab35ff5fff0d116142d3d4664f1c3cb804068b40614f"});
    installerSigningInfoMap.put("com.heytap.market",new String[]{"289cc3429f496460b66ac2a876c040091aaef55b5eb942c2ce697ef3b1201459"});
    installerSigningInfoMap.put("com.vivo.appstore",new String[]{"bcc35d4d3606f154f0402ab7634e8490c0b244c2675c3c6238986987024f0c02"});
    installerSigningInfoMap.put("com.sec.android.app.samsungapps",new String[]{"fba3af4e7757d9016e953fb3ee4671ca2bd9af725f9a53d52ed4a38eaaa08901"});

    String[] pinnedSigningInfos = installerSigningInfoMap.get(installerPackageName);
    if (pinnedSigningInfos != null) {
        Signature[] signingInfos = getSigningSignaturesFromPackageName(installerPackageName);
        for (Signature sig: signingInfos) {
            for (String pinnedSigningInfo:pinnedSigningInfos) {
                if (pinnedSigningInfo.equalsIgnoreCase(getSha256Hash(sig.toByteArray()))) {
                    return true;
                } else {
                    Log.d(TAG, "[!] Package: " + installerPackageName + ", pinned signature: " + pinnedSigningInfo);
                }
            }
        }
    } else {
        // installer is not in whitelist
        Log.d(TAG, "[!] installer is not in whitelist ");
        return false;
    }

    return trust;
}

private ArrayList<String> getPossibleInstallerFromDeviceBrand(String _deviceBrand) {
    ArrayList<String> possibleInstaller = new ArrayList<>();
    ArrayList<String> defaultInstaller = new ArrayList<>(Arrays.asList("com.android.vending", "com.google.android.feedback"));
    switch (_deviceBrand) {
        case "samsung":
            possibleInstaller = new ArrayList<>(Arrays.asList("com.sec.android.app.samsungapps"));
        case "vivo":
            possibleInstaller = new ArrayList<>(Arrays.asList("com.vivo.appstore"));
        case "oppo":
            possibleInstaller = new ArrayList<>(Arrays.asList("com.heytap.market"));
        case "redmi":
            possibleInstaller = new ArrayList<>(Arrays.asList("com.xiaomi.mipicks"));
        case "huawei":
            possibleInstaller = new ArrayList<>(Arrays.asList("com.huawei.appmarket"));
        default:
            possibleInstaller = defaultInstaller;
    }
    possibleInstaller.addAll(defaultInstaller);

    return possibleInstaller;
}

private Signature[] getSigningSignaturesFromPackageName(String packageName)  {

    Signature[] signatures = null;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        PackageInfo packageInfo = null;
        try {
            packageInfo = getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES);
            signatures = packageInfo.signingInfo.getApkContentsSigners();
        } catch (PackageManager.NameNotFoundException e) {
            Log.i(TAG, "[!] Package not found >>" + packageName +"<<");
        }
    } else {
        PackageInfo packageInfo = null;
        try {
            packageInfo = getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
            signatures = packageInfo.signatures;
        } catch (PackageManager.NameNotFoundException e) {
            Log.i(TAG, "[!] Package not found >>" + packageName +"<<");
        }
    }

    if (signatures != null && signatures.length > 0) {
        for (Signature sig: signatures) {
            Log.i(TAG, "[!] Package: " + packageName + ", Signature: " + getSha256Hash(sig.toByteArray()));
        }
    }
    return signatures;
}

private static String getSha256Hash(byte[] input) {
    try {
        MessageDigest digest = null;
        try {
            digest = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e1) {
            e1.printStackTrace();
        }
        digest.reset();
        return bin2hex(digest.digest(input));
    } catch (Exception ignored) {
        return null;
    }
}

private static String bin2hex(byte[] data) {
    StringBuilder hex = new StringBuilder(data.length * 2);
    for (byte b : data)
        hex.append(String.format("%02x", b & 0xFF));
    return hex.toString();
}

Sample of Expected Behavior

In this section, we demonstrate what are the expected behaviors to detect untrusted AAS enabled applications using the trust of installer approach. We develop 3 applications which are:

Mc Dropper: an application having its package name “com.sec.android.app.samsungapps” and acts like a dropper app. The Mc Dropper will install the Mc Malware app using PackageInstaller API. After installation, the installer of Mc Malware will be the Mc Dropper which has its package name “com.sec.android.app.samsungapps”.

Mc Malware: an application that requires AAS feature, acts like malware but just automatically grant READ_SMS permission.

Mc Checker: an application that implements the logics described in the earlier section to detect and distinguish untrusted AAS app (Mc Malware) from other AAS apps (system apps and apps downloaded from actual trusted stores).

Allow System AAS Apps

In this case, there are multiple accessibility services enabled but all of them are system apps. We consider they are trusted services and can be run normally during using our Mc Checker app.

You may have seen in the pidcat results that we have installed the Mc Malware but didn’t enable the Android Accessibility Service for it yet. In this case, the Mc Checker doesn’t care it (If you wish, you can implement logics to validate all installed AAS apps and notify your users of untrusted apps.).

Allow AAS Apps Downloaded from Trusted Stores

Now we download an application (Voice Access) from Google Play Store and enable AAS for it. The Mc Checker detect the installer package is “com.android.vending” and it has the same SHA256 signature of its signing information with the pinned signature. In this case, the Mc Checker trusts the Voice Access application.

Detect AAS Apps Downloaded from An Untrusted Store

We have already install the Mc Malware using Mc Dropper app (the dropper has its package name com.sec.android.app.samsungapps). Now we enable the AAS for the Mc Malware.

In this case, the Mc Malware has its installer’s package name com.sec.android.app.samsungapps which is in the whitelist. If the checker does not pin the correct signing information of the com.sec.android.app.samsungapps package, the Mc Checker can be bypassed with this kind of technique. But the Mc Checker has already pinned the correct signing information of the com.sec.android.app.samsungapps, so the Mc Checker can detect this untrusted store and mark the Mc Malware as untrusted AAS app.