Meta Quest 2: Protection by means of offense

  • Meta’s Native Assurance group frequently performs guide code critiques 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 assessment on a privileged service known as VR Runtime which offers VR companies to shopper purposes 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 might 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 might execute arbitrary native code in VR Runtime. Doing so gave us a fair higher understanding of what exploitation may 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 offer the VR expertise on Quest {hardware}, together with firmware, kernel modifications, machine drivers, system companies, SELinux insurance policies, and purposes.

As an Android variant, VROS has lots of the identical security measures as different fashionable Android methods. For instance, it makes use of SELinux insurance policies to cut back the assault surfaces uncovered to unprivileged code operating on the machine. Due to these protections, fashionable Android exploits usually require chains of exploits towards quite a few vulnerabilities to achieve management over a tool. Attackers making an attempt to compromise VROS should overcome comparable challenges.

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

On VROS, VR purposes are basically common Android purposes. Nonetheless, these purposes talk with quite a lot of system companies and {hardware} to offer the VR expertise to customers.

VR Runtime

VR Runtime is a service that gives VR options similar to time warp and composition to shopper VR purposes. The service is contained throughout the com.oculus.vrruntimeservice course of as a part of the com.oculus.systemdriver (VrDriver.apk) package deal. The VrDriver package deal 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 purposes. 

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 straight with each other.

VR utility / VR Runtime connections

All VR purposes use Runtime IPC to hook up 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 shopper facet of the VR Runtime implementation and use this underneath the hood to carry out numerous VR operations supported by VR Runtime similar to time warp.

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

  1. A loader is linked to all VR purposes 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 get hold of 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 within com.oculus.vrruntimeservice.
    2. This course of is mediated by the native runtimeipcbroker course of, which performs permissions checks and different hand-off duties in order that the shopper and server can talk straight.
    3. From this level ahead the connection makes use of UNIX pipes and shared reminiscence areas for shopper/server communication.

The VR Runtime assault floor

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

Since untrusted purposes 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 will likely be in a position to carry out operations on the machine reserved for privileged purposes. Due to this, all inputs from untrusted purposes to VR Runtime ought to be scrutinized closely.

A very powerful inputs that VR Runtime processes from untrusted purposes 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 under:

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 purposes 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’d not have in any other case.

We selected to pursue exploitation from the attitude of an utility that escalates its privileges on the headset. Such an utility might 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 kind of message that the VR Runtime may obtain over Runtime IPC. Here’s a redacted code snippet of what the vulnerability regarded 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 based mostly on what’s acquired over Runtime IPC. This implies each request.Function 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.Function and request.Standing are attacker managed and PerformanceManagerState->IdealFeaturesState.features_  is a statically-sized array, the vulnerability offers an attacker the power 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 might 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 fancy stateful piece of software program and there are lots of attention-grabbing potential targets inside its .bss part. The best corruption goal for us was a perform pointer that:

  1. Is saved at an arbitrary offset proper after the worldwide array. That is vital as a result of it means we are able to 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 vital as a result of and not using a name web site invoking the perform pointer, we are able to’t take over the management movement.

To enumerate the corruption targets that have been reachable from the write primitive, we used Ghidra to manually analyze the format of the .bss part of the libvrruntimeservice.so 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 could see under.

We then looked for ahead reachable corruption targets that have been contained throughout the libvrruntimservice.so binary. Fortunate for us, we discovered an array of perform pointers which might be dynamically resolved at runtime and saved inside a world occasion of an ovrVulkanLoader object. The perform pointers contained inside ovrVulkanLoader level into the libvulkan.so module offering the Vulkan interface. The Vulkan interface perform 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 regarded for a perform pointer that we knew might be invoked not directly from an RPC command. We selected to overwrite the vkGetPhysicalDeviceImageFormatProperties perform pointer, which might be known as from a management movement originating from the CreateSwapChain Runtime IPC RPC command.

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

To hijack management movement, we first used the write primitive to deprave the vkGetPhysicalDeviceImageFormatProperties perform pointer after which crafted an RPC command that triggered the CreateTextureSwapChainVulkan perform. This ultimately allowed us to regulate this system counter:

Bypassing Tackle Area Structure 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 handle house of the goal. Due to ASLR, we had no information of the goal handle house: We didn’t know the place libraries have been loaded and didn’t know the place the heap or stack was. Figuring out these places is extraordinarily helpful for an attacker as a result of they will redirect the execution movement 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 particular case of JOP).

Bypassing ASLR is a standard downside in fashionable exploitation and the reply is often to:

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

We explored each of these choices and ultimately stumbled upon one thing somewhat attention-grabbing:

$ 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 possibly can 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 handle house from the zygote64 course of because the VR Runtime course of. Which means that the loaded libraries within the zygote64 course of at fork time might 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 have now 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 data, we enumerated all shared libraries in each handle areas and regarded for code reuse devices in them. At this level there have been actually hundreds of thousands of code reuse devices in a file that we would have liked to sift by means of to assemble a JOP chain and attain 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 movement, 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 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 attitude 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 can be executed robotically as a part of the loaded library’s initialization perform.

Carrying out 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 perform 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 checklist 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 under:

Recall that the $x0 register on the time of the management movement switch to dlopen corresponds to the trail argument. The issue we now needed to clear up 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 have been in a position to 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 handle.

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 movement hijack. This meant that in principle we may merely transfer $x21 or a worth offset from $x21 into $x0. This might give us our managed path argument to dlopen, fixing our downside.

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

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

We may then use one other gadget to set $x1 (the second perform 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 places in reminiscence offset from $x21 (ovrVulkanLoader). We ended up utilizing a number of RPC instructions to overwrite reminiscence in the best way we would have liked for establishing our gadget state and solely afterwards triggering the management movement 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 movement by triggering a perform 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 under is what it regarded 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 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 may use to enhance the safety posture of Meta merchandise. We gained’t checklist all of the outcomes on this submit however listed below are a number of the most notable.

RELRO for perform pointers in RW world reminiscence

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

This method offers flexibility to builders to make use of vendor libraries offering a standard API throughout merchandise (e.g., libvulkan.so). The draw back is that the perform 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 perform pointers should not protected by compiler mitigations similar to management movement integrity.

As an final result of our exploitation train, we explored completely different methods to guard these perform pointers after their preliminary task. One technique was to attempt 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 modifications to the VR Runtime code to mark perform pointers in world reminiscence to be learn solely after we initialized them. Had this safety been in place it might have made our exploitation rather more troublesome. We are actually engaged on generalizing this method by constructing an LLVM compiler go that implements the approach.

Ideas on SELinux

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

Android helps this habits as a result of it permits for updates to privileged purposes exterior of OTA updates. This enables privileged purposes 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 situation, we really feel it’s price calling out as a possible space for enchancment on Android. Generally, we don’t imagine that privileged purposes ought to be capable to execute code owned by lesser privileged purposes.

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 critiques, assault floor discount, exploit mitigations, and extra. As well as, Meta additionally affords a bug bounty program to incentivize safety analysis throughout its total exterior assault floor, together with the VR and AR merchandise.