Search This Blog

Monday, July 12, 2021

RedpwnCTF 2021 Chromium SBX Tasks Writeup (Empires and Deserts)

This weekend, I participated in RedpwnCTF with my team Starrust Crusaders under the alias "The Static Lifetime Society", coming in second place overall. Since this was my first time doing a Chromium SBX challenge, I thought it would be a good idea to make a writeup. I'm still relatively unfamiliar with Mojo concepts and sbx escape, so feel free to point out any mistakes I make.

Here are some relevant resources I used to help me understand Chromium's architecture and sandbox escape techniques. I would recommend giving them a read before continuing.

Intro to Mojo

NotDeGhost's Intro Post about SBX

Chromium Architectural Overview

Git Repo of Previous Real World SBX Escapes

PlaidCTF Mojo Writeup

Google Quals 2019 Monochromatic Writeup


We were given a Chromium binary, mojojs bindings, and a source patch. The only difference between the two parts is that part one is run with the CTF_CHALLENGE_EASY_MODE environment variable set. Chromium is also run with MojoJS bindings enabled. Usually in fullchain exploits, one would have to compromise the renderer first, overwrite the blink::RuntimeEnabledFeatures::is_mojo_js_enabled_  variable, refresh the page, and then attempt to escape the sandbox. Since we already have bindings enabled, we won't have to worry about any of that. Here's the provided patch:

The Wreck struct holds a size, a length_to_use, an optional BigBuffer, and a DesertType enum with variants DESOLATE and EMPTY. Sand is an array of wrecks. The most important struct is Ozymandias, which has the methods Visage and Despair,  a pointer reserved for a mmap page and an UnguessableToken. UnguessableTokens are cryptographically secure 128 bit random values. Note the author commented that the struct is 0x100 bytes, which can be verified as well when it hits in the new operator in that function (find the mangled name, and attach to the main privileged browser process to break).

The CreateKingofKings functions allows us to have the renderer request an interface from the browser process for the Ozymandias objects. Despair loops over the wrecks in a sand argument, allocating a new uint8_t array with a size determined by the wreck's size field. It memsets it as 0 in both options (although the first option doesn't seem so from source, I believe the compiler optimized the argument to 0 as you can see in disassembly). For the DESOLATE option, if your BigBuffer has data and a size greater than or equal to the wreck's size field, it transfers the contents to the uint8_t array up to the array's size. Then, it creates a base::span (which is like std::span) based on the allocated uint8_t array's start and your current wreck's length_to_use field. Visage is a backdoor; it requires a uint8_t vector and an UnguessableToken. If your token matches the current Ozymandias's UnguessableToken, the vector will be copied to a mmap rwx page and run as shellcode. One last thing to note is that when disassembling the constructor, you can see the UnguessableToken is a static variable. While randomized in different browser instances, it will remain the same within the same browser usage session.

Since the CTF_CHALLENGE_EASY_MODE env variable is set, we have a trivial heap OOB read because the length_to_use can now be bigger than size and the BigBuffer constructed from the span later will use length_to_use to bound the span. Moreover, the part that is unbounded by the size but bounded by the length_to_use will remain uninitialized. As the author mentioned, this concept is based on this real life sbx escape bug

Based on these facts, the exploit plan is pretty simple. I don't know much about PartitionAlloc behavior, but it's pretty easy to see from a few leaks of uninitialized memory that chunks of similar sizes get returned in the same regions. This means that we can just spray some Ozymandias objects, and then also spray some chunks that allow for OOB uninitiailized read, and leak the token so we can abuse the backdoor. 

Now, it should be pretty easy to just send in a reverse shell shellcode and escape the sandbox. However, one issue does remain, in that the mojojs bindings for UnguessableToken requires JS numbers, which won't preserve the accuracy for many possible token values. However, this is a simple patch. I performed the following change in the bindings to encode them as doubles:

Then, this was my final exploit (I just used a x86_64 linux rev shell shellcode from shellstorm):

As for the html website to send to the browser bot, I used the following (which was based off of NotDeGhost's post):

Here was the result from running the exploit on remote from my teammate's VPS (thanks Strellic!):


Now, the env variable is disabled. So what could possibly allow us to leak UnguessableTokens then? I have to admit that this step took an embarrassingly long time.

I first spent several hours reading up on this post about common Chromium IPC vulns and tried to compare them to the diff. Nothing was found this way.

Another idea teammates hypothesized was that maybe there is a TOCTOU race condition between when it checks for size and length_to_use and when length_to_use is used. However, data is serialized when passed to privileged browser process, so us trying to race in renderer is useless and will not apply changes there (and the window is too small since we need to abuse sizes of approximately 0x100).

Lastly, what really caught my attention is that if you have it not hit any of the cases in the switch statement, you will still get an uninitialized read. To do that, you need a new enum value, and unfortunately, Mojo validates all enums (unless the keyword extensible is used) along with several other validation checks. I thought this was an interesting scenario as we can change how we send things from the mojojs bindings and wanted to see if validation can be bypassed somehow. As I was messing around with wreck types, I noticed that I suddenly achieved a leak when I set my BigBuffer tag to 2, which stands for invalid storage. Debugging the codeflow, I noticed that the contents of the DesertType enum became 0 there somehow (if you set it to zero normally from renderer before serializing and sending, a validation error will occur and your renderer process will be killed). I wasn't too sure why, but according to NotDeGhost after my solve, it is because this enum causes deserialization to fail, so the rest of the struct will not be populated (hence leaving DesertType to 0). The NOTREACHED() statement in the diff is not compiled into release builds, allowing for us to trigger this bug and get uninitialized read when it works with wreck structs.

This time, however, we can't just spray some allocations and have an OOB leak data outside the chunk. We will need to free some OzymandiasImpl objects, which can be done pretty easily with .ptr.reset() as it is an interface implementation. Here is the final exploit:

Here is the result from remote:

Overall, I thought these were amazing challenges and finally pushed me to mess around a bit with Chromium sandbox escapes. Though introductory challenges into this complex topic, many of the concepts and basic techniques can probably be re-applied on more difficult sandbox escape challenges.

Here is the author's writeup! Make sure to check it out.

No comments:

Post a Comment