Security mitigations are crucial to protect Android against the exploitation of memory corruption vulnerabilities. Unfortunately, Android's performance-oriented system architecture undermines these mitigations. We explain how probabilistic mitigations, including Android's newly introduced hardened memory allocator, are affected.
Large parts of the code running on Android are written in memory unsafe C++, which is prone to memory corruption vulnerabilities. Several publicly known exploits demonstrate how an adversary achieves arbitrary code execution on Android. For example, the Stagefright vulnerabilities[1] allowed a remote attacker to compromise thevictim's phone by sending a crafted MMS message.
As code complexity increases, memory corruption vulnerabilities remain a common vector for exploitation. While software testing aims to uncover and patch as many bugs as possible, it is inherently incomplete. Mitigations are therefore crucial to protect systems. Depending on the vulnerability, mitigations prohibit exploitation or, at least, increase complexity and cost. Mitigations are not free and come with some performance impact and increase system complexity. Balancing the costs and benefits of mitigations is therefore crucial along with a careful integration into the overall system design. Following best practices, Android compounds several mitigations to maximize protection.
We discuss how performance optimizations in Android's system design undermine the security guarantees of probabilistic mitigations and the impact on Android security.
Android deploys several well-known mitigations: Data Execution Prevention (DEP), Stack Canaries, Address Space Layout Randomization (ASLR), and, since Android 11, the hardened memory allocator Scudo. Except for DEP all these mitigations are probabilistic. Probabilistic, because they rely on a secret, which if guessed by the attacker, voids any protection guarantees.
The stack canary is a 32/64-bit value stored just before the return address on the stack. The value of the stack canary is verified before returning from a function to detect stack-based buffer overflows. If the attacker guesses the value of the canary, the attacker can forge a stack buffer overflow payload that includes the canary and passes the canary verfication.
ASLR randomizes the memory layout of processes. For an attacker this means that the address of code reuse gadgets will be different across multiple executions of the same program. To successfully mount a code reuse attack an attacker has to guess the address of these gadgets.
Scudo is a memory allocator designed to prevent the exploitation of heap-based vulnerabilities. Scudo uses secrets to protect the integrity of allocator metadata and to randomize the layout of allocations.
Correctly guessing these secrets is infeasible as the likelihood of each guess is low (ASLR is the most likely to be guessed with a probability of around 1/228) and wrongly guessed secrets will cause the process to crash. The only viable alternative for an attacker is to find additional vulnerabilities that leak the mitigation's secrets. This is possible because the secrets are stored in memory (for ASLR, the secret is stored implicitly in memory as the memory layout itself is the secret). Weaponizing such an additional memory leak vulnerability increases the cost and complexity of developing a functional exploit. Unfortunately, as we will see, one of Android's performance optimizations shares these secrets across most processes, undermining the protection guarantees of the aforementioned mitigations for some attack vectors.
Android runs on diverse hardware. One challenge for low-end hardware is executing several apps concurrently. Apps run on top of the Dalvik Virtual Machine (DVM) as well as a large amount of Android framework code and resources (around 4GB). Initializing the DVM and loading all of the framework resources requires significant time, increasing both process startup time and memory consumption. As an optimization, Android forks a pre-loaded runtime environment to spawn new app processes.
Fork is a system call available on Linux-based systems, including Android, which creates a copy of the calling process. The created child process has the same memory layout and content as its parent. Fork is fast and memory efficient since the child and parent process share the initial underlying memory pages. New app processes are forked from a single Zygote process, which has already initialized the DVM and all the framework resources. App processes forked from the "Zygote" process can directly start loading and executing app-specific code, leveraging the already loaded DVM and framework[2]. With this performance optimization, Android provides "instant" app startup times, even on lower-end hardware.
All child processes forked from the same parent process share the same initial memory layout and content. On Android, all app processes fork from the Zygote process. This initial memory, inherited from the Zygote process, contains the secrets of the memory corruption mitigations. For example, both stack canaries and ASLR memory layouts are shared across all app processes. Disclosing the secrets of any of these processes enables an adversary to attack all other processes. This simplifies exploitation as an adversary can leak information from one process to attack another process.
For example, a malicious app could exploit other apps to escalate privileges. When such a malicious app attempts to exploit a memory corruption vulnerability in another Zygote-forked process, the malicious app already knows the ASLR layout and stack canary and does not need to leak them.
While the effects of forking on ASLR and the canary are known[3], it also affects Android's most recent mitigation, the Scudo memory allocator.
Scudo is a hardened heap memory allocator designed to increase the cost and complexity of exploiting heap-based memory corruption vulnerabilities in Android userspace programs[4]. The heap is a memory region where programs can dynamically allocate and free memory. The allocator handles allocation (malloc) and deallocation (free) requests and manages the heap memory. The Linux default allocator is optimized for speed and efficiency. On Android, Scudo was designed with security as the main design priority, trading off some performance for security. Today, heap-based vulnerabilities are the most common and also most commonly exploited type of memory corruption vulnerability[5]. To protect Android processes from such vulnerabilities, Scudo is the default allocator since Android 11. Scudo's two key security features are the protection of inline heap metadata and the randomization of allocations.
Heap metadata contains information about the heap's state, such as the size of an allocation or the addresses of freed allocations. Due to the heap's dynamic nature, allocators usually store parts of their metadata inline on the heap. A common example of this is the header of an allocation, which contains information about the size of the allocation, usually stored just before the address returned by malloc. Overwriting inline heap metadata is a common approach to exploiting heap vulnerabilities as it confuses the allocator. The default Linux allocator stores most of its metadata inline on the heap, such as pointers to free allocations. For this allocator, the security community has compiled a large compendium of techniques that escalate a heap-bound memory corruption vulnerability into an arbitrary memory write primitive or code execution by manipulating this inline metadata[6]. Scudo protects itself against such attacks by signing inline metadata and verifying the signature before it processes the metadata. If the signature check fails, Scudo safely aborts the process.
Many heap vulnerabilities only become exploitable with certain heap layouts (i.e., they require a specific arrangement of adjacent heap objects). For most allocators, the address of allocations is deterministic, and thus attackers can manipulate the heap layout by interacting with the target process. Scudo prevents this by randomizing the address of allocations. In Scudo the same sequence of a allocations/deallocations across multiple runs of the same program will result in a different heap layout each time, preventing attackers from arranging heap objects in a specific layout.
Both these security measures rely on the confidentiality of secrets: a secret key to sign inline heap metadata and a secret seed to randomize allocations. Both of these Scudo-specific secrets suffer the same fate as the stack canary: They are shared across all Zygote-forked processes. In combination with sharing ASLR, a malicious app can predict the exact address where chunks of other Zygote-forked processes will be allocated. To tamper with inline metadata, the malicious app may simply read out its Scudo secret key and forge valid signatures.
As these security measures are compromised by the design of the Android runtime environment, exploiting heap vulnerabilities in Zygote-forked processes protected by Scudo remain feasible. Even targetting the allocator itself becomes feasible, since Scudo assumes that its security measures are effective, it forgoes additional sanity checks.
In our work "Exploiting Android's hardened Memory Allocator"[7] we present two exploitation techniques that coerce Scudo into allocating a chunk at an attacker's chosen address. We demonstrate that these techniques are feasible by exploiting the Zygote-forked, Scudo-protected, system server from an attacker app.
Unfortunately, performance optimizations may compromise security mitigations. Mitigations and performance form a tight bond and their interactions need to be carefully considered to avoid any undesirable side effects. By discussing how Android's forking optimization voids secret-based memory corruption mitigations we demonstrate weaknesses in the recently introduced hardened allocator (Scudo). The current Android runtime model of forking the same Zygote process for all apps brings performance but drastically reduces the effectiveness of secret-based mitigations such as stack canaries, ASLR, and Scudo, the new heap allocator.
Going forward, we urge system researchers to work closely with security researchers, especially when designing new mitigations to avoid the negative interplay between mitigations and optimizations.