Secure Channel Manager

Table of Contents

Overview

Qualcomm chose to name the channel through which the "Normal World" interacts with the "Secure World" via SMC opcodes - SCM (Secure Channel Manager).

The "Secure World" is also called TrustZone.

According to ARM Ltd., TrustZone is:

"…a system-wide approach to security for a wide array of client and server computing platforms, including handsets, tablets, wearable devices and enterprise systems. Applications enabled by the technology are extremely varied but include payment protection technology, digital rights management, BYOD, and a host of secured enterprise solutions."

More about TrustZone here and here.

The documentation provided by Qualcomm in the relevant source files is quite extensive, and is enough to get quite a good grip on the format of SCM commands.

Putting it shortly, SCM commands fall into one of two categories:1

  • Regular SCM Call
  • Atomic SCM Call

Regular SCM Call

These calls are used when there is information that needs to be passed from the "Normal World" to the "Secure World", which is needed in order to service the SCM call. The kernel populates the following structure:

/**
 * struct scm_command - one SCM command buffer
 * @len: total available memory for command and response
 * @buf_offset: start of command buffer
 * @resp_hdr_offset: start of response buffer
 * @id: command to be executed
 * @buf: buffer returned from scm_get_command_buffer()
 *
 * An SCM command is laid out in memory as follows:
 *
 *      ------------------- <--- struct scm_command
 *      | command header  |
 *      ------------------- <--- scm_get_command_buffer()
 *      | command buffer  |
 *      ------------------- <--- struct scm_response and
 *      | response header |      scm_command_to_response()
 *      ------------------- <--- scm_get_response_buffer()
 *      | response buffer |
 *      -------------------
 *
 * There can be arbitrary padding between the headers and buffers so
 * you should always use the appropriate scm_get_*_buffer() routines
 * to access the buffers in a safe manner.
 */
struct scm_command {
        u32     len;
        u32     buf_offset;
        u32     resp_hdr_offset;
        u32     id;
        u32     buf[0];
};

And the TrustZone kernel, after servicing the SCM call, writes the response back to the scm_response structure:

/**
 * struct scm_response - one SCM response buffer
 * @len: total available memory for response
 * @buf_offset: start of response data relative to start of scm_response
 * @is_complete: indicates if the command has finished processing
 */
struct scm_response {
        u32     len;
        u32     buf_offset;
        u32     is_complete;
};

In order to allocate and fill these structures, the kernel may call the wrapping function scm_call, which receives pointers to kernel-space buffers containing the data to be sent, the location to which the data should be returned, and most importantly, the service identifier and command identifier.

Each SCM call has a "category", which means which TrustZone kernel subsystem is responsible for handling that call. This is denoted by the service identifier. The command identifier is the code which specifies, within a given service, which command was requested.

After the scm_call function allocates and populates the scm_command and scm_response buffers, it calls an internal __scm_call function which flushes all the caches (inner and outer caches), and calls the smc function.

This last function actually executes the SMC opcode, transferring control to the TrustZone kernel, like so:

static u32 smc(u32 cmd_addr)
{
        int context_id;
        register u32 r0 asm("r0") = 1;
        register u32 r1 asm("r1") = (u32)&context_id;
        register u32 r2 asm("r2") = cmd_addr;
        do {
                asm volatile(
                        __asmeq("%0", "r0")
                        __asmeq("%1", "r0")
                        __asmeq("%2", "r1")
                        __asmeq("%3", "r2")
#ifdef REQUIRES_SEC
                        ".arch_extension sec\n"
#endif
                        "smc    #0      @ switch to secure world\n"
                        : "=r" (r0)
                        : "r" (r0), "r" (r1), "r" (r2)
                        : "r3");
        } while (r0 == SCM_INTERRUPTED);

        return r0;
}

Note that R0 is set to 1, R1 is set to point to a local kernel stack address, which is used as a "context ID" for that call, and R2 is set to point to the physical address of the allocated scm_command structure.

This "magic" value set in R0 indicates that this is a regular SCM call, using the scm_command structure. However, for certain commands where less data is required, it would be rather wasteful to allocate all these data structures for no reason. In order to address this issue, another form of SCM calls was introduced.

Atomic SCM Call

For calls in which the number of arguments is quite low (up to four arguments), there exists an alternate way to request an SCM call.

There are four wrapper functions, scm_call_atomic_[1-4], which correspond to the number of arguments requested. These functions can be called in order to directly issue an SMC for an SCM call with the given service and command IDs, and the given arguments.

Here's the code for the scm_call_atomic1 function:

/**
 * scm_call_atomic1() - Send an atomic SCM command with one argument
 * @svc_id: service identifier
 * @cmd_id: command identifier
 * @arg1: first argument
 *
 * This shall only be used with commands that are guaranteed to be
 * uninterruptable, atomic and SMP safe.
 */
s32 scm_call_atomic1(u32 svc, u32 cmd, u32 arg1)
{
        int context_id;
        register u32 r0 asm("r0") = SCM_ATOMIC(svc, cmd, 1);
        register u32 r1 asm("r1") = (u32)&context_id;
        register u32 r2 asm("r2") = arg1;

        asm volatile(
                __asmeq("%0", "r0")
                __asmeq("%1", "r0")
                __asmeq("%2", "r1")
                __asmeq("%3", "r2")
#ifdef REQUIRES_SEC
                        ".arch_extension sec\n"
#endif
                "smc    #0      @ switch to secure world\n"
                : "=r" (r0)
                : "r" (r0), "r" (r1), "r" (r2)
                : "r3");
        return r0;
}

Where SCM_ATOMIC is defined as:

#define SCM_ATOMIC(svc, cmd, n) (((((svc) << 10)|((cmd) & 0x3ff)) << 12) | \
                                SCM_CLASS_REGISTER | \
                                SCM_MASK_IRQS | \
                                (n & 0xf))

Note that both the service ID and the command ID are encoded into R0, along with the number of arguments in the call (in this case, 1). This is instead of the previous "magic" value of 1 used for regular SCM calls.

This different value in R0 indicates to the TrustZone kernel that the following SCM call is an atomic call, which means that the arguments will be passed in using R2-R5 (and not using a structure pointed to by R2).

Analysing SCM calls

Now that we understand how SCM calls work, and we've found the handling function in the TrustZone kernel which is used to handle these SCM calls, we can begin disassembling the SCM calls to try and find a vulnerability in one of them.

I'll skip over most of the analysis of the SCM handling function, since most of it is boilerplate handling of user input, etc. However, After switching the stack over to the TrustZone area and saving the original registers with which the call was performed, the handling function goes on to process the service ID and the command ID in order to see which internal handling function should be called.

In order to easily map between the service and command IDs and the relevant handling function, a static list is compiled into the TrustZone image's data segment, and is referenced by the SCM handling function. Here is a short snipped from the list:

scm-fun-list.png

As you can see, the list has the following structure:

  • Pointer to the string containing the name of the SCM function
  • "Type" of call
  • Pointer to the handling function
  • Number of arguments
  • Size of each argument (one DWORD for each argument)
  • The Service ID and Command ID, concatenated into a single DWORD - For example, the tz_blow_sw_fuse function above, has the type 0x2002 which means it belongs to the service ID 0x20 and its command ID is 0x02.

Footnotes:

Author: Shi Shougang

Created: 2017-02-06 Mon 23:26

Emacs 24.3.1 (Org mode 8.2.10)

Validate