0ddc0de: Welcome to our talk, a fiasco in Android Trusted Execution Environments, and the
overwhelming amount of audience interested in Trusted Execution Environments.
As we said, we are Philipp, gannimo, and me, myself, 0ddc0de, we are
security researchers working at EPFL in Switzerland, and we do all sorts
of systems and software security, and this is one of our little adventures
into the Android software stack targeting Trusted Execution Environments.
The whole story starts with a device that ended up in our offices.
This was a Xiaomi Redmi Note 11S 5G, and as a good security
researcher, you of course want to turn this device into a
playground so that you can do actual security research on there.
And what you need for that is, of course, root on these devices, right?
And how you do this nowadays, just a quick wrap-up, you go to the Settings menu,
find this magic version entry, click it five times, you become a developer, okay?
First step.
Then you need to find this OEM unlock feature that lets you unlock
the bootloader to change some of the partition on this device, okay?
Also kind of easy on these devices. Then you need to create a Mi account, right?
Some Xiaomi internal thing, and bind it to your device
that has an active SIM card with a working mobile data
connection, and only then they will let you unlock your device.
Then you need to install some shady Windows -only software to unlock your device.
And Xiaomi is so nice to let you rethink for seven days, 168
hours, if you actually want to really unlock your device, right?
So you sit back, you need to wait for another seven days, then you come back
to your shady Windows software, and they actually let you unlock it, okay?
That's kind of halfway through.
Then you go the regular way of just magisk -rooting your device, changing some of
the partition to include your su binary, and then you can escalate your privileges.
So you are basically there, right?
You have full control over the user space, and you can potentially
modify the boot partition where the kernel, the Linux kernel
resides, and you basically have full control over the entire device.
But this is not really where the story ends, right?
So you have this decision to make, right?
Like, do you take the blue pill, and you wake up next morning, and you're
still rude, everything is fine, and you believe what you want to believe?
Or you take the red pill, and you join us on the
adventure into the deep, deep internals of these systems.
And of course we're going to take the red pill, right?
So a trusted execution environment is this kind of separated
execution stack that runs alongside your normal software stack.
So you're probably familiar with the left side here,
your Netflixes, your Duolingo, and whatnot, right?
And the Linux kernel running on these Android devices.
And then you have the separate execution context
called the trusted execution environment here in green.
And all the kind of security sensitive workloads are handled in here.
So your biometric authentication, when you enter a pin
pattern or password, this is something happening in there.
Like the keys and cryptographic key management involved in your full disk
encryption and file-based encryption, this is all related to this execution context.
And the kind of hardware mechanism behind this is ARM trust zone
on modern smartphones that are typically based on ARM chips, right?
So as we heard in the introduction, trusted here means that it's vendor trusted, right?
You as the owner of the device, you don't get to execute or modify
any sort of code running inside of this execution environment.
Like dynamic analysis and kind of third-party based kind of security
research into this context is very, very tedious and locked down.
So you, as the owner of the device, don't get to do that.
To give you a little bit more of an actual technical detail, so
ARM came up with this idea of separating their execution modes
into like two contexts, the normal world and the secure world.
And the secure world is kind of synonymous with the trusted execution environment.
And here you have four different privilege levels that ARM calls exception levels.
And the lowest exception level, exception level zero, hosts
the user space applications in both of these contexts.
Below that, on exception level one, you have the operating systems on Android.
That's typically here on the left side a Linux kernel.
And you can switch to the higher privilege level using privileged instructions, right?
The SVC, the supervisor call instruction here.
Then EL2 is typically reserved for hypervisors.
That's not really relevant in the scope of this talk. We won't talk about this.
And then at the lowest and the highest level, you have the secure monitor, right?
And you can reach this with another privileged
instructions from the operating system layer using SMCs.
This is a privileged instruction for secure monitor calls.
So this secure monitor has a very important function.
It is there to switch between these execution modes
using like a bit in one of the status registers.
So now you need to switch back and forth between these
modes, kind of doing this dance to go back and forth.
And what that enables is this IPC channel between applications
that run in your user space and applications that run in the
user space of the trusted execution environments, right?
This is where like your fingerprint application would
run and handle your biometric authentication data.
So the threat model here is that the untrusted context here on the left, right?
So the user space and the operating system is considered untrusted in this context.
And you have the immediate attack surface here going down to the secure
monitor and going down to the trusted applications using this IPC channel.
And in the context of the talk, the next slides will focus on this IPC
layer and how we used this attack surface to gain a code execution inside
one of the trusted applications on the device we were targeting here.
And the vulnerability goes back to some work we did
prior to this talk that we dubbed global confusion.
And this was like a pattern of bugs in trusted
applications all across the Android TE ecosystem.
And it goes something like this.
So you can have this world split and there's this trusted application that wants
a reference to memory where it can find some data and process some data, right?
So a pointer, essentially.
But now a client application comes by and thinks,
well, which is going to pass a value, okay?
So instead of a pointer, it's passing a value.
This value is passed down to the rich operating system over to the trusted operating system.
And the trusted operating system happily just forwards it to the trusted application.
And now in the design of these APIs used in here, they made the mistake that
they gave all the responsibility to kind of check for this type confusion.
If I'm handling a pointer or a value, a scalar, in the context of
this trusted application, they delegated this to the developer, right?
And, of course, some of them just forget to do this check.
And what happens is that you essentially have a raw
pointer into the address space of this trusted application.
And depending on the logic of this trusted application, you immediately get a
read or write primitive in the virtual address space of this trusted application.
And that's exactly what we found here in one of the trusted applications
that were shipped on Xiaomi devices in the key install trusted application.
And there's a little bit of simplified C code that illustrates this vulnerability.
Like we made this a bit easier to just show you what is happening.
We have two parameters here. One of them is this pointer to a key block.
A key block has a little bit of a header. And there's a key count.
And the key count indicates how many key slices will follow.
Each of these key slices have a key type, okay?
So here we only have one key slice.
Then you have this other parameter, a key types out, which is just an array that
should get all the different key types that are contained in this key block, okay?
So here it would just copy 0x42 to this key types out array.
Now, unfortunately, this trusted application in some version suffered from
this global confusion vulnerability that I've just been talking about.
And the key types out pointer is subject to this type confusion,
where we can immediately have a pointer pointing to an arbitrary
location in the address space of the trust application.
That gives us an immediate, like, four-byte write
primitive into the address space of this trust application.
That's all fine, because MediaTek patched this vulnerability after we disclosed it in 2023.
But unfortunately, the trusted execution environment deployed on many of
these devices don't support a feature called rollback prevention, right?
Where you can just take an outdated and vulnerable version of the
same trusted application, deploy it on the device, and just load this
vulnerable blob and can still make use of the capabilities, right?
And that's what we did here.
If you want to know more details about global confusion, this
rollback issue, we have a talk from Black Alps last year and some
academic publication with Usenix security also from last year.
So how do you actually carry out one of these rollback attacks?
Well, in Magisk, that's pretty easy. You have this overlay file system feature.
We have this magic directory.
You kind of define the files and the file structure that you want to overlay.
You reboot the device, and then the system just
overlays the files that you specified there, right?
So from there, as you can see here on the slide, this trusted
application will be overlaid with what we put there, and we
effectively already carried out this rollback attack once we load it.
So now we have this four-byte write primitive, but where do we write in the address space?
And in some older Xiaomi devices, there was this device node
exposed by the Linux kernel where we could get a little bit
of logging output from the trusted execution environment.
Now, unfortunately, in later devices, like the one we were dealing with here, they
decided to add some encryption logic to make our life a little bit more painful.
We dug down and kind of tried to find out how this encryption works.
So you load this trusted application.
Those are like nicely loadable ELF files into your favorite disassembler and decompiler.
You'll find this function, which is the printf function
essentially logging to this logging infrastructure, and you
kind of chase this function across the different shared objects.
And what you will find, you will end up in this shared object you can see here.
There's this disable log encryption flag, right?
So in the one case, it will print everything in plain text, and in the other
case, when it's set to zero, it will print everything that is encrypted, right?
So now our goal is, of course, set this flag in some way.
Looking through the dynamic symbols of these shared objects,
we found that the symbol is undefined in most of these shared
objects shipped with this trusted execution environment.
But in one of them, which is the libuc_c.so, it's defined in the BSS segment, right?
So since the trust application here are lacking basically any sort of
exploit mitigation, there's no address space layout randomization, we
could just kind of brute force our way using this 4-byte write primitive
and find this offset in this shared object to disable the encryption.
That's what you see on this slide, so you kind of start probing, and once
you've found it, there's a bit of the log string that is encrypted before
we disabled the encryption, and then later the plain text shows up again.
Now, that's all good.
We have a bit of kind of introspection into what is happening
inside of the TE now, but we want to escalate our privileges further
and actually get code execution inside of this thing, right?
So, and very similar to other Linux-based systems, although this one is not Linux-based,
they implement a mechanism to resolve function pointers to other shared objects
using this procedure linkage table and global offset table mechanism, right?
Where you have essentially a table here of function pointers that kind of get
adjusted to wherever the actual function is located during dynamic linking time.
And so one easy way to just hijack the control flow is use our
4-byte write primitive and overwrite the pointer here, right?
This is what we did.
There's a bit of code inside of this key install
trusted application that looks something like this.
We allocate a little chunk of memory, and then we copy
from a buffer that we control data into this chunk.
Some stuff is happening, and then later free is called on this chunk again, right?
So, and as I said before, now we can basically
change function pointers inside of this binary.
So we change the TE free to a log printf function, and
we control everything that's inside of this heap chunk
just to convince ourselves that hijacking the control flow here works.
And indeed, instead of freeing this chunk, right, just the string that we
provided is printed in the log of this trusted execution environment now.
So now we can kind of hijack function pointers and call
like into existing functions, but we thought, well, this is
the entire mitigations and how this looks like is so bad.
They will definitely have some readable, writable, and
executable memory in there so that we can just like bring
our own shell code and execute arbitrary logic that we want.
And we probe for this just using this technique here.
You have different pages with different permissions.
We just used our write primitive to kind of probe all the different pages, okay?
If you have unmapped memory, if you have memory with no
permissions, only readable permissions, or readable and executable
permissions, it will end up in a crash immediately, okay?
So that's kind of excluded.
Then you have the scenario where you have a read and writable page.
There we can actually write our shell code, but then later, when we
do this trick again, right, where we adjust a pointer in the global
offset table and change the TE3 to a pointer to that memory page,
and we jump to the page we just inserted our shell code to, it
will also result in a crash because it's not executable, right?
We just diverted control flow to a page that is not executable.
And then in the last scenario here where we actually have
a readable, writable, and executable page, we can dump our
shell code, and we can actually jump there and safely return.
And this is our oracle to get to know, okay, we actually own this thing now.
So just to demonstrate, like a little demonstration, of course,
we want to do something reasonable instead of just returning.
So we implemented a little bit of a Fibonacci thing here, just injected
that, and our trusted application can now calculate Fibonacci numbers.
That's a very important capability.
And now we'll hand over to Philip, who will walk you through what kind of
interesting things you can actually do when you, from this context now,
target the higher privilege levels from this trusted application.
Philipp: Okay, so we're executing code at secure exception level zero,
so that's the user space of the trusted execution environment.
So we have code execution in the TE, but why not go further?
Why not completely compromise the application processor?
Let's get code execution at EL3, so in the secure monitor.
Intuitively, since we already have code execution in a trusted application,
what makes sense to attack would be all of the functionality inside of the TE
that's not reachable from the normal world.
So if we escalate our privileges enough inside of the trusted execution environment,
we should be able to fairly easily pwn the secure monitor.
To actually escalate privileges inside the TE, we need to know what is running there,
because there are a lot of different TE implementations,
and they have vastly different architectures.
We are looking at a Xiaomi device with a MediaTek system on
chip, and that means the TE implementation is the BeanPod TE.
And what's special about the BeanPod TE is their system
actually runs on top of the L4E, or aka Fiasco, microkernel.
And that makes actually escalating privileges inside of the TE a bit more interesting,
because if we were dealing with like a monolithic kernel, we just attack the TZOS kernel,
we'd get a secure exception level 1 code execution, and then from there,
that's basically equivalent to having code execution at EL3.
But because it's a microkernel, attacking the microkernel is not super feasible, right?
The thing we're dealing with here, Fiasco, it's open source, probably well audited,
even though it's an older version, so not the most promising target.
So what can we actually do? Well, the thing that's left is the user space, right?
Might sound counterintuitive, but hear me out.
So one of those binaries is going to be our trusted application,
and then there's going to be a bunch of other user space binaries
that communicate with each other over inter-process communication.
And because the kernel itself doesn't implement any privileged functionality,
there's going to be some user space processes that have privileges
that if it was a monolithic kernel design, that stuff would be running in the kernel.
But because it's a microkernel, now it's running in the user space.
And if we pwn just the right one of those highly privileged user space processes,
or enough of them, we can probably use that as a jumping-off point
to then pwn the secure monitor. So that's the plan.
First off, figure out with which processes we can communicate over IPC,
find a bug that we can trigger over IPC, exploit that bug to pivot
to that other user space application,
and then rinse and repeat until we pwn a process that's privileged enough.
So let's start. First off, what can our key install TA communicate with?
The way IPC works in Fiasco is you have a channel with a client and a server end,
and processes have capabilities that give them access
to either the client end and the server end.
So if a process has access to the client end of a channel,
it can send IPC messages, so it can be a client.
And then if a process has access to the server end,
it can act as an IPC server and receive those messages.
First question, what capabilities does our trusted application have?
We'll do this dynamically.
Every Fiasco process has a L4E global env in its address space,
and this thing stores a pointer to a structure
that stores the current process's capabilities.
So what we're going to do in our shellcode
is we're just going to iterate over this structure
and basically print out the name of all the capabilities
our trusted application has.
That gives us the channels that our trusted application can send IPC messages to.
The second thing we need to know is,
okay, what is actually listening on the other side?
What are the binaries that we need to reverse engineer to find the bugs?
The way we did this is we looked at the Lua script
that's used to bootstrap the user space of the Fiasco.
Well, the Fiasco uses to bootstrap the user space.
Basically, this Lua script sets up all of the IPC channels
and then starts processes.
Here is like a snippet where the SST server binary started,
and you have this caps argument
where on the right side are basically the capabilities based on the channels.
And for example, this SST server
has the server capability for the UT SST system channel.
So our trusted application, if it has access to this UT SST system channel,
an IPC message sent from there would be handled in the SST server itself.
And so basically combining the capabilities and this Lua script,
we get this list of basically the attack surface
that's reachable from IPC of our trusted application.
So on the left side are the capabilities, on the right side are the binaries.
And for this talk, we'll look at two, a memory client and a SST client.
One of those is going to be enough to basically pwn the secure monitor,
and the other one is a sort of thought experiment.
So we're going to look first at the memory client capability,
which allows our trusted application to talk to the UT memory binary.
Why do we look at this one specifically?
Well, this is the snippet in the Lua script
where this UT memory IPC server started. And this UT memory IPC server
has a very interesting capability. It's called Sigma Zero.
And in Fiasco, Sigma Zero is a user space process
that has the ability to map arbitrary physical memory.
And any other user space process that has the Sigma Zero capability
can talk to the Sigma Zero process over IPC
and ask Sigma Zero to map arbitrary physical memory.
So basically, this UT memory process is super privileged.
It has these kernel-like privileges,
which is it can map arbitrary physical memory.
So if we pwn this UT memory process, we can map arbitrary physical memory,
and we're basically done. So we need to find a bug in the dispatch function
of this UT memory server. This is just some cleaned-up decompilation code
of the main function of the UT memory server. Basically, what's happening
is there's this UT memory server object. It has two fields, a Vtable,
that it's a pointer to a bunch of function pointers.
One of those function pointers is the dispatch function
that is actually handling incoming IPC messages.
And there's also a Sigma Zero capability field
that stores the Sigma Zero capability. And then this server object is registered
with the TZ memory IPC channel. That is the IPC channel
that our trusted application can communicate with.
And then the dispatch function itself doesn't do much.
It just reads out a bunch of IPC arguments
and then calls the Sigma Zero IPC function,
where the first argument is the Sigma Zero capability,
and then a bunch of our IPC arguments are in there.
And this Sigma Zero IPC function is a copy
of the map mem function inside of libsigma0,
which is part of the source code of Fiasco.
And the libsigma0 is essentially the client library that clients of Sigma0 use
to ask Sigma0 to map arbitrary physical memory.
So what is happening is we're sending an IPC request
from our trusted application to UT memory.
The IPC request arguments essentially get forwarded
to Sigma0 via the map mem function. So at this point, we were like, okay,
probably we can just map arbitrary physical memory with our trusted application.
What's the easiest way to test this out?
We don't want to make the IPC request ourselves because it's kind of annoying.
You need to set up like an IPC structure, all that stuff.
Instead, what we're going to do is we're just going to use the client library
of this UT memory binary. This is in libre_mem_base.so.
There's this utpfmmap function. And after some reverse engineering,
we figure out the signature. It's basically physical address high,
physical address low for the 64-bit physical address,
size, permissions, and a pointer where the virtual address will be written to.
And we can just call this function in our shellcode
and see if we're able to map physical memory. What do we want to map?
Well, the plan is to attack the secure monitor. So let's just try to map this guy.
This is an output from /prog/ atflog, which on MediaTek devices
basically gives you the secure monitor logging output.
And this thing graciously prints the physical address
where the secure monitor is mapped.
So we're just going to plug this into our shellcode,
and we're going to look through the mapped memory
for a string that we know has to be there based on the secure monitor log.
And lo and behold, it actually works.
We can actually use this memory client capability to map the secure monitor.
So that's it. All this talking for pretty much our trust application
is already one of those super privileged user space processes
that can map arbitrary physical memory. The only thing that's left to do
is actually get code execution in the secure monitor.
There's two things that we need to consider.
So we can map the physical memory of the secure monitor.
We can map the executable section of the secure monitor,
read-writable in our trust application,
and we can basically patch the code of the secure monitor.
One problem is that the executable section of the secure monitor is very crowded.
There's no area where there's clearly no code. It's all full of code,
and if we clobber the wrong thing with our shell code,
the phone's going to crash in the secure monitor,
and then we need to reboot the phone, and it's very annoying.
So that's one problem. Where do we write our shell code?
Second problem is how can we trigger our shell code
where we're not relying on another system component to trigger it?
We want to trigger it ourselves.
We solved the first problem by basically writing... So, okay, first,
so the secure monitor, as Marcel mentioned before,
basically waits for SMC calls from the EL1 kernel level,
and the MediaTek secure monitor
basically has, like, different SMC handling functions for different stages
as well as different sort of which component is making the SMC call
and has one function that's handling SMC calls during the boot stage.
And, well, at the point where we're running our exploits,
the phone has already booted, so all that code is hopefully dead code,
and that's where we're going to write our shell code,
inside of the SMC handling functions during the bootloader stage.
Second question, how can we trigger the shell code on demand?
Basically, there's this SMC handling function
for SMC calls coming from the normal world Linux kernel,
and this SMC function doesn't do anything. It just returns zero.
So what we're going to do is we're just going to patch this return zero
to, instead of returning zero, jump to our EL3 shell code, execute our shell code,
and then our shell code is just going to return zero itself.
So just in case this thing actually gets used, nothing would break,
because, well, nothing is supposed to happen except the return zero.
Finally, how can we actually trigger this SMC call?
Basically, we need to do the SMC call from EL1,
but both our proof of concept in, like, the normal world
and the trust application we compromised is running at EL0.
How can we make the SMC call?
Well, we're just going to use the memory client capability
to patch the Linux kernel. We're going to patch the sys_quotactl system call,
which, based on the Android source code, isn't used anywhere,
so hopefully we're not going to break anything.
We're going to patch this sys_quotactl system call
to, instead of doing whatever the system call normally does,
just make the SMC call to trigger our patched SMC handler.
And I know this is kind of convoluted, so I have the diagram here.
Basically, what happens in the beginning,
okay, our proof of concept exploits the trusted application,
and we're running our shell code there.
Then we use the memory client capability to map secure monitor
the executable section into our trust application.
We're going to write the EL3 shellcode into the bootloader SMC function.
We're going to patch the kernel SMC handler to jump to our EL3 shellcode.
Then we're going to map the Linux executable section,
patch the sys_quotactl system call,
so it'll do the SMC call to our patched kernel SMC handler.
And then, finally, our POC is going to make the sys_quotactl system call itself.
That's going to trigger the SMC call.
That's going to trigger a jump to our shellcode.
And then, finally, we're executing shellcode in EL3. This thing just does a print,
but in a few slides, I'll actually show you
some fun stuff you can do once you have code execution at EL3.
Okay, but first off, before we go there,
let's actually do a little thought experiment because it's kind of cheating.
We have this super powerful memory client capability. What if we didn't have this?
For this, let's look at the SST client capability
that allows our trusted application to communicate with the SST server.
Why the SST server? Well, if our trusted application didn't have
the memory client capability, the SST server would have that.
So, let's say, so if we pwn the SST server,
we can basically then do the same thing we did before as a trust application.
And how do we pwn the SST server?
Well, basically, there's this super trivial stack buffer overflow
where one of the IPC arguments is used as the size
in a mem copy to a stack buffer. As discussed before, there's no SLR.
There's also no stack canaries. So, exploiting this thing is pretty trivial.
We didn't do it all the way to the end because we didn't need to.
This is just basically us sending a ROP chain
that just prints something to the log and then crashes the thing.
And this is the logging output where we're sending the IPC request
to the SST server, and then it triggers the ROP chain.
So, okay, even if we didn't have memory client right away,
there would have been plenty of bugs to essentially end up at some point
in some capacity where we can talk to the UT memory IPC server.
Okay, so now we have an EL3 shellcode execution.
I'll show you two fun things that you can do. First off is a circus trick
where you basically set up the phone in a way so any pin you input works.
And then the second thing is we're going to dump some DRM keys.
Okay, first off, any pin works. So the way pin authentication works on Android
is basically your pin is sent to the gatekeeper trusted application,
and the gatekeeper trusted application checks if the pin is correct or not.
If it is, it'll basically send back an AUF token,
and that AUF token is then used by the normal world
to another trusted application, and that'll finally unlock your phone.
So if you just want to disable the pin as whatever over ADB, that's trivial.
What we want to do is we want the whole system
to think and work as if there was pin authentication,
but we can actually input any pin we want. And for that to work,
we need to actually be able to patch the gatekeeper trusted application
to essentially always accept any pin. How do we do this?
Well, what we're going to do on a high level
is just going to use our EL3 shellcode to search through the TZOS memory,
or the TE memory, look for the gatekeeper trusted application,
and then patch some instructions. Which instructions do we patch?
Well, the way the gatekeeper works on BeanPod
is basically when it's verifying a pin, like two function calls deep,
it'll do an indirect function call to a verify function.
This is the disassembly you see here. And this BLX R3 is essentially jumping
to the address in R3, and that's where, you know,
as long as the trusted application is working normally,
is where the pin verification is happening. So what we're going to do
is we're just going to patch this BLX R3 to just return true.
Because the only side effect of this function call is the return value,
and all it does is check, did it return 0 or did it return 1?
If the verify function returns 1, the pin is correct, and the phone is unlocked.
So we just change this instruction to a move R01, R0 holds the return value,
and then 1 is going to be true. So we do this, and then we'll basically be able
to input any pin and get our phone unlocked. We wanted to do a demo,
but it's a bit sketchy, so I just have the video, but I have the phone with me.
If you want to convince yourself that it works, just come by afterwards
and try it out. Basically, so here you see the phone. This is in its normal state,
so I try 333, doesn't work, 2222, doesn't work, and then 1111 is the correct pin.
So that's how it's supposed to be. Basically, then, I'm going to run the exploit,
which does all of the stuff to finally execute the EL3 shellcode
to patch the gatekeeper. And now I'm going to try the pins I tried before
that didn't work, and they should work. So 3353 works, and then 2222 also works.
Okay. So that's the circus trick. Let's do something more useful.
Let's dump some DRM keys. We're going to do this
for a very popular DRM implementation, and we're going to dump the L1 device key.
So that is the TE hardware-backed device key that gives you access
to your 4K video streams or whatever. We need two things. First, we need a way
to trigger DRM interaction because it's very annoying. It's very complicated
to get this thing to do anything. And then the second thing we need to do
is we need to figure out what we actually want to dump. So first thing
is actually straightforward. We're using ExoPlayer, which is like some
Android media framework testing app that basically lets you
try out all the different APIs there are, and this thing allows you
to also test out your DRM. And I know in the beginning
we kind of ranted about Xiaomi, but actually they're super nice
because even though the phone is rooted, we can still do L1 DRM.
So it still works, and that means somewhere there should be some DRM key.
What do we actually want to dump? The way it works for this DRM implementation,
there's a key box that acts as the root of trust, and inside of there
there's a device key. And this device key is an AS key that will basically be used
to derive the content decryption keys down the road. So there's like
a super complicated protocol, but this device key is like the factory
provisioned root of trust. So if we can get access to this, we basically have
everything we want. Where's this thing stored? Well, the DRM trusted application
has this very convenient function. It's called a key box get device key,
where basically the device key is copied to an outbuffer, and the key box itself
is a global variable somewhere in the data section of the trust application.
So I'm kind of like a broken record here. What we're going to do
in our EL3 shellcode, we're going to look for the DRM trusted application,
we're going to dump the data section, and we'll have our L1 device key.
And, well, that's it. I mean, there's not much to see on this slide,
but, you know, with this, you can finally make backups of the movies
that you paid for. Okay, this is just for reference. So if you want
to do this yourself, you need a Xiaomi device with a MediaTek system on chip.
If it's released sometime before 2023, it'll use BeanPod. Since 2023,
they started replacing the BeanPod TE with their own newer TE. So if you still
want to do this on your phones, there's like a non-exhaustive
list of like recent phones that have the BeanPod TE. And yeah,
that's it from my side. For the conclusion, I'm going to hand over to Matthias.
Cool. Very nice. Thank you, Philipp. So we've seen how kind of this whole system
is set up, and you follow this on a very long chain and a very long exploration
of systems. And I want to kind of wrap this up and lift this up
to a bit of a higher level. And essentially, like we're running
a highly compartmentalized system in a trusted execution environment.
And if you compare this to a monolithic kernel where everything runs
in a single space, right, everything runs with high interaction,
as soon as you add compartmentalization and you introduce separation and privilege
separation between individual components, you're kind of wrapping up and enabling
some separation and some guards between these parts.
And the more you build this up, the more you segment systems
into individual compartments and the more you need to have individual trust
into these small components. For example, you want to get access
to this highly privileged component in the executive suite to actually get
your privileges that you want to have. But when you're designing
a compartmentalization system, you have to be very careful
about the privileges you set up, the policies you set up,
and how you kind of build up this whole system of trust, one layer upon the next.
And in the Android ecosystem, this is extremely complicated
and extremely challenging to get right. So what went wrong in the system
that we've looked at here, right? We started in the normal world and moved over
to the secure world and kind of got two paths to get full code execution.
The first one through a privileged misconfiguration, a kind of policy bug
that was too permissive, allowing us to kind of exploit and access the underlying
physical memory. But then on the other hand, there were privileged services
that had access to the same kind of permissions with very trivial bugs
that would have enabled the same kind of abstraction. So when you're working
in a microkernel-based system, you have to be extremely careful
on who actually gets these permissions, who gets these privileges,
and how to secure them across the different layers,
including this interesting aspect of priority inversion, privilege inversion,
where a user-based process actually gets access to physical memory,
to map physical memory away from even the secure monitor. Now, as we're designing
more secure systems, we have to wonder how we can actually secure
such a complex environment and how to build this up step by step.
We had this great design with a likely bug-free microkernel. You've seen it.
We haven't even touched the fiasco kernel itself, but the vulnerability
was in the policy and in the way how the different user-space processes
were interacting with each other. We are wondering in our research group,
how can we actually write policies for such systems? How can we break up
the privileges in a way that makes this safer, in a way to interact
in this environment? And this calls for new research into hardware mechanisms,
policy generation, language features, and kind of ways
to port all this legacy code in this big mess that Android actually is.
So what we've seen here today in the talk is an exploit chain
through a bunch of bugs and misconfigurations starting in user land on Android
in the normal world with EL0, a user process that kind of bridged and escalated
into the secure world starting as a normal application
through a rollback attack vector that is known for a couple of years,
exploiting a simple type confusion vulnerability after the rollback
actually succeeded. And then after having code execution in the secure world,
we escalated through this incomplete permission check or policy violation into EL3
through this overly relaxed IPC policy. And Philip also showed as a bonus
second route through the SAC overflow due to missing mitigations,
due to missing checks and bad coding practices of these services.
So there's a couple of key lessons to be had here. First, right,
when you're having a secure microkernel, you have to be extremely careful
about the policies that you're running. Second, you have to be also,
you also have to check all the other services that get permissions and privileges
such as these auxiliary services that could similarly be used for exploitation.
And third, you obviously have to check how deep the rabbit hole goes
to see where you end up. We demonstrated the first publicly exploit
against the beanpot. TEE, the microkernel -based systems, can be vulnerable.
If you're interested in learning more about this, Philip brought the phone.
We can demonstrate this later on. Feel free to reach out to us.
We're happy to talk and engage. Join us in our research group if you're interested
in exploring these systems further. And thank you very much for your attention.
Questions? Well, thank you very much for this wonderful and educational talk.
We still got some time left for questions. Yes? Microphone two, please. Exactly.
Hello. Thanks for the talk. So, if I understood correctly how the exploit works,
basically getting code execution in EL3 was just for flexing
because you already could read physical memory in EL0 anyways, right? And right.
Pretty much, yeah. Okay, that's all I wanted to know. Yes? Microphone two, please.
So, the basis of the whole exploit was a vulnerable trusted environment
application in, which was, for example, vulnerable to global confusion, right?
I saw in the screenshots of the rooting that this was executed on a MIUI 13 phone.
Xiaomi has recently rolled out HyperOS. Have you checked
if any of these vulnerable applications have been patched or is it still doable?
Well, for example, on this Xiaomi Redmi Note 11. Yeah. So, the firmware version
that we did everything on is actually HyperOS. So, I guess they did, like,
like, I think HyperOS mostly sort of front-end changes or user-end changes.
All of the TE stuff didn't really change. So, that's, yeah.
Do you have it Magisk-rooted? No, not yet, sadly. Okay, otherwise,
like, come over. Come back in seven days. Microphone one, please. Yes.
So, I have a bit of a question, a bit more technical question about why, I mean,
imagine you didn't have a rooted phone, right? So, I assume
it would be a bit more pain to interact with the TA, right?
If you don't have root privileges. So, imagine you just have a phone
that you cannot root. How would you go around doing this kind of research,
in this case, for this specific TA, for example? Well, I mean, what you need to do
is you basically need to do, instead of, you know, doing the me account
and whatever, do something more fun and try to get, like, code execution
in the Linux kernel. Like, exploit a bug there and then, you know,
you can interact with the trust application that way. Or you find a user-space bug
that lets you escalate to root. I guess that's... So, and how is the
key install TA actually used by the phone? That's an excellent question.
I don't think it is, actually. I don't think that the... Like, we look for this...
This... So, the way it works... Yeah, the UID. The way it works, somewhere,
there should be, like, a user-space library that, you know, uses that UID
to connect, to communicate with the thing. But we didn't find anything.
But I think it's like... This is a MediaTek trusted application.
And I think this is just, like, shipped on, like, most MediaTek devices
and just kind of sits there. So, I mean, Xiaomi basically bought
this system-on-chip from MediaTek and MediaTek ships this board support package
that already comes with, you know, a bunch of stuff and they just didn't dare
to change anything about it. Thanks. Microphone 2, please. I once had the chance
to talk to someone who was designing the Intel the Intel version of this
and their view was that this is not even secure. Like, this whole thing
is basically broken by design, which I kind of agree with because theirs is
completely isolated. So, I mean, in a very, very differently isolated. So,
here you have these, you can switch from one to the other and that's it, right?
This is this trust zone system, whereas in the secure enclave,
the enclave is self-sufficient so it can run on its own completely different,
completely isolated from the other secure enclaves. Like, one of the reasons
why you could hop into the operating system was because you could just map memory.
That's impossible in a secure enclave environment. So, my question is, like,
do you think that this system is so broken that we shouldn't do this? I mean,
a side note to this is that I actually also worked a little bit with trust zone
and there they forgot to flip one bit and you could just read it. Like,
it was kind of a joke almost. Like, setting it up is also extremely tedious
and error prone. Like, setting it up to be actually correctly separated.
But even when it is, it's only two, whereas secure enclave, of course,
is as many as you want. You're comparing to very different systems,
which is always very difficult. So, for secure enclave, the execution mode
is you have a lot of unprivileged enclaves being separated from each other.
With the secure world here, you've got a highly privileged domain that is running,
that is giving you a second world at higher privileges.
It can be implemented securely, right? Assuming a small, secure monitor
that separates between the two parts and the user process in the secure world
not giving access to physical memory would be a big start and then rolling out
a couple of mitigations that would help. But it's very hard to compare
as the execution modes are very different and they also are
meant for different purposes. ARM made this even more interesting to what form
with the more modern CCA architectures that implement something like enclaves
but it's a different story. Do you want to add anything? Thank you.
Microphone 2, please. One quick question when I first looked for my work into this
was in around 2022 there was a paper release about rollback attacks
on the Samsung TEE which basically showed that because those TEEs don't implement
rollback protection correctly by design it's not a bug it's a feature
that trusted applications from the Samsung S7 could be run on the Samsung S22
making it possible to abuse vulnerabilities which were at this point
five years old and my question would be with the Xiaomi's is it like also
like this that you could run basically trusted applications
from older Xiaomi phones or was it just like within like one phone
that you could roll back to an older version of that phone?
I think this is actually what we did here so the trust application
that I was using was from the Xiaomi Redmi 9A or something which was I don't know
2017, 18 I guess and the 11S was you know two generations later and we could still
load this trust application but Xiaomi Redmi was I mean and the Beanpot TA
was a special case we wrote this nice paper that kind of documents it well
was definitely one of the implementations that didn't even consider
having some sort of a version counter where you can somehow reject and outdated
trust application so Xiaomi especially in this configuration
was a bit of a special case thank you yes microphone two please nice talk
and awesome exploit I'm curious about a question about if this box also be
exploitable on the new versions such as the 64 address space or some mediatekTOS
such as they apply the anti-buffers ASLR or something or some devices with secure
secure chip SE if if this bug also export in these devices well so I guess
specifically for Beanpot I'm pretty sure this only exists in 32-bit and you know
there's no security mitigations the key install TA does exist on other phones
so you'll find this on like Kineb like I don't know like whatever some mediatek
Kineb devices or other mediatek or like I even saw it on like
Samsung mediatek device and on there the T that is used is gonna be like let's say
specifically for Samsung Tigris is gonna have
ASLR and a 64-bit address space and the exploit there is gonna be much much harder
but like this justification is very broken like if so it should but obviously
like that's like completely hypothetical because
I mean you still have the primitive it's just a question of if it's still possible
but we we didn't try this on other TE's so we only did it for BeanPod okay