This page looks best with JavaScript enabled

Google CTF 2020 teleport: Chromium sandbox escape

 ·  ☕ 13 min read

Teleport

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?

1. Story

Hi, last week I participated in Google CTF 2020 with my team pwnPHOfun

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.

You may want to checkout the exploit code.

No IDA/Ghidra were used during the creation of this work. I used only GDB.

2. Overview

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 Mojo::rce

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 ;)

But the rce primitive only provides us code execution inside the renderer process, which is strictly sandboxed.

The 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

The primitive 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.

4. Googling

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 @_tsuro or Stephen Roettger, and you can find his name in chall.patch

  • 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

With the 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 Mojo::rce function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  void Mojo::rce(DOMArrayBuffer* shellcode) {
    size_t sz = shellcode->ByteLengthAsSizeT();
    sz += 4096;
    sz &= ~(4096llu-1);
    void *mm = mmap(0, sz, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
    ...
    memcpy(mm, shellcode->Data(), shellcode->ByteLengthAsSizeT());
    void (*fn)(void) = (void (*)(void)) mm;
    fn();
  }

So the function copy our code into a newly-allocated Read-Write-Executable(rwx) page, and then execute it, right?

1
2
3
4
   0x0000000009088315 <+277>:	mov    rdi,rbx ; dest
   0x0000000009088318 <+280>:	mov    rsi,r15 ; source
   0x000000000908831b <+283>:	call   0xa00f7e0 <memcpy@plt>
   0x0000000009088320 <+288>:	call   rbx

The above was the assembly equivalent for 3 last lines of code. There are 2 things worth mentioned:

  • rbx and rdi will store the address of the rwx page
  • r15 will store the address of our original buffer

This enables us to RETURN an arbitrary number of values from the shellcode by writing to [r15+X],
then read it back in JavaScript.

For me, I read the return pointer from [rsp] to get a function pointer,
and derive the renderer’s _text base.

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 (node:port)

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

1
2
3
4
5
class COMPONENT_EXPORT(MOJO_CORE_PORTS) Node {
  ...
  const NodeName name_;
  ...
}

A port knows its name and its node’s name

1
2
3
4
5
6
7
class Port : public base::RefCountedThreadSafe<Port> {
  // The Node and Port address to which events should be routed FROM this Port.
  // Note that this is NOT necessarily the address of the Port currently sending
  // events TO this Port.
  NodeName peer_node_name;
  PortName peer_port_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)

1
2
3
4
5
6
7
class COMPONENT_EXPORT(MOJO_CORE_PORTS) Node {
  ...
  const NodeName name_;
  ...
  std::unordered_map<LocalPortName, scoped_refptr<Port>> ports_;
  ...
}

By reading the browser process’s memory and traverse through ports_, it’s possible to steal a privileged port.

One possible pointer path is g_core->node_controller_->node_

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 node_controller_ in g_core, you could try disassemble this function

1
2
3
4
5
6
NodeController* Core::GetNodeController() {
  base::AutoLock lock(node_controller_lock_);
  if (!node_controller_)
    node_controller_.reset(new NodeController(this));
  return node_controller_.get();
}

this pointer is always passed as the first argument

1
  0x0000000003723fba <+10>:	mov  r15,rdi

and this time, it is stored in r15 register

The following code should be equivalent to the if(!node_controller_)

1
2
  0x0000000003723fc9 <+25>: mov  rbx,QWORD PTR [r15+0x30]
  0x0000000003723fcd <+29>: test rbx,rbx

Or the below should be equivalent to the return statement

1
2
3
4
5
  0x0000000003723ffd <+77>:  mov  rbx,QWORD PTR [r15+0x30]
  0x0000000003724001 <+81>:  mov  rdi,r14
  0x0000000003724004 <+84>:  call 0xa00fad0
  0x0000000003724009 <+89>:  mov  rax,rbx ; return value
  0x000000000372400c <+92>:  add  rsp,0x8

So the offset is probably +0x30.

The way of finding the remaining offsets is left as an exercise to the readers.

7.1.2. F**k C++/Traversing std::unordered_map

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int Node::GetPort(const PortName& port_name, PortRef* port_ref) {
  PortLocker::AssertNoPortsLockedOnCurrentThread();
  base::AutoLock lock(ports_lock_);
  auto iter = ports_.find(port_name);
  if (iter == ports_.end())
    return ERROR_PORT_UNKNOWN;
...
  *port_ref = PortRef(port_name, iter->second);
  return OK;
}

because it used the .find method

Disassembling it will give you a loooong and complicated(?) function, but there are a few interesting points

1
2
3
4
  0x0000000006d6c443 <+51>:	mov  rdi,QWORD PTR [r14+0x50]
  0x0000000006d6c447 <+55>:	mov  r12d,0xfffffff6
  0x0000000006d6c44d <+61>:	test rdi,rdi
  0x0000000006d6c450 <+64>:	je   0x6d6c5d2 ; not found

There’s a nullcheck here, which is equivalent to this one. So 0x50 is probably where bucket_count() is

Continuing the path through a bunch of calculation with constants:

1
2
3
  0x0000000006d6c4f4 <+228>: mov  rax,QWORD PTR [r14+0x48]
  0x0000000006d6c4f8 <+232>: mov  rax,QWORD PTR [rax+r8*8]
  0x0000000006d6c4fc <+236>: test rax,rax

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

1
__next_pointer __nd = __bucket_list_[__chash];

So __bucket_list_ is probably at offset +0x48

An 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

1
2
3
4
__bucket_list                         __bucket_list_;
pair<__first_node, __node_allocator>  __p1_;
pair<size_type, hasher>               __p2_;
pair<float, key_equal>                __p3_;

So __p1_.first will be our first element (.begin()).

With the .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 __bucket_list_ (+0x58) is a good educated guess for the .begin() pointer.

Reference

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.
@_tsuro

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:

After doing that, the leaked port will be inserted into your node’s ports_ map.

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.

There are probably many ways of doing this, including ways that allow you to call functions directly from JavaScript.
However, I will stick to the assembly this time and use the Mojo::rce primitive.

In my shellcode, there will be a common pattern, which looks like this

1
2
  mov rax, 0x4141414141414141
  call rax

The 0x4141414141414141 value will be encoded as it as 8 consecutive little-endian bytes in the machine code. The JavaScript code will be responsible to replace it with the correct address calculated from the leak.

8.3. Sending our messages

In the Core object, there are some interesting APIs

The purposes of them are clear just by their names.

However, the 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. URLLoaderFactoryProxy.prototype.createLoaderAndStart), you will see that it uses the JavaScript API 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?

