monochromatic writeup
- 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:
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):
Now we can call a method e.g. CreateDog
on BeingCreatorImpl
object:
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:
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:
Here is a bigger example which shows how to use added Mojo interfaces:
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:
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:
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:
Dog:
Cat:
This is a std::string
structure for strings that are longer than 22 bytes:
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:
- Fries the dog
-
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.
-
It modifies
name.ptr
to a value ofname.ptr
of another cat. -
Cat B changes the name to a very big one.
- 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
- Inside look at modern web browser (part 1) - Overview of various chrome processes and their purpose
- Inside look at modern web browser (part 2) - Something about renderer and browser process
- Mojo - A main Mojo page with different links
- Intro to Mojo & Services - The best Mojo tutorial you can find
- p0 blog post - A very similar vulnerability