Persistence Techniques¶
Surviving device reboots, app kills, and user attempts at removal. Android's process lifecycle aggressively terminates background apps to conserve resources, so malware must actively fight to stay alive. The most resilient families layer multiple persistence mechanisms, ensuring that if one is killed, another restarts it.
MITRE ATT&CK
| ID | Technique | Tactic |
|---|---|---|
| T1624.001 | Event Triggered Execution: Broadcast Receivers | Persistence |
| T1541 | Foreground Persistence | Persistence, Defense Evasion |
| T1398 | Boot or Logon Initialization Scripts | Persistence |
| T1626.001 | Abuse Elevation Control Mechanism: Device Administrator Permissions | Privilege Escalation |
Requirements
| Requirement | Details |
|---|---|
| Boot persistence | RECEIVE_BOOT_COMPLETED (normal permission, auto-granted) |
| Background execution | FOREGROUND_SERVICE (normal permission, auto-granted) |
| Battery exemption | REQUEST_IGNORE_BATTERY_OPTIMIZATIONS |
| Anti-uninstall | BIND_DEVICE_ADMIN (requires user activation) |
| Self-restart | BIND_ACCESSIBILITY_SERVICE (system manages lifecycle) |
Boot Receiver¶
The simplest and most common persistence method. Registering a BroadcastReceiver for BOOT_COMPLETED causes Android to start the malware's component every time the device boots.
<receiver android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent serviceIntent = new Intent(context, MalwareService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
}
}
Multiple boot actions are registered because some OEMs (HTC, Xiaomi) fire vendor-specific boot broadcasts in addition to or instead of the standard one.
Foreground Service¶
Android 8+ kills background services within minutes. The standard workaround is a foreground service, which requires a visible notification but is protected from the system's background execution limits.
Stealth Foreground Service with Hidden Notification
public class PersistentService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
NotificationChannel channel = new NotificationChannel(
"stealth", " ", NotificationManager.IMPORTANCE_MIN);
channel.setShowBadge(false);
getSystemService(NotificationManager.class).createNotificationChannel(channel);
Notification notification = new Notification.Builder(this, "stealth")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(" ")
.build();
startForeground(1, notification);
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
The notification channel uses IMPORTANCE_MIN and a blank name to make the notification as invisible as possible. START_STICKY tells Android to restart the service if the system kills it. SpyNote and Anubis both rely on this pattern.
Invisible via POST_NOTIFICATIONS Denial (Android 13+)¶
Android 13 introduced POST_NOTIFICATIONS as a runtime permission. When this permission is denied (either the user declines the prompt or the app never requests it), the mandatory foreground service notification is suppressed from the notification drawer. The service continues running indefinitely -- protected from the system's background process killer -- with no notification icon or shade entry visible to the user.
The app starts the foreground service while it has a visible Activity (one of several exemptions allowing foreground service starts on Android 12+), then the user closes the app. The service persists as a foreground service process, protected from the system's process killer, with its notification invisible because the permission was never granted.
The service is not completely undetectable: Android 13 introduced the FGS Task Manager, accessible via a button at the bottom of the notification shade that shows the number of apps running in the background. Tapping it opens an "Active apps" dialog listing each app with a "Stop" button. However, most users are unaware of this feature, making it a weak remediation. If a foreground service runs for 20+ hours in a 24-hour window, the system pushes a separate notification (though mediaPlayback and location service types are exempt).
This is simpler and more effective than IMPORTANCE_MIN tricks. Adware families exploit it at scale: every app in a fleet starts its foreground service while TOP, then relies on the denied notification permission to run with no drawer notification.
Scheduled Execution¶
JobScheduler¶
Schedules work that survives process death. The system manages when the job runs based on constraints (network, charging, idle).
ComponentName serviceName = new ComponentName(context, MalwareJobService.class);
JobInfo jobInfo = new JobInfo.Builder(1337, serviceName)
.setPersisted(true)
.setPeriodic(15 * 60 * 1000)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build();
JobScheduler scheduler = context.getSystemService(JobScheduler.class);
scheduler.schedule(jobInfo);
setPersisted(true) makes the job survive reboots (requires RECEIVE_BOOT_COMPLETED). The minimum periodic interval is 15 minutes on Android 7+.
AlarmManager¶
For more precise timing. setExactAndAllowWhileIdle() fires even during Doze mode, though Android 12+ restricts exact alarms and requires SCHEDULE_EXACT_ALARM or USE_EXACT_ALARM.
AlarmManager alarmManager = context.getSystemService(AlarmManager.class);
Intent intent = new Intent(context, WakeUpReceiver.class);
PendingIntent pending = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 60_000,
pending);
WorkManager¶
Android's WorkManager API is designed for deferrable, guaranteed background work. Malware abuses it by creating self-rescheduling workers that create an infinite execution loop surviving app kills and reboots.
public class PersistentWorker extends Worker {
@Override
public Result doWork() {
performMaliciousTask();
WorkManager.getInstance(getApplicationContext())
.enqueue(new OneTimeWorkRequest.Builder(PersistentWorker.class)
.setInitialDelay(30, TimeUnit.SECONDS)
.build());
return Result.success();
}
}
Each execution enqueues the next run with a short delay (typically 15-60 seconds). WorkManager persists its work queue in a SQLite database (app_data/databases/androidx.work.workdb), so pending work survives process death and reboots. Unlike JobScheduler (minimum 15-minute periodicity), WorkManager's OneTimeWorkRequest chaining has no minimum interval, enabling near-continuous execution.
The pattern is difficult to detect at the manifest level because WorkManager registration is purely programmatic. Look for Worker subclasses that call WorkManager.enqueue() from within doWork().
SyncAdapter Persistence¶
The app registers as a sync adapter for a custom account type. Android's sync framework periodically triggers the adapter, providing reliable background execution without visible notifications or foreground services. The sync adapter runs in its own process and benefits from the system's built-in retry, backoff, and scheduling logic.
The setup requires four manifest components:
- An
AccountAuthenticatorservice that creates the fake account type - An
authenticator.xmlresource defining the account type - A
SyncAdapterservice that performs the actual work - A
syncadapter.xmlresource linking the adapter to the account type and a content authority
<service android:name=".auth.AuthService" android:exported="true">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service android:name=".sync.SyncService" android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>
At runtime, the app creates the account and enables periodic sync:
Account account = new Account("SyncAccount", "com.app.account.type");
AccountManager.get(context).addAccountExplicitly(account, null, null);
ContentResolver.setIsSyncable(account, authority, 1);
ContentResolver.setSyncAutomatically(account, authority, true);
ContentResolver.addPeriodicSync(account, authority, Bundle.EMPTY, 3600);
The onPerformSync() callback fires periodically. The app uses this to sync data to C2, refresh configuration, or trigger any background work. The sync framework is exempt from most background execution restrictions because it's a core Android platform feature.
Aggressive implementations register multiple account types with separate SyncAdapter pairs (e.g., a "regular" account and a "guest" account), doubling the execution surface. Requires GET_ACCOUNTS, AUTHENTICATE_ACCOUNTS, WRITE_SYNC_SETTINGS, and MANAGE_ACCOUNTS permissions, all of which are normal (auto-granted) on older API levels.
Mandrake used this technique to maintain periodic C2 communication.
Multi-Process Keep-Alive¶
A persistence pattern where the app runs multiple processes that monitor and resurrect each other. The manifest declares separate processes (e.g., :dpro1, :dpro2) for different service components. Each process binds to services in the other processes via AIDL IPC and polls them on a short interval (every 3-5 seconds). If any process dies, the surviving processes detect the broken binding and restart it.
Main process ←──AIDL bind──→ :dpro1 (polling service)
↑ ↑
└────AIDL bind────→ :dpro2 (watchdog)
↑
:dpro1 ←─bind──┘
The polling service in :dpro1 calls a pollingAction() method on the main process's bound service via IPC. If the call fails (process dead), a resurrection service re-launches it. The main process does the same for :dpro1. The :dpro2 process exists as a third watchdog monitoring both.
This mutual binding pattern makes the app very difficult to kill without force-stopping all processes simultaneously. Killing one process causes the others to immediately respawn it. The pattern appears in aggressive ad SDKs that need continuous background execution for push notification ads and scheduled ad displays.
Force-stop from Settings > Apps is the only reliable way to kill all processes at once, as it terminates all of an app's processes atomically.
Accessibility Service Persistence¶
An active accessibility service is managed by the system and automatically restarted if it crashes. As long as the user doesn't manually revoke the toggle in Settings, the service persists indefinitely across reboots.
This makes accessibility the most reliable persistence mechanism available without root. The malware can also use accessibility to prevent its own removal -- detecting when the user navigates to Settings > Apps and pressing "Back" or "Home" before they can reach the uninstall button.
Anti-Uninstall Techniques¶
Device Admin¶
Activating as a device administrator prevents uninstallation. The user must deactivate the admin first, but the malware can use accessibility to block navigation to the deactivation screen.
Cerberus combined device admin with accessibility: any attempt to open device admin settings triggers the accessibility service to press Home, making deactivation nearly impossible without ADB or safe mode.
Hiding from Launcher¶
Removing the launcher Activity from the manifest (or disabling the component at runtime) hides the app from the app drawer. The user can still find it in Settings > Apps, but most users won't think to look there.
PackageManager pm = getPackageManager();
pm.setComponentEnabledSetting(
new ComponentName(this, LauncherActivity.class),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
Joker, FluBot, and many RATs use this immediately after initial execution.
Firmware-Level Persistence¶
Pre-installed Malware¶
Triada achieved persistence by infecting the device firmware during manufacturing. The malware was embedded in the system partition (read-only at runtime), surviving factory resets and any user-level remediation. Only reflashing the firmware with a clean image removes it.
This represents the most resilient form of persistence on Android. Discovered in budget devices where supply chain compromise occurred at the factory or during distribution.
Root-Based System Installation¶
Pegasus and other state-sponsored malware use exploit chains to gain root, then install themselves as a system app in /system/app/ or /system/priv-app/. System apps persist across factory resets and receive elevated privileges. Short of reflashing the firmware, the malware is permanent.
Battery Optimization Exemption¶
Android's Doze mode and App Standby buckets restrict background execution. Malware requests exemption:
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
This shows a system dialog. Some families use accessibility to auto-tap "Allow" on this dialog. Others disguise the request behind a fake loading screen so the user doesn't realize what they're approving.
Device Protected Storage¶
Android 7.0 introduced Direct Boot and device protected storage -- a storage area accessible before the user unlocks the device for the first time after a reboot. By declaring android:directBootAware="true" and android:defaultToDeviceProtectedStorage="true" in the manifest, an app's SharedPreferences and files are stored in a credential-protected context that survives the locked state.
Malware uses this to ensure its configuration, C2 URLs, and operational state persist and are accessible immediately at boot, before the user enters their PIN/password. Combined with a BOOT_COMPLETED receiver (which fires during Direct Boot for direct-boot-aware apps), this enables malicious activity before the device is fully unlocked.
Detection: check for directBootAware="true" in the manifest, especially on components that don't have a legitimate reason for pre-unlock execution.
Launcher Icon Toggle¶
Activity-Alias Switching¶
Instead of disabling the launcher activity entirely (which can be reversed via pm enable), malware uses multiple activity-alias declarations pointing to the same target activity. One alias is the visible launcher entry; the other is a hidden replacement.
<activity-alias
android:name=".VisibleLauncher"
android:targetActivity=".MainActivity"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".HiddenLauncher"
android:targetActivity=".MainActivity"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.INFO" />
</intent-filter>
</activity-alias>
At runtime, the app enables the hidden alias (which uses INFO category instead of LAUNCHER, so it won't appear in the app drawer) and disables the visible one:
pm.setComponentEnabledSetting(
new ComponentName(this, "com.app.VisibleLauncher"),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(
new ComponentName(this, "com.app.HiddenLauncher"),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
The app disappears from the launcher but remains findable in Settings > Apps. The INFO category alias keeps the app discoverable by the system for deep links and intents, maintaining functionality while being invisible to the user.
MAX_INT Priority Receivers¶
BroadcastReceivers can declare android:priority up to Integer.MAX_VALUE (2147483647). Receivers with higher priority receive broadcasts before lower-priority ones and can abort ordered broadcasts. Malware uses this to:
- Preempt competing receivers: fire before any other app's receiver for system events like
BOOT_COMPLETEDorCONNECTIVITY_CHANGE - Monitor all app lifecycle events: register for
PACKAGE_ADDED,PACKAGE_REMOVED,PACKAGE_REPLACEDat maximum priority to track every app install/uninstall on the device - React to device state changes: monitor battery, storage, locale, wallpaper, and Bluetooth state changes to trigger context-aware behavior (e.g., show ads when the device is charging)
<receiver android:exported="true" android:priority="2147483647">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
Ad fraud SDKs use these to detect competing app installs and trigger ad displays. The receiver bodies are often empty stubs at build time, populated dynamically via remote configuration at runtime.
MessageQueue.IdleHandler¶
A low-visibility persistence technique. MessageQueue.IdleHandler callbacks execute whenever the main thread's message queue becomes idle (i.e., between UI events). Malware registers an IdleHandler to run code during these gaps:
Returning true keeps the handler registered permanently. This provides continuous opportunistic execution without timers, alarms, or services. The execution is invisible in process dumps and doesn't appear as a running service or scheduled job.
Hellodaemon Keep-Alive¶
HelloDaemon (com.xdandroid.hellodaemon) is an open-source Chinese keep-alive library designed to prevent Android from killing background processes. It combines multiple persistence mechanisms into a single SDK: JobScheduler with setPersisted(true), repeating AlarmManager alarms, BOOT_COMPLETED receivers, and cross-process daemon binding. If one mechanism is killed, the others detect it and restart.
The library uses a custom broadcast action (com.xdandroid.hellodaemon.CANCEL_JOB_ALARM_SUB) for internal coordination between its components.
HelloDaemon appears in aggressive adware, data collection SDKs, and push notification frameworks targeting Chinese OEM devices (Xiaomi, Huawei, Oppo, Vivo), which kill background apps more aggressively than stock Android. Detection: hellodaemon in class names, the custom broadcast action, or xdandroid package references.
FCM Silent Push Wake¶
Firebase Cloud Messaging provides a remote wake mechanism for apps killed by the system or swiped from recents. When an app registers for FCM, Google Play Services (not the app itself) maintains the push channel. A silent data message sent from the server causes GMS to deliver the message to the app, restarting its process if necessary.
FCM wake survives the system killing the process for memory pressure, swiping from recents, and app standby buckets. However, it does not survive force-stop: when a user explicitly force-stops an app via Settings > Apps, Android sets FLAG_STOPPED on the package, and FCM messages are dropped, not queued. The only way to resume delivery is for the user to manually launch the app again.
Despite the force-stop limitation, FCM is a reliable backup wake channel because most users close apps by swiping from recents (which does not set FLAG_STOPPED), not by navigating to Settings to force-stop. Adware families register for FCM from multiple apps, so a server push to any one app can trigger cross-app wake via broadcast or bound service connections.
OEM-Specific Persistence¶
Chinese OEMs (Xiaomi, Huawei, Oppo, Vivo) maintain their own autostart managers that independently restrict background apps. Even with RECEIVE_BOOT_COMPLETED and battery optimization disabled, these OEMs may kill the app unless it is whitelisted in their proprietary autostart list.
Malware targeting these regions often includes OEM-specific code that detects the manufacturer and launches the appropriate settings intent to guide (or force via accessibility) the user into whitelisting the app.
OEM Autostart Managers
When testing on Xiaomi, Huawei, Oppo, or Vivo devices, check for autostart whitelist entries under OEM-specific settings. Malware that works reliably in the wild on these devices has likely solved the OEM background-kill problem -- look for Build.MANUFACTURER checks and vendor-specific Intent actions in the decompiled code.
Platform Lifecycle¶
| Android Version | API | Change | Offensive Impact |
|---|---|---|---|
| 1.0 | 1 | BOOT_COMPLETED broadcast available |
Basic boot persistence from day one |
| 5.0 | 21 | JobScheduler introduced |
Persistent scheduled execution surviving process death |
| 8.0 | 26 | Background service limits | Services killed within minutes; foreground service with notification required |
| 8.0 | 26 | Implicit broadcast restrictions | BOOT_COMPLETED exempt, still delivered to manifest receivers |
| 10 | 29 | Background activity launch restrictions | Cannot start activities from background; use USE_FULL_SCREEN_INTENT or accessibility |
| 10 | 29 | Background location limits | Foreground service with location type required |
| 12 | 31 | Foreground service launch restrictions | Cannot start foreground service from background except via boot receiver, alarm, or accessibility |
| 12 | 31 | Exact alarm restrictions | SCHEDULE_EXACT_ALARM or USE_EXACT_ALARM required |
| 13 | 33 | POST_NOTIFICATIONS runtime permission required |
Social engineer the grant, or use silent channels created pre-upgrade |
| 14 | 34 | Foreground service type requirements | Must declare specific foreground service type in manifest |
| 15 | 35 | Further restrictions on foreground service types | Some types (e.g., dataSync) limited to 6 hours |
Each restriction pushed malware toward more creative solutions. The overall trend is layering multiple persistence methods so that at least one survives the increasingly aggressive background restrictions.
Layered Persistence
Modern banking trojans never rely on a single persistence mechanism. Expect to find at least two or three methods in any sample -- typically a boot receiver combined with a foreground service and accessibility service persistence. Disabling only one layer during analysis may give the false impression that the malware has been neutralized.
Persistence Method Comparison¶
| Method | Survives Reboot | Survives Force Stop | Stealth | Reliability | Min Android |
|---|---|---|---|---|---|
| Boot receiver | Yes | No | High | High | All |
| Foreground service | No | No | Low (notification) | High | 8+ |
| JobScheduler | Yes (persisted) | No | High | Medium | 5+ |
| WorkManager (self-rescheduling) | Yes | No | Very high | High | 5+ |
| AlarmManager | No | No | High | Medium | All |
| Sync adapter | Yes | No | High | Medium | All |
| FCM silent push | Yes | No (FLAG_STOPPED blocks) | Very high | High | All (requires GMS) |
| MessageQueue.IdleHandler | No | No | Very high | Low | All |
| Multi-process keep-alive | No | No (but self-resurrects) | High | High | All |
| Accessibility service | Yes | Yes (if enabled) | Medium | Very high | 4.1+ |
| Device admin | N/A (anti-uninstall) | N/A | Low | High | All |
| Device protected storage | Yes (pre-unlock) | N/A (data survival) | High | High | 7+ |
| System app / firmware | Yes | Yes | Very high | Permanent | All |
Families by Persistence Strategy¶
| Family | Primary Persistence | Secondary | Anti-Uninstall |
|---|---|---|---|
| Triada | Firmware | System app | Factory reset resistant |
| Pegasus | Root + system install | Multiple | Survives factory reset |
| SpyNote | Foreground service | Boot receiver | Hides from launcher |
| Anubis | Boot receiver | Foreground service | Device admin |
| Cerberus | Accessibility | Boot receiver | Device admin + accessibility block |
| Joker | JobScheduler | Boot receiver | Hides from launcher |
| Hook | Foreground service | Boot receiver + accessibility | Device admin |
| FluBot | Boot receiver | Foreground service | Hides from launcher, accessibility block |
| Mandrake | Sync adapter | Boot receiver | Hides from launcher |
| GodFather | Accessibility | Foreground service | Accessibility block |
Detection During Analysis¶
Static Indicators
RECEIVE_BOOT_COMPLETEDin manifest with aBroadcastReceiverFOREGROUND_SERVICEwithIMPORTANCE_MINorIMPORTANCE_NONEnotification channelsDeviceAdminReceiverdeclared in manifestSyncAdapterandAccountAuthenticatorXML metadatasetComponentEnabledSetting()calls targeting launcher activityREQUEST_IGNORE_BATTERY_OPTIMIZATIONSin manifest
Dynamic Indicators
- Service immediately started after boot broadcast received
- Notification channel created with empty name or minimal importance
- Device admin activation prompt shown shortly after install
- Navigation to autostart manager or battery optimization settings via intent
- Accessibility service preventing navigation to app management screens