The function URLLoaderFactory::CreateLoaderAndStart has 2 special parameters: mojo::PendingReceiver<mojom::URLLoader> receiver and mojo::PendingRemote<mojom::URLLoaderClient> client. PendingReceiver 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 uint32_t to Core::AppendMessageData.

If you inspect the message generated by MessageV0Builder, its array of handles will contain 2 elements, equivalent to receiver and client. These elements are strings: URLLoaderInterfaceRequest, and URLLoaderClientPtr, respectively.

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 URLLoaderClient

1
2
3
4
5
6
  var client = new network.mojom.URLLoaderClientPtr();
  Mojo.bindInterface(
    network.mojom.URLLoaderClient.name,
    mojo.makeRequest(client).handle,
    "process"
  );

This parameter mojo.makeRequest(client).handle also seems like a handle. Its implementation can be viewed here.

1
2
3
4
5
6
7
8
9
template <typename Interface>
InterfaceRequest<Interface> MakeRequest(
    InterfacePtr<Interface>* ptr,
    scoped_refptr<base::SequencedTaskRunner> runner = nullptr) {
  MessagePipe pipe;
  ptr->Bind(InterfacePtrInfo<Interface>(std::move(pipe.handle0), 0u),
            std::move(runner));
  return InterfaceRequest<Interface>(std::move(pipe.handle1));
}

It seems to create a MessagePipe, which will create 2 MojoHandles: handle0 and handle1.

  • handle0 is binded to the passed Ptr
  • handle1 is binded to a newly created InterfaceRequest

Lucky to us, handles are generated increasingly. So we can predict the handles of the Ptr and 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:

By sending an invalid message (set the first uint32_t (num_bytes) to 0), you can trigger a validation error at this line and the verbose logging will print something like this

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 Node::PrepareToForwardUserMessage

8.6.2. Getting the correct function parameters

This is a trivial part. Just take a look at the JavaScript Mojo binding code.

9. Closing words

The devil is actually in the details, isn’t it ;)

9.1. Shoutout

  • To Stephen, for creating this challenge, and pointing out my missing bit (after the CTF, ofc). Thank you a lot.

9.2. Reference

Share on

Nguyen Hoang Trung
WRITTEN BY
Nguyen Hoang Trung
Hobbyist Security Researcher