Android 可保护用户免受恶意应用的侵害,并提供值得信赖的界面体验。Activity 安全框架包含规则和平台限制。这些规则和限制可防止不必要的界面中断、任务劫持和其他安全威胁。这些威胁与应用组件何时以及如何显示在屏幕上有关。此框架的一个关键组件限制了从后台启动 activity。
后台 activity 启动限制
当不在前台、没有可见 activity 的应用或从其他应用收到的 PendingIntent 尝试启动新 activity 时,就会发生后台 activity 启动 (BAL)。这是后台 activity 启动 (BAL)。
虽然存在正当的使用情形(例如闹钟应用启动时),但不受限制的 BAL 会导致用户体验不佳,并产生安全漏洞。
为什么会受到限制?
自 Android 10(API 级别 29)起,平台对应用何时可以从后台启动 activity 施加了限制。这些保护措施有助于防范恶意应用行为,并通过缓解常见滥用行为(包括以下行为)来改善用户体验:
- 界面劫持和弹出式广告:某个后台应用在用户当前正在互动的应用之上意外启动一个 activity(通常是广告),从而劫持用户的会话。
- 钓鱼式攻击和冒充:某个后台应用启动一个冒充其他应用的 activity(例如,某个合法应用的虚假登录界面),以窃取用户凭据。这通常是通过“Activity Sandwich”攻击实现的,即在合法应用的任务堆栈中插入恶意 activity。
- 点按劫持:一个后台应用在另一个应用上显示透明或模糊的 activity,以拦截用户的点按操作,并诱骗用户执行意外的操作。
- 应用唤醒:一个应用的后台组件唤醒另一个应用的前台组件,以非法提升日活跃用户数指标。
前台服务(用于持续性任务)
如果您的应用需要在后台执行用户需要了解的长时间运行的任务(例如播放音乐或跟踪锻炼),则应使用前台服务。前台服务必须显示用户无法关闭的持久性通知。此通知可以提供互动式控件(例如音乐应用的播放/暂停按钮)。这样一来,用户可以随时了解情况并掌控局面,但不会因全屏活动而受到打扰。
遵循此层次结构(从标准通知开始,仅在必要时升级到更具侵入性的选项),您可以为用户打造更好、更可预测的体验。
允许执行后台 activity 的情况(例外情况)
如果满足以下任一条件,应用可以从后台启动 activity:
- 该应用具有可见窗口,例如在前台运行的 Activity。
- 应用是当前的输入法 (IME)。
- activity 是从系统发送的
PendingIntent(例如,从通知点按)启动的。 - 应用已获得用户授予的
SYSTEM_ALERT_WINDOW权限。 - 应用已获得
START_ACTIVITIES_FROM_BACKGROUND权限。 - 应用受已获准启动后台 activity 的服务绑定。
- 启动由设备的启动器应用发起,例如当用户点按应用图标或与 widget 互动时。
- 启动来自必须始终运行的操作系统核心部分,例如 Telephony 服务启动来电屏幕。
新的安全加固和必需的选择启用功能
为了进一步增强安全性,Android 引入了更严格的规则,要求使用 PendingIntent 和 IntentSender 启动 activity 的应用必须明确选择启用。只有在创建 PendingIntent 的应用或发送 PendingIntent 的应用选择启用以授予其后台启动特权时,才允许启动。
在大多数情况下,发送 PendingIntent 的应用应选择启用,因为该应用通常是用户直接与之互动的应用(例如,点按按钮)。
发送方必须选择启用 PendingIntent
如果您的应用以 Android 14(API 级别 34)或更高版本为目标平台,则在发送 PendingIntent 时,系统不再默认授予其 BAL 权限。如果您未明确选择启用,则 activity 启动可能会被阻止,除非 PendingIntent 的创建者已授予其自己的权限。
为确保启动成功,发送方应选择启用,通过调用 ActivityOptions.setPendingIntentBackgroundActivityStartMode() 来授予其权限,推荐模式为 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE(在 SDK 36 中添加)。
这是一种更严格、更安全的模式。仅当发送应用在发送 PendingIntent 时在屏幕上可见时,才会授予权限。这样可以更有效地确保 activity 启动是用户与应用互动直接引发的结果。
使用 ActivityOptions.setPendingIntentBackgroundActivityStartMode() 授予权限。
// Sender Side
ActivityOptions options = ActivityOptions.makeBasic()
.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE);
try {
myPendingIntent.send(options.toBundle());
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "The PendingIntent was canceled", e);
}
// Sender Side
val options = ActivityOptions.makeBasic().apply {
pendingIntentBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE
}
try {
myPendingIntent.send(options.toBundle())
} catch (e: PendingIntent.CanceledException) {
Log.e(TAG, "The PendingIntent was canceled", e)
}
创作者必须选择启用 PendingIntent
如果您的应用以 Android 15(API 级别 35)或更高版本为目标平台,那么创建 PendingIntent 的应用不再默认授予其后台启动权限。如需允许发送者使用您应用的 BAL 权限,您必须明确选择启用。
使用 ActivityOptions.setPendingIntentCreatorBackgroundActivityStartMode() 授予权限。
// Creator Side
Intent intent = new Intent(context, MyActivity.class);
ActivityOptions options = ActivityOptions.makeBasic().setPendingIntentCreatorBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
PendingIntent pendingIntent = PendingIntent.getActivity(context, REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE, options.toBundle());
// Creator Side
val intent = Intent(context, MyActivity::class.java)
val options = ActivityOptions.makeBasic().apply {
pendingIntentCreatorBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
}
val pendingIntent = PendingIntent.getActivity(context, REQUEST_CODE, intent,
PendingIntent.FLAG_IMMUTABLE, options.toBundle())
使用 IntentSender 启动
使用 IntentSender 启动 activity 时,也适用相同的 BAL 限制。由于 IntentSender 是通过 PendingIntent.getIntentSender 获取的,因此也需要遵循类似的选择加入要求。
- 从 Android 14(API 34)开始,使用 Context.startIntentSender() 需要在发送方选择启用。您还必须在此处提供
ActivityOptions软件包。
ActivityOptions options = ActivityOptions.makeBasic()
.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
context.startIntentSender(myIntentSender, fillInIntent, flagsMask,
flagsValues, extraFlags, options.toBundle());
val options = ActivityOptions.makeBasic().apply {
pendingIntentBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
}
context.startIntentSender(myIntentSender, fillInIntent, flagsMask,
flagsValues, extraFlags, options.toBundle())
- 从 Android 17(API 37 及更高级别)开始,使用 IntentSender.sendIntent() 需要在发送方选择启用。
ActivityOptions options = ActivityOptions.makeBasic()
.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
myIntentSender.sendIntent(context, code, intent, onFinished, handler,
requiredPermission, options.toBundle());
val options = ActivityOptions.makeBasic().apply {
pendingIntentBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
}
myIntentSender.sendIntent(context, code, intent, onFinished, handler,
requiredPermission, options.toBundle())
- 使用 ActivityResultLauncher
:此 AndroidX API 在内部使用 Context.startIntentSender(),因此会受到 BAL 限制的影响。
序列图:BAL 限制
此图展示了使用 PendingIntent 安全启动 Activity 的过程。成功启动取决于有效的权限链,其中至少有一个参与的应用授予了其权限,并且能够从后台启动 activity
- 创建和委托(应用 A - 创建者)
- Creator 应用构建
PendingIntent - 如果目标 SDK 为 35 及更高版本,创建者必须使用 setPendingIntentCreatorBackgroundActivityStartMode() 明确委托其 BAL 权限,才能使用这些权限。默认情况下,系统不会委托任何权限。
- 然后,
PendingIntent会传递到另一个应用(应用 B)
- Creator 应用构建
- 启动和贡献(应用 B - 发送者)
- 稍后,发送方应用通过调用
PendingIntent.send()来启动。 - 如果以 SDK 34 及更高版本为目标平台,发送方必须使用 setPendingIntentBackgroundActivityStartMode() 明确贡献自己的权限,才能使用自己的权限。默认情况下,系统不会委托任何权限。
- 稍后,发送方应用通过调用
- Android 系统安全验证
- Android 系统会拦截启动请求并执行安全检查。
- 它会评估两个条件:
- 创作者是否已委托其权限,并且创作者应用目前是否满足一般 BAL 例外情况之一?
- 发件人是否贡献了其权限,并且发件人应用目前是否满足一般 BAL 例外情况之一?
- 结果
- 允许:如果满足第 3 步中的至少一个条件,则验证权限链。Android 系统会启动目标 activity,而发送方会收到成功结果。
- BLOCKED:如果两个条件均不满足,系统会阻止启动。 发送方应用不会收到指示失败的直接返回值或异常。相反,Android 系统会在内部向 Logcat 记录“Background activity launch blocked!”消息,开发者必须检查该消息以进行调试。
任务劫持防范
为防止任务内劫持攻击(例如“Activity Sandwich”),Android 15 针对以 API 级别 37 或更高版本为目标平台的应用引入了新规则。
- 规则 1:在单个任务中,只有属于同一应用(即具有相同 UID)的 activity 才能启动任务中当前最顶层的 activity。
- 规则 2:只有前台任务中与最上层 activity 的 UID 匹配的 activity 才能创建新任务或将其他现有任务带到前台。
开发者选择启用任务内保护措施
此行为可从目标 SDK 37 开始启用,您必须明确选择启用。它旨在防止任务内劫持(或 Activity Sandwiching),即恶意应用可能会在您的应用的任务中启动 activity,以冒充您的应用并窃取用户数据。
启用保护功能
如需选择启用 ASM,请在 AndroidManifest.xml 文件中将 android:allowCrossUidActivitySwitchFromBelow 属性设置为 false。这是一项应用级设置,默认情况下会保护应用中的所有 activity。
为特定活动创建例外情况
如果您已为应用启用此功能,但需要允许其他应用启动特定的受信任 activity,则可以创建有针对性的例外情况。如需将单个 Activity 从此保护中排除,请在该 Activity 的 onCreate() 方法中调用 setAllowCrossUidActivitySwitchFromBelow(true)。这样,您就可以启动该 activity,同时保持应用的其他部分受到保护。
问题排查
使用正则表达式过滤 Logcat 以查找相关消息。标记 ActivityTaskManager 经常使用,按 ActivityTaskManager 过滤有助于隔离日志。
了解关键日志消息
启动被阻止(错误):此消息表示 activity 启动被阻止。
- 含义:由于发送者(以 SDK 34 及更高版本为目标平台)或创建者(以 SDK 35 及更高版本为目标平台)缺少必要的 PendingIntent 选择启用,因此拒绝启动 activity。
- 操作:您必须更新代码,以包含正确的 ActivityOptions 选择启用。
分析日志时,请检查以下字段:
- realCallingPackage:发送 PendingIntent 的应用。这是发件人。
- callingPackage:创建 PendingIntent 的应用。这是创作者。
严格模式
从 Android 16 开始,应用开发者可以启用严格模式,以便在 activity 启动被阻止时(或在应用的目标 SDK 提高时有被阻止的风险时)收到通知。
以下示例代码展示了如何在 Application、Activity 或其他应用组件的 Application.onCreate() 方法中尽早启用该功能:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectBlockedBackgroundActivityLaunch()
.penaltyLog()
.build());
)
}