Microsoft 在多个流行的 Android 应用程序中发现了与路径遍历相关的漏洞模式,该漏洞模式可能使恶意应用程序覆盖易受攻击应用程序主目录中的文件。此漏洞模式的影响包括任意代码执行和令牌盗窃,具体取决于应用程序的实现。任意代码执行可以为威胁参与者提供对应用程序行为的完全控制。同时,令牌盗窃可以为威胁行为者提供对用户帐户和敏感数据的访问权限。

我们在 Google Play 商店中发现了几个易受攻击的应用程序,这些应用程序的安装量超过 40 亿次。我们预计该漏洞模式可以在其他应用程序中找到。我们分享这项研究,以便开发者和发行商可以检查他们的应用是否存在类似问题,酌情修复,并防止将此类漏洞引入新应用或版本。随着所有平台的威胁不断演变,安全研究人员、安全供应商和更广泛的安全社区之间的行业协作对于提高所有人的安全性至关重要。Microsoft 仍然致力于与安全社区合作,共享漏洞发现和威胁情报,以跨平台保护用户。

发现此问题后,我们发现了几个易受攻击的应用程序。作为负责任披露政策的一部分,我们通过 Microsoft 安全漏洞研究 (MSVR) 的协调漏洞披露 (CVD) 通知应用程序开发人员,并与他们合作解决该问题。我们要感谢 Xiaomi, Inc. 和 WPS Office 安全团队调查并修复了此问题。截至 2024 年 2 月,已为上述应用程序部署了修复程序,建议用户保持其设备和已安装的应用程序是最新的。

认识到更多应用可能会受到影响,我们与 Google 合作,在 Android 开发者网站上发布了一篇文章,在高可见度的位置提供指导,以帮助开发者避免将这种漏洞模式引入其应用中,从而提高开发者对该问题的认识。我们还要感谢 Google 的 Android 应用安全研究团队在解决此问题方面的合作。

在这篇博文中,我们通过对漏洞模式的总体概述,然后重点介绍 Android 共享目标,继续提高开发者和用户的意识,因为它们最容易受到此类攻击。我们通过一个实际的代码执行案例研究,展示了超出移动设备范围的影响,甚至可能影响本地网络。最后,我们为用户和应用程序开发人员提供指导,并说明协作对于提高所有人安全性的重要性。

概述:Android 上的数据和文件共享

Android 操作系统通过为每个应用程序分配自己的专用数据和内存空间来强制隔离。为了便于数据和文件共享,Android 提供了一个称为内容提供程序的组件,该组件充当接口,用于以安全的方式管理数据并将其公开给其他已安装的应用程序。如果使用得当,内容提供商可提供可靠的解决方案。但是,不正确的实现可能会引入漏洞,从而绕过应用程序主目录中的读/写限制。

Android 软件开发工具包 (SDK) 包括 FileProvider 类,该类是 ContentProvider 的子类,可在已安装的应用程序之间共享文件。需要与其他应用程序共享其文件的应用程序可以在其应用清单中声明 FileProvider,并声明要共享的特定路径。

每个文件提供程序都有一个称为“权限”的属性,该属性在系统范围内标识它,并且可以由使用者(想要访问共享文件的应用)用作地址的一种形式。这种基于内容的模型与 Web 模型非常相似,但消费者不是使用 http 方案,而是使用内容方案和权限,然后是他们想要访问的文件的伪路径。

例如,假设应用程序 com.example.server 共享 file:///data/data/com.example.server/files/images 目录下的某些文件,而这些文件之前已使用名称 shared_images 声明为共享则使用者可以使用 content://[authority]/shared_images/[sub-path]/[filename] URI 来索引这些文件。

数据共享应用程序通常使用 Android 清单的 grantUriPermissions 属性以及用于定义读取或写入操作模式的特殊标志来提供访问权限。数据共享应用程序创建并向使用者发送一个 Intent,该 Intent 提供对文件的临时细化访问。最后,当提供程序收到文件访问请求时,它会解析与传入 URI 对应的实际文件路径,并向其返回文件描述符。

实施陷阱

这种基于内容提供程序的模型提供了定义良好的文件共享机制,使服务应用程序能够以安全的方式与其他应用程序共享其文件,并进行精细控制。但是,我们经常遇到这样的情况:使用应用程序不验证它收到的文件的内容,最令人担忧的是,它使用服务应用程序提供的文件名将接收到的文件缓存在使用应用程序的内部数据目录中。如果服务应用程序实现自己的恶意版本的 FileProvider,则它可能会导致使用应用程序覆盖关键文件。

