Trustfall: An RSA Heap Underwrite Into OP-TEE's Secure World · ByteRay Blog
← ALL POSTS
OP-TEE is the TrustZone Trusted Execution Environment that ships on many Arm phones, set-top boxes, and embedded devices. It splits the system in two. The Normal World runs Linux and ordinary apps. The Secure World runs the OP-TEE core at S-EL1 and Trusted Applications (TAs) at S-EL0, and it holds the things the platform actually wants to protect: device keys, secure storage, DRM material, attestation. Normal World asks Secure World to do sensitive work through the TEE Client API and an SMC into the monitor.
A memory-corruption bug inside the OP-TEE core is worth more than the same bug in a Linux driver, because the core is the thing standing between an untrusted OS and the secrets. We found multiple vulnerabilities in OP-TEE and sent 3 fixes upstream so far.
VULN-1 : The RSA NOPAD underwrite
RSA NOPAD is "textbook" RSA with no padding: the caller hands in a block the size of the modulus and the core does the modular exponentiation on it directly. When OP-TEE is built with the mbedTLS crypto backend, the software encrypt path left-aligns the input into a modulus-sized scratch buffer before the exponentiation:
rsa.len = crypto_bignum_num_bytes((void *)&rsa.N); /* modulus length */<br>blen = CFG_CORE_BIGNUM_MAX_BITS / 8;<br>buf = malloc(blen);<br>memset(buf, 0, blen);<br>memcpy(buf + rsa.len - src_len, src, src_len); /* no length check */
The destination is buf + rsa.len - src_len. The intent is a right-aligned copy: for an input shorter than the modulus, rsa.len - src_len is a small positive offset and the value lands flush against the end of the buffer. Nothing checks that src_len is less than or equal to rsa.len. These are size_t values, so once the input is longer than the modulus the subtraction wraps around, the "offset" becomes a huge unsigned number, and buf + (that) resolves to an address before buf. The memcpy then copies the whole attacker-supplied input starting there.
intended (input modulus):
dest = buf - k<br>+----------+-----------------------------+<br>| under- | RSA scratch |<br>| write | |<br>+----------+-----------------------------+<br>buf
The number of bytes written before buf is however much longer than the modulus the attacker made the input, and every one of those bytes is attacker-controlled. A Normal World client reaches this through a completely ordinary path: it opens a session to a TA, the TA calls TEE_AsymmetricEncrypt with an RSA NOPAD key, and the core hits the copy. No debugger, no artificial entry point. The malicious length crosses the TrustZone boundary and the core corrupts its own heap.
Heap Grooming to Write-What-Where primitive
The OP-TEE core heap is managed by BGET, a boundary-tag allocator (lib/libutils/isoc/bget.c). Each chunk carries a small header in front of it that records the block size and a prevfree field the allocator uses to find and coalesce the neighbor below it. Free chunks are threaded onto a free list and merged with adjacent free space when released.
one BGET chunk
+--------+-----------------------------+<br>| header | payload |<br>+--------+-----------------------------+<br>^ ^<br>| |<br>size, prevfree pointer handed back to the caller
Two properties decide the exploit. First, BGET carves a request out of a free block from the high end, so a run of allocations comes back at descending addresses, the object you allocate after another one sits below it, at a lower address. Second, the bytes immediately below any live chunk are either the next object's payload or that object's header.
allocation order A, B, C out of one free block:
low addr high addr<br>+----------------+------+------+------+<br>| free space | C | B | A |<br>+----------------+------+------+------+<br>newest allocation sits lowest
Now line that up with the underwrite. The copy runs backwards off the front of the RSA scratch buffer, toward lower addresses, into whatever sits below it.
Grooming a victim under the buffer
The goal is to make the RSA scratch buffer land directly above an object we chose, so the underwrite falls into it. BGET is deterministic, so with control over which core allocations happen and when (each reachable through ordinary TA calls that make the core allocate and free) the layout is repeatable.
Pack the heap so the relevant size class is contiguous and predictable, removing stale holes.
Lay down the object we want to hit, interleaved with disposable placeholders of the same size class.
Free one placeholder to open a hole sitting just above a chosen victim.
Trigger the RSA operation. Its scratch buffer is the same size class, so it reuses that hole, and the underwrite now reaches down into the victim.
step 2 victim V interleaved with placeholders P
low high<br>... +-----+-----+-----+-----+ ...<br>| V | P | V | P |<br>+-----+-----+-----+-----+
step 3 free a placeholder above a victim -> hole
... +-----+------+-----+-----+ ...<br>| V | hole | V | P |<br>+-----+------+-----+-----+
step 4 RSA scratch buffer...