When I first saw the release notes for the new Android Ice Cream Sandwich (ICS) platform, I was excited to see that Google mentioned that “Android 4.0 now provides address space layout randomization”. For the uninitiated, ASLR randomizes where various areas of memory (eg. stack, heap, libs, etc) are mapped in the address space of a process. Combined with complementary mitigation techniques such as non-executable memory protection (NX, XN, DEP, W^X, whatever you want to call it), ASLR makes the exploitation of traditional memory corruption vulnerabilities probabilistically difficult.
However, ASLR is commonly an all-or-nothing proposition. If ASLR is not applied to all areas of memory in a process, its effectiveness is often nullified. A single executable mapping that is mapped in a static location in the address space is often sufficient to construct a ROP payload. For example, this was the indeed case with the OS X prior to 10.7, where the dynamic linker was not randomized, providing a sufficient gadget source for a ROP payload.
So, let’s take a look at this new-fangled ICS 4.0 platform and see if ASLR was properly and fully implemented.
ASLR with the Linux Kernel
It’s a bit misleading to talk about how Android implements ASLR. In fact, ASLR is primarily provided by the Linux kernel, which happens to be the OS of the Android platform. Usually, in Linux-land, ASLR can apply to a variety of memory areas:
- Stack: The userspace stack mapping set up by the kernel during exec(2) should be sufficiently randomized. Stack randomization is performed by the randomize_stack_top() function.
- Heap: The heap location returned by the brk(2) system call when a program is first exec’ed should be randomized. Heap randomization is performed by the arch_randomize_brk() function.
- Libs and mmap: After NX was introduced, static library mapping led to the popularity of ret-to-libc and more generic ret-to-lib attacks. The location of libraries and other mmap’ed regions should be randomized.
- Exec: Even if you’re randomized the mapping of all the shared libaries that an executable uses, you still need to randomize the location of the executable itself when it is mapped into the address space. Otherwise, the executable mapping can be used as a source for ROP gadgets.
- Linker: On most Linux systems, the ld.so dynamic linker provided by glibc can self-relocate itself, so its mapping is randomized. However, as we’ll see, this isn’t the case for all linkers.
- VDSO: The VDSO (Virtual Dynamically-linked Shared Object) is an executable mapping of a virtual shared library provided by the kernel for syscall transitions. However, most Android devices run on the ARM architecture, which doesn’t use a VDSO.
So while a modern kernel on a recent desktop or server Linux distribution will commonly offer full ASLR, there’s plenty of opportunity to screw up any of the above ASLR mappings with improper system, toolchain, libc, or linker configurations.
ASLR Support in Android 2.x
Prior to ICS 4.0, ASLR in Android was almost non-existent. One way to easily and quickly test the ASLR support of a platform is to dump the memory map for a process across multiple executions. For example, the /proc/pid/maps for the vold process across multiple executions on a Nexus S device running Android 2.3.4 looks like the following:
As the memory maps show, the only randomized memory region across two consecutive executions of the vold process is the stack. The executable region (vold), heap, libraries (libc.so), and linker are loaded at the same locations in the address space.
Where does our stack randomization come from on Android? From the arch-independent code in fs/binfmt_elf.c that is invoked when executing ELF binaries. More specifically, the load_elf_binary() function calls randomize_stack_top():
#ifndef STACK_RND_MASK
#define STACK_RND_MASK (0x7ff >> (PAGE_SHIFT – 12)) /* 8MB of VA */
#endif
static unsigned long randomize_stack_top(unsigned long stack_top)
{
unsigned int random_variable = 0;
if ((current->flags & PF_RANDOMIZE) &&
!(current->personality & ADDR_NO_RANDOMIZE)) {
random_variable = get_random_int() & STACK_RND_MASK;
random_variable <<= PAGE_SHIFT;
}
#ifdef CONFIG_STACK_GROWSUP
return PAGE_ALIGN(stack_top) + random_variable;
#else
return PAGE_ALIGN(stack_top) – random_variable;
#endif
}
And that’s where we get our small bit of stack ASLR in Android 2.x! It’s clear from these results that ASLR is almost non-existent in Android 2.x. So how does ASLR fare in the latest and great ICS 4.0 that has touted ASLR as a new feature?
ASLR Support in Android ICS 4.0
Prior to mid-2010, the ARM architecture support in the Linux kernel didn’t actually support any ASLR (besides the arch-independent stack randomization). Why? Well, despite there being a huge number of ARM-powered devices out there in the world, there wasn’t as much of a security focus on ARM until the explosion of our modern consumer mobile devices. In June 2010, three commits from Nicolas Pitre changed that.
The first commit (cc92c28b2d) added support for randomization of the libs/mmap mappings:
— a/arch/arm/mm/mmap.c
+++ b/arch/arm/mm/mmap.c
@@ -80,6 +81,9 @@ arch_get_unmapped_area(struct file *filp, unsigned long addr,
start_addr = addr = TASK_UNMAPPED_BASE;
mm->cached_hole_size = 0;
}
+ /* 8 bits of randomness in 20 address space bits */
+ if (current->flags & PF_RANDOMIZE)
+ addr += (get_random_int() % (1 << 8)) << PAGE_SHIFT;
full_search:
if (do_align)
The second commit (990cb8acf2) added support for randomization of the heap/brk mapping:
— a/arch/arm/kernel/process.c
+++ b/arch/arm/kernel/process.c
@@ -421,3 +422,9 @@ unsigned long get_wchan(struct task_struct *p)
} while (count ++ < 16);
return 0;
}
+
+unsigned long arch_randomize_brk(struct mm_struct *mm)
+{
+ unsigned long range_end = mm->brk + 0x02000000;
+ return randomize_range(mm->brk, range_end, 0) ? : mm->brk;
+}
The third commit (e4eab08d60) added support for randomization of the executable mapping:
— a/fs/binfmt_elf.c
+++ b/fs/binfmt_elf.c
@@ -800,7 +800,7 @@ static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
* default mmap base, as well as whatever program they
* might try to exec. This is because the brk will
* follow the loader, and is not movable. */
-#ifdef CONFIG_X86
+#if defined(CONFIG_X86) || defined(CONFIG_ARM)
load_bias = 0;
#else
load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE – vaddr);
These three commits were included in the 3.x kernel shipping with the Galaxy Nexus, so Android 4.0 should inherit full ASLR support just by using an updated Linux kernel, right? Ideally, yes, but let’s take a look at a real Galaxy Nexus device (running 4.0.2) to see what randomization is actually present:
Well, there’s a slight improvement: the location of libc.so and other shared libraries mapped into the vold address space are randomized. But why aren’t the heap, executable, and linker mappings randomized?
Heap Randomization
According to commit 990cb8acf2, the heap/brk mapping should be randomized. However, the heap randomization enabled in that commit is still contingent on a sysctl parameter, kernel.randomize_va_space.
If kernel.randomize_va_space is set to 1, the mmap, stack, VDSO, and executable mapping are randomized. However, the heap is not randomized unless kernel.randomize_va_space is set to 2. The reasoning behind this setting is that some legacy applications assume that the heap is mapped in a specific location.
So, checking out the Galaxy Nexus device, we see that the randomize_va_space attribute is still set to 1, preventing the heap mapping from being randomization:
shell@android:/ # cat /proc/sys/kernel/randomize_va_space
1
What happens if we manually set kernel.randomize_va_space to 2? Let’s try:
shell@android:/ # echo 2 > /proc/sys/kernel/randomize_va_space
shell@android:/ # cat /proc/sys/kernel/randomize_va_space
2
Re-running our vold binary with randomize_va_space set to 2, we see that the heap location is now successfully randomized:
Executable Randomization
So if the kernel supports randomizing ELF executable mappings and randomize_va_space is set to an appropriate value, why isn’t the Android platform randomizing the location of the vold binary?
In order for executable mappings to be randomized, the binary being executed needs to be compiled appropriately so that it can be relocated at runtime. In particular, the binary must be compiled with GCC’s -pie -fPIE flags to result in a Position Independent Executable (PIE).
One easy way to check whether a binary is PIE-enabled is with the readelf command. If readelf reports that the binary is of type EXEC, it means that it is not a PIE. If readelf reports a type of DYN, it means that it is a PIE and can be randomized by the kernel when it is mapped.
Running readelf on the vold binary on the Galaxy Nexus device shows the non-PIE EXEC type ELF binary:
$ arm-eabi-readelf -h vold | grep Type
Type: EXEC (Executable file)
Running readelf on a PIE-binary on a normal non-Android Linux system shows the expected DYN type:
$ readelf -h /bin/cat | grep Type
Type: DYN (Shared object file)
To address this issue, all executables on the Android system need to be compiled with PIE support. Unfortunately, PIE does impose a non-trivial performance penalty for architectures that with limited GPRs, but that may be an acceptable trade-off for certain high-risk Android executables.
Linker Randomization
Last but certainly not least, the custom dynamic linker that Android uses it not randomized. This is a serious concern as the linker will be present at a static address across most, if not all, processes on the system. The executable linker mapping also provides a plentiful source for gadgets that can be used to construct a ROP payload when exploiting a memory corruption vulnerability.
Simply put, the amount of code available in the static dynamic linker mapping completely nullifies the efficacy of the ASLR in ICS 4.0. Hopefully Google modifies it’s dynamic linker to support relocation in the near future.
Additional ASLR Concerns on Android
Regardless of the platform version, ASLR in Android is also complicated by two issues that are a bit more inherent in the current platform design and architecture:
- 32-bit address space: So far, our mobile devices have remained limited to 32-bit address spaces. With a 32-bit system, there’s limited address space available to shift memory regions around, making ASLR less effective especially in scenarios where an attacker may have multiple tries to exploit the process. It’s not unreasonable to consider mobile devices that may address more than 4 GB of physical memory and require a move to 64-bit CPUs, allowing much more address space for randomization, but who knows how far off in the future that is.
- Zygote system process: The Zygote process on the Android platform acts as the prototypical process for all new Android applications. When a new process is launched, the Zygote will fork(2) its process and launch the requested app. This is simply a performance optimization that eliminates the overhead of spinning up a new Dalvik VM from scratch when an application is launched. However, it also means that the memory mappings inherited from the Zygote address space are identical across all Dalvik applications. One could imagine a scenario where a malicious app on a victim’s device leaks address mappings from its own process off to an attacker to assist in exploiting another process (eg. the browser) that might have higher privilege or valuable data. It’s a bit of an exotic scenario though, so the Zygote issue is fairly low risk for now.
Wrap-up
Unfortunately, the ASLR support in Android 4.0 did not live up to expectations and is largely ineffective for mitigating real-world attacks, due to the lack of randomization of the executable and linker memory regions. It also would be beneficial to randomize the heap/brk by setting kernel.randomize_va_space=2.
In addition to ASLR, Android could certainly stand to beef up some of it’s other exploit mitigation mechanisms. Non-executable memory support was recently added and GCC’s stack protector is now enabled in the default NDK CFLAGS, but other mitigations are still lacking. RELRO is missing allowing GOT overwrites as demonstrated in Stealth’s GingerBreak exploit.
I’d also love to see code signing support similar to iOS, which prevents the introduction of new executable code in an address space. In addition, code signing would hamper the ability to pull down additional malicious code at runtime, a technique I demonstrated two years ago at SummerCon that the RootSmart malware authors have recently adopted.
Let’s just hope that we don’t have to wait for another major version release of the Android platform to get the same exploit mitigations that have been available on desktop and server platforms for years.
This post first appeared on the Duo Security blog. Jon Oberheide is a co-founder and CTO of Duo.