September 26, 2023
  • Meta’s Native Assurance workforce repeatedly 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 workforce 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 consumer purposes on VROS, the Android Open Supply Challenge (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 might 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 good higher understanding of what exploitation might appear 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 comprises customizations on prime of AOSP to supply the VR expertise on Quest {hardware}, together with firmware, kernel modifications, gadget drivers, system companies, SELinux insurance policies, and purposes.

As an Android variant, VROS has lots of the similar security measures as different trendy Android methods. For instance, it makes use of SELinux insurance policies to cut back the assault surfaces uncovered to unprivileged code operating on the gadget. Due to these protections, trendy Android exploits sometimes require chains of exploits in opposition to quite a few vulnerabilities to realize 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 purposes are primarily common Android purposes. Nonetheless, these purposes 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 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 purchasers and servers. A local dealer course of known as runtimeipcbroker sits within the center between purchasers and servers and manages the preliminary connection, after which purchasers and servers talk immediately with each other.

VR utility / VR Runtime connections

All VR purposes 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 underneath the hood to carry out numerous 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 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 receive 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 obligations in order that the consumer and server can talk immediately.
    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 purposes on VROS is untrusted_app. These purposes 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 include 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 danger. If an untrusted utility is ready to exploit a vulnerability within the VR Runtime code will probably be in a position to carry out operations on the gadget reserved for privileged purposes. Due to this, all inputs from untrusted purposes to VR Runtime needs to be scrutinized closely.

Crucial 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 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 purposes and have adb / shell entry. This doesn’t imply customers are in a position to get root on their gadgets, nevertheless 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 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, nevertheless 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 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 mirror::RPCResult_Complete;
    )

The request parameter is an object that’s constructed based mostly on what’s obtained 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 steady and could be repeated.

Hijacking control-flow

The tip objective 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 numerous attention-grabbing 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 necessary 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 necessary as a result of and not using a name web site invoking the operate pointer, we will’t take over the management stream.

To enumerate the corruption targets that had 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 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 might 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 regarded 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 known as from a management stream originating from the CreateSwapChain Runtime IPC RPC command.

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

To hijack management stream, 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 House Structure 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 deal with house of the goal. Due to ASLR, we had no data of the goal deal with house: We didn’t know the place libraries had 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 stream to loaded libraries and reuse a few of their code. It is a approach often known as jump-oriented programming (JOP) or return-oriented programming (a particular case of JOP).

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

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

We explored each of these choices and finally 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 deal with 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 data of the place quite a few libraries reside in reminiscence. Under 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 data, we enumerated all shared libraries in each deal with areas and regarded 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 objective.

...
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 stream, 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 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 situation 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 can be executed mechanically 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 listing 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 stream 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 had 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 deal with.

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 stream 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 downside.

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

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 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 establishing our gadget state and solely afterwards triggering the management stream hijack. 

Utilizing this method, we arrange the gadget state to mix the 2 devices above and had 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 stream 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 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:   "/information/app/com.oculus.vrexploit-OjL813hdSAtlc3fEkJKdrg==/lib/arm64/libinject-arm64.so"

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

At that time, we achieved our objective and had 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 potential with a give attention to actionable objects we might use to enhance the safety posture of Meta merchandise. We received’t listing all of the outcomes on this submit however listed below are a few of the most notable.

RELRO for operate pointers in RW international reminiscence

One of many patterns we observed early within the train was that the VR Runtime service contained many operate pointers in international 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 deal with. 

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 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 international 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 stream integrity.

As an final result of our exploitation train, we explored completely different methods to guard these operate 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 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 modifications to the VR Runtime code to mark operate pointers in international reminiscence to be learn solely after we initialized them. Had this safety been in place it could have made our exploitation way more troublesome. We are actually engaged on generalizing this method by constructing an LLVM compiler move that implements the approach.

Ideas on SELinux

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

Android helps this conduct as a result of it permits for updates to privileged purposes outdoors 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 /information/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. Basically, we don’t imagine that privileged purposes ought to be capable to execute code owned by lesser privileged purposes.

About Meta’s Native Assurance workforce

The Meta Native Assurance workforce 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 critiques, 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.