Introduction to TrustZone and Android TEE Drivers
The ARM TrustZone technology provides a hardware-enforced isolation mechanism, creating a “Secure World” alongside the “Normal World.” On Android devices, this is leveraged by a Trusted Execution Environment (TEE) to host sensitive operations like secure key storage, DRM, and biometric authentication within Trusted Applications (TAs). Communication between applications in the Normal World (CAs) and TAs in the Secure World primarily occurs via kernel-level TEE drivers.
Understanding these communication interfaces is crucial for security researchers, enabling vulnerability discovery, reverse engineering proprietary functionalities, and deeply analyzing the device’s security posture. This article provides a hands-on guide to reverse engineering TEE drivers to extract their communication protocols.
Identifying and Locating TEE Drivers
The first step is to locate the relevant TEE driver. On most Android devices, these drivers expose a character device in the /dev directory. Common names include /dev/qseecom (Qualcomm), /dev/teedev (generic/OP-TEE), or /dev/mtee (MediaTek). You can list device files to find potential candidates:
adb shell ls -l /dev | grep tee
Once identified, you need the corresponding kernel module (.ko file) or the kernel image (vmlinux) if it’s built-in. For modules, you might find them in /vendor/lib/modules or similar paths. For a built-in driver, you’ll need the device’s kernel image and a suitable disassembler/decompiler like Ghidra or IDA Pro.
Common Communication Mechanisms: IOCTL and Shared Memory
TEE drivers typically use two primary mechanisms for Normal World to Secure World communication:
ioctl()calls: Used for sending commands, status queries, and small data payloads. Eachioctlcommand is identified by a unique number and often corresponds to a specific operation within the TEE.- Shared Memory: For larger data transfers, such as input/output buffers for cryptographic operations or large data structures. The Normal World application allocates a buffer, shares it with the TEE driver, and the driver then makes it accessible to the Secure World.
Analyzing IOCTL Handlers
The ioctl system call is the primary entry point for userspace applications to interact with TEE drivers. Our goal is to map ioctl command numbers to their handler functions and understand the data structures they expect.
Step 1: Locate the IOCTL Dispatch Function
Load the kernel module or vmlinux into Ghidra/IDA Pro. Search for the `file_operations` structure associated with the TEE device. This structure typically contains a pointer to the driver’s ioctl handler function. For example, look for something like:
struct file_operations qseecom_fops = { .owner = THIS_MODULE, .unlocked_ioctl = qseecom_ioctl, // Or .compat_ioctl .open = qseecom_open, .release = qseecom_release,};
The function pointed to by unlocked_ioctl (or compat_ioctl for 32-bit compatibility on 64-bit kernels) is our target.
Step 2: Decompile and Map IOCTL Commands
Navigate to the identified ioctl dispatch function (e.g., qseecom_ioctl). Inside this function, you’ll typically find a large switch statement or a series of if-else if blocks that differentiate between various ioctl command numbers. Each case will lead to a specific handler function.
long qseecom_ioctl(struct file *file, unsigned int cmd, unsigned long arg){ // ... switch (cmd) { case QSEECOM_IOCTL_SEND_CMD: return qseecom_send_command_handler((void __user *)arg); case QSEECOM_IOCTL_SET_BW_PROFILING: return qseecom_set_bw_profiling_handler((void __user *)arg); // ... default: // Handle unknown commands return -EINVAL; }}
From this, you can extract the `ioctl` command numbers (e.g., QSEECOM_IOCTL_SEND_CMD) and their corresponding handler functions.
Step 3: Reconstruct IOCTL Input Structures
For each handler function, analyze how it uses the arg parameter. The arg is typically a pointer to a user-space structure. The kernel code will use functions like copy_from_user() to read this structure into kernel memory. By examining the fields accessed and their sizes, you can reconstruct the input structure.
For example, if qseecom_send_command_handler copies 0x20 bytes from user-space and then accesses fields at offsets 0x0, 0x4, 0x8, 0x10, etc., you can infer a structure like this:
struct qseecom_send_cmd_req { uint32_t command_id; uint32_t param_type; uint32_t buffer_len; uint64_t buffer_ptr; // Pointer to shared memory buffer};
Repeat this process for all significant `ioctl` commands. This systematic reconstruction yields the complete API exposed by the TEE driver.
Understanding Shared Memory Allocation and Usage
Many TEE operations involve large input/output buffers. These are typically handled via shared memory. The Normal World application allocates a buffer (often using ION memory allocator on Android) and then passes a file descriptor or a physical address identifier to the TEE driver via an ioctl call.
Step 1: Identify Shared Memory IOCTLs
Look for ioctl commands that deal with memory allocation, mapping, or buffer registration. Examples might include QSEECOM_IOCTL_REGISTER_FD, QSEECOM_IOCTL_CREATE_BUFFER, or similar.
Step 2: Trace Memory Handling
Analyze the handler functions for these memory-related ioctls. You’ll often see:
- Calls to `ion_import_fd_for_share()` or similar functions to convert a userspace file descriptor into a kernel-manageable buffer.
- Mapping operations using `dma_buf_get()` and `dma_buf_vmap()` to make the memory accessible to the kernel.
- The driver passing the physical address or an opaque handle of this shared memory to the Secure World via an SMC (Secure Monitor Call).
By tracking these functions, you can understand how shared memory is established and how its pointers are communicated to the TAs in the Secure World. This is critical for forging your own input buffers when interacting with the TEE.
Practical Example: Tracing QSEECOM
Let’s consider a hypothetical interaction with Qualcomm’s QSEECOM driver. A client application might initiate a command:
int qseecom_fd = open("/dev/qseecom", O_RDWR);if (qseecom_fd < 0) { perror("Failed to open /dev/qseecom"); return -1;}struct qseecom_send_cmd_req req = { .command_id = 0x1001, // Example TA command ID .param_type = 0, .buffer_len = 128, .buffer_ptr = (uint64_t)shared_buffer; // Pointer to a pre-allocated shared buffer};ioctl(qseecom_fd, QSEECOM_IOCTL_SEND_CMD, &req);
When reverse engineering, after decompiling qseecom_ioctl, you’d find QSEECOM_IOCTL_SEND_CMD (which could be defined as _IOWR('Q', 1, struct qseecom_send_cmd_req) in a header). You’d then analyze qseecom_send_command_handler. This handler would likely:
- Copy
reqfrom userspace. - Validate
command_idandbuffer_len. - Prepare an SMC call, passing the
command_id, and a Secure World-compatible address/handle forshared_buffer. - Invoke the SMC to transfer control to the Secure Monitor, which dispatches to the relevant TA.
Understanding this flow allows you to craft your own Normal World clients to interact with TAs, potentially uncovering vulnerabilities or undocumented functionalities.
Conclusion
Extracting TrustZone communication interfaces from Android kernel TEE drivers is a complex but rewarding process. By systematically analyzing ioctl handlers and shared memory mechanisms using tools like Ghidra or IDA Pro, security researchers can reconstruct the precise API exposed by the TEE. This knowledge is fundamental for advanced security audits, fuzzing TEE interfaces, and ultimately enhancing the security of Android devices. The detailed steps outlined here provide a solid foundation for diving into the fascinating world of TrustZone reverse engineering.
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →