以三星为例,如何对TrustZone进行逆向工程和漏洞利用(下篇)

传送门:上篇https://www.anquanke.com/post/id/96730

概要

本文主要讲解了如何对三星的TrustZone进行逆向工程和漏洞利用。在下篇中,我主要将介绍我发现的漏洞,具体漏洞如下:
SVE-2017-8888:身份验证绕过和tlc_server缓冲区溢出
SVE-2017-8889:ESECOMM Trustlet栈缓冲区溢出
SVE-2017-8890:ESECOMM Trustlet越界读取内存
SVE-2017-8891:ESECOMM Trustlet栈缓冲区溢出
SVE-2017-8892:ESECOMM Trustlet栈缓冲区溢出
SVE-2017-8893:ESECOMM Trustlet任意写入
大家可以在我的Git中,找到我提交给三星的全部PoC:https://github.com/puppykitten/tbase

 

目标选择

最一开始,我就将目光聚焦在Trustlet上。我要强调的是:如果能事先通过某种方式获得系统级权限,那么将会打开大量Trustlet层级的攻击面。但是,我希望能找到一种方式,能够在无特权的情况下实现攻击。因此,我开始尝试寻找一些可以利用的进程。
我采取了最简单的方法,grep系统二进制文件、库和暴露Binder接口的字符串,并借助它们实现我想要的功能。在理解了如何在安卓系统中暴露T-Base接口的共享库(在上篇文章中已经详细讲解)的情况下,我实现了进一步的攻击:

shell@herolte:/ $ service list | grep com.sec
shell@herolte:/system/lib64 $ strings -f * | grep mcNotify   
shell@herolte:/system/bin$ strings -f * | grep onTransact
root@herolte:/proc# strings -f /proc/*/maps | grep libtlc

由此,我确定tlc_server是一个通过Binder实际暴露对若干Trustlet访问的进程,并且能够在默认安装中运行。
从tlc_server的主要代码中我们可以看到,共支持5个不同的Trustlet:

  v3 = argc;
  v4 = argv;
  __android_log_print(4LL, "TLC_SERVER", "tlc_cerver main starts");
  if ( v3 == 1 )
  {
    __android_log_print(4LL, "TLC_SERVER", "service name was not provided: defaulting to CCM");
    strncpy(&service_name, aCCM, 31LL);
  }
  else
  {
    if ( v3 != 2 )
    {
      v6 = -1;
      __android_log_print(6LL, "TLC_SERVER", "usage: tlc_server <CCM|DCM|ESECOMM|TUI|PUF>");
      goto LABEL_15;
    }
    v5 = v4[1];
    if ( (unsigned __int64)strnlen(v4[1], 32LL) > 0x1F )
    {
      v6 = -1;
      __android_log_print(6LL, "TLC_SERVER", "too long g_service_name");
      goto LABEL_15;
    }
    strncpy(&service_name, v5, 31LL);
    __android_log_print(4LL, "TLC_SERVER", "Service Name = %s", &service_name);
    if ( (unsigned int)strcmp("CCM", &service_name)
      && (unsigned int)strcmp("DCM", &service_name)
      && (unsigned int)strcmp("ESECOMM", &service_name)
      && (unsigned int)strcmp("TUI", &service_name)
      && (unsigned int)strcmp("PUF", &service_name) )
    {
      v6 = -1;
      __android_log_print(6LL, "TLC_SERVER", "Only 'CCM', 'DCM', 'ESECOMM', 'TUI', and 'PUF' are supported");
      goto LABEL_15;
    }
  }
  v28 = 0LL;
  v29 = 0LL;
  v30 = 0LL;
  v31 = 0LL;
  v32 = 0LL;
  v33 = 0LL;
  v34 = 0LL;
  v35 = 0LL;
  if ( (unsigned int)strcmp(&service_name, "CCM") )
  {
    if ( (unsigned int)strcmp(&service_name, "DCM") )
    {
      if ( (unsigned int)strcmp(&service_name, "ESECOMM") )
      {
        if ( (unsigned int)strcmp(&service_name, "TUI") )
        {
          if ( (unsigned int)strcmp(&service_name, "PUF") )
          {
LABEL_14:
            v6 = -1;
            __android_log_print(6LL, "TLC_SERVER", "fill_comm_data failed!");
            goto LABEL_15;
          }
          strncpy(&v28, "libtlc_tz_puf_km.so", 31LL);
          strncpy(&v32, "puf_get_comm_data", 31LL);
        }
        else
        {
          strncpy(&v28, "libtlc_tima_tui.so", 31LL);
          strncpy(&v32, "tui_get_comm_data", 31LL);
        }
      }
      else
      {
        strncpy(&v28, "libtlc_tz_esecomm.so", 31LL);
        strncpy(&v32, "esecomm_get_comm_data", 31LL);
      }
    }
    else
    {
      strncpy(&v28, "libtlc_tz_dcm.so", 31LL);
      strncpy(&v32, "dcm_get_comm_data", 31LL);
    }
  }
  else
  {
    strncpy(&v28, "libtlc_tz_ccm.so", 31LL);
    strncpy(&v32, "ccm_get_comm_data", 31LL);
  }