共享目标

简单来说,共享目标是一个 Android 应用,它声明自己处理其他应用发送的数据和文件。可以作为共享目标的常见应用程序类别包括邮件客户端、社交网络应用程序、消息传递应用程序、文件编辑器、浏览器等。在常见情况下,当用户单击文件时,Android 操作系统会触发共享表对话框,要求用户选择文件应发送到的组件:

显示 OneDrive、OneNote、Outlook 等应用的 Android 共享表对话框。

图 1.“Android 共享表单”对话框

虽然这种类型的引导式文件共享交互本身可能不会触发针对共享目标的成功攻击,但恶意 Android 应用程序可以创建自定义的明确意图,并在用户不知情或未经用户批准的情况下,使用恶意文件名将文件直接发送到共享目标。从本质上讲,恶意应用程序正在替换其自己的恶意 FileProvider 实现,并提供使用应用程序不当信任的文件名。

显示恶意应用与共享目标 APK 之间的流攻击步骤的图表。首先,将处理文件的请求发送到 APK,APK 会回复文件名请求。恶意应用回复名称,APK 允许,授予恶意应用交付最终恶意负载的能力。

图2.脏流攻击

在图 2 中,左侧的恶意应用创建了一个显式 Intent,该 Intent 以右侧共享目标的文件处理组件为目标,并附加一个内容 URI 作为 intent 的额外内容。然后,它使用 startActivity API 调用将此意向发送到共享目标。

在此之后,我们审查的大多数共享目标似乎都遵循特定的代码模式,其中包括以下步骤:

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

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

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

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

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

冲击

我们在 Google Play 商店中发布的多个 Android 应用程序的当前版本中发现了此漏洞模式,其中至少有四个应用程序的安装量超过 5 亿次。在每种情况下,我们都会负责任地向供应商披露。我们发现的两个易受攻击的应用程序示例是 Xiaomi Inc. 的文件管理器(1B+ 安装)和 WPS Office(500M+ 安装)。

在Xiaomi Inc.的文件管理器中,我们能够在版本V1-210567中获取任意代码执行。在我们披露后,小米发布了 V1-210593 版本,我们验证了该漏洞已得到解决。在 WPS Office 中,我们能够在版本 16.8.1 中获取任意代码执行。在我们披露后,WPS 发布并通知我们该漏洞已从版本 17.0.0 开始得到解决。

潜在影响因实施具体情况而异。例如,Android 应用程序从 shared_prefs 目录读取其服务器设置是很常见的。在这种情况下,恶意应用可能会覆盖这些设置,从而导致易受攻击的应用与攻击者控制的服务器通信,并发送用户的身份验证令牌或其他敏感信息。

在最坏的情况下(并不罕见),易受攻击的应用程序可能会从其数据目录加载本机库(与更安全的 /data/app-lib 目录相反,在该目录中,库受到保护,不会被修改)。在这种情况下,恶意应用程序可以使用加载库时执行的恶意代码覆盖本机库。在下一节中,我们将使用小米公司的文件管理器来说明这种情况。我们演示了恶意应用程序覆盖应用程序的共享首选项、将本机库写入应用程序的内部存储以及导致应用程序加载库的能力。这些操作提供了具有文件管理器的用户 ID 和权限的任意代码执行。

在以下各节中,我们将重点介绍此案例,并深入探讨此漏洞模式的技术细节。

案例研究:小米公司的文件管理器

小米公司的文件管理器是小米设备的默认文件管理器应用程序,在 Google Play 商店中以 com.mi.android.globalFileexplorer 的软件包名称发布,已被安装超过 10 亿次。

小米的文件管理器配置文件根据 Android 排名

