Post

Ethiopian banking Malware (Pharma+)/(CBE Vacancy)

In-depth analysis of the Pharma+ and CBE Vacancy Ethiopian Android banking malware.

Ethiopian banking Malware (Pharma+)/(CBE Vacancy)

Introduction

Recently, I received an alert about a new malware strain circulating under the names Pharma+ and CBE Vacancy, reportedly linked to the CBE app.

cbe warning notification Figure 1: CBE phishing warning

After receiving the notification, I decided to track down a sample for analysis. Thanks to m, we managed to obtain a copy of the malware, which allowed me to begin dissecting the malware.

Technical anlaysis

Decompiling the cbevacancy.apk malicious app using JADX we can find the AndroidManifest.xml file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!-- AndroidManifest.xml Highlights -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="32" android:compileSdkVersionCodename="13" package="np.manager" platformBuildVersionCode="32" platformBuildVersionName="13">
    <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="33"/>
    <uses-feature android:name="android.hardware.telephony" android:required="false"/>
    <uses-permission android:name="android.permission.CALL_PHONE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <permission android:name="np.manager.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:protectionLevel="signature"/>
    <uses-permission android:name="np.manager.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
    <application android:theme="@style/Theme.AppCompat" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:debuggable="true" android:supportsRtl="true" android:extractNativeLibs="false" android:usesCleartextTraffic="true" android:roundIcon="@mipmap/ic_launcher" android:appComponentFactory="androidx.core.app.CoreComponentFactory">
        <activity android:name="np.manager.൉" android:exported="false"/>
        <activity android:name="np.manager.MainActivity2" android:exported="false"/>
        <service android:name="np.manager.ą³¾" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="false">
            <intent-filter>
                <action android:name="android.accessibilityservice.AccessibilityService"/>
            </intent-filter>
            <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility"/>
        </service>
        <activity android:name="np.manager.ą“" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <provider android:name="androidx.startup.InitializationProvider" android:exported="false" android:authorities="np.manager.androidx-startup">
            <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
            <meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androiAnalyzing the permissions declared in the AndroidManifest.xml file dx.startup"/>
        </provider>
    </application>
</manifest>

Analyzing the permissions declared in the AndroidManifest.xml file the following permissions are requested by the app

permissionsdescription
android.permission.CALL_PHONEinitiate a phone call .
android.permission.INTERNETAllows applications to open network sockets.
android.permission.POST_NOTIFICATIONSpost notifications to the system notification area
android.permission.SYSTEM_ALERT_WINDOWcreate windows that are displayed on top of other applications
np.manager.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSIONcustom permission
android.permission.BIND_ACCESSIBILITY_SERVICEMonitor device screen activities

and the app requires android version android:minSdkVersion="24" . The minimum Android version required is 7.0 (Nougat). android:compileSdkVersion="32" The app was compiled using the Android 12L (API 32) SDK, but it targets Android 13 android:targetSdkVersion="33" optimized for Android 13

It uses Unicode characters for class variable and method names to make the RE more complex.

Abusing the Accessibility Service: A Closer Look

Android’s android.permission.BIND_ACCESSIBILITY_SERVICE is designed to help users with disabilities by granting applications advanced accessibility features. With this permission, an app can take control of the entire screen simulating clicks, swipes, and other gestures as well as managing keyboard input, reading screen content, and even opening or closing other applications.

accessibility_permissions Figure 2: Accessibility permission abused by the malicious app

However, when misused, this powerful permission can also facilitate malicious activities. When decompiling malicious service class (identified as np.manager.ą³¾) we can observe that it records keystrokes along with the corresponding package names of the active applications. These logs were intended to be saved to a file path structured as:

/Config/sys/apps/log/log-YYYY-MM-DD.txt

The log entries were formatted in a specific way:

base64encode(nullvalue + "#" + keystroke + "#" + eventType)

Interestingly, the malware was designed to write these logs to the device’s internal storage. However, due to a missing WRITE_EXTERNAL_STORAGE permission in the AndroidManifest file the malware’s logging attempt failed. Additionally, the malware contained a conditional check comparing two variables with the values "[off_keylog]" and "on".

using frida to hook and modify the variables to be equal and change the path so that it doesn’t write to its package folder(since we don’t need write permission there) we can make the app write to

