Android移动安全第九章_PendingIntent安全
移动安全
PendingIntent 是 Android 中一种延迟执行的 Intent 包装机制。它允许一个 App 把自己的操作权限"委托"给其他 App 或系统,在未来某个时刻代为执行。这个委托机制如果处理不当,就是权限提升的入口。
> 系列目录: > > 1. Android 组件导出安全 > 2. Android Intent 安全 > 3. Android Binder 服务安全 > 4. Android ContentProvider 安全 > 5. Android WebView 安全 > 6. Android UI 欺骗与钓鱼 > 7. Android Deep Link 安全 > 8. Android 广播安全 > 9. Android PendingIntent 安全(本章) > 10. Android 系统设置安全 > 11. Android SSRF 与网络安全 > 12. Android 加密与数据存储安全 > 13. Android 认证与证书校验 > 14. Android Zip Slip 路径遍历 > 15. Android Fragment Injection > 16. Android SELinux 与沙箱机制 - - - - - - 1. 前言 ----- 前面几章讲的 Intent、广播、组件导出,都是"直接调用"——调用方自己发出 Intent,系统根据调用方的身份和权限决定是否放行。PendingIntent 不一样,它是"间接调用"。 PendingIntent 的工作方式是:App A 创建一个 PendingIntent,把它交给 App B(或系统)。当 B 在未来某个时刻触发这个 PendingIntent 时,系统以 A 的身份和权限来执行其中的 Intent,而不是以 B 的身份。 这个机制在 Android 中使用非常广泛: - 通知(Notification):App 创建通知时,把点击后要执行的操作包装成 PendingIntent 交给系统通知服务。用户点击通知时,系统代替 App 执行这个操作 - 闹钟(AlarmManager):App 设置定时任务时,把到时间后要执行的操作包装成 PendingIntent 交给 AlarmManager - 桌面小部件(AppWidget):小部件的点击事件通过 PendingIntent 传递给 App 处理 问题在于:如果 App A 创建的 PendingIntent 中的 Intent 是"空的"或"不完整的",拿到这个 PendingIntent 的 App B 可以往里面填充内容,然后触发执行——执行时用的是 A 的身份和权限。这就是 PendingIntent 劫持的基本原理。 - - - - - - 2. PendingIntent 基础 ------------------- ### 2.1 创建方式 PendingIntent 有三种创建方式,分别对应三种组件操作: ```java // 启动 Activity PendingIntent pi = PendingIntent.getActivity(context, requestCode, intent, flags); // 启动 Service PendingIntent pi = PendingIntent.getService(context, requestCode, intent, flags); // 发送广播 PendingIntent pi = PendingIntent.getBroadcast(context, requestCode, intent, flags); ``` 参数说明: - context:创建者的上下文,决定了 PendingIntent 执行时使用谁的身份 - requestCode:请求码,用于区分同一个 App 创建的不同 PendingIntent - intent:要执行的 Intent - flags:控制 PendingIntent 行为的标志位 ### 2.2 Flags flags 参数对安全性影响很大: | Flag | 含义 | |---|---| | FLAG\_IMMUTABLE | PendingIntent 创建后不可修改(Android 12+ 默认要求) | | FLAG\_MUTABLE | PendingIntent 创建后可以被修改 | | FLAG\_UPDATE\_CURRENT | 如果已存在相同的 PendingIntent,更新其中的 Intent extras | | FLAG\_CANCEL\_CURRENT | 如果已存在相同的 PendingIntent,取消旧的,创建新的 | | FLAG\_ONE\_SHOT | PendingIntent 只能被使用一次 | FLAG\_MUTABLE 和 FLAG\_IMMUTABLE 是安全方面最需要关注的。FLAG\_MUTABLE 意味着拿到 PendingIntent 的一方可以修改其中 Intent 的内容(通过 `send(Context, int, Intent)` 方法传入额外的 Intent 来填充)。 ### 2.3 身份委托机制 PendingIntent 的核心特性是身份委托。用一个例子说明: ```java // App A(系统签名 App,UID 1000)创建 PendingIntent Intent intent = new Intent(this, InternalActivity.class); PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE); // 把 pi 通过某种方式传给 App B(普通 App,UID 10086) ``` 当 App B 调用 `pi.send()` 时,系统以 App A 的身份(UID 1000)执行 Intent。即使 InternalActivity 是 App A 的未导出组件,也能被成功启动——因为执行者的身份是 A 自己。 这就是为什么 PendingIntent 是权限提升的入口:普通 App 拿到系统 App 创建的 PendingIntent 后,可以借用系统 App 的身份执行操作。 - - - - - - 3. PendingIntent 劫持 ------------------- ### 3.1 隐式 Intent 的 PendingIntent 最经典的 PendingIntent 劫持场景是:创建者使用隐式 Intent(不指定目标组件)来构造 PendingIntent。 ```java // 不安全:隐式 Intent 的 PendingIntent Intent intent = new Intent("com.example.ACTION_PROCESS"); intent.putExtra("token", "secret_token_123"); PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_MUTABLE); ``` 这个 PendingIntent 被触发时,系统会把广播发给所有注册了 `com.example.ACTION_PROCESS` 的 Receiver。攻击者注册一个匹配的 Receiver 就能收到广播,获取其中的 token。 更严重的是,如果 PendingIntent 是 FLAG\_MUTABLE 的,攻击者拿到它之后可以修改 Intent 的目标: ```java // 攻击者拿到 PendingIntent 后 Intent fillIntent = new Intent(); fillIntent.setClassName("com.victim.app", "com.victim.app.InternalActivity"); pendingIntent.send(context, 0, fillIntent); // 以受害 App 的身份启动其内部组件 ``` ### 3.2 空 Intent 的 PendingIntent 比隐式 Intent 更危险的是空 Intent。有些开发者创建 PendingIntent 时传入一个空的 Intent,打算后续再填充: ```java // 极度不安全:空 Intent PendingIntent pi = PendingIntent.getActivity(context, 0, new Intent(), PendingIntent.FLAG_MUTABLE); ``` 拿到这个 PendingIntent 的任何一方都可以完全控制 Intent 的内容——action、component、data、extras 全部可以填充。执行时使用的是创建者的身份和权限。 ### 3.3 通知中的 PendingIntent 泄露 通知是 PendingIntent 最常见的使用场景,也是泄露的高发区。App 创建通知时,把 PendingIntent 交给系统的 NotificationManager。如果通知可以被其他 App 读取(通过 NotificationListenerService),其中的 PendingIntent 就可能被提取出来。 NotificationListenerService 是 Android 提供的一个系统服务,允许 App 在用户授权后监听所有通知。用户在"设置 → 通知访问权限"中授权后,App 就能读取所有通知的内容,包括其中的 PendingIntent。 ```java // 监听通知的 App public class NotifListener extends NotificationListenerService { @Override public void onNotificationPosted(StatusBarNotification sbn) { Notification notification = sbn.getNotification(); PendingIntent contentIntent = notification.contentIntent; if (contentIntent != null) { // 拿到了目标 App 的 PendingIntent // 如果是 FLAG_MUTABLE 的,可以修改后执行 try { Intent fillIntent = new Intent(); fillIntent.setClassName("com.victim.app", "com.victim.app.InternalSettingsActivity"); contentIntent.send(context, 0, fillIntent); } catch (PendingIntent.CanceledException e) { // PendingIntent 已被取消 } } } } ``` ### 3.4 BroadcastReceiver 中的 PendingIntent 有些 App 通过广播传递 PendingIntent,让接收方在处理完某些逻辑后回调。如果广播是隐式的,攻击者可以注册 Receiver 截获广播,拿到其中的 PendingIntent: ```java // 受害 App 发送包含 PendingIntent 的广播 Intent callbackIntent = new Intent(this, CallbackReceiver.class); PendingIntent callback = PendingIntent.getBroadcast(this, 0, callbackIntent, PendingIntent.FLAG_MUTABLE); Intent broadcastIntent = new Intent("com.example.ACTION_REQUEST"); broadcastIntent.putExtra("callback", callback); sendBroadcast(broadcastIntent); // 隐式广播,任何 App 都能收到 ``` 攻击者收到广播后,提取 PendingIntent 并修改后执行: ```java public class AttackReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { PendingIntent callback = intent.getParcelableExtra("callback"); if (callback != null) { Intent fill = new Intent(); fill.setClassName("com.victim.app", "com.victim.app.AdminActivity"); try { callback.send(context, 0, fill); } catch (PendingIntent.CanceledException e) {} } } } ``` - - - - - - 4. FLAG\_MUTABLE 与 FLAG\_IMMUTABLE ---------------------------------- ### 4.1 可变性的影响 PendingIntent 的可变性(mutability)决定了它被传递给其他方后是否可以被修改。 FLAG\_MUTABLE 的 PendingIntent,接收方可以通过 `send(Context, int, Intent)` 传入一个 fillIntent 来修改原始 Intent 的内容。fillIntent 中设置的字段会覆盖或补充原始 Intent: ```java // 创建者:FLAG_MUTABLE,Intent 指定了 action 但没指定 component Intent intent = new Intent("com.example.ACTION_VIEW"); PendingIntent pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE); // 接收方:通过 fillIntent 补充 component Intent fillIntent = new Intent(); fillIntent.setComponent(new ComponentName("com.victim.app", "com.victim.app.SecretActivity")); pi.send(context, 0, fillIntent); // 最终执行的 Intent:action=ACTION_VIEW, component=SecretActivity ``` FLAG\_IMMUTABLE 的 PendingIntent 则不允许修改。接收方调用 `send()` 时传入的 fillIntent 会被忽略(extras 除外——如果原始 Intent 中没有设置某个 extra key,fillIntent 中的对应 key 仍然会被合并进去)。 ### 4.2 什么时候需要 FLAG\_MUTABLE 有些场景确实需要 FLAG\_MUTABLE: - 内联回复通知(Direct Reply):系统需要把用户输入的文本填充到 PendingIntent 的 extras 中 - 与 `AlarmManager.setExact()` 配合使用时,某些系统版本要求 PendingIntent 可变 - 需要和 `PendingIntent.FLAG_UPDATE_CURRENT` 配合更新 extras 但大多数场景下,FLAG\_IMMUTABLE 就够了。如果不确定,优先使用 FLAG\_IMMUTABLE。 ### 4.3 Android 12 的强制要求 Android 12(API 31)开始,创建 PendingIntent 时必须显式指定 FLAG\_MUTABLE 或 FLAG\_IMMUTABLE,否则会抛出异常: ```php java.lang.IllegalArgumentException: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent. ``` 这个改动强制开发者思考 PendingIntent 是否需要可变。但它不能阻止开发者为了省事直接加 FLAG\_MUTABLE。 - - - - - - 5. 实际攻击场景 --------- ### 5.1 LaunchAnywhere LaunchAnywhere 是 PendingIntent 劫持的一个经典利用模式,最早在 Android 系统的 AccountManagerService 中被发现。 AccountManagerService 在处理添加账户的流程中,会向第三方 Authenticator App 发送一个 PendingIntent(通过 AccountAuthenticatorResponse)。如果 Authenticator 返回的结果中包含一个 KEY\_INTENT,AccountManagerService 会用自己的身份(system\_server,UID 1000)启动这个 Intent。 攻击流程: 1. 恶意 App 注册一个自定义的 AccountAuthenticator 2. 调用 `AccountManager.addAccount()` 触发添加账户流程 3. 系统调用恶意 App 的 Authenticator 4. Authenticator 在返回结果中放入一个指向系统内部组件的 Intent 5. AccountManagerService 以 system 身份启动这个 Intent 这个漏洞的本质是:系统服务信任了第三方 App 提供的 Intent,并以自己的高权限身份执行。后续 Android 版本通过检查返回的 Intent 的目标组件是否属于调用者来修复了这个问题。 ### 5.2 通知劫持提权 一个更常见的场景:系统 App 创建通知时使用了 FLAG\_MUTABLE 的 PendingIntent,且 Intent 不够具体(没有指定 component)。 ```java // 系统 App 创建通知(不安全的写法) Intent intent = new Intent("com.system.ACTION_SETTINGS"); PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE); Notification notification = new Notification.Builder(this, channelId) .setContentTitle("系统更新") .setContentText("点击查看详情") .setContentIntent(pi) .build(); notificationManager.notify(1, notification); ``` 拥有 NotificationListenerService 权限的恶意 App 可以: 1. 监听到这条通知 2. 提取其中的 PendingIntent 3. 用 fillIntent 修改目标为系统内部的敏感 Activity 4. 以系统 App 的身份启动 ### 5.3 Widget 点击劫持 桌面小部件(AppWidget)的点击事件通过 PendingIntent 实现。AppWidgetProvider 在 `onUpdate()` 中为小部件的各个 View 设置 PendingIntent: ```java RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout); Intent intent = new Intent(context, WidgetClickHandler.class); PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_MUTABLE); views.setOnClickPendingIntent(R.id.widget_button, pi); ``` 如果 PendingIntent 是 FLAG\_MUTABLE 的,且 Intent 不够具体,Launcher App(桌面启动器)理论上可以修改 PendingIntent 的内容。虽然主流 Launcher 不会这么做,但自定义 Launcher 或恶意 Launcher 可能利用这一点。 - - - - - - 6. 版本演进 ------- ### 6.1 Android 6.0(API 23) 引入了 `PendingIntent.getCreatorPackage()` 和 `PendingIntent.getCreatorUid()` 方法,允许接收方查询 PendingIntent 的创建者信息。但这些信息仅供参考,不能作为安全校验的依据——恶意 App 可以创建 PendingIntent 后传递给其他 App,接收方看到的创建者是恶意 App,但执行时用的身份也是恶意 App 的,所以这个信息本身不构成安全问题。 ### 6.2 Android 12(API 31) 强制要求指定 FLAG\_MUTABLE 或 FLAG\_IMMUTABLE。这是 PendingIntent 安全方面最重要的一次改动。 同时,Android 12 还限制了 FLAG\_MUTABLE 的 PendingIntent:即使是 mutable 的,fillIntent 也不能覆盖原始 Intent 中已经设置的 component 和 action。只有原始 Intent 中未设置的字段才能被填充。 ```java // Android 12+:原始 Intent 已设置 component Intent intent = new Intent(); intent.setComponent(new ComponentName("com.example", "com.example.MyActivity")); PendingIntent pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE); // 接收方尝试覆盖 component —— 在 Android 12+ 上不生效 Intent fill = new Intent(); fill.setComponent(new ComponentName("com.victim", "com.victim.Secret")); pi.send(context, 0, fill); // 最终执行的仍然是 com.example.MyActivity ``` 这个限制大幅降低了 FLAG\_MUTABLE PendingIntent 的攻击面,但没有完全消除——extras 仍然可以被填充。 ### 6.3 Android 14(API 34) 进一步收紧了 PendingIntent 的安全限制: - 对后台启动 Activity 的限制更严格,通过 PendingIntent 从后台启动 Activity 需要创建者在创建时显式授权(通过 `ActivityOptions.setPendingIntentBackgroundActivityStartMode()`) - 对 PendingIntent 的 sender 身份校验更严格 - - - - - - 7. 演示 ----- 下面用配套的演示 App(com.demo.pendingintentsecurity)来展示 PendingIntent 劫持的效果。 Demo App 模拟了一个常见场景:App 发送通知时,PendingIntent 指向一个中转 Activity(DispatchActivity),该 Activity 根据 extras 中的 `target_class` 决定跳转目标。原始 Intent 没有设置 `target_class`,正常情况下跳转到默认页面。但因为 PendingIntent 使用了 FLAG\_MUTABLE,攻击者可以通过 fillIntent 注入 `target_class` extra,控制跳转到任意内部组件。 ### 7.1 创建不安全的通知 VulnNotificationActivity 创建通知时使用了 FLAG\_MUTABLE 的 PendingIntent: ```java // 显式 Intent 指向 DispatchActivity,但 FLAG_MUTABLE Intent intent = new Intent(this, DispatchActivity.class); // 没有设置 target_class extra PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); ``` DispatchActivity 根据 extras 决定跳转: ```java String targetClass = getIntent().getStringExtra("target_class"); if (targetClass != null) { Intent intent = new Intent(); intent.setClassName(getPackageName(), targetClass); startActivity(intent); // 跳转到 target_class 指定的组件 } else { startActivity(new Intent(this, MainActivity.class)); // 默认跳转 } ``` 通过 ADB 发送通知: ```bash adb shell am start -n com.demo.pendingintentsecurity/.VulnNotificationActivity ``` logcat 输出: ```php W VulnNotification: 通知已发送 W VulnNotification: PendingIntent: 显式 Intent → DispatchActivity, FLAG_MUTABLE W VulnNotification: 原始 Intent 未设置 target_class,正常跳转到默认页面 W VulnNotification: 但 FLAG_MUTABLE 允许 fillIntent 注入 target_class extra ``` ### 7.2 执行劫持 HijackDemoActivity 模拟攻击者的行为——获取到通知中的 mutable PendingIntent 后,通过 fillIntent 注入 `target_class` extra: ```java // 获取到与通知中相同的 PendingIntent Intent originalIntent = new Intent(this, DispatchActivity.class); PendingIntent pi = PendingIntent.getActivity(this, 0, originalIntent, PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_NO_CREATE); // 构造 fillIntent,注入 target_class Intent fillIntent = new Intent(); fillIntent.putExtra("target_class", "com.demo.pendingintentsecurity.InternalSecretActivity"); pi.send(context, 0, fillIntent); ``` 通过 ADB 触发劫持: ```bash adb shell am start -n com.demo.pendingintentsecurity/.HijackDemoActivity \ --ez auto_start true ``` logcat 输出: ```php W HijackDemo: 成功获取到已有的 mutable PendingIntent W HijackDemo: 劫持成功:注入 target_class=InternalSecretActivity W HijackDemo: DispatchActivity 将跳转到未导出的内部组件 W DispatchActivity: 收到 target_class: com.demo.pendingintentsecurity.InternalSecretActivity W DispatchActivity: 已跳转到: com.demo.pendingintentsecurity.InternalSecretActivity W InternalSecret: InternalSecretActivity 被通过 PendingIntent 劫持启动 W InternalSecret: 内部机密页面已打开,敏感数据已暴露 ``` InternalSecretActivity 是 exported=false 的内部组件,正常情况下外部无法启动。但攻击者通过 fillIntent 向 mutable PendingIntent 注入了 `target_class` extra,DispatchActivity 读取后跳转到了内部机密页面:  如果这个 PendingIntent 使用 FLAG\_IMMUTABLE,fillIntent 中的 extras 不会被合并到原始 Intent 中(原始 Intent 未设置的 key 除外的规则在 Android 12+ 上有变化,但 FLAG\_IMMUTABLE 从根本上阻止了修改),攻击就无法成功。 - - - - - - 8. 总结 ----- PendingIntent 的安全问题围绕"身份委托"展开。创建者把自己的执行权限包装进 PendingIntent 交给其他方,如果 PendingIntent 是可变的且 Intent 不够具体,接收方就能借用创建者的身份执行任意操作。 回顾一下: - PendingIntent 以创建者的身份执行,不是触发者的身份 - FLAG\_MUTABLE 允许接收方修改 Intent 内容,是劫持的前提 - 隐式 Intent 或空 Intent 的 PendingIntent 攻击面最大 - 通知、广播回调、Widget 是 PendingIntent 泄露的常见渠道 - Android 12 强制要求声明 mutability,并限制了 fillIntent 对已设置字段的覆盖 - Android 14 进一步限制了通过 PendingIntent 从后台启动 Activity 下一章讲 Android 系统设置安全。Settings.System、Settings.Secure、Settings.Global 这三个命名空间存储了设备的各种配置,部分设置项的读写权限控制不够严格,可能被第三方 App 利用来修改设备行为。
发表于 2026-05-20 09:00:25
阅读 ( 1149 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
bReaK_1
15 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!