SBX Intro

Lately, I’ve been getting into Chrome sandbox exploitation. Having found and exploited a few sandbox escape vulnerabilities, I thought it would be fun to include these in a CTF. Unfortunately, one issue I faced while learning SBX is lack of online resources.

I think conceptually, this attack surface is not exceedingly complex - at least compared to the renderer. In this blog post, I aim to provide a high level overview of SBX concepts, which will hopefully speed along the learning process.

What is SBX

In terms of the scope of this blog post, I will be blackboxing the sandboxing internals and assume it works as intended. Thus, the goal is to find a vulnerability in the IPC channels between the sandboxed renderer process and the privileged browser process.

This blog post will also concentrate on the Mojo interface attack surface, where I have spent most of my time. This attack surface is also more suitable for CTFs because they expose complex functionality directly to Javascript with MojoJS bindings.

Note that on some platforms, certain utility processes may have more privileges. For example, the Network Service runs in process on Android.

Getting the Code

The official documentation is quite complete. Setting up your own chromium build isn’t actually that complicated - it just takes a long time.

The general workflow is:

# get repo tools
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git $HOME/depot_tools
$ echo 'export PATH="$PATH:${HOME}/depot_tools"' >> ~/.bashrc
$ source ~/.bashrc

$ mkdir ~/chromium && cd ~/chromium
# this command will take a long time
$ fetch --nohooks chromium
$ cd src
$ ./build/install-build-deps.sh
$ gclient runhooks

$ gn gen out/build-dir
# edit args.gn
$ vi out/build-dir/args.gn
$ autoninja -C out/build-dir chrome

Mojom Interfaces

From the renderer process, we are able to interact with the browser process in a variety of ways. One way is through Mojom interfaces. These .mojom files define the format and methods in which IPC can occur. One thing to note is that Mojo interactions are largely asynchronous. For example, you will need to pass in a callback or await a promise in order to get the return value of a mojom method call (unless the method is annotated with [Sync]).

Conceptually, these interfaces have a remote end and a receiver end. A method call on the remote end - for example through the MojoJS bindings - will eventually lead to a method call on the implementation on the receiver end. The receiver can be bound in any process, although for SBX it will almost always be bound in the browser. Mojo internals will handle all of the message routing.

For more information, refer to the C++ binding documentation.

MojoJS Bindings

We’ve talked a lot about MojoJS bindings at a high level, but how do you actually use them?

Mojo bindings are located in the out/build-dir/gen directory. A certain subset of MojoJS bindings are autogenerated when you build the chrome target. To explicitly generate MojoJS bindings for a particular interface, locate the mojom("XXX") build target in the respective BUILD.gn file and build the _js target. For more information, see mojom.gni.

You can then copy these bindings to your payload directory with this convenient script taken from https://crbug.com/1001503.

#! /usr/bin/python

import os
import shutil
import sys

base_path = sys.argv[1]
for path, dirs, files in os.walk(base_path):
  for file in files:
    if file == 'mojo_bindings.js':
      shutil.copyfile(os.path.join(path, file), os.path.join('./', file))

    if file.endswith('.mojom.js'):
      target_path = os.path.join('./', path[len(base_path) + 1:])
      try:
        os.makedirs(target_path)
      except:
        pass
      shutil.copyfile(os.path.join(path, file), os.path.join(target_path, file))

To use:

$ mkdir mojojs && cd mojojs && /path/to/copy.py /path/to/chromium/src/out/build_dir

After generating the mojojs bindings, you will want to load them from js. For example:

<script src="/mojojs/mojo_bindings.js"></script>
<script src="/mojojs/gen/third_party/blink/public/mojom/appcache/appcache.mojom.js"></script>

Now that you have the mojo js bindings loaded, you will want to bind your first interface. To do this, you will need to create a “remote” in javascript. We then want to pass the receiver to the browser process. Recall that method invocations on our remote will then be passed to the receiver in the browser process.

To bind our receiver, we use the Mojo.bindInterface javascript method, which passes to Mojo::bindInterface.

We then stick the interface we want to bind into the magic formula below:

  const ptr = new {{module_path}}.{{interface_name}}Ptr();
  Mojo.bindInterface({{module_path}}.{{interface_name}}.name, mojo.makeRequest(ptr).handle);

For example, if we wanted to bind an AppCacheBackend defined in appcache.mojom.

  • module_path: blink.mojom
  • interface_name: AppCacheBackend
  const ptr = new blink.mojom.AppCacheBackendPtr();
  Mojo.bindInterface(blink.mojom.AppCacheBackend.name, mojo.makeRequest(ptr).handle);

We would then be able to perform method invocations on the ptr object. For example, we could call the RegisterHost method with ptr.registerHost(hostReceiver, frontend, hostId). Note how the case changes. All method and variable names are camelcased in MojoJS bindings.

Threading

Threading with MojoJS is one of the more complex components, and thread manipulation in the browser process allows for some very interesting exploits.

Recall that at a high level, a method invocation on a remote somehow results in a call on the receiver implementation. Naturally, the question arises: what happens in between?

In the browser process, there are two named threads: IO and UI. Mojo messages are processed on the IO thread.

The thread on which the receiver implementation is bound (or if there is a specific task runner passed to the bound receiver) is the thread on which the implementation methods will be called. For example, the AppCacheBackend implementation is bound on the UI thread – it’s not immediately clear from this linked code but you can trace backwards to PopulateFrameBinders. Thus, all the methods of AppCacheBackendImpl would be invoked on the UI thread.

As an implentation detail, if an interface is bound on the IO thread, there will not be a thread hop, nor will the mojo parser post the task to be executed later. Instead, the method will be directly invoked. An interesting observation is that by blocking the IO thread, we are able to block future Mojo messages from being posted.

On the other hand, if the interface is bound on a different thread, for example the UI thread, a thread hop will occur and the method will run asynchronously.

Threading is probably not a serious consideration during CTFs, but understanding such internals is still interesting and has its applications.

Template

For the lazy pwners, here’s a nice template to copy:

var express = require('express');
var app = express();

app.use((req, res, next) => {
  console.log(decodeURIComponent(req.originalUrl));
  next();
})

app.use(express.static(__dirname + "/public"));

app.listen(1337, 'localhost', () => {
  console.log("listening!")
});

server.js

<script src="/mojojs/mojo_bindings.js"></script>
<script src="/mojojs/gen/third_party/blink/public/mojom/interface.mojom.js"></script>
<script>
  const log = msg => {
    fetch("/log?log=" + encodeURIComponent(msg));
  }
  const sleep = ms => new Promise(res => setTimeout(res, ms));
  window.onerror = e => log(e);

  (async () => {
    try{
      // pwn here
    }catch(e){
      log("error");
      log(": " + e.stack);
    }
  })();

sbx.html

For an example SBX exploit, see my previous blog post on Adult CSP, which I wrote for DiceCTF 2021.