/storage/emulated/0/Android/data/np.manager/files/Config/sys/apps/log/log-YYYY-MM-DD.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
Java.perform(function() {
  console.log("[Frida] Script loaded successfully!");

  try {

    var TargetClass = Java.use("np.manager.\u0CFE");
    var clazz = TargetClass.class;
    // Get the field for obfuscated "Ģ–"
    var fieldOn = clazz.getDeclaredField("Ģ–");
    fieldOn.setAccessible(true);

    // Get the field to "Ģ—"
    var fieldOff = clazz.getDeclaredField("Ģ—");
    fieldOff.setAccessible(true);

    // Retrieve original values using reflection (pass null since these are static)
    var originalOn = fieldOn.get(null);
    var originalOff = fieldOff.get(null);
    console.log("Before modification:");
    console.log("keylog_on_var (Ģ–): " + originalOn);
    console.log("keylog_off_var (Ģ—): " + originalOff);

    // Set the variables to be equal 
    fieldOff.set(null, originalOn);

    // Verify the change by reading the fields again
    var newOn = fieldOn.get(null);
    var newOff = fieldOff.get(null);
    console.log("After modification:");
    console.log("keylog_on_var (Ģ–): " + newOn);
    console.log("keylog_off_var (Ģ—): " + newOff);
  } catch (e) {
    console.log("Error modifying keylog fields: " + e);
  }

  var Environment = Java.use('android.os.Environment');
  var File = Java.use('java.io.File');
  var Context = Java.use('android.content.Context');

  // Redirect getExternalStorageDirectory to the app's external files directory
  Environment.getExternalStorageDirectory.implementation = function() {
      var currentApp = Java.use('android.app.ActivityThread').currentApplication();
      var context = currentApp.getApplicationContext();
      var externalDir = context.getExternalFilesDir(null); // Gets Android/data/np.manager/files/
      console.log('[+] Redirecting external storage to: ' + externalDir.getAbsolutePath());
      return externalDir;
  };

  //hook the file write and redirect it to the app's private directory
  var targetClass = Java.use('np.manager.\u0CFE');
  targetClass["\u0322"].implementation = function (text) {
    try {
        var Environment = Java.use('android.os.Environment');
        var File = Java.use('java.io.File');
        var SimpleDateFormat = Java.use('java.text.SimpleDateFormat');
        var Date = Java.use('java.util.Date');
        var StringBuffer = Java.use('java.lang.StringBuffer');
        var FieldPosition = Java.use('java.text.FieldPosition');

        var myDate = Date.$new();
        var dateFormat = SimpleDateFormat.$new("yyyy-MM-dd");
        var stringBuffer = StringBuffer.$new();
        var fieldPosition = FieldPosition.$new(0); // Dummy position

        dateFormat.format(myDate, stringBuffer, fieldPosition);
        var formattedDate = stringBuffer.toString();

        var baseDir = Environment.getExternalStorageDirectory().getAbsolutePath();
        var logFile = baseDir + "/Config/sys/apps/log/log-" + formattedDate + ".txt";
        console.log("\x1b[92m[Hook] Log File Path: " + logFile + "\x1b[0m ");
        console.log("\x1b[1m\x1b[31m[Hook] Log Content: " + text + "\x1b[0m");

    } catch (e) {
        console.error("[Hook] Error occurred: " + e.message);
    }
    return this["\u0322"](text);
  };

});

frida_write_redirect_hook Figure 3: Frida script redirecting keylogger logs to external storage

USSD Exploitation

The malware actively searches for specific keywords related to CBE USSD banking service using findAccessibilityNodeInfosByText. Some of the targeted strings include:

referenced_banking_strings

Once these strings are detected on the screen, the malware automates interactions by simulating clicks and setting text fields with its own predefined values. This allows it to perform unauthorized USSD transactions, such as transferring money without the user’s knowledge.

ussd_settext

Uninstallation Prevention Mechanism

The malware prevents its uninstallation by monitoring for specific keywords related to uninstallation, such as ā€œuninstallā€. When such strings are detected, the app automatically triggers an action that redirects the user to the home screen, effectively interrupting the uninstallation process.

There was also an additional condition that compared the current date against a hardcoded deadline: May 25, 2025. The logic suggested that the anti-uninstallation feature should be active only before this date. However, even after manipulating the system date to both before and after the deadline, I was still unable to uninstall the app. This indicates either a flaw in the date-checking logic or an additional hidden mechanism reinforcing the persistence.

anti_uninstall

C2 Communication

The malware communicates with its Command and Control (C2) server via simple HTTP GET requests, sending data to the following endpoint:

hxxps[:]//ethioteiegram[.]000webhostapp[.]com/store_data.php

It transmits information related to USSD transaction results. Although the malware includes keylogging functionality, it is disabled by default, meaning no keylogging data is exfiltrated unless manually enabled.

It ignores any server responses, which suggests that its C2 communication is strictly one-way.

send_to_c2

Mitigation Strategies

  • Only install apps from Google Play Store

  • Never enable Accessibility Services for untrusted apps

Indicators of Compromise (IoCs):

IndicatorsIndicator type
6198c8d04da84ee44a66ab29df45c97f83ab6683ffd536189d1b1aae29b7ede9SHA256
hxxps[:]//ethioteiegram[.]000webhostapp[.]com/store_data.phpC2 URL
/Config/sys/apps/log/log-*.txtFilenames
np.managerPackage name

VirusTotal Scan Result

This post is licensed under CC BY 4.0 by the author.