Android FileProvider特性与Intent重定向漏洞

Posted Tr0e

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android FileProvider特性与Intent重定向漏洞相关的知识,希望对你有一定的参考价值。

文章目录

前言

FileProvider 是 android 7.0 出现的新特性,它是 ContentProvider 的子类,可以通过创建一个 Content URI 并赋予临时的文件访问权限来代替 File URI 实现文件共享。Intent 重定向漏洞能够使得攻击者借助受害者 APP 的身份发送一个恶意 Intent,从而达到恶意攻击的目的,比如获得原本无权访问的 FileProvider 的访问权限。

分区存储

了解 FileProvider 之前先了解下 Android 10.0 提出的分区存储机制,虽然分区存储机制晚于 FileProvider 出现,但二者均是 Android 系统对应用数据的访问控制保护机制。

分区存储是 Android 10 开始引进的 Android 系统存储管理机制,它允许 App 读取和写入 App 自身创建的文件而不需要任何存储权限。Android 系统根据存储位置的不同,可以分为内部内部存储和外部存储,内部存储就不用多说了,而外部存储又分为私有空间和公共空间。

Android 外部存储空间(sdcard)中数据存储可以分为两大类:

分类路径特点
私有存储 (Private Storage)/sdcard/Android/data/packageName1)每个应用在内部存储种都拥有自己的私有目录,其它应用看不到,彼此也无法访问到该目录;2)私有目录存放 app 的私有文件,会随着 App 的卸载而删除。
共享存储 (Shared Storage)/sdcard/Downloads(Pictures)1)除了私有存储以外,其他的一切都被认定是共享存储,比如:Downloads、Documents、Pictures 、DCIM、Movies、Music 等;2)公有目录下的文件不会跟随 APP 卸载而被删除。

在 Android 10 以前,只要程序获得了 READ_EXTERNAL_STORAGE 权限,就可以随意读取外部的存储公有目录;同时只要程序获得了WRITE_EXTERNAL_STORAGE权限,就可以随意在写入外部存储的公有目录上新建文件或文件夹。

于是 Google 在 Android 10 中提出了分区存储,意在限制程序对外部存储中公有目录的使用,分区存储对内部存储私有目录和外部存储私有目录都没有影响。

Android 10/11

为了避免混乱,先来总结下 Android 10 分区存储机制带来的数据访问的特点和区分:

Android 11 增进

分区存储机制很好地规范了 Android App 的存储行为,让它们读自己该读的,写自己该写的。但是有的应用天生就需要对 SD 卡进行全方位的访问,比如各种文件浏览器、垃圾清理软件等等,虽然很多所谓的垃圾清理软件本身就是最该被清理的垃圾……

对此,Android 11 引入了一个新的权限:

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

有了这个权限,就可以跟以前的版本一样随意玩耍了。那么是不是可以直接申请这个权限就可以了呢?机智如我,是可以的,不过应用市场不让上架…所以大部分 App 是不允许使用这个权限的。

同时如果要申请此权限,需要打开设置界面,让用户手动设置:

intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivityForResult(intent, 10010)

出现的授权界面长这样:

共享存储空间

分区存储最大的影响就是外部存储空间的共享目录不再可以被 APP 随意访问了。共享目录下的文件需要通过 MediaStore API 或者 Storage Access Framework 方式访问:

  1. MediaStore API 在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请任何存储权限,所有者拥有文件的所有权;
  2. MediaStore API 访问其他应用在共享目录创建的媒体文件(图片、音频、视频),需要申请存储权限;
  3. MediaStore API 不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt 等), 只能够通过 Storage Access Framework 方式访问,调用 Storage Access Framework API 会启动系统的文件选择器向用户申请操作指定的文件。

MediaStore API

Android 系统会自动扫描外部存储空间,将媒体文件按类型添加到系统预定义的 Images、Videos、Audio files、Downloaded files 集合中。Android Q 通过 MediaStore.Images、MediaStore.Video、MediaStore.Audio、MediaStore.Downloads 访问相对应共享目录文件资源。预定义集合所对应的目录如下表所示:

Storage Access Framework

Android 10 里唯一一种访问其他应用创建的非媒体文件的途径是使用存储访问框架 (Storage Access Framework) 提供的文档选择器。SAF 访问外部存储空间的共享目录的文件不需要申请任何权限,原因很简单,它需要启动系统的文件选择器向用户申请操作指定的文件,有用户交互过程的动作本身就是权限管控了,自然也就可以不用预先申请权限。

下表总结了分区存储如何影响文件访问:

更多分区存储的知识可以参见博文:Android 分区存储适配总结

FileProvider

Android 7.0 之前,文件的 Uri 以 file:/// 形式提供给其他 app 访问,Android 7.0 之后,分享文件的 Uri 发生了变化。为了提高私有目录的安全性,防止应用信息的泄漏,从 Android 7.0 开始,私有目录的访问权限被限制。开发人员不再能够简单地通过 file://URI 访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。

