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.
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.
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);
AccountManager Sync Adapter¶
An underused but effective persistence method. The malware registers as a sync adapter for a custom account type. Android's sync framework periodically triggers the adapter, providing reliable execution without visible notifications.
The sync adapter runs in its own process and benefits from the system's built-in retry and scheduling logic. Mandrake used this technique to maintain periodic C2 communication.
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.
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.
Android Mitigations¶
| Restriction | Version | Impact | Malware Workaround |
|---|---|---|---|
| Background service limits | Android 8+ | Services killed within minutes | Foreground service with notification |
| Background location limits | Android 10+ | Location only while app is visible | Foreground service with location type |
| Foreground service launch restrictions | Android 12+ | Cannot start foreground service from background | Boot receiver, alarm, or accessibility event as trigger |
| Exact alarm restrictions | Android 12+ | SCHEDULE_EXACT_ALARM required |
Use setAndAllowWhileIdle() (inexact) or request permission |
| Notification permission | Android 13+ | POST_NOTIFICATIONS required (runtime permission) |
Social engineer the grant, or use silent notification channels created pre-upgrade |
| Background activity launch restrictions | Android 10+ | Cannot start activities from background | USE_FULL_SCREEN_INTENT or accessibility performGlobalAction |
| Foreground service type requirements | Android 14+ | Must declare foreground service type in manifest | Declare appropriate type or use alternative persistence |
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+ |
| AlarmManager | No | No | High | Medium | All |
| Sync adapter | Yes | No | High | Medium | All |
| Accessibility service | Yes | Yes (if enabled) | Medium | Very high | 4.1+ |
| Device admin | N/A (anti-uninstall) | N/A | Low | High | All |
| 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