一、漏洞概要

在5月1日,微软发文披露了有关“DirtyStream”(脏流)攻击漏洞的技术细节。脏流攻击的攻击模式与路径遍历相关,可以实现任意文件读取,通过特殊方法可以实现任意文件覆盖,从而可以完成任意代码执行以及凭证窃取。该漏洞源于对安卓内容提供商系统的不当使用,该系统管理对旨在在不同应用程序之间共享的结构化数据集的访问。目前已验证的几个存在该漏洞的APP,在谷歌商城的安装量超过40亿。

二、产生原因

该漏洞的主要产生原因在于开发者不正确的实现内容提供商Android 提供的一个称为内容提供程序的组件),导致引入漏洞,从而绕过应用程序主目录中的读/写限制。

2.1 FileProvider

为了提高私有目录的安全性,防止应用信息的泄漏,从 Android 7.0 开始,应用私有目录的访问权限被做限制。具体表现为,开发人员不能够再简单地通过 file:// URI 访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。新的访问方式由FileProvider取代。

谷歌开发指南对FileProvider的介绍如下:"FileProvider ContentProvider ( 有助于 App 共享文件更加安全的组件 )的一个特殊的子类,通过 content:// Uri 获取一个文件而不是 file:/// Uri。 当你创建一个包含有 content URIIntent 的时候,Content URI赋予你临时的读写权限。为了可以给一个目标app( 原文client app )发送一个特定的content URI,你也可以通过调用Intent.setFlags() 去添加权限,这些权限一直有效,只要接收的 Activity 在栈中还存活着。要是跳转到Service,只要这个Service在运行,权限就有效。相比较而言,用file:/// Uri来获取文件,你不得不修改系统底层的文件权限.在你改变文件权限之前,你授予权限,对于任何app都是可用的,所以授予这个级别的权限从根本上是不安全的(就是liunx的rwx)。content URI提高了文件安全访问的级别,使FileProvider成为Android的安全体系重要组成部分。"

可谓是成也FileProvider,败也FileProvider。看似更安全的FileProvider,却是导致此漏洞“罪魁祸首”。

2.2 漏洞产生

FileProvider简言之就是一个用于在安卓设备上已安装的程序间进行数据共享的一个特殊子类。在正确的使用情况下,不会造成安全隐患或漏洞。但是当使用应用程序不验证它(FileProvider)收到的文件的内容时,事情将会变的不可控。例如当FileProvider使用服务应用程序提供的文件名将接收到的文件缓存在使用应用程序的内部数据目录中,如果服务应用程序实现自己的恶意版本的 FileProvider,则它可能会导致使用应用程序覆盖关键文件。实现流程图如下:

在上图 中,左侧的恶意应用创建了一个显式 Intent,该 Intent 以右侧共享目标的文件处理组件为目标,并附加一个内容 URI 作为 intent 的额外内容。然后,它使用 startActivity API 调用将此意向发送到共享目标。大多数存在漏洞的共享目标遵循以下特定的步骤:

  1. 从远程文件提供程序请求实际文件名

  2. 使用此文件名初始化随后用于初始化文件输出流的文件

  3. 使用传入内容 URI 创建输入流

  4. 将输入流复制到输出流

由于流氓应用程序控制文件的名称和内容,因此通过盲目信任此输入,共享目标可能会覆盖其私有数据空间中的关键文件,这可能会导致严重后果。

2.3 实例演示

在V1-210567版本的小米文件管理器(谷歌商城版)中,存在该漏洞。虽然在目前最新的V1-210593版本中已修复该漏洞,但之前的漏洞版本仍然可以作为漏洞研究。

该程序具有对外部存储的完全访问权限,以及其他诸多权限

比较特殊的是,它还提供了一个垃圾文件清理插件以及连接到远程 FTP 和 SMB 共享的能力

该应用导出了 CopyFileActivity,这是 com.android.fileexplorer.activity.FileActivity 的活动别名,用于处理从文件复制到的文件操作

