Please write a full-chain exploit for Chrome. The flag is at /home/user/flag. Maybe there’s some way to tele<port> it out of there?
Hi, last week I participated in Google CTF 2020 with my team
Although I didn’t solve the challenge in time for the points,
still, here is a writeup for the challenge
teleport for you.
I like to write detailed articles that are understandable and replicable to my past self. Feel free to skip any parts. Here is a table of content for you.
- 1. Story
- 2. Overview
- 3. Leaking the browser process
- 4. Googling
- 5. Leaking the renderer process
- 6. Nodes and Ports
- 7. Leaking ports' names
- 8. What do we do with stolen ports?
- 8.1. Factory of network requests
- 8.2. Making the leaked ports ours
- 8.3. Sending our messages
- 8.4. Writing our messages
- 8.5. To know who our receivers are
- 8.6. Where are my factory ??
- 9. Closing words
You may want to checkout the exploit code.
No IDA/Ghidra were used during the creation of this work. I used only GDB.
The challenge files include a patch for chromium version 84.0.4147.94,
which basically has 2 features.
The first one is the
Pwn object, and a code execution(?) primitive
Both could be trivially used through
MojoJS, which is enabled for us.
2.1. Sandboxed or unsandboxed
On the first sight, the challenge seems unexpectedly easy, or wasn’t it ;)
rce primitive only provides us code execution inside the renderer process, which is strictly sandboxed.
Pwn object is on the unsandboxed browser process, provides an address leak of itself and an arbitrary memory read primitive.
2.2. Provided primitives
So we have 2 primitives
- Sandboxed code execution inside renderer process
- Arbitrary read inside browser process
3. Leaking the browser process
Pwn::this() will return the address of itself, which is a C++ object.
As every C++ object have its
vtable, containing pointers to all instance methods, located at offset
0x0. By dereference the pointer returned by
Pwn::this() twice, you will get a function pointer. Subtracting it to a constant offset, you can find the
_text base of the browser’s process.
Because no obvious way to get code execution inside the browser’s process, I started looking around on the internet and found this article,
Which is, by itself, interesting:
First the article is written by
Stephen Roettger, and you can find his name in
Second, these words in the article is also interesting:
… used from a compromised renderer
… if you have an info leak vulnerability in the browser process
Isn’t that was our case ;)
Later, my teammate found this speech, also given by Stephen
Wasn’t that a smart way to make people read your article and watch your talk? ;)
Anyway, I highly recommend you watch those to get a basic overview of the solution or even solve it yourself.
5. Leaking the renderer process
rce primitive in our hands, the sky is your limit…
First, we want a pointer in our renderer process to be able to reuse Chrome’s code.
Take a look at the
So the function copy our code into a newly-allocated Read-Write-Executable(rwx) page, and then execute it, right?
The above was the assembly equivalent for 3 last lines of code. There are 2 things worth mentioned:
rdiwill store the address of the rwx page
r15will store the address of our original buffer
This enables us to RETURN an arbitrary number of values from the shellcode by writing to
For me, I read the return pointer from
[rsp] to get a function pointer,
and derive the renderer’s
6. Nodes and Ports
Node could be understood as process; when you launch chrome, it will spawn multiple children to isolate their data in case of compromisation, and each of them is a node.
Node’s name is a 128-bit random integer
A node has multiple local ports listening for messages; each of them has an attached endpoint which will consume the messages.
Similar to node, port’s name is also a 128-bit random integer
A port is addressed using its node’s name and its name (
Knowing a port’s name and its node’s name is equivalent to being able to send messages to that port.
“[…] any Node can send any Message to any Port of any other Node so long as it has knowledge of the Port and Node names. […] It is therefore important not to leak Port names into Nodes that shouldn’t be granted the corresponding Capability.”
Security section of Mojo core
A node knows its own name
A port knows its name and its node’s name
7. Leaking ports' names
A node keeps track of its name, its local ports, and its remote ports (ports from another nodes that is known to this node)
By reading the browser process’s memory and traverse through
ports_, it’s possible to steal a privileged port.
One possible pointer path is
Just traverse that and dump all the ports' names.
7.1. Finding offsets
7.1.1. Simple structures
Finding offsets isn’t a trivial task when you haven’t familiar with assembly and memory, but a way to do that is to disassemble functions where that field is accessed.
If you are experienced in finding offsets, it is okay to skip this part.
For example, to find the offset of
g_core, you could try disassemble this function
this pointer is always passed as the first argument
and this time, it is stored in
The following code should be equivalent to the
Or the below should be equivalent to the return statement
So the offset is probably
The way of finding the remaining offsets is left as an exercise to the readers.
7.1.2. F**k C++/Traversing
Okay, now how do we dump all ports?
The worst thing about C++ containers is that their methods are inlined
One of our candidates for disassembling this time is
because it used the
Disassembling it will give you a loooong and complicated(?) function, but there are a few interesting points
There’s a nullcheck here, which is equivalent to this one. So
0x50 is probably where
Continuing the path through a bunch of calculation with constants:
The second statement of the above code is what we want.
[rax+r8*8] is an array access, with
rax holding the base address,
r8 is probably the array index and
8 is surely the element size.
And it’s definitely this line
__bucket_list_ is probably at offset
std::unordered_map works by calculate the
__constrain_hash() of the key and put the element(key-value) in the equivalent bucket.
Each bucket is implemented as a linked list.
At this point, it is reasonable for anyone to try to iterate all non-null elements(bucket) to dump all the elements by traversing the linked list.
However, this turns out to be a bad way to do so and I could even find duplicate elements and bad pointers.
However, with a bit of more time, you will find this defintion
__p1_.first will be our first element (
.begin() pointer, it is possible to iterate through all elements just like a linked list. Inspecting the memory, you will find that
+0x10 from the
+0x58) is a good educated guess for the
8. What do we do with stolen ports?
8.1. Factory of network requests
One of the good candidates for a good target is a privileged
URLLoaderFactory, which relies in the network service, and has the ability to make network requests (
URLLoaderFactory::CreateLoaderAndStart), with files
URLLoaderFactories are wrapped by
CorsURLLoaderFactories, which enforced CORS to all requests.
To isolate origins, factories created with renderers cannot be used to make requests to another origins.
However, the browser can create factories (
process_id_==kBrowserProcess) allowing arbitrary network requests with no CORS enforced to be made.
If we could get such factory from the browser, we could upload any files to our server.
However, I noticed a code path that allows you to create a large amount of privileged URLLoaderFactories using service workers. If you create a service worker with navigation preload enabled, every top-level navigation would create such a loader. By simply creating a number of iframes and stalling the requests on the server side, you can keep a few thousand loaders alive at the same time.
To do so is pretty trivial, just make sure to use HTTPS and you are good to go with the service worker.
8.2. Making the leaked ports ours
To send messages to the leaked ports' names, we need to register it to our node. Below is my way of doing it:
- Create a new port on our node using
- Initialize it with our leaked names using
After doing that, the leaked port will be inserted into your node’s
8.2.1. Calling functions from shellcode
It is impractical to run an assembler in the exploit to compile your shellcode with the functions' addresses, as they shift around all the time under ASLR.
However, I will stick to the assembly this time and use the
In my shellcode, there will be a common pattern, which looks like this
8.3. Sending our messages
Core object, there are some interesting APIs
The purposes of them are clear just by their names.
Core::SendMessage API takes a
MojoHandle message_pipe_handle (an
uint32_t) as a parameter, which is the receiving port.
To get a
MojoHandle, we can use the API
MojoHandle CreatePartialMessagePipe(const ports::PortRef& port),
which creates handles for our newly-created ports.
Later, I found the function
mojo::WriteMessageRaw, which takes our port’s
MojoHandle, message buffer, and an array of
MojoHandles(?) and send the message.
Unfortunately, it takes a C++ object
MessagePipeHandle, which is not so easy to create. So all I can do was replicate its function calls.
8.4. Writing our messages
If you take a look at the binding JS code (i.e.
MessageV0Builder to craft a message. That function will return a
Message object, which contains a buffer, and an array of handles.
Our message obviously should contain the buffer, but what are the handles?
URLLoaderFactory::CreateLoaderAndStart has 2 special parameters:
mojo::PendingReceiver<mojom::URLLoader> receiver and
PendingRemote indicate that these are shared objects, which are accessed through ports.
To pass these objects as parameters, you need to pass their handles, just 2
If you inspect the message generated by
MessageV0Builder, its array of handles will contain 2 elements, equivalent to
client. These elements are strings:
So we need to pass 2 handles, an
InterfaceRequest and an
Ptr. But how do we figure them out?
Here is the code to create a
mojo.makeRequest(client).handle also seems like a handle. Its implementation can be viewed here.
It seems to create a MessagePipe, which will create 2
handle0is binded to the passed
handle1is binded to a newly created
Lucky to us, handles are generated increasingly. So we can predict the handles of the
InterfaceRequest from the handle of our port.
8.5. To know who our receivers are
While creating this exploit, I ran into a programming bug which prevents my message buffer being copied. This leads me to discover a way to know which object is behind the port:
Mojo error in NetworkService:Validation failed for network.mojom.CookieAccessObserver [master] MessageHeaderValidator [VALIDATION_ERROR_UNEXPECTED_STRUCT_HEADER]
That’s it, now you know who are you sending to.
8.6. Where are my factory ??
I stucked and cannot find any factories within the leaked ports. There are even some ports which never responds to any of my messages.
At this point (ofc after the CTF has ended), Stephen points out my missing bit: I didn’t set the messages'
sequence_num. It seems like the Mojo system use this number to prevent message duplication.
This number is increased by one when a message is sent. Fortunately, the correct
sequence_num is stored in the
Port object in the field
next_sequence_num_to_send, which can be leaked from where we found our ports in browser process’s memory.
8.6.1. Setting the sequence_num
Let me remind you that
MojoMessageHandle is actually a pointer to a
UserMessageEvent. Unfortunately, the function
set_sequence_num is inlined so the offset isn’t free. However, you could get it by disassembling
8.6.2. Getting the correct function parameters
9. Closing words
The devil is actually in the details, isn’t it ;)
- To Stephen, for creating this challenge, and pointing out my missing bit (after the CTF, ofc). Thank you a lot.