September 23, 2023
  • Meta’s Native Assurance staff recurrently 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 staff at Meta (a part of the Product Safety group) carried out a code overview on a privileged service referred to as VR Runtime which offers VR providers 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 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 might occur on VROS we determined to make use of this chance to write 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 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 accommodates customizations on high of AOSP to offer the VR expertise on Quest {hardware}, together with firmware, kernel modifications, machine drivers, system providers, SELinux insurance policies, and functions.

As an Android variant, VROS has lots of the similar safety features as different fashionable Android methods. For instance, it makes use of SELinux insurance policies to cut back the assault surfaces uncovered to unprivileged code working on the machine. 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 comparable 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 providers 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 consumer VR functions. 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 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 utility / VR Runtime connections

All VR functions use Runtime IPC to hook up with the VR Runtime server working 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 beneath the hood to carry out varied 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 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 working 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 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 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 danger. 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 functions. Due to this, all inputs from untrusted functions to VR Runtime ought to be scrutinized closely.

An important 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 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 units, 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 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 might 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 mirror::RPCResult_Complete;
    )

The request parameter is an object that’s constructed primarily based 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 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 might be repeated.

Hijacking control-flow

The top aim 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 fancy stateful piece of software program and there are loads of fascinating 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 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 vital as a result of and not using a name website invoking the perform pointer, we will’t take over the management circulate.

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 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 you can see beneath.

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 appeared 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 referred to as from a management circulate 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 circulate, 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 manage this system counter:

Bypassing Handle House 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 troublesome for exploits to foretell the tackle house of the goal. Due to ASLR, we had no data 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 areas is extraordinarily helpful for an attacker as a result of they will redirect the execution circulate to loaded libraries and reuse a few of their code. This can be a method often called jump-oriented programming (JOP) or return-oriented programming (a selected case of JOP).

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

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

We explored each of these choices and ultimately stumbled upon one thing reasonably 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 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 tackle 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 will likely be loaded on the similar 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 now have detailed data of the place quite a few libraries reside in reminiscence. Beneath reveals 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 tens of millions of code reuse devices in a file that we wanted to sift by to assemble a JOP chain and achieve our aim.

...
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 really write the exploit to execute a payload of our selecting within the VR Runtime course of. 

Exploitation

As a reminder, our exploitation situation was from the angle of an already put in untrusted utility. Our strategy 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 routinely as a part of the loaded library’s initialization perform.

Engaging in this meant we wanted 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 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 remedy 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 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 idea we might merely transfer $x21 or a worth 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 wanted and likewise allowed us to maintain management circulate:

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

We might 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 might 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 wanted for organising our gadget state and solely afterwards triggering the management circulate hijack. 

Utilizing this strategy, 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 circulate 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 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 completed our aim 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 doable with a give attention to actionable objects we might use to enhance the safety posture of Meta merchandise. We gained’t checklist all of the outcomes on this publish however listed below are a number of the most notable.

RELRO for perform pointers in RW world reminiscence

One of many patterns we observed early within the train was that the VR Runtime service contained many perform pointers in world reminiscence. The VR Runtime course of masses 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 tackle. 

This strategy 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 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 will not be protected by compiler mitigations similar to management circulate integrity.

As an final result of our exploitation train, we explored completely different methods to guard these perform pointers after their preliminary project. 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 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 at the moment are engaged on generalizing this strategy by constructing an LLVM compiler go that implements the method.

Ideas on SELinux

One of the crucial irritating issues for us throughout exploit growth was the constraints imposed on us by SELinux. With that stated, we have been pleasantly shocked that we might 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 (usually put in to platform_app, system_app, or priv_app) to execute code beneath /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 outdoors of OTA updates. This enables 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 challenge, we really feel it’s value calling out as a possible space for enchancment on Android. On the whole, we don’t imagine that privileged functions ought to have the ability to execute code owned by lesser privileged functions.

About Meta’s Native Assurance staff

The Meta Native Assurance staff 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 gives a bug bounty program to incentivize safety analysis throughout its complete exterior assault floor, together with the VR and AR merchandise.