Ethiopian banking Malware (Pharma+)/(CBE Vacancy)
In-depth analysis of the Pharma+ and CBE Vacancy Ethiopian Android banking malware.
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.
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
permissions | description |
---|---|
android.permission.CALL_PHONE | initiate a phone call . |
android.permission.INTERNET | Allows applications to open network sockets. |
android.permission.POST_NOTIFICATIONS | post notifications to the system notification area |
android.permission.SYSTEM_ALERT_WINDOW | create windows that are displayed on top of other applications |
np.manager.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION | custom permission |
android.permission.BIND_ACCESSIBILITY_SERVICE | Monitor 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.
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);
};
});
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:
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.
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.
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.
Mitigation Strategies
Only install apps from Google Play Store
Never enable Accessibility Services for untrusted apps
Indicators of Compromise (IoCs):
Indicators | Indicator type |
---|---|
6198c8d04da84ee44a66ab29df45c97f83ab6683ffd536189d1b1aae29b7ede9 | SHA256 |
hxxps[:]//ethioteiegram[.]000webhostapp[.]com/store_data.php | C2 URL |
/Config/sys/apps/log/log-*.txt | Filenames |
np.manager | Package name |