更重要的是,我发现两个tlc_server实例也在默认运行:一个是用于ESECOMM(与设备上的eSE硬件元件进行通信的Trustlet,用于安全支付交易),另一个是用于CCM(客户端证书管理)。
所以,接下来,我们就要观察Binder接口,找到一个不借助特权的利用方式。
注:我针对这些漏洞进行了独立研究,但后来发现Gal Beniamini也发现了tlc_server的Binder接口中的漏洞,这些漏洞已经在2017年被修复。

 

SVE-2017-8888:身份验证绕过和tlc_server缓冲区溢出

我在tlc_server中发现了两个漏洞。一个是内存破坏漏洞,恐怕非常难以借助此漏洞来实现远程代码执行。另一个是身份验证绕过,看上去似乎也非常不起眼。
针对这两个漏洞,我们必须要阅读Binder接口的函数。我们可以使用tlc_server的logcat输出,并跟踪字符串引用。

__int64 __fastcall binder_handler(android::IPCThreadState *IPCThreadState, unsigned int cmd_, const android::Parcel *data, android::Parcel *reply_, unsigned int flags_)
{
 // (...)
   switch ( cmd )
  {
    case 0u:
      __android_log_print(4LL, "TLC_SERVER", "OPENSWCONN");
      if ( !(unsigned __int8)android::Parcel::checkInterface(parcel_data, (char *)IPCThreadState_ + 16) )
        goto LABEL_93;
      if ( !reply )
        goto LABEL_90;
      if ( *((_DWORD *)IPCThreadState_ + 20) > 0 )
        goto LABEL_35;
      tlc_comm_cxt_ptr = (comm_cxt_t **)malloc(8LL);
      if ( !tlc_comm_cxt_ptr )
      {
        __android_log_print(6LL, "TLC_SERVER", "tlc_server_ctx_t malloc failed");
        goto LABEL_126;
      }
      *tlc_comm_cxt_ptr = 0LL;
      comm_cxt = create_comm_ctx(               // TLC_COMM_TYPE:
                                                // 0 - proxy
                                                // 1 - direct
                                                // 
                                                // ==> we create direct
                                                // 
                                                // 
                                                // direct_comm_cxt()
                                                // - instantiate directCommImpl with these parameters
                                                //    - includes root (== device id, switched out to 0) and process (==uuid)
                                                // - call tlc_open
                                                //    - mcOpenDevice(0) and mcOpenSession to uuid,
                                                // after it mmaps the TLC buffer, to sendmsglen+recvmsglen length
                   TLC_COMMUNICATION_TYPE_DIRECT,
                   comm_data_root,
                   comm_data_root_strlen,
                   comm_data_process,
                   comm_data_process_strlen,
                   comm_data_max_sendmsg_size,
                   comm_data_max_recvmsg_size);
      *tlc_comm_cxt_ptr = comm_cxt;
      if ( !comm_cxt )
      {
        __android_log_print(6LL, "TLC_SERVER", "Failed to establish secure world communication");
        free(tlc_comm_cxt_ptr);
        goto LABEL_126;
      }
      if ( !(unsigned int)strcmp(&service_name, "ESECOMM") )
      {
        __android_log_print(4LL, "TLC_SERVER", "ESECOMM tlc_server connecting to SPI");
        if ( (unsigned __int16)secEseSPI_open() )
        {
          __android_log_print(6LL, "TLC_SERVER", "*** secEseSPI_open failed : %d ***");
          free(tlc_comm_cxt_ptr);
LABEL_126:
          v52 = "Ctx creation failed - TZ app not loaded";
          tlc_comm_ctx_ptr_global = 0LL;
          goto OPEN_HANDLED;
        }
      }
      tlc_comm_ctx_ptr_global = tlc_comm_cxt_ptr;
      //(...)
    case 1u:
      __android_log_print(4LL, "TLC_SERVER", "CLOSESWCONN");   
      //(...)
    case 2u: 
      __android_log_print(4LL, "TLC_SERVER", "COMM");
      //(...)
      android::defaultServiceManager(v73);      // sp<IServiceManager> sm = defaultServiceManager()
      //(...)
      if ( sm )
      {
        v78 = *(int (__fastcall **)(__int64, int *))(*sm + 32LL);
        android::String16::String16((android::String16 *)&recv_msg_len, "SEAMService");
        v78(v77, &recv_msg_len);                // defaultServiceManager->addService()
      //(...)
        isAuthorized_fnptr = *(__int64 (__fastcall **)(__int64, _QWORD, signed __int64, int *, _QWORD **))(*sm__ + 32LL);
        android::String16::String16((android::String16 *)&recv_msg_len, "knox_ccm_policy");
        android::String16::String16((android::String16 *)&send_msg_len, "C_SignInit");
        v83 = isAuthorized_fnptr(sm__, mCallingPid, 0xFFFFFFFFLL, &recv_msg_len, &send_msg_len);
                                                // sm->isAuthorized(mCallingPid, -1, service_name, &sm (?))
                                                // can the calling pid call to the knox_ccm_policy service?
        //(...)
        if ( v83 )
        {
              v84 = "isAuthorized() returns an error!";
        }
        else
        {
          v74 = (*((__int64 (__fastcall **)(comm_cxt_t *))(*tlc_comm_ctx_ptr_global)->vtable_ptr + 8))(*tlc_comm_ctx_ptr_global);
              // tlc_communicate()
     //(...)
    case 3u:
      __android_log_print(4LL, "TLC_SERVER", "COMM_VIA_ASHMEM");
      if ( !(android::Parcel::checkInterface(parcel_data, (char *)IPCThreadState_ + 16) & 1) )
      {
        v30 = -1;
        goto LABEL_119;
      }
      if ( !v6 )
        goto LABEL_72;
      v43 = 0LL;
      v44 = (_DWORD *)((char *)IPCThreadState_ + 92);
      do
      {
        if ( *(v44 - 2) == v13 || *v44 == v13 )
          goto LABEL_53;
        v43 += 2LL;
        v44 += 4;
      }
      while ( v43 < 1024 );
      if ( (_DWORD)v43 == 1024 )
        goto LABEL_70;
LABEL_53:
      if ( (unsigned int)android::Parcel::readInt32(parcel_data, &msglen.recv_len) )

从反编译的代码中,我们可以发现以下内容:

  1. OPENSWCONN / CLOSESWCONN不需要权限;
  2. 对于命令2(COMM),tlc_server将使用SEAMS来验证调用者的权限;
  3. 命令3(COMM_VIA_ASHMEM)不会进行验证。

由此,我们已经发现了一个身份验证绕过漏洞,通过使用命令3,而不是命令2,使费特权进程可以打开到某个Trustlet的会话,并发送任意命令。
除了这一利用方式之外,命令3的也被用于实现缓冲区溢出,原因在于其允许sendlen和recvlen为负值。在Gal Beniamini报告这一漏洞后,这一点已经得到解决,但修复是不完整的。问题在于,共享内存总会被映射为sendlen的大小,但在将命令发送到Trustlet之后,会使用recvlen将结果写入映射的区域。针对sendlen和recvlen,会验证是否符合0 < len < max_len,但不会检查sendlen是否严格大于recvlen。由于该区域被映射,所以必须要跨越页的边界才能导致缓冲区溢出。实际上这是可行的,因为最大允许长度为4416,会被映射2页,而在sendlen < 0x1000的情况下则为1页。
通常情况下,tlc_server不会直接执行其他任何映射操作,因此对这个漏洞的利用看起来非常具有挑战性。coax tlc_server分配这样的大小是可能的,分配器将会为它们进行映射。实际上,我还没有深入考虑这一种利用方式,因为我希望尝试一些其他的漏洞利用。

 

ESECOMM Trustlet介绍

接下来,我们看看ESECOMM Trustlet。这个Trustlet实现了一个eSE(嵌入式安全元件)的借口,这二者的通信基于ISO7816标准。Trustlet使用SEC_SPI安全驱动程序通过SPI与eSE通信。通过查看Linux内核源代码,我们发现还有一个Linux内核驱动程序(请查看/drivers/spi/spi-s3c64xx.c)。我认为,这是针对将接口直接暴露给安卓的设备版本。无论哪种情况,这段代码解释了内存映射的输入输出,并帮我们理解Trustlet的实际作用。
对我们来说,最重要的是,Trustlet实施“SCP03全平台安全通道协议”。它使用基于APDU的协议来设置密码信息(Diffie-Hellman密钥)以创建安全通道。请注意,在这种情况下,与Trustlet通信的不安全世界客户端使用主密钥,我们会直接提供私钥。
当对ESECOMM Trustlet进行逆向工程时,有一个“非常嫌疑犯”向我们提供了有力的帮助。

  1. 协助我们标记tlApi调用。
  2. 三星在日志消息中有使用原始函数名称的习惯。例如:
    snprintf(logbuffer, 119, "Enter %s", "process_SCPHandleApduResponse");
    logbuffer[119] = 0;
    tlApiLogPrintf_0("%sn", logbuffer);
    if ( !state_ )
    {
     snprintf(logbuffer, 119, "%s:%d :: Error, %sn", "process_SCPHandleApduResponse");
     logbuffer[119] = 0;
     tlApiLogPrintf_0("%sn", logbuffer);
     JUMPOUT(&locret_AD5E);
    }
    
  3. 通过/proc/sec_log中的adb Shell读取这些日志。

 

SVE-2017-8889:ESECOMM Trustlet栈缓冲区溢出

所有与SCP有关的命令,都是TLV编码的APDU。有一个实用程序函数用于解析TLV:

int __fastcall parse_tlvs_from_APDU(parsed_tlvs_t *out_apdus, char *in_buf, int start_offset, int total_length)
{
  parsed_tlvs_t *parsed_tlvs_t; // r4
  char *in_buf_; // r8
  int total_length_; // r7
  int offset; // r5
  int i; // r6
  tlv_t *tlv_obj; // r0
  int ret; // r0

  parsed_tlvs_t = out_apdus;
  in_buf_ = in_buf;
  total_length_ = total_length;
  offset = start_offset;
  if ( out_apdus && out_apdus->num_slots_used ) // 
                                                // so this is basically an array of TLV
                                                // objects that we parse out.
                                                // 
                                                // a TLV instance always point to a tag object
                                                // also, which is its kind basically.
  {
    for ( i = 0; parsed_tlvs_t->num_slots_used > i; ++i )
      free_tlv_obj(parsed_tlvs_t->tlv_array[i]);
  }
  parsed_tlvs_t->num_slots_used = 0;
  while ( 1 )
  {
    tlv_obj = create_TLV_obj_wrap(in_buf_, offset, total_length_);// checks apdu len, but nothing about destination length
    if ( !tlv_obj )
      break;
    parsed_tlvs_t->tlv_array[parsed_tlvs_t->num_slots_used++] = tlv_obj;
    offset += get_apdu_len(tlv_obj);
    if ( offset == total_length_ )              // 
                                                // we have parsed everything, return.
    {
      ret = parsed_tlvs_t->num_slots_used;
      JUMPOUT(&return_);
    }
  }
  JUMPOUT(&return_);
}

函数自身会进行一些检查,来确保TLV是合法的:

  1. 解析会在到达total_length时停止;
  2. 每个解析的TLV会被验证,要求不能大于total_length的长度,也不能大于0x400。
    然而,这里却存在一个问题:并没有对TLV的数量检查,假如TLV的数量过多,那么解析出来的结构就会完全覆盖数组out_apdus。
    使用的结构定义如下:
00000000 tlv_t           struc ; (sizeof=0x413, mappedto_34)
00000000 filled          DCB ?
00000001 multiple_tlvs_for_tag DCB ?
00000002 fill_1          DCB ?
00000003 fill_2          DCB ?
00000004 tag_obj_ptr     DCD ?                   ; offset
00000008 len_field_len   DCB ?
00000009 fill_3          DCB ?
0000000A fill_4          DCB ?
0000000B fill_5          DCB ?
0000000C tlv_length      DCD ?
00000010 tlv_value_OR_num_extra_tlvs DCB ?
00000011 fill_6          DCB ?
00000012 fill_7          DCB ?
00000013 fill_8          DCB ?
00000014 next_tlv_ptrs_array DCB 1023 dup(?)
00000413 tlv_t           ends
00000413
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 parsed_tlvs_t   struc ; (sizeof=0x44, mappedto_33)
00000000 tlv_array       DCD 16 dup(?)           ; this is an array of 16 tlv objects
00000040 num_slots_used  DCD
00000044 parsed_tlvs_t   ends

换句话说,parsed_tlvs_t最多只能容纳16个TLV对象。如果APDU中有超过16个TLV,解析将会导致缓冲区溢出。
这个漏洞可以由多个调用者触发:
parse_ca_cert
parse_scp_param
process_SCPHandleApduResponse
process_SCP11bHandleApduResponse
process_SCP11bDhHandleApduResponse
除parse_scp_param以外,所有名称都是基于二进制文件中字符串的原始函数名称。对于parse_scp_param,这是首先从process_ConstructSecureChannel调用的解析函数。
所有的路径都会存在同样的问题。例如,对于parse_ca_cert(),输出结构parsed_tlvs_t被分配在栈上,这意味着可以触发栈缓冲区溢出。

 

SVE-2017-8890:ESECOMM Trustlet越界读取内存

parse_tlvs_from_APDU存在的另一个问题,是它对于TLV解析不会在输入缓冲区结束时运行的保证是不充分的。从上面的伪代码中可以看到,只会对偏移量是否等于总长度进行检查。然而,每个TLV的长度都可能会增加1或多个字节,这就意味着total_length的值可以一直增加下去,解析也不会终止,从而导致读取缓冲区不足。事实上,触发该漏洞是非常容易的,即使在发送内容只有一些0的情况下,也能够导致Trustlet崩溃。

 

SVE-2017-8891:ESECOMM Trustlet栈缓冲区溢出

之前的栈缓冲区溢出漏洞并不是最方便的利用方式,因为我们借助该漏洞并不能直接控制要写入的内容。幸运的是,还有一个更方便的漏洞。
这一漏洞位于函数parse_ca_cert()中。
漏洞原因非常简单:在处理process_ScpInstallCaCert命令时,首先在APDU中发送的CA会被解析。其中应该会有几个字段,首先是CA的ID(tag 0x42)。一旦TLV被解析,CA ID的值就会从TLV结构复制到32字节的本地栈变量中。
由于没有进行任何长度检查,APDU TLV解析自身包含的检查只会确保TLV不会大于0x400字节,所以就可以将有效载荷放在输入之中,从而导致栈BOF被利用。

signed int __fastcall parse_ca_cert(char *msg_payload, int total_req_payload_len, void *caid, int *caid_len, char *curveid, void *pubkey_buf, int *pubkey_buf_len)
{
  //(…)

  v7 = msg_payload;
  total_req_payload_len_ = total_req_payload_len;
  caid_ = caid;
  v10 = caid_len;
  v11 = -1;
  memset_to_0(&out_buf, 0x44u);
  if ( parse_tlvs_from_APDU(&out_buf, v7, 0, total_req_payload_len_) < 0 )
  {
    snprintf(logbuffer, 119, "%s:%d :: Error, %sn", "parse_ca_cert", 73, "failed to parse TLV");
    logbuffer[119] = 0;
    tlApiLogPrintf_0("%sn", logbuffer);
    return 3;
  }
  tlv_caid = find_tlv_obj_in_parsed_tlvs(&out_buf, (char *)&caid_tag_value);
  tlv_caid_ = tlv_caid;
  if ( tlv_caid )
  {
    memcpy_w(caid_, &tlv_caid->tlv_value_OR_num_extra_tlvs, tlv_caid->tlv_length);

具有漏洞的目标缓冲区,位于parse_ca_cert调用者的栈上:

signed int __fastcall process_ScpInstallCaCert(tci_msg_add_ca_payload_t *reqmsg, tci_rsp_payload_t *rspmsg)
{
  tci_msg_add_ca_payload_t *reqmsg_; // r4@1
  tci_rsp_payload_t *rspmsg_; // r5@1
  char *reqmsg_payload_; // r7@1
  signed int result; // r0@2
  char pubkey[512]; // [sp+Ch] [bp-244h]@2
  char caid[32]; // [sp+20Ch] [bp-44h]@2
  char curveid; // [sp+22Ch] [bp-24h]@2
  int pubkey_len; // [sp+230h] [bp-20h]@2
  unsigned int caid_len; // [sp+234h] [bp-1Ch]@2

  reqmsg_ = reqmsg;
  rspmsg_ = rspmsg;
  reqmsg_payload_ = reqmsg->payload;
  if ( validate_input_len(reqmsg->payload, reqmsg->total_len, reqmsg->payload, (char *)&reqmsg->total_len) )
                                              //ONLY verifies the total input len, not related to TLV lengths
  {
    result = parse_ca_cert(reqmsg_payload_, reqmsg_->total_len, caid, (int *)&caid_len, &curveid, pubkey, &pubkey_len);
(…)

为了保证完整性,以下是非常易读的validate_input_len:

BOOL __fastcall validate_input_len(BOOL payload_start__, unsigned int len, char *payload_start_, char *payload_end)
{
  if ( payload_start__ )
    payload_start__ = payload_start_ <= payload_end
                   && payload_start__ >= payload_start_
                   && payload_end >= payload_start__
                   && payload_end >= len
                   && &payload_end[-len] >= payload_start__;
  return payload_start__;
}

 

SVE-2017-8892:ESECOMM Trustlet栈缓冲区溢出

ESECOMM Trustlet的下一个漏洞出现在函数parse_scp_param中。该函数被process_ConstructSecureChannel()的CMD_TZ_SCP_ConstructSecureChannel命令的处理程序调用。
这一漏洞与CA ID漏洞非常相似:在处理CMD_TZ_SCP_ConstructSecureChannel命令时,包含着建立安全通道所需加密参数的APDU首先被解析。输出被解析成以下格式:

00000000 scp_t           struc ; (sizeof=0x110, mappedto_36)
00000000 protocol        DCB ?
00000001 key_id          DCB ?
00000002 key_version     DCB ?
00000003 key_usage       DCB ?
00000004 key_length      DCB ?
00000005 key_type        DCB ?
00000007 field_7         DCB ?
00000008 dh_p            DCB 252 dup(?)
00000104 field_104       DCD ?
00000108 dh_P_len        DCD ?
0000010C dh_G            DCB ?
0000010D field_10D       DCB ?
0000010E field_10E       DCB ?
0000010F field_10F       DCB ?
00000110 scp_t           ends

在process_ConstructSecureChannel,该结构在栈上被实例化:

signed int __fastcall process_ConstructSecureChannel(int unk_type, tci_msg_payload_t *reqmsg, tci_rsp_payload_t *respmsg)
{
  tci_msg_payload_t *reqmsg_; // r6
  tci_rsp_payload_t *respmsg_; // r7
  int protocol_type; // r10
  char *reqmsg_payload; // r11
  int calling_uid; // r9
  char *v8; // r3
  state_t *v9; // r8
  char *v10; // r3
  state_t *state; // r8
  signed int v13; // r0
  signed int v14; // r4
  scp_t parsed_scp; // [sp+8h] [bp-138h]

  reqmsg_ = reqmsg;
  respmsg_ = respmsg;
  protocol_type = unk_type;
  reqmsg_payload = reqmsg->payload;
  if ( !validate_input_len(reqmsg_payload, reqmsg->total_len, reqmsg_payload, &reqmsg[1])// 
    || !validate_input_len(respmsg_->payload, respmsg_->total_len, respmsg_->payload, &respmsg_[1]) )
  {
    goto LABEL_6;
  }
  scp_state_dump("construct secure channel");
  calling_uid = reqmsg_->calling_uid;
  v9 = get_current_state(reqmsg_->calling_uid);
  if ( v9 )
  {
    snprintf(logbuffer, 119, "cleanup existing state", v8);
    logbuffer[119] = 0;
    tlApiLogPrintf_0("%sn", logbuffer);
    free_scp_state(v9);
    scp_state_dump("construct secure channel, cleaned ongoing channel for callinguid");
  }
  hex_print_value("parsing scp_param", reqmsg_payload, reqmsg_->total_len);
  if ( parse_scp_param(reqmsg_payload, reqmsg_->total_len, &parsed_scp) )

为此,必须存在多个标签,如下面parse_scp_param反编译后的伪代码所示:

signed int __fastcall parse_scp_param(char *input_payload, int total_length, scp_t *parsed_scp)
{
  char *input_payload_; // r8@1
  int total_length_; // r9@1
  scp_t *parsed_scp_; // r4@1
  signed int v6; // r5@1
  signed int v7; // r0@4
  const char *v8; // r1@4
  tlv_t *v10; // r0@8
  tlv_t *v11; // r0@9
  tlv_t *v12; // r0@10
  tlv_t *v13; // r0@11
  tlv_t *v14; // r0@12
  tlv_t *v15; // r0@13
  int v16; // r0@14
  signed int v17; // r0@17
  const char *v18; // r1@17
  tlv_t *dh_tlv; // r0@23
  tlv_t *dh_tlv_; // r8@23
  tlv_t *v21; // r0@24
  tlv_t *v22; // r0@16
  int i; // r4@31
  parsed_tlvs_t parsed_apdus; // [sp+8h] [bp-60h]@1

  input_payload_ = input_payload;
  total_length_ = total_length;
  parsed_scp_ = parsed_scp;
  memset_to_0(&parsed_apdus, 68u);
  v6 = 0;
  if ( !input_payload_ || !parsed_scp_ )
  {
    v7 = 43;
    v8 = "invalid parameter";
    goto LABEL_5;
  }
  memset_to_0(parsed_scp_, 272u);
  if ( parse_tlvs_from_APDU(&parsed_apdus, input_payload_, 0, total_length_) < 0 )
  {
    v7 = 47;
    v8 = "bad apdu";
LABEL_5:
    snprintf(logbuffer, 119, "%s:%d :: Error, %sn", 325987, v7, v8);
    logbuffer[119] = 0;
    tlApiLogPrintf_0((const char *)&loc_10F20, logbuffer);
    return 4;
  }
  v10 = find_tlv_obj_in_parsed_tlvs(&parsed_apdus, scp_param_tag_values);
  if ( !v10 )
  {
    v17 = 50;
    v18 = "can't find protocol";
    goto FAIL;
  }
  parsed_scp_->protocol = v10->tlv_value_OR_num_extra_tlvs;
  v11 = find_tlv_obj_in_parsed_tlvs(&parsed_apdus, &scp_param_tag_values[1]);
  if ( !v11 )
  {
    v17 = 54;
    v18 = "can't find key-id";
    goto FAIL;
  }
  parsed_scp_->key_id = v11->tlv_value_OR_num_extra_tlvs;
  v12 = find_tlv_obj_in_parsed_tlvs(&parsed_apdus, &scp_param_tag_values[2]);
  if ( !v12 )
  {
    v17 = 58;
    v18 = "can't find key-version";
    goto FAIL;
  }
  parsed_scp_->key_version = v12->tlv_value_OR_num_extra_tlvs;
  v13 = find_tlv_obj_in_parsed_tlvs(&parsed_apdus, &scp_param_tag_values[3]);
  if ( !v13 )
  {
    v17 = 62;
    v18 = "can't find key-usage";
    goto FAIL;
  }
  parsed_scp_->key_usage = v13->tlv_value_OR_num_extra_tlvs;
  v14 = find_tlv_obj_in_parsed_tlvs(&parsed_apdus, &scp_param_tag_values[9]);
  if ( !v14 )
  {
    v17 = 66;
    v18 = "can't find key-length";
    goto FAIL;
  }
  parsed_scp_->key_length = v14->tlv_value_OR_num_extra_tlvs;
  v15 = find_tlv_obj_in_parsed_tlvs(&parsed_apdus, &scp_param_tag_values[5]);
  if ( !v15 )
  {
    v17 = 70;
    v18 = "can't find key-type";
    goto FAIL;
  }
  v16 = (unsigned __int8)v15->tlv_value_OR_num_extra_tlvs;
  parsed_scp_->key_type = v16;
  if ( v16 == 0x89 )
  {
    dh_tlv = find_tlv_obj_in_parsed_tlvs(&parsed_apdus, &scp_param_tag_values[7]);
    dh_tlv_ = dh_tlv;
    if ( !dh_tlv )
    {
      v17 = 76;
      v18 = "can't find dh param (p)";
      goto FAIL;
    }
    memcpy((_DWORD *)parsed_scp_->dh_p, (int *)&dh_tlv->tlv_value_OR_num_extra_tlvs, dh_tlv->tlv_length);
                                                // so this is straight up copying the dh param TLV value,
                                                // using the L from the TLV.
                                                // 
                                                // there is a max length check on this, but it is not sufficient:
                                                // - check against 1024 AND
                                                // - check against input length (max 512)
                                                // 
                                                // but the stack buffer copied into is less than 300 bytes long.

 

SVE-2017-8893:ESECOMM Trustlet任意写入

与之前的几个漏洞不同,该漏洞无法在不提权的情况下直接利用。其原因在于,tlc_server自身增加了一个格式化(Sanitization)步骤。
然而,只要攻击者能够使用/dev/mobicore接口(借助任何一个有权限的系统进程,例如@oldfresher曾尝试过的远程利用链)就可以触发内存损坏漏洞。
漏洞的产生原因在于,使用了TCI缓冲区来确定请求缓冲区和相应缓冲区的范围。通常情况下,TCI缓冲区会被如下格式的头部填充:

00000000 tcibuf_hdr      struc ; (sizeof=0x219, mappedto_27)
00000000 id              DCD ?
00000004 calling_uid     DCD ?
00000008 envelope_len    DCD ?
0000000C status          DCD ?
00000010 tcimsg_payload  tci_msg_payload_t ?
00000219 tcibuf_hdr      ends

字段envelope_len用于确定响应消息部分开始的缓冲区内偏移量。在通过tlc_server进行通信后,我们实际上使用的是libtlc_direct_comm.so,并且正确设置了这个字段:

__int64 __fastcall direct_comm_ctx::comm_request(direct_comm_ctx *this)
{
  direct_comm_impl_t *direct_comm_impl; // x8
  unsigned int v2; // w19
  __int64 TCI_buf_addr; // x9
  __int64 v4; // x21
  bool v5; // zf
  const char *v6; // x2
  unsigned int max_recvmsg_size; // w20
  unsigned int v8; // w0

  direct_comm_impl = (direct_comm_impl_t *)*((_QWORD *)this + 1);
  v2 = 65542;
  if ( !direct_comm_impl )
  {
    v6 = "direct comm_request: NULL implementation pointer";
    goto LABEL_9;
  }
  TCI_buf_addr = direct_comm_impl->TCI_buf_addr;// ?????
  v4 = direct_comm_impl->unk_must_be_zero;
  if ( TCI_buf_addr )
    v5 = v4 == 0;
  else
    v5 = 1;
  if ( v5 )
  {
    v6 = "tlc_request: NULL pointer data";
LABEL_9:
    __android_log_print(6LL, "TZ: mc_tlc_communication", v6);
    return v2;
  }
  max_recvmsg_size = direct_comm_impl->max_recvmsg_size;
  *(_DWORD *)(TCI_buf_addr + 8) = direct_comm_impl->max_sendmsg_size; // can't set to arbitrary when used via tlc_server
  v8 = tlc_communicate((__int64)&direct_comm_impl->device_id);

然而,如果我们可以写入WSM,然后触发通知命令,而不通过tlc_server到达Trustlet,那么envelope_len就能被任意定义。
其中的问题在于,我见过的很多Trustlet代码直接就信任了这个字段。以下ESECOMM Trustlet入口点的代码片段展现了从不安全世界处理输入内容的起始过程:

void __fastcall Main(_DWORD *tciBuf, unsigned int tciBufLen)
{
  tcibuf_hdr *tciBuf_; // r6@1
  unsigned int tciBufLen_; // r9@1
  char *logbuffer_; // r5@4
  tcibuf_hdr *respmsg; // r7@5
  _BYTE *logbuffer_end; // r5@5
  int envelope_len; // ST00_4@5
  int cmd_id; // r8@5
  int ret_val; // r10@7
  _BYTE *logbuffer_end_; // r5@7

  tciBuf_ = (tcibuf_hdr *)tciBuf;
  tciBufLen_ = tciBufLen;
  if ( !tciBuf || tciBufLen < 4397 )
  {
    ((void (__fastcall *)(signed int, signed int))stru_1000.lib_addr)(4, -1);// tlApiExit()
    JUMPOUT(&stru_1000);
  }
  snprintf(logbuffer, 119, "TL_TZ_ESECOMM: tciBufferLen = %d = 0x%x", tciBufLen, tciBufLen);
  logbuffer_ = logbuffer;
  logbuffer[119] = 0;
  tlApiLogPrintf_0((const char *)&off_7364, logbuffer);
  snprintf(logbuffer, 119, "Trustlet TL_TZ_ESECOMM: Starting");
  logbuffer[119] = 0;
  tlApiLogPrintf_0((const char *)&off_7364, logbuffer);
  while ( 1 )
  {
    tlApiWaitNotification(-1);
    respmsg = (tcibuf_hdr *)((char *)tciBuf_ + tciBuf_->envelope_len);
                                            // tciBuf__[2] is the max_sendmsg_size ... NORMALLY
                                            // but, if the command header is malformed,
                                            // respmsg can become an arbitrary pointer

    snprintf(logbuffer, 119, "TL_TZ_ESECOMM: tciBufferLen = %d = 0x%x", tciBufLen_, tciBufLen_);
    logbuffer_[119] = 0;
    logbuffer_end = logbuffer_ + 119;
    tlApiLogPrintf_0((const char *)&off_7364, logbuffer);
    envelope_len = tciBuf_->envelope_len;
    snprintf(logbuffer, 119, "TL_TZ_ESECOMM: sendmsg envelope length = %d = 0x%x");
    *logbuffer_end = 0;
    tlApiLogPrintf_0((const char *)&off_7364, logbuffer);
    snprintf(logbuffer, 119, "TL_TZ_ESECOMM: sendmsg = %p, respmsg = %p", tciBuf_, respmsg);
    *logbuffer_end = 0;
    tlApiLogPrintf_0((const char *)&off_7364, logbuffer);
    cmd_id = tciBuf_->id;
    snprintf(logbuffer, 119, "Trustlet TL_TZ_ESECOMM: Got a message!");
    *logbuffer_end = 0;
    logbuffer_ = logbuffer_end - 119;
    tlApiLogPrintf_0((const char *)&off_7364, logbuffer);
    if ( cmd_id >= 0 )
    {
      snprintf(logbuffer, 119, "TZ_ESECOMM: we got a command");
      logbuffer_[119] = 0;
      tlApiLogPrintf_0((const char *)&off_7364, logbuffer);
      ret_val = process_cmd((int)esecomm_cxt_something, cmd_id, tciBuf_, (char *)respmsg);
      respmsg->id = cmd_id | 0x80000000;
                          // respmsg address is completely controlled!

在Main中存在一个问题,无论命令处理是成功还是失败,响应值都会被写入到respmsg指针,在envelope_len被控制时,该指针可以是任意地址。
但不巧的是,对respmsg的写入在process_cmd内部的逻辑中也是重复的。发现很多代码路径都包含对respmsg有效载荷长度的检查,但实际上这里也是存在问题的。这些检查,会验证由respmsg字段指示的有效载荷长度是否符合给定命令类型的respmsg的预期大小。然而,Main中的初始respmsg分配可能已经指向了某个任意地址,因此该检查是没有意义的。
其中的一些,将会被tciBufLen > envelope_len检查覆盖,但并不是完全覆盖,因为在大多数情况下,respmsg的起始部分都采用写入的方式。最经典的例子是在process_DECRYPT:

int __fastcall process_cmd_decrypt(void *esecomm_cxt, char *reqmsg, char *rspmsg)
{
  (…)

  reqmsg_ = reqmsg;
  rspmsg_ = rspmsg;
  (…)
  snprintf(logbuffer, 119, "TL_TZ_ESECOMM: process_DECRYPT: entering");
  logbuffer[119] = 0;
  tlApiLogPrintf_0((const char *)&loc_89EC, logbuffer);
  *(_DWORD *)rspmsg_ = 10;
  *((_WORD *)rspmsg_ + 2) = 255;
  *(_DWORD *)(rspmsg_ + 4362) = 0;
  (...)

所以,假如在Main()中增加了一个检查,那么它必须要知道respmsg正确的最大偏移量,这个偏移量可以被任何命令处理写入,并且会确保envelope_len永远不会过大。

 

SVE-2017-8893:为什么没有竞争条件?

在研究过程中,我们可能会有这样的一个思路:即使没有提升权限,我们也可以按照自己的需要写入TCI缓冲区,将Binder调用到tlc_server,然后调用tlc_communicate()来触发到Trustlet的通知,并在Trustlet世纪获取到安全世界内的运行计划之前,重写(修正后的)envelope_len。
事实证明,这一思路是错误的。其原因在于,当使用COMM_VIA_ASHMEM时,tlc_server中的binder_handler()在我们的命令中实际上是mmap,然后会从映射的缓冲区复制到独立的TCI缓冲区。也就是说,我们从来没有真正地直接访问TCI缓冲区。这也就意味着,tlc_server无法满足它在多个客户端之间多路复用(Multiplexing)的基本目标,仍然是使用ESECOMM Trustlet的单个开放Session(也就是单个TCI WSM)。

 

接下来的计划

接下来,我将对其他一些漏洞进行详细分析,特别是:
SVE-2017-9008:CCM Trustlet整数溢出
SVE-2017-9009:CCM Trustlet整数溢出
SVE-2017-10638:CCM Trustlet会话劫持漏洞
SVE-2017-8973:TIMA驱动程序缓冲区溢出
SVE-2017-8974:TIMA驱动程序竞争条件漏洞
SVE-2017-8975:TIMA驱动程序竞争条件漏洞
TIMA驱动程序信息泄漏导致KASLR绕过
敬请期待!

(完)