活动安全性

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 引入了更严格的规则,要求使用 PendingIntentIntentSender 启动 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 启动是用户与应用互动直接引发的结果。

待处理 intent 表
图 1:启动后台 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())
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())

序列图:BAL 限制

待处理 intent 表
图 2:使用 PendingIntent 安全启动 activity 的过程

此图展示了使用 PendingIntent 安全启动 Activity 的过程。成功启动取决于有效的权限链,其中至少有一个参与的应用授予了其权限,并且能够从后台启动 activity

  1. 创建和委托(应用 A - 创建者)
    1. Creator 应用构建 PendingIntent
    2. 如果目标 SDK 为 35 及更高版本,创建者必须使用 setPendingIntentCreatorBackgroundActivityStartMode() 明确委托其 BAL 权限,才能使用这些权限。默认情况下,系统不会委托任何权限。
    3. 然后,PendingIntent 会传递到另一个应用(应用 B)
  2. 启动和贡献(应用 B - 发送者)
    1. 稍后,发送方应用通过调用 PendingIntent.send() 来启动。
    2. 如果以 SDK 34 及更高版本为目标平台,发送方必须使用 setPendingIntentBackgroundActivityStartMode() 明确贡献自己的权限,才能使用自己的权限。默认情况下,系统不会委托任何权限。
  3. Android 系统安全验证
    1. Android 系统会拦截启动请求并执行安全检查。
    2. 它会评估两个条件:
    3. 创作者是否已委托其权限,并且创作者应用目前是否满足一般 BAL 例外情况之一?
    4. 发件人是否贡献了其权限,并且发件人应用目前是否满足一般 BAL 例外情况之一?
  4. 结果
    1. 允许:如果满足第 3 步中的至少一个条件,则验证权限链。Android 系统会启动目标 activity,而发送方会收到成功结果。
    2. 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 启动被阻止。

分析日志时,请检查以下字段:

  • 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());
     )
 }