官方提供了替代方案——FileProvider,FileProvider 生成的 Uri 会以 content:// 的形式分享给其他 app 使用。content 形式的 Uri 可以让其他 app 临时获得读取 (Read) 和写入 (Write) 权限,只要我们在创建 Intent 时,使用 Intent.setFlags() 添加权限,那么只要接收 Uri 的 app 在接收的 Activity 任务栈中处于活动状态,添加的权限就会一直有效,直到 app 被任务栈移除。

在 Android 7.0 以前,为了访问 file:/// 形式的 Uri,我们必须修改文件的权限。修改后的权限对所有 app 都是有效的,这样的行为是不安全的。 而使用了 FilePrrovider 后 content:// 形式的 Uri 让 Android 的文件系统更安全,对于分享的文件,接收方 app 只拥有临时的权限,减少了我们 app 内部的文件被其他 app 恶意操作的行为。

作为四大组件之一的 ContentProvider,一直扮演着应用间共享资源的角色。这里我们要使用到的 FileProvider,就是 ContentProvider 的一个特殊子类,帮助我们将访问受限的 file://URI 转化为可以授权共享的 content://URI

提供FileProvider

FileProvider 的完整使用步骤:

  1. Mainfest.xml 中注册 FileProvider;
  2. 配置、指定共享目录范围 ;
  3. 使用 FileProvider 生成 Content URI;
  4. 给 Uri 授予临时权限并分享这个 URI 给另一个 App。

下面来根据一个 Github 的实例程序 android-file-provider-demo 学习下 FileProvider 的使用,该项目包含两个简易 APK(FileProvider 和 FileReceiver),分别用于提供 FileProvider 和接收 FileProvider 提供的数据。

先来看下提供 FileProvider 的程序。

1、创建 FileProvider

在 manifest 文件中添加 pvodier 标签,配置如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.demo.fileprovider">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
            android:authorities="com.demo.fileprovider"
            android:name="android.support.v4.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/paths" />
        </provider>
    </application>
</manifest>

以上 FileProvider 的配置说明:

属性说明
android:nameAndroid 提供的 FileProvider 的实现类
android:authorities相当于一个用于认证的暗号,在分享文件生成 Uri 时,会通过它的值生成对应的 Uri,该值是一个域名,一般格式为 “包名.fileprovider”
android:exported必须指定为 false,表示该 FileProvider 只能本应用使用,不是 public 的
android:grantUriPermissions值为true,表示允许赋予临时权限,即设置为共享
meta-data子标签指定配置共享目录的配置文件

2、设置共享目录

res/xml 中创建一个资源文件(如果 xml 目录不存在,先创建),名字随便(一般叫 file_paths.xml)。

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

在 paths 节点内部支持以下几个子节点,分别为:

子节点含义
root-path代表设备的根目录 new File("/")
files-path代表 APP 内部存储空间私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径
cache-path代表内部存储的 cache 目录,与 Context.getCacheDir() 获取的路径对应
external-path代表外部存储 (sdcard) 的根目录,与 Environment.getExternalStorageDirectory() 获取的路径对应。
external-files-path代表外部存储空间 APP 私有目录下的 files/ 目录,与 Context.getExternalFilesDir(null)获取的路径对应
external-cache-path外部存储空间 APP 私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir()
external-media-path代表 app 外部存储媒体区域的根目录,与Context.getExternalMediaDirs()获取的路径对应

3、使用 FileProvider 生成 Content URI

先看下该部分完整的 MainActivity 代码:

package com.demo.fileprovider;

import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Intent;
import android.net.Uri;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.UUID;

public class MainActivity extends AppCompatActivity 

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

    public void provide(View view) 
        String content = "Hello FileProvider! ".concat(String.valueOf(System.currentTimeMillis()));
        File file = new File(getFilesDir(), UUID.randomUUID().toString().concat(".txt"));
        if (!writeFile(file, content)) 
            return;
        
        Uri uri = FileProvider.getUriForFile(this, "com.demo.fileprovider", file);
        Intent intent = new Intent().setClassName("com.demo.filereceiver", "com.demo.filereceiver.MainActivity");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        ClipData clipData = new ClipData(new ClipDescription("Meshes", new String[]ClipDescription.MIMETYPE_TEXT_URILIST), new ClipData.Item(uri));
        intent.setClipData(clipData);
        startActivity(intent);
    

    private boolean writeFile(File file, String content) 
        FileOutputStream stream = null;
        try 
            if (!file.exists()) 
                boolean created = file.createNewFile();
                if (!created) 
                    return false;
                
            
            stream = new FileOutputStream(file);
            stream.write(content.getBytes());
            stream.flush();
            stream.close();
            return true;
         catch (IOException e) 
            Log.e("provider", "IOException writing file: ", e);
         finally 
            try 
                if (stream != null) 
                    stream.close();
                
             catch (IOException e) 
                Log.e("provider", "IOException closing stream: ", e);
            
        
        return false;
    

