手机频繁弹出广告,已经成为不少安卓用户的烦恼。这些粗制滥造的广告,通常在用户正常使用手机时弹出,霸占锁屏,占据桌面,覆盖正常使用的应用程序,甚至严重影响用户对手机的正常使用。
图 形形色色的后台弹窗广告
rameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
的
shouldAbortBackgroundActivityStart
函数中,该函数先是对调用app进行一些判断,如果满足条件,则直接允许后台弹窗。这里以Android 12代码为例,列觉一些重要条件:
-
对最重要的ROOT_UID、SYSTEM_UID、NFC_UID允许后台弹窗
final int callingAppId = UserHandle.getAppId(callingUid); 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;
}
上面这些if分支判断完毕后, 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;
由此可见,安卓系统的后台弹窗限制实现在一系列的 if语句中,有着许许多多的例外条件,因此容易出现漏洞,被黑灰产恶意app绕过。 03 被黑灰产广泛使用的绕过手法特征 通过对后台弹窗恶意App进行分析,OPPO子午安全实验室和安第斯智能护盾团队发现了一些广为使用的后台弹窗手法,这些手法曾经利用了安卓系统的原生漏洞。
3.1 CVE-2020-0500:利用输入法设置不安全的PendingIntent 在 InputMethodManagerService.java
中存在着不安全的PendingIntent,用于设置输入法,并没有设置FLAG_IMMUTABLE, 该系统身份的PendingIntent可以通过系统APIActivityManager.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));
恶意App 获得这个不安全的PendingIntent之后,将base Intent的包名修改为指向恶意App自己。由于这是系统产生的PendingIntent,代表了SYSTEM_UID的身份和权限,因此在调用 PendingIntent.send 时就可以绕过后台弹窗的限制。 下面是一个利用CVE-2020-0500的后台弹窗恶意样本截图,要实现后台弹窗,恶意样本在AndroidManifest文件声明了很多Activity处理action为 android.settings.INPUT_METHOD_SETTINGS 的Intent过滤器。尽管并非输入法应用,样本却在AndroidManifest.xml中配置了大量这样的Activity,用于接收劫持来的系统Intent,弹出各式各样的广告。
漏洞利用代码在样本中进行了加固。但运行样本,可以观察到样本利用CVE-2020-0500的动态特征,以uid 1000 SYSTEM_UID的身份,启动自身,且Intent action为 android.settings.INPUT_METHOD_SETTINGS ,包名为样本自身。
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
Android系统通过设置PendingIntent.FLAG_IMMUTABLE进行修复,但是恶意app在修复后的手机中尝试漏洞利用,仍然可以拉起系统输入法设置界面,可能对用户造成一定的困扰。 3.2 CVE-2021-39758:利用VirtualDisplay创造“可见”窗体 恶意App创建虚拟显示VirutualDisplay,在该VirutalDisplay上显示一个Presentation,然后延时启动弹窗线程
Presentation其实就是一个自定义的在非主屏上显示的Dialog,而弹窗线程就是简单的打开Activity
这里后后台弹窗成功的原因在于,VirutualDisplay上创建的窗体尽管无法被用户看到,但系统却误认为存在可见窗体从而满足了后台弹窗的放行条件。
3.3 CVE-2022-20197:利用system_server Pacel缓存缺陷 该漏洞在Google修复之前,就被各种恶意App广泛应用于后台弹窗当中。其利用手法如下: (1)创建通知并发送,通知中的content Intent被设置用于后台弹窗。通知发送过后,立即取消通知,使用户感知不到通知的存在。
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);
}
(2)紧接着使用定时器进行延时启动,使用同一个PendingIntent。注意这里 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);
}
这里延时启动Activity,在后台也可以成功。经调试可发现,原因在于,这里定时器使用的PendingIntent携带后台启动token。
$ 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;
}
漏洞形成的原因在于Android系统服务的缓存机制,恶意App申请在通知中使用的PendingIntent对象,system_server NotificationManagerService使用完毕后将其放入Parcel缓存池。后续恶意App继续申请在定时器中使用时,system_server AlarmManagerService将其从Parcel缓存池中取出,仍然携带通过mClassCookie传入的后台启动token,因此可以后台弹窗成功。 除了绕过原生安卓系统的安全限制实现后台弹窗,我们也发现许多黑灰产恶意App还具备针对手机厂商的定制弹窗手法,这些手法只能在某一品牌的手机中成功弹窗。这表明,猖獗的弹广告行为背后,存在着一只支撑黑灰产牟利的技术研究团队,也在持续地研究安卓系统和手机厂商所存在的后台弹窗漏洞,并编写利用exp、制作成弹广告sdk,为恶意App所使用。 04 OPPO安全的应对
4.1 安第斯智能护盾在行动 安第斯智能护盾是OPPO自研、行业首创的应用全生命周期的安全隐私防护系统,全方位识别并拦截应用在上架、下载、安装、启动、运行、升级、卸载等各阶段的风险,为用户打造安全可靠的应用使用环境。针对上述后台弹窗手法,目前OPPO安第斯智能护盾已经集成相应的检测规则予以识别并在手机终端上线拦截功能,对利用后台弹窗漏洞的恶意App进行打击。 安第斯智能护盾在对影响用户使用的恶意后台弹窗应用进行了全方位的跟踪处理:
发现:关注商店评论数据,利用NLP算法自动识别出用户反馈的高可疑恶意弹窗应用; 检测:深度分析恶意弹窗手法,自研引擎,对后台弹窗识别率高达95%以上; 取证:构建应用运行沙箱集群,对高可疑应用进行养殖,对恶意后台弹窗进行自动取证; 处置:对检测到的恶意后台弹窗应用高效联动处置,软件商店下架、浏览器下载拦截、手机管家提示卸载以及应用智能管控; 打击:构建开发者知识图谱,挖掘恶意应用背后的黑灰产团伙,对恶意开发者禁止上架应用和使用法律手段进行打击;
在2023年,经智能护盾发现同步下架恶意应用519款,封禁高风险开发者130家,拦截恶意弹窗类应用10亿次, 并且应用智能管控每天拦截1500W次以上的恶意后台弹窗行为, 此类问题的客诉较去年下降80%。
4.2 子午安全实验室的努力 作为专注互联网业务安全攻防研究的专业团队,OPPO子午安全实验室也对后台弹窗恶意App样本进行了深入分析,提取了恶意App绕过安卓系统安全限制的手法,一方面同步给安第斯智能护盾团队转化为检测特征,另一方面,也告知Google进行修复,通过攻防研究推动安卓生态的净化。前述CVE-2022-20197就是一个被恶意App广泛利用的0day漏洞,基于安第斯智能护盾对恶意样本的捕获,OPPO子午实验室首先发现了漏洞利用特征和安卓系统中的根因,向Google通报,Google迅速对漏洞进行了修复,并对OPPO子午实验室进行了致谢。 对于后台弹窗这种漏洞类型,尽管绕过了Android系统后台startActivity的限制,但由于无法直接获取用户隐私,Google之前一直将此类漏洞评级为中危,这意味着漏洞只能在最新版本的安卓手机中修复,但不会将patch应用到所有版本。这也给恶意App带来了可乘之机,给手机厂商全版本修复漏洞、彻底解决问题制造了门槛。 但经过子午安全实验室的技术分析以及用户数据展示,目前Google已经认识到后台弹窗漏洞被恶意App在野利用的现状以及对安卓生态口碑的破坏力,提升此类漏洞的评级为高危,修复补丁将发布到最近三年的安卓大版本当中。同时,最近的安卓大版本也一直在加强对后台startActivity的限制,在系统层面引入了诸多安全限制,后台弹窗漏洞被恶意App广泛利用的态势已经得到了有效抑制。 05 建议 尽管在系统层面,后台弹窗漏洞已进行了修复。在产品层面,安第斯智能护盾也构建了应用安全的防护体系来保障用户的使用体验,但是“道高一尺,魔高一丈”,绕过后台弹窗弹广告还有许多结合社会工程学,并非漏洞利用的方式,例如欺骗用户开启悬浮窗权限。同时,恶意开发者为了牟取暴利不断变异样本逃避检测、四处作恶,这加大了检测的难度。比如:
-
快速生成多款“换皮”应用,形成广撒网; -
恶意应用之间进行相互推广,一旦中招就会安装多款恶意应用; -
恶意应用植入手机后隐藏图标,很难被发现; -
恶意应用进行云端控制,在特定地区触发后台弹窗行为,逃避取证;
-
使用Andoid 10及以后版本的安卓手机,及时更新补丁,这样可以防止恶意App利用安卓系统的后台弹窗漏洞进行弹广告; -
不要轻易向陌生应用开启输入法、壁纸、悬浮窗等权限,因为这些权限能够使恶意App不受限制地弹出广告; -
定期使用系统自带手机管家进行全盘扫描,及时清理病毒应用; -
定期对已安装的应用进行梳理,清理不常用或者不是主动下载的应用。
最新动态
原文始发于微信公众号(OPPO安全应急响应中心):恶意App后台弹窗技术手法分析