由于此活动是导出的,因此可以由同一设备上安装的任何应用程序触发,方法是使用明确的操作意图 SEND SEND_MULTIPLE 并附加与文件流对应的内容 URI。

在收到这样的意图后,浏览器会执行有效性检查,但是该检查存在问题

如上所述,initCopyOrMoveIntent 方法调用 checkValid 方法,将内容 URI 作为参数传递(步骤 1 和 2)。但是,checkValid 方法旨在处理文件路径,而不是内容 URI。它始终为内容 URI 返回 true。相反,更安全的做法是将字符串解析为 URI,包括确保方案是预期值(在本例中为文件,而不是内容)。checkValid 方法通过使用传入字符串作为 File 类构造函数的参数初始化文件对象,并将其规范路径与与应用程序主目录对应的路径进行比较(步骤 3 和 4)来验证复制或移动操作不会影响应用的专用目录。给定一个内容 URI 作为路径,File 构造函数对其进行规范化(遵循 Unix 文件系统规范化),因此 getCanonicalPath 方法返回一个以“/content:/”开头的字符串,该字符串将始终通过有效性检查。更具体地说,应用对远程内容提供程序执行 _size_display_name _data 列的查询(请参阅下面的第 48 行)。然后,它使用这些行返回的值来初始化 com.android.fileexplorer.mode.c 类的对象的字段

假设从外部文件提供程序返回的 _display_name _data 值是目标目录的相对路径,则从上述方法退出后,这些类字段将包含如下所示的值:


如上所示,此文件模型的路径(变量 d 和 e)指向应用程序主目录中的文件,因此附加到传入意图的文件流将写入特定位置。

此外,还有之前提到的该应用程序使用插件来清理设备的垃圾文件。当应用程序加载此插件时,它使用两个本机库:libixiaomifileu.so,它从 /data/app 目录中获取,libixiaomifileuext.so 从主目录中获取:


由于应用对 /data/app 文件夹没有写入权限,因此无法替换存储在其中的 libixiaomifileu.so 文件。执行代码的最简单方法是将 libixiaomifileuext.so 替换为恶意代码。 但是,尝试这样做会失败,因为在这种特殊情况下,该漏洞只能用于在主目录中写入新文件,而不能覆盖现有文件。 因此需要确定应用程序如何加载 libixiaomifileu.so

经研究发现,在应用程序加载此库之前,它遵循以下步骤

  1. 计算文件 libixiaomifileu.so 的哈希值,位于 /data/app 目录中

  2. 将此哈希值与分配给“libixiaomifileu.so_hm5”字符串的值进行比较,该字符串是从com.mi.android.globalFileexprorer_preferences.xml文件中提取的

  3. 如果值不匹配,请在主目录的 /files/lib 路径中搜索 libixiaomifileu.so 文件

  4. 如果在那里找到该文件,请计算其哈希值,并再次将其与 shared_preferences 文件夹中的值进行比较

  5. 如果哈希值匹配,则使用 System.load 方法加载 /files/lib 下的文件

鉴于此行为,为了使用文件管理器的用户 ID 执行代码,攻击者必须执行以下步骤:

  1. 使用路径遍历漏洞将恶意库另存为 /files/lib/libixiaomifileu.so(该文件尚不存在该目录,因此覆盖不是问题)

  2. 计算此库的哈希值以替换libixiaomifileu.so_hm5字符串的值

  3. 使用显式意图触发垃圾清理器插件,因为加载本机库的活动是导出的

但是此处的第2点与前文相悖,目前无法覆盖已存在的文件。为了解决该问题,需要使用到SharedPreferences类的实现。当 Android 应用程序使用 getSharedPreferences API 方法检索 SharedPreferences 类的实例时,将共享首选项文件的名称作为参数,然后 SharedPreferencesImpl 类的构造函数执行以下步骤

  1. 使用提供给 getSharedPreferences 方法的名称创建新的文件对象,后跟 .xml 扩展名,后跟 .bak 扩展名

  2. 检查此文件是否存在,如果存在,请删除原始 xml 文件并将其替换为在第一步中创建的文件

