Unbox Your Phone — Part III.
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 that I have written for my reports I’ve sent to Samsung in the git.
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:
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:
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.
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,
- Samsung’s habit of using original function names in log messages, e.g.:
- the ability to read these logs via adb shell from /proc/sec_log
*I wrecked my brain on what adversary this whole SCP business is useful against in this setup, but I couldn’t see it. I have the suspicion that the main reason to support this is the need to be GP compliant. Nonetheless, if that’s wrong, I’d love to hear from somebody with more insight! The way I see it, if you don’t have privileges to see the communication between the client an the ESECOMM Trustlet, you won’t see the channel anyway. But if you do, you can sniff the plaintext communication that shows all the private key material anyway. Similar thought process applies to the Trustlet-eSE interface. Anyway, I put this to the side, because I didn’t work on this further, as Samsung Pay wasn’t supported by any merchant in my home country at the time, so there wasn’t really a use-case for me to dynamically inspect even.
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:
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:
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:
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.
The destination buffer in question is on the stack of parse_ca_cert’s caller:
For completeness, here’s the not-terribly-easy-on-the-eye validate_input_len as well:
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:
In the case of process_ConstructSecureChannel, this structure is instantiated on the stack:
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.
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:
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:
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:
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:
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.
SVE-2017–8893: Why is there no race condition?
To close out this post, let me also address this question, because I’ve also confused myself over it like 20 times over the past year :)
The idea may arise, that even without elevated privileges, we could write the TCI buffer as we like, make the Binder call to tlc_server, have that result in invoking tlc_communicate() to trigger a notification to the Trustlet, and before the Trustlet actually gets scheduled to run inside the SWd, rewrite the (correctly adjusted) envelope_len.
The reason this does not work is that, remember that binder_handler() in tlc_server actually mmaps in our command when using COMM_VIA_ASHMEM and then copies the data over from the mmapped buffer to the separate TCI buffer. Which is to say, we never actually have direct access to the TCI buffer itself in this scenario; which is just as well since that would mean that tlc_server couldn’t even satisfy it’s basic goal of multiplexing between multiple clients but still using a single open session (meaning a single TCI WSM) with the ESECOMM Trustlet.
The next post in this series is going to cover the remaining vulnerabilities that I have reported during last year, in particular:
- SVE-2017–9008: Integer overflow in CCM Trustlet
- SVE-2017–9009: Integer overflow in CCM Trustlet
- SVE-2017–10638: Session hijack vulnerability in CCM Trustlet
- SVE-2017–8973: Buffer overflow in TIMA driver
- SVE-2017–8974: Race condition vulnerability in TIMA driver
- SVE-2017–8975: Race condition vulnerability in TIMA driver
- TIMA driver information leak to KASLR bypass