传送门:上篇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) )
从反编译的代码中,我们可以发现以下内容:
- OPENSWCONN / CLOSESWCONN不需要权限;
- 对于命令2(COMM),tlc_server将使用SEAMS来验证调用者的权限;
- 命令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进行逆向工程时,有一个“非常嫌疑犯”向我们提供了有力的帮助。
- 协助我们标记tlApi调用。
- 三星在日志消息中有使用原始函数名称的习惯。例如:
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); }
- 通过/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是合法的:
- 解析会在到达total_length时停止;
- 每个解析的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绕过
敬请期待!