通过这种行为,我们能够将com.mi.android.globalFileexprorer_preferences.xml.bak保存在共享首选项文件夹下(因为在应用程序运行时它不太可能存在),因此当应用程序尝试验证哈希时,原始 xml 文件已经被我们自己的副本替换。在此之后,通过使用单个意图启动垃圾清理器插件,我们能够诱骗应用程序加载恶意库而不是 /data/app 文件夹下的库,并使用浏览器的用户 ID 执行代码。

同时,由于该应用程序还具有使用 FTP 和 SMB 协议连接到远程文件共享的功能,并且用户凭据以明文形式保存在 /data/data/com.mi.android.globalFileexplorer/files/rmt_i.properties 文件中

如果第三方应用能够利用此漏洞并获取代码执行,则攻击者可以检索这些凭据。然后,影响将进一步扩大,因为当用户请求打开远程共享时,浏览器会创建目录 /sdcard/Android/data/com.mi.android.globalFileexplorer/files/usbTemp/,用于保存用户检索的文件:


这意味着远程攻击者将能够读取或写入本地网络的 SMB 共享文件,前提是设备已连接到该共享。FTP共享也是如此,因为它们的处理方式完全相同。

综上所述,可以得出攻击小米文件管理器的完整流程

步骤 1 中,用户打开一个恶意应用程序,该应用程序可能伪装成文件编辑器、消息传递应用程序、邮件客户端或任何一般应用程序,并要求用户保存文件。当用户尝试保存此类文件时,无论他们选择哪种目标路径来保存它,恶意应用程序都会强制文件浏览器应用程序将其写入其内部 /files/lib 文件夹下。然后,恶意应用程序可以使用明确的意图(不需要用户交互)启动垃圾清理程序,这将导致使用浏览器的 ID 执行代码(步骤2)。在步骤 3 中,攻击者使用任意代码执行功能从 rmt_i.properties 文件中检索 SMB 和 FTP 凭据。随后,攻击者现在可以跳转到步骤 5 并使用被盗的凭据直接访问共享。或者,在检索共享凭据后,移动设备可以连接到本地网络(步骤 4)并访问 SMB 或 FTP 共享,从而允许攻击者通过 /sdcard/Android/data/com.mi.android.globalFileexplorer/files/usbTemp/ 文件夹访问共享文件(步骤 5)。

三、影响范围

微软公开的受影响的app如下:

小米文件管理器  ≤ V1-210567

WPS Office  < 17.0.0

 

其他受影响的app以及版本暂未可知。

四、修复措施

为了防止这些问题,可以使用以下方法来避免该漏洞:

  1. 在处理其他应用程序发送的文件流时,最安全的解决方案是在缓存接收的内容时完全忽略远程文件提供程序返回的名称。例如使用随机生成的名称,因此即使传入流的内容格式不正确,它也不会篡改应用程序。

  2. 如果方法1不可行,开发人员需要采取额外的步骤来确定缓存的文件已写入专用目录。由于传入的文件流通常由内容 URI 标识,因此第一步是可靠地识别和清理相应的文件名。除了筛选可能导致路径遍历的字符外,在执行任何写入操作之前,开发人员还必须通过执行对 File.getCanonicalPath 的调用并验证返回值的前缀来验证缓存文件是否在专用目录中。

  3. 另一方面需要注意的是开发人员尝试从内容 URI 中提取文件名的方式。开发人员通常使用 Uri.getLastPathSegment(),它返回最后一个路径 URI 段的 (URL) 解码值。攻击者可以在此段中使用 URL 编码字符(包括用于路径遍历的字符)构建 URI。使用返回的值缓存文件可能会再次使应用程序容易受到此类攻击。