Meta Quest 2: Protection by offense

  • Meta’s Native Assurance group usually performs handbook 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 software over Runtime IPC.

In 2021, the Native Assurance group at Meta (a part of the Product Safety group) carried out a code evaluation on a privileged service referred to as VR Runtime which gives VR companies to consumer functions on VROS, the Android Open Supply Mission (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 software.

This vulnerability by no means made it into manufacturing. However to get a greater understanding of how exploitation might 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 a fair higher understanding of what exploitation might seem like 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 incorporates customizations on high of AOSP to supply the VR expertise on Quest {hardware}, together with firmware, kernel modifications, gadget drivers, system companies, SELinux insurance policies, and functions.

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

Picture supply:

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 comparable to 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 offers it permissions past what are given to regular Android functions. 

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

VR software / 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 facet of the VR Runtime implementation and use this below the hood to carry out varied VR operations supported by VR Runtime comparable to time warp.

This course of could 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 library put in as a part of VrDriver.apk. The loader will get hold of the addresses of capabilities inside related to the general public VrApi or OpenXR interface.
  3. After the loader’s execution:
    1. The VR software will create a Runtime IPC connection to the VR Runtime server operating within com.oculus.vrruntimeservice.
    2. This course of is mediated by the native runtimeipcbroker course of, which performs permissions checks and different hand-off obligations in order that the consumer and server can talk straight.
    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 embrace these which might be put in from the Meta Quest Retailer in addition to these which might be sideloaded onto the gadget. The untrusted_app area is restrictive and meant to comprise the minimal SELinux permissions that an software 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 software is ready to exploit a vulnerability within the VR Runtime code it is going to be in a position to carry out operations on the gadget 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 people 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 situation that we thought of.

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 in a position to get root on their gadgets, however it does give them a considerable amount of flexibility for interacting with the headset that they might not have in any other case.

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

The vulnerability

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

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

          .status_ = request.Standing;     
          .fidelity_ = request.Constancy;
// ...
      response = true;
      return replicate::RPCResult_Complete;

The request parameter is an object that’s constructed based mostly 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 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 power to carry out arbitrary 8-byte-long corruptions at arbitrary offsets (32-bit restrict). Any VR software can set off this vulnerability by sending a specifically crafted SetPerformanceIdealFeatureState Runtime IPC message. Furthermore, the vulnerability is secure and could be repeated.

Hijacking control-flow

The tip purpose for our exploit was arbitrary native code execution. We wanted 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 perfect 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 web site that invokes it. That is essential as a result of with out a name web site invoking the operate pointer, we will’t take over the management circulation.

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

We then looked for ahead reachable corruption targets that have been contained throughout the binary. Fortunate for us, we discovered an array of operate pointers which might be dynamically resolved at runtime and saved inside a world occasion of an ovrVulkanLoader object. The operate pointers contained inside ovrVulkanLoader level into the 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 seemed 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 could be referred to as from a management circulation 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 circulation, we first used the write primitive to deprave the vkGetPhysicalDeviceImageFormatProperties operate pointer after which crafted an RPC command that triggered the CreateTextureSwapChainVulkan operate. This ultimately allowed us to regulate this system counter:

Bypassing Tackle House Format Randomization (ASLR) 

We turned this corruption primitive into one thing that allowed us to regulate this system counter of the goal. Address Space Layout Randomization (ASLR) is an exploit mitigation that makes it troublesome 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 have been loaded and didn’t know the place the heap or stack was. Understanding these places is extraordinarily helpful for an attacker as a result of they will redirect the execution circulation to loaded libraries and reuse a few of their code. It is a method often called jump-oriented programming (JOP) or return-oriented programming (a particular case of JOP).

Bypassing ASLR is a typical drawback in fashionable exploitation and the reply is normally to:

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

We explored each of these choices and ultimately stumbled upon one thing relatively 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’ll be able to see that our software 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 shall be loaded on the similar addresses in each of these processes.

That is extraordinarily helpful as a result of it implies 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 reveals an instance the place the module is loaded at 0x7dae043000 in each processes:

$ adb shell cat /proc/1898/maps | grep
7dae043000-7dae084000 r--p 00000000 fd:00 286     /apex/
7dae084000-7dae11e000 --xp 00040000 fd:00 286     /apex/
7dae11e000-7dae126000 r--p 000d9000 fd:00 286     /apex/
7dae126000-7dae129000 rw-p 000e0000 fd:00 286     /apex/
$ adb shell cat /proc/7519/maps | grep
7dae043000-7dae084000 r--p 00000000 fd:00 286     /apex/
7dae084000-7dae11e000 --xp 00040000 fd:00 286     /apex/
7dae11e000-7dae126000 r--p 000d9000 fd:00 286     /apex/
7dae126000-7dae129000 rw-p 000e0000 fd:00 286     /apex/

Utilizing this information, we enumerated all shared libraries in each tackle areas and seemed 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 by 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 circulation, knew the place a big subset of libraries loaded within the VR Runtime are positioned in reminiscence, and had a listing of code reuse devices. The subsequent step was to really write the exploit to execute a payload of our selecting within the VR Runtime course of. 


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

Engaging in 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 based mostly 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 circulation switch to dlopen corresponds to the trail argument. The issue we now needed to remedy was how can we load $x0 with a pointer to a string we management? That is difficult as a result of the one place we have been in a position to insert managed information 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 circulation hijack. This meant that in concept we might merely transfer $x21 or a price offset from $x21 into $x0. This might give us our managed path argument to dlopen, fixing our drawback.

After hours of sifting by devices, we ultimately discovered one which did precisely what we would have liked and likewise allowed us to maintain management circulation:

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

We might 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 might overwrite a number of places 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 establishing our gadget state and solely afterwards triggering the management circulation hijack. 

Utilizing this method, we arrange the gadget state to mix the 2 devices above and have been in a position to load our shared module giving us arbitrary native code execution:

  // Corrupt the `vulkanLoader.vkGetPhysicalDeviceImageFormatProperties` pointer which is
  // at +0x68. We hijack management circulation by triggering a operate name in
  // ovrSwapChain::CreateTextureSwapChainVulkan.
  // First gadget in
  //  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 ="") + 0xb3'ac;
  Corruptions.emplace_back(vkGetPhysicalDeviceImageFormatPropertiesOffset, FirstGadget);

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

And beneath is what it seemed 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:   "/information/app/com.oculus.vrexploit-OjL813hdSAtlc3fEkJKdrg==/lib/arm64/"

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

At that time, we achieved our purpose and have been in a position to 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 attainable with a give attention to actionable objects we might use to enhance the safety posture of Meta merchandise. We received’t record all of the outcomes on this put up however listed below are a few of the most notable.

RELRO for operate pointers in RW world reminiscence

One of many patterns we seen 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 gives flexibility to builders to make use of vendor libraries offering a typical API throughout merchandise (e.g., 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 have 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 comparable to management circulation integrity.

As an final result of our exploitation train, we explored completely different methods to guard these operate pointers after their preliminary project. One technique was to attempt to mirror the well-known full relocation read-only (RELRO) mitigation that’s used to guard tips that could 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 will have made our exploitation far more troublesome. We at the moment are engaged on generalizing this method by constructing an LLVM compiler go that implements the method.

Ideas on SELinux

One of the irritating issues for us throughout exploit growth was the constraints imposed on us by SELinux. With that mentioned, we have been pleasantly shocked that we might load a .so library out of an untrusted software’s information listing as a privileged software. It is because Android’s default SELinux coverage allows privileged functions (usually put in to platform_app, system_app, or priv_app) to execute code below /information/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 outdoors 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 software is put in to /information/app, however retains its privileged SELinux context. 

Whereas we didn’t develop an answer to this problem, we really feel it’s value calling out as a possible space for enchancment on Android. Generally, 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 embrace fuzzing, static evaluation, structure/implementation opinions, assault floor discount, exploit mitigations, and extra. As well as, Meta additionally affords a bug bounty program to incentivize safety analysis throughout its whole exterior assault floor, together with the VR and AR merchandise.