顺带附上视图文件 activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.demo.fileprovider.MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Provide"
        android:onClick="provide"/>
</RelativeLayout>

前两步已经将共享目录的配置工作全部完成了,此处需要继续将原先所采用的 file:// 替换成 FileProvoider 需要用到的 content://,这就需要用到 FileProvider.getUriForFile() 方法了:

File file = new File(getFilesDir(), UUID.randomUUID().toString().concat(".txt"));
Uri uri = FileProvider.getUriForFile(this, "com.demo.fileprovider", file);

应注意到其中 “com.demo.fileprovider” 参数就是 FileProvider 中设置的 authorities 属性值。

4、给 Uri 授予临时权限并分享这个 URI 给另一个 App

当我们生成一个 content:// 的 Uri 对象之后,第三方应用其实还无法对其直接使用,还需要对这个 Uri 接收的 App 赋予对应的权限才可以。

授权方式有两种,上面的方法属于第一种,实现代码:

Intent intent = new Intent().setClassName("com.demo.filereceiver", "com.demo.filereceiver.MainActivity");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
ClipData clipData = new ClipData(new ClipDescription("Meshes", new String[]ClipDescription.MIMETYPE_TEXT_URILIST), new ClipData.Item(uri));
intent.setClipData(clipData);
startActivity(intent);

使用 intent 对象提供的 setClipData() 方法可以一次性传递多个 URI 对象,然后使用 setFlags() 或者 addFlags() 方法设置读写权限,可选常量值选择 FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION。这种形式的授权方式,权限有效期截止至其它应用所处的堆栈销毁,并且一旦授权给某一个组件后,该应用的其它组件拥有相同的访问权限。

另外一种授权的方法是通过 Context 的 grantUriPermission() 方法授权:

//三个参数分别表示授权访问 URI 对象的其他应用包名,授权访问的 Uri 对象和授权类型常量值
context.grantUriPermission(String toPackage, Uri uri, int modeFlags)
//对应的撤销权限的接口如下
//context.revokeUriPermission(Uri uri, int modeFlags);

拥有授予权限的 Content URI 后,便可以通过 startActivity() 方法启动其他应用并传递授权过的 Content URI 数据。

访问FileProvider

以上已经提供了 FileProvider,点击 Buntton 即可发送携带 FileProvider 读写权限的 Intent 给到 com.demo.filereceiver 应用,接收方的代码如下:

package com.demo.filereceiver;

import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import java.io.IOException;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity 

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

        TextView textView = (TextView) findViewById(R.id.textView);

        Intent intent = getIntent();
        ClipData clipData = intent.getClipData();
        if (clipData.getItemCount() == 1) 
            ClipData.Item item = clipData.getItemAt(0);
            Uri uri = item.getUri();
            String content = readUri(uri);
            if (content == null) 
                textView.setText("Error reading Uri ".concat(uri.toString()));
             else 
                textView.setText(content);
            
        
    

    private String readUri(Uri uri) 
        InputStream inputStream = null;
        try 
            inputStream = getContentResolver().openInputStream(uri);
            if (inputStream != null) 
                byte[] buffer = new byte[1024];
                int result;
                String content = "";
                while ((result = inputStream.read(buffer)) != -1) 
                    content = content.concat(new String(buffer, 0, result));
                
                return content;
            
         catch (IOException e) 
            Log.e("receiver", "IOException when reading uri", e);
         finally 
            if (inputStream != null) 
                try 
                    inputStream.close();
                 catch (IOException e) 
                    Log.e("receiver", "IOException when closing stream", e);
                
            
        
        return null;
    

以上程序最终将访问到 URI 格式如下的文件:

content://com.demo.fileprovider/shared_files/xxx.txt

更多 FileProvider 特性的使用方式请参见博文:Android 7.0 行为变更 通过FileProvider在应用间共享文件吧

Intent重定向

最后进入本文的重点——Intent 重定向漏洞,该类型的攻击模式(漏洞)主要的危害为:访问不可导出的组件与越权访问 FileProvider。以下内容来自:Android App安全之Intent重定向详解

访问不可导出的组件

首先回顾下什么是可导出组件,导出组件一般有以下三种形式:

<

以上是关于Android FileProvider特性与Intent重定向漏洞的主要内容,如果未能解决你的问题,请参考以下文章

Android FileProvider特性与Intent重定向漏洞

android打开与分享文件

android打开与分享文件

android打开与分享文件

android打开与分享文件

Android中ContentProvider和FileProvider有啥区别