图3.根据 Android 排名的小米文件管理器配置文件(来源:文件管理器

除了对设备的外部存储具有完全访问权限外,该应用程序还请求许多权限,包括安装其他应用程序的能力:

显示应用权限的代码屏幕截图

图4.应用程序权限的快照

此外,它还提供了一个垃圾文件清理插件以及连接到远程 FTP 和 SMB 共享的能力:

使用文件管理器连接到远程共享的屏幕截图。

图5.使用文件管理器连接到远程共享

脆弱性评估结果

在调查过程中,我们发现该应用导出了 CopyFileActivity,这是 com.android.fileexplorer.activity.FileActivity 的活动别名,用于处理从文件复制到的文件操作:

复制到 CopyFileActivity 事件的屏幕截图。

图6.触发 CopyFileActivity 的副本

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

在收到这样的意图后,浏览器会执行有效性检查,我们发现这是不够的:

显示验证传入复制文件请求的步骤的代码屏幕截图。

图7.验证传入的复制文件请求

如上所述,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 类的对象的字段:

从远程内容提供程序获取文件元数据的代码的屏幕截图。

图8.从远程内容提供程序获取文件元数据

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

显示调用方法 a 后初始化的文件模型的代码屏幕截图

图 9.调用方法 a 后初始化的文件模型

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

获取代码执行

如前所述,该应用程序使用插件来清理设备的垃圾文件:

垃圾文件清理器插件用户界面的屏幕截图

图 10.垃圾文件清理器插件用户界面

当应用程序加载此插件时,它使用两个本机库:libixiaomifileu.so,它从 /data/app 目录中获取,libixiaomifileuext.so 从主目录中获取:

显示使用 medusa 跟踪的加载的本机库的代码屏幕截图

图 11.使用 medusa 跟踪加载的本机库

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

我们的评估表明,在应用程序加载此库之前,它遵循以下步骤:

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

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

显示com.mi.android.globalFileexprorer_preferences.xml的代码的屏幕截图

图 12.com.mi.android.globalFileexprorer_preferences.xml

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

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

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

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

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

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

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

敏锐的读者可能已经注意到,第二步要求攻击者强制浏览器覆盖com.mi.android.globalFileexprorer_preferences.xml,正如我们已经提到的,这是不可能的。

为了克服此限制,我们参考了 SharedPreferences 类的实际实现,其中我们发现,当 Android 应用程序使用 getSharedPreferences API 方法检索 SharedPreferences 类的实例时,将共享首选项文件的名称作为参数,然后 SharedPreferencesImpl 类的构造函数执行以下步骤

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

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

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

冲击

我们选择使用此应用程序作为展示的一个原因是,其影响超出了用户的移动设备。该应用程序提供了使用 FTP 和 SMB 协议连接到远程文件共享的选项,并且用户凭据以明文形式保存在 /data/data/com.mi.android.globalFileexplorer/files/rmt_i.properties 文件中:

以明文形式显示 SMB 或 FTP 凭据的代码屏幕截图

图 13.以明文形式保存的 SMB/FTP 凭据

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

显示保存在外部存储中的 SMB 共享文件的代码屏幕截图

图 14.SMB 共享文件,保存在外部存储中

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

显示保存在外部存储中的 FTP 共享文件的代码屏幕截图

图 15.FTP 共享文件,保存在外部存储中

综上所述,开发流程如下图所示:

显示攻击者如何获取对本地共享的远程访问权限的图表,如文本中所述。

图 16.远程访问本地共享

步骤 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)。

建议

认识到这种漏洞模式可能很普遍,我们与 Google 的 Android 应用安全研究团队分享了我们的发现。我们与 Google 合作,为 Android 应用程序开发人员编写了指南,以帮助他们识别和避免这种模式。我们建议开发人员和安全分析师熟悉 Google 提供的出色的 Android 应用程序安全指南,并利用 Android SDK 附带并与 Android Studio 集成的 Android Lint 工具(辅以 Google 额外的以安全为重点的检查)来识别和避免潜在漏洞。GitHub 的 CodeQL 还提供识别漏洞的功能

为了防止这些问题,在处理其他应用程序发送的文件流时,最安全的解决方案是在缓存接收的内容时完全忽略远程文件提供程序返回的名称。我们遇到的一些最健壮的方法使用随机生成的名称,因此即使传入流的内容格式不正确,它也不会篡改应用程序。

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

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

对于最终用户,我们建议通过 Google Play 商店(或其他适当的受信任来源)使移动应用程序保持最新状态,以确保安装解决已知漏洞的更新。用户应仅安装来自受信任来源的应用程序,以避免潜在的恶意应用程序。我们建议在更新之前通过小米应用程序访问 SMB 或 FTP 共享的用户重置凭据并调查任何异常行为。Android 上的 Microsoft Defender for Endpoint 可以提醒用户和企业注意恶意应用程序,Microsoft Defender 漏洞管理可以识别具有已知漏洞的已安装应用程序。

迪米特里奥斯·瓦尔萨马拉斯

Microsoft 威胁情报