Summary
This is the third part of a blog series covering my security research into Samsung’s TrustZone.
This post covers the following vulnerabilities that I have found:
SVE-2017–8888: Authentication Bypass + Buffer overflow in tlc_server
SVE-2017–8889: Stack buffer overflow in ESECOMM Trustlet
SVE-2017–8890: Out-of-bounds memory read in ESECOMM Trustlet
SVE-2017–8891: Stack buffer overflow in ESECOMM Trustlet
SVE-2017–8892: Stack buffer overflow in ESECOMM Trustlet
SVE-2017–8893: Arbitrary write in ESECOMM Trustlet
You can find all the PoCs on github.
Target Selection
The first thing for me was deciding on Trustlets to focus on. I can not emphasize enough: if you build on some other way of getting to system level privileges, tons of new Trustlet-level attack surface opens up. But, I wanted to find something that can be reached from unprivileged processes. So I first tried to find processes that might expose some.
I did this the simplest possible way: grepping system binaries, libraries, and exposed Binder interfaces for strings that suggested they implement functionality I would care about. This was helped by the understanding of the shared libraries that expose T-base interfaces in Android (see the first post in the series for details). Here is some of this truly next-level hacking I did:
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
From this, I identified tlc_server as a process that actually exposes access to several Trustlets via Binder and runs on a default installation.
Immediately from tlc_server’s main we can see that five different Trustlets are supported:
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);
}
More importantly, I found that two tlc_server instances actually run by default as well: one for ESECOMM (Trustlet for communicating with an eSE hardware element on the device, used for secure payment transactions) and one for CCM (Client Certificate Manager).
So the next step was to look at that Binder interface and find a way to take advantage of it with no privileges.
(Sidenote: I was not aware at the time I started my research, but Gal Beniamini also found vulnerabilities in the tlc_server’s Binder interface. Those have been fixed since the start of 2017.)
SVE-2017–8888: Authentication Bypass + Buffer overflow in tlc_server
I found two vulnerabilities in tlc_server. One is a memory corruption issue that seems very challenging to exploit for RCE, but the other is a trivial to exploit authentication bypass.
For both we have to look at the function that implements the Binder interface. Finding this handler from scratch would require painstakingly reversing quite a few object’s vtables until we follow enough constructors to stumble upon it. Instead, we can just use logcat output of tlc_server and follow string references.
__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) )
goto LABEL_105;
recvlen = msglen.recv_len;
max_recvmsg_len = (*((__int64 (**)(void))(*tlc_comm_ctx_ptr_global)->vtable_ptr + 4))();
// direct_comm_cxt->get_max_recvmsg_size()
recvlen_ = msglen.recv_len;
max_recvmsg_len_1 = max_recvmsg_len;
max_recvmsg_len_2 = (unsigned int)(*((__int64 (__fastcall **)(comm_cxt_t *))(*tlc_comm_ctx_ptr_global)->vtable_ptr
+ 4))(*tlc_comm_ctx_ptr_global);
// direct_comm_cxt->get_max_recvmsg_size()
if ( recvlen > max_recvmsg_len_1 || recvlen_ & 0x80000000 ) //negative check: fix to older bug
//this was one of the tlc_server bugs reported by Gal
{
v65 = "TLC_SERVER";
v66 = "Invalid recv message length! %d > %d";
}
else
{
__android_log_print(
4LL,
"TLC_SERVER",
"Recv message length is %d, max length is %d",
recvlen_,
max_recvmsg_len_2);
if ( (unsigned int)android::Parcel::readInt32(parcel_data, (int *)&msglen) )
goto LABEL_105;
sendlen_ = msglen.send_len;
sendlen_max = (*((__int64 (**)(void))(*tlc_comm_ctx_ptr_global)->vtable_ptr + 3))();
//direct_comm_cxt->get_max_sendmsg_size()
recvlen_ = msglen.send_len;
v52 = *tlc_comm_ctx_ptr_global;
if ( sendlen_ <= sendlen_max && !(msglen.send_len & 0x80000000) ) //negative check: fix to older bug
{
sendmsg_buf = (*((__int64 (__fastcall **)(comm_cxt_t *, _QWORD))v52->vtable_ptr + 6))(
v52,
(unsigned int)msglen.send_len);
if ( sendmsg_buf )
{
fd = android::Parcel::readFileDescriptor(parcel_data);
mmap_buf = mmap(0LL, msglen.send_len, 3LL, 1LL, fd, 0LL);// MMAP happens to SEND len
if ( mmap_buf != -1 )
{
memcpy(sendmsg_buf, mmap_buf, msglen.send_len);// memcpy happens with SEND len
v56 = (*((__int64 (**)(void))(*tlc_comm_ctx_ptr_global)->vtable_ptr + 8))();
__android_log_print(4LL, "TLC_SERVER", "comm_request() finished");
if ( v56 )
{
__android_log_print(6LL, "TLC_SERVER", "tlc_communicate failed: ret = 0x%08x", v56);
}
else
{
android::Parcel::writeInt32(v6, msglen.recv_len);
recvmsg_buf = (*((__int64 (**)(void))(*tlc_comm_ctx_ptr_global)->vtable_ptr + 7))();
if ( recvmsg_buf )
{
memcpy(mmap_buf, recvmsg_buf, msglen.recv_len);
// response memcpy happens with RECV len!
// the following condition would cause BOF:
//
// send_len << recv_len < recv_max_len < send_max_len
//
// the max send len is 4416
// the max recv len is 4416
v20 = munmap(mmap_buf, msglen.send_len);
From the decompiled code we can find out the following:
- no permissions are required for OPENSWCONN/CLOSESWCONN
- for command 2 (COMM), tlc_server would use SEAMS to verify the caller’s permissions
- however, for command 3 (COMM_VIA_ASHMEM), no such thing happens!
So just like that, we have identified an authentication bypass: by using command 3 instead of command 2, an unprivileged process can open a session to a Trustlet and send arbitrary commands to it.
Beyond this problem, the implementation of command 3 also used to have a buffer overflow due to allowing negative sizes passed as sendlen and recvlen. This has been fixed since (after Gal Beniamini reported it). However, the fix was incomplete. The problem is that the shared memory is mmap’ed always to the sendlen size, however after sending the command to the Trustlet, the result is written back to the mmapp’ed area using recvlen. Even though both sendlen and recvlen are verified to be 0 < len < max_len, it is not checked that sendlen is not strictly larger than recvlen. Since the area is mmap’ed, the difference between sendlen and recvlen has to cross a page-boundary to cause a buffer overflow. This is feasible as the maximum allowed length is 4416, which results in 2 pages mapped, as opposed to 1 for sendlen < 0x1000.
Normally, tlc_server doesn’t do any other mmap operations directly, so exploiting this bug would seem pretty challenging. It may be possible to coax tlc_server into allocating objects of such sizes that the allocator will use mmap for them; I haven’t really looked into this further because I was happy enough with the other (comparatively trivial to exploit) bug.
Introduction to the ESECOMM Trustlet
Moving on to the ESECOMM Trustlet. This trustlet implements an interface to an eSE (embedded Secure Element). The communication between those two are based on ISO7816. The Trustlet uses the SEC_SPI Secure Driver to talk to the eSE via SPI. Looking at the Linux kernel sources, there is also a Linux kernel driver (see /drivers/spi/spi-s3c64xx.c). I think this is for device versions where the interface is directly exposed to Android? In either case, this code explains the memory mapped I/O and helps understanding what the Trustlet is doing.
Most importantly for us, the Trustlet implements the “SCP03 Global Platform Secure Channel Protocol”. This uses an APDU based protocol for setting up cryptography information (Diffie Hellman keys) in order to form a Secure Channel. Note that in this case, the NWd client talking to the Trustlet is a master of keys here, we directly provide the private key material.
When it came to reverse engineering the ESECOMM Trustlet, the “usual suspects” were major help:
- labeling tlApi calls,
- the ability to read logs via adb shell from /proc/sec_log
- Samsung’s habit of using original function names in log messages, e.g.:
snprintf(logbuffer, 119, "Enter %s", "process_SCPHandleApduResponse");
logbuffer[119] = 0;
tlApiLogPrintf_0("%s\n", logbuffer);
if ( !state_ )
{
snprintf(logbuffer, 119, "%s:%d :: Error, %s\n", "process_SCPHandleApduResponse");
logbuffer[119] = 0;
tlApiLogPrintf_0("%s\n", logbuffer);
JUMPOUT(&locret_AD5E);
}
SVE-2017–8889: Stack buffer overflow in ESECOMM Trustlet
All the commands that are sent in relation to SCP are TLV-encoded APDUs. There is one utility function that is used to parse TLVs:
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_);
}
The function itself uses some checks to make sure that TLVs are legit:
- the parsing stops when it reaches total_length
- each parsed TLV is verified to not be longer than total_length; in addition they are verified to not be longer that 0x400
However, there is a problem: there are no checks to make sure that the NUMBER of TLVs that are present are not so large that the parsed out structures completely fill out the array out_apdus.
The used structures have the following definitions:
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
In other words, parsed_tlvs_t can only hold up to 16 TLV objects. In case an APDU has more than 16 TLVs in it, the parsing will cause a buffer overflow.
This vulnerability can be triggered via several callers:
- parse_ca_cert
- parse_scp_param
- process_SCPHandleApduResponse
- process_SCP11bHandleApduResponse
- process_SCP11bDhHandleApduResponse
All of the names except from parse_scp_param are the original function names, based on strings in the binary. For parse_scp_param, this is the parsing function first called from process_ConstructSecureChannel.
All paths are susceptible to the same issue. As an example, in the case of parse_ca_cert(), the output structure parsed_tlvs_t is allocated on the stack, which means that a stack buffer overflow can be triggered.
SVE-2017–8890: Out-of-bounds memory read in ESECOMM Trustlet
Another problem with parse_tlvs_from_APDU is that the way it makes sure that TLV parsing does not run off the end of the input buffer is insufficient. As visible from the pseudocode I pasted above, the only check is for the offset being equal to the total length. However, each TLV may increment the length by more than 1 byte, which means that the total_length value can trivially be incremented past, which will mean that parsing won’t terminate anymore, leading to out-of-buffer reads. In fact this is almost too trivial to trigger, even a case of sending just some zeroes as the input results in a crash of the Trustlet. (And yes, I realized this while I was trying to understand why all kinds of foobar messages already crashed the Trustlet :))
SVE-2017–8891: Stack buffer overflow in ESECOMM Trustlet
The previous stack buffer overflow is not the most convenient one, as we don’t have direct control over what we are going to write. Luckily, there are also better ones :)
The next vulnerability in the ESECOMM trustlet is in the function parse_ca_cert().
The issue is straightforward: when processing the process_ScpInstallCaCert command, the first thing that happens is the CA that is sent in the APDU is parsed. This is expected to have several fields, first among them is the CA id (tag 0x42). Once the TLVs are parsed, the value of the caid is copied over from the TLV structure into a local stack variable that is 32 bytes long.
Since this happens without any length checks, the check in the APDU TLV parsing itself that only makes sure that the TLV won’t be longer that 0x400 bytes or the entire input payload is not sufficient to avoid a trivially exploitable stack 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, %s\n", "parse_ca_cert", 73, "failed to parse TLV");
logbuffer[119] = 0;
tlApiLogPrintf_0("%s\n", 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);
The destination buffer in question is on the stack of parse_ca_cert’s caller:
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);
(…)
For completeness, here’s the not-terribly-easy-on-the-eye validate_input_len as well:
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: Stack buffer overflow in ESECOMM Trustlet
The next vulnerability in the ESECOMM trustlet is in the function parse_scp_param. This one is called from the handler for the CMD_TZ_SCP_ConstructSecureChannel command, process_ConstructSecureChannel().
The issue is very similar to the caid one: when processing the CMD_TZ_SCP_ConstructSecureChannel command, the first thing that happens is the APDU containing the cryptographic parameters required for establishing a secure channel are parsed. The output is parsed into a structure of the following format:
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
In the case of process_ConstructSecureChannel., this structure is instantiated on the stack:
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("%s\n", 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) )
Several tags must be present for this, as visible from the decompiled pseudocode of parse_scp_param below.
Once the TLVs are parsed, the value of the DH p parameter is copied over from the TLV structure into a local stack variable. The maximum checks on the L value of the DH parameter TLV are insufficient, because they still allow a length larger than the stack buffer that the parameter is copied into.
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, %s\n", 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: Arbitrary writes in ESECOMM Trustlet
This vulnerability is a case where the same bug manifests in many places in the code. It was frequent enough that I didn’t try to pinpoint every single one methodically, I just gave Samsung the examples that I found. Let’s hope they fixed every single one! :)
Unlike the previously reported vulnerabilities though, these issues cannot be directly triggered without elevating privileges in the scenario that I used. That is because the tlc_server actually adds a sanitization step itself.
However, as soon as an attacker is able to use the /dev/mobicore interface directly (by compromising any one of the many system processes that are allowed to, like this very recent and very amazing remote exploit chain by @oldfresher does), these memory corruption vulnerabilities could all be triggered.
The problem itself is in the way the TCI buffer is used in order to determine the range of the request buffer and the response buffer. Normally, the TCI buffer is filled with a header that has the following format:
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
The field envelope_len is itself used to determine the offset within the buffer where the response message part starts. In the case where we communicate over tlc_server, we are actually using libtlc_direct_comm.so, and that sets this field correctly:
__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);
However, if we can write to the WSM whatever and then trigger a Notify command to reach the Trustlet without going through tlc_server, then the envelope_len can be anything.
The problem is that much of the Trustlet code I came across just trusted this field inherently. The following snippet from the ESECOMM trustlet’s entry point shows the very start of processing input from NWd:
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!
Immediately in Main, the problem is that regardless of whether the command processing fails or succeeds, the response value will always be written to the respmsg pointer, which can be any arbitrary address when envelope_len is controlled.
Unfortunately, this kind of write to respmsg is repeated throughout the logic inside process_cmd as well. I note that many code paths do include checks on the respmsg’s payload length, but this is actually a red herring here! These checks verify that the length of payload indicated by the field of respmsg falls in line with the expected size of respmsg for the given command type. This however doesn’t do anything to the fact that the initial respmsg assignment out in Main might have already flipped respmsg to point to some arbitrary address.
Some of this would be covered by a tciBufLen > envelope_len check, but not entirely, because in most cases, there are writes way past the start of respmsg that happen. The biggest example I found is in 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;
(...)
So, assuming a check is added in Main(), it will have to know correctly the maximum offset from respmsg that may be written to by any command processing and then make sure that envelope_len is never too large with respect to that.