• Event: Google CTF 2019 Quals
  • Category: pwn
  • Points: 453
  • Solves: 3

The monochromatic challenge is my task for Google CTF 2019 Quals, it was tested by Sergei Glazunov from p0, and this blog post is the explanation of his exploit.

This challenge is placed in sandbox category. The idea of this category during Google CTF is that you already have RCE and your task is to escape a sandbox.

The challenge consists of a modified chrome binary and a diff of source code.

Also the service.py is provided which simply reads your URL from stdin and loads it in chrome. You can notice a strange flag --enable-blink-features=MojoJS which will be described a little bit later.

Some basic stuff

I recommend you to read Inside look at modern web browser (part 1) and Inside look at modern web browser (part 2). In short: chrome consists of several different processes. They communicate between themselves using Mojo IPC. The renderer is responsible for rendering a webpage - for example it parses HTML and executes javascript. Renderer is sanbdoxed so it’s not possible to have access to the OS and do any harm inside the browser. Also we can’t attack another websites because of site isolation. In the past people were escaping the sandbox via bugs in sandbox itself or in rechable OS components but the sandbox became more mature and this is now much more unlikely. However, with RCE we gain an ability to communicate with interface bindings inside browser process and maybe we can discover some vulnerabilities there.

In this challenge we already “have RCE” inside renderer process because chrome is run with a flag --enable-blink-features=MojoJS which enables this communication mechanism from javascript.

The really best is to read Intro to Mojo & Services. I will try to TL;DR but reading this link is better.

Let’s take a look at the diff. I added 5 Mojo interfaces: Dog, Cat, Person, BeingCreator and Food.

Interfaces are defined in *.mojom files, for example let’s take a look at being_creator_interface.mojom file:

interface BeingCreatorInterface {
  CreatePerson() => (blink.mojom.PersonInterface? person);
  CreateDog() => (blink.mojom.DogInterface? dog);
  CreateCat() => (blink.mojom.CatInterface? cat);
};

and person_interface.mojom

interface PersonInterface {
  GetName() => (string name);
  SetName(string new_name) => ();
  GetAge() => (uint64 age);
  SetAge(uint64 new_age) => ();
  GetWeight() => (uint64 weight);
  SetWeight(uint64 new_weight) => ();
  CookAndEat(blink.mojom.FoodInterface food) => ();
};

You can think about this like protobuf struct, but here you have definitions of methods you can call from one process on the object inside a different process. During chrome compilation these files automatically generate files *.mojom.h and *mojom.js. But *.mojom files are not enough, they also need to have their implementation in C++ or javascript. As an example this is the implementation of PersonInterface in C++. The endpoint that receives messages is called binding and when its bounded to the implementation calling methods remotely is possible. These calls are asynchronous and you can call methods on C++ objects from javascript and vice versa.

In this challenge there is one interface implementation that has binding bounded to, and we can communicate with it. Its BeingCreator in C++ inside a browser process.

To communicate with it from javascript include generated js files which are inside challenge files:

<script src="/mojo_bindings.js"></script>
<script src="/person_interface.mojom.js"></script>
<script src="/cat_interface.mojom.js"></script>
<script src="/dog_interface.mojom.js"></script>
<script src="/food_interface.mojom.js"></script>
<script src="/being_creator_interface.mojom.js"></script>

You need to include exactly these files provided in this challenge, because at every chrome compilation Mojo IPC protocol gets different magic numbers. This is a protection against exploits.

Create a javascript endpoint to send messages (InterfacePtr):

being_creator_interface_ptr = new blink.mojom.BeingCreatorInterfacePtr();
Mojo.bindInterface(blink.mojom.BeingCreatorInterface.name, 
	mojo.makeRequest(being_creator_interface_ptr).handle);

Now we can call a method e.g. CreateDog on BeingCreatorImpl object:

dog = (await being_creator_interface_ptr.createDog()).dog;

This method creates a new object of a DogInterfaceImpl inside the browser process.

Now we can communicate with this newly created object - this is an example of calling a method that doesn’t return anything:

await dog.setName('Z');

So you know how to communicate with being creator, people, dogs, cats, but what about food? You can see that it has an interface but no implementation. But we can create our own implementation in javascript, the difference is that objects of this constructor will be placed in the renderer process and the memory layout is much different that objects of C++ class.

All methods need to be asynchronous:

FoodInterfaceImpl.prototype = {
   getDescription: async () => {
   },
   setDescription: async (arg) => {
     return {'description': 'this is a description'};
   },
};

let food_impl = new FoodInterfaceImpl();
let food_impl_ptr = new blink.mojom.FoodInterfacePtr();
food_impl.binding.bind(mojo.makeRequest(food_impl_ptr));

// And we can provide it as an argument to person's method
await person.cookAndEat(food_impl_ptr);

Here is a bigger example which shows how to use added Mojo interfaces:

<html><body>
<script src="/mojo_bindings.js"></script>
<script src="/person_interface.mojom.js"></script>
<script src="/cat_interface.mojom.js"></script>
<script src="/dog_interface.mojom.js"></script>
<script src="/food_interface.mojom.js"></script>
<script src="/being_creator_interface.mojom.js"></script>
<script>
(async function poc() {
	
    // Create a js implementation of FoodInterface
    function FoodInterfaceImpl() {
      this.binding = new mojo.Binding(blink.mojom.FoodInterface, this);
    }
    
    FoodInterfaceImpl.prototype = {
        getDescription: async () => {
          console.log("GetDescription");
          return {'description': 'this is a description'};
        },
        setDescription: async (arg) => {
        },
        getWeight: async () => {
          console.log("getWeight callback");
          return {'weight': 100};
        },
        setWeight: async (arg) => {
        },
    };
	
    // Obtain an endpoint to send messages to BeingCreatorImpl object.
    being_creator_interface_ptr = new blink.mojom.BeingCreatorInterfacePtr();
    Mojo.bindInterface(blink.mojom.BeingCreatorInterface.name,
                       mojo.makeRequest(being_creator_interface_ptr).handle);

    // Create a PersonInterfaceImple object.
    let person = (await being_creator_interface_ptr.createPerson()).person;
    console.log(person);

    // Call some methods
    await person.setWeight(10);
    let weight = (await person.getWeight()).weight;
    console.log(weight);

    // Create FoodInterfaceImpl object
    let food_impl = new FoodInterfaceImpl();
    let food_impl_ptr = new blink.mojom.FoodInterfacePtr();
    food_impl.binding.bind(mojo.makeRequest(food_impl_ptr));
    
    // Call some methods
    await person.cookAndEat(food_impl_ptr); 
    weight = (await person.getWeight()).weight;
    console.log(weight);
    console.log("done");
})();
</script></body></html> 

We are almost done with basics but you need to know to which process attach the debugger. Below is a ps ax result after running chrome with GUI using command: ./chrome --enable-blink-features=MojoJS --disable-gpu

26267 pts/3    tl+    0:01 /home/f/Desktop/src/binary/chrome --enable-blink-features=MojoJS --disable-gpu
26270 pts/3    S+     0:00 /home/f/Desktop/src/binary/chrome --type=zygote
26272 pts/3    S+     0:00 /home/f/Desktop/src/binary/chrome --type=zygote
26291 pts/3    Sl+    2:08 /home/f/Desktop/src/binary/chrome --type=gpu-process --field-trial-handle=103463055034769538,7634138529180820187,131072 --gpu-preferences=KAAAAAAAAAAgAAAgAQAAAAAAAAAAAGAAAA
26295 pts/3    Sl+    0:00 /home/f/Desktop/src/binary/chrome --type=utility --field-trial-handle=103463055034769538,7634138529180820187,131072 --lang=en-US --service-sandbox-type=network --service-re
26310 pts/3    S+     0:00 /home/f/Desktop/src/binary/chrome --type=broker
26365 pts/3    Sl+    0:00 /home/f/Desktop/src/binary/chrome --type=renderer --file-url-path-alias=/gen=/home/f/Desktop/src/binary/gen --field-trial-handle=103463055034769538,7634138529180820187,1310
26378 pts/3    Sl+    0:00 /home/f/Desktop/src/binary/chrome --type=renderer --file-url-path-alias=/gen=/home/f/Desktop/src/binary/gen --field-trial-handle=103463055034769538,7634138529180820187,1310

The browser process in the first one - 26267 and the renderers are the processes with --type=renderer flag.

Understanding Callbacks

A vulnerability is very similar to this one described at p0 blog post. When you call CookAndEat method from javascript, GetWeight method of Food object is called and the result is passed to AddWeight. But the code looks a bit strange because code flow of interface implementations in C++ is based on callbacks:

void PersonInterfaceImpl::AddWeight(
    PersonInterfaceImpl::CookAndEatCallback callback,
    blink::mojom::FoodInterfacePtr foodPtr, uint64_t weight_) {
  weight += weight_;
  std::move(callback).Run();
}

void PersonInterfaceImpl::CookAndEat(blink::mojom::FoodInterfacePtr foodPtr,
                                     CookAndEatCallback callback) {
  blink::mojom::FoodInterface *raw_food = foodPtr.get();

  raw_food->GetWeight(base::BindOnce(&PersonInterfaceImpl::AddWeight,
                                     base::Unretained(this),
                                     std::move(callback), std::move(foodPtr)));
}

Methods of interface implementations don’t return values via return statement. Instead it works in a way that a method expects a callback as an argument (argument CookAndEatCallback callback in PersonInterfaceImpl::CookAndEat). Calling this callback informs that the method finished its execution, optionally a return value (or values) can be provided (in an argument of .Run()).

Inside PersonInterfaceImpl::CookAndEat raw_food->GetWeight is called that takes 1 argument which is also a callback - it will be called after GetWeight finishes. A function base::BindOnce binds a function with arguments, it’s similar to functools.partial in python. Here it binds PersonInterfaceImpl::AddWeight with all arguments it needs except of one - weight_.
The first but not visible argument of this method is this - it’s passed here as: base::Unretained(this) which is just wrapped this.
The second argument is std::move(callback) - our callback that we have got in an argument of PersonInterfaceImpl::CookAndEat.
When function raw_food->GetWeight ends, it passes its return value - weight_ to this callback (base::BindOnce(..)) as an argument - PersonInterfaceImpl::AddWeight is called with all needed arguments. Inside PersonInterfaceImpl::AddWeight the callback that we have got at the beginning is called (std::move(callback).Run();) which informs that PersonInterfaceImpl::CookAndEat finished its execution.

Now you can ask why we use normal return statement in javascript implementations of interfaces? It still works in a callback way but it’s invisible (look at GetWeight method, it returns a value, but after this PersonInterfaceImpl::AddWeight is called).

Exploitation

But where is the vulnerability? Surprisingly it’s possible to free objects of interface implementations from javascript using the following code:

person.ptr.reset();

So you can free a dog, person or a cat inside GetWeight method of FoodInterfaceImpl. PersonInterfaceImpl::AddWeight will be still called with this argument pointing to a freed area so with weight += weight_; we are able to change 8 bytes of memory to any value.

Classes PersonInterfaceImple, DogInterfaceImpl and CatInterfaceImple contain fields name, weight and age but the order is different for each class.

Person:

uint64_t age;
uint64_t weight;
std::string name;

Dog:

uint64_t weight;
std::string name;
uint64_t age;

Cat:

std::string name;
uint64_t age;
uint64_t weight;

This is a std::string structure for strings that are longer than 22 bytes:

char *ptr;
long length;
long capacity;

So if we free a dog and allocate a cat on this place, we are able to modify name.ptr field. The exploit written by Sergei Glazunov does this. Here is a full code to his commented out exploit. It works in following steps:

  1. Fries the dog
  2. It does feng shui in a way that cat A was placed in the place of the freed dog and name buffers of 2 cats are adjacent in the memory. The first character of a name is ID of a cat.

  3. It modifies name.ptr to a value of name.ptr of another cat.

  4. Cat B changes the name to a very big one.

  5. A new cat is created in the place of freed memory. Its name points to fake table array which at the beginning contains not meaningful data. by reading a name of cat A, we get the pointers to fake vtable, and real vtable of a cat. Then we can fill fake vtable buffer with appropriate pointers, gadgets, etc. Now we can overwrite real vtable of cat C to fake one and call a method on cat C.

References