CopperheadOS: Feature Review Part 1

May 14, 2018
android security copperheados review

Update: this post was written before the primary maintainer of CopperheadOS was forced out of the company and CopperheadOS stopped being maintained. I have since moved on to building my own privacy focused OS, but this is still a fun look into some of the interesting security hardening features that existed in CopperheadOS.


CopperheadOS is an Android based mobile operating system with a focus on privacy and security. It builds on the latest stable release of the Android Open Source Project, which is essentially Android without all the Google stuff in it. On top of this, they have built an impressive list of features and additions that provide a more secure and private Android experience. Currently, CopperheadOS is only supported on a small number of phones (mainly Pixel devices), as devices need to meet strict security requirements to be considered.

I thought it would be fun to dive a bit deeper into some of the features listed in their technical overview. We’ll attempt to gain a better understanding of each feature, dig into some of the code, and see if we can show that it’s working as expected.

Part 1 - Exec-based Spawning

The very first feature listed in their technical overview is an exec-based spawning model:

CopperheadOS spawns applications from the Zygote service in the traditional Unix way with fork and exec rather than Android’s standard model using only fork.

Let’s first dig into what some of this terminology means.


First, what is the Zygote service? Zygote is an Android process that is started early in the boot process. When it starts, it boots an Android Runtime (ART) VM (think optimized JVM for mobile devices) and it loads all the core Android framework components into memory. The Zygote service is the parent process of all Android applications, as it is used to start all future applications after initialization. -

We can see for ourselves that the Zygote service is really the parent process of all Android applications by first finding the process ID (PID) for Zygote and then searching for all processes with this parent PID.

$ adb shell
marlin:/ $ ps -A | grep 'zygote'
root           598     1 4053648  43460 poll_schedule_timeout 0 S zygote64
root           599     1 1400592  19652 poll_schedule_timeout 0 S zygote
webview_zygote 1216    1 4092436  28976 poll_schedule_timeout 0 S webview_zygote64

marlin:/ $ ps -A -o PPID,ARGS=CMD | grep 598
598 system_server
598 com.quicinc.cne.CNEService
598 com.qualcomm.qti.telephonyservice

Fork and exec

Second, what does the traditional Unix way with fork and exec mean?

In traditional Unix-like operating systems, a program requests services from the operating system kernel by using system calls. Fork and exec are two examples of system calls used for creation and execution of processes.

fork() is used by a parent process to divide itself into two identical processes. Everything is the same about these two processes including open files, register state, and all memory allocations, which includes the program’s executable code. This new process (which is a child of the parent) has a new PID. The fork() function returns the child’s PID to the parent, while it returns 0 to the child, in order to allow the two identical processes to distinguish one another.

exec() is used to run an executable file in the context of an already existing process, replacing the previous executable. Since a new process is not created, the original PID does not change, but the machine code, data, heap, and stack of the process are replaced by those of the new program.

So fork and exec is just the combination of these two techniques where a process is forked into two processes and then exec is run to replace it with a new executable. This fork-exec model can easily be shown in a Linux environment. So as an example, if I launch a new shell it will first fork the existing shell process and then run exec to replace the child process. We can see below that the child shell has a new PID and the parent PID is set to the original shell’s PID.

$ echo "Current shell process ID is: $$"
Current shell process ID is: 31098

$ /bin/bash -c 'echo "New shell process ID is: $$ and parent process ID is $PPID" && sleep 30'
New shell process ID is: 31144 and parent process ID is 31098

If we open a separate shell while this is still sleeping, we can use strace to see the parent process is waiting for the child process to exit, which will happen only after the sleep command completes.

$ strace -p 31098
Process 31098 attached

Why exec-based spawning?

So why is this exec-based spawning model more secure? The documentation has the following reason listed:

This results in the address space layout and stack/setjmp canaries being randomized for each spawned application rather than having the same layout and canary values reused for all applications until reboot. In addition to hardening applications from exploitation, this also hardens the base system as the large, near root equivalent system_server service is spawned from the Zygote.

Wikipedia can tell us more about address space layout randomization and stack canaries.

Android has had full ASLR support for all processes since Android 4.0, but with the standard Android fork only model, immediately after fork, the parent and child address spaces are identical. While address space layout randomization is still in effect for both the parent and the child process, it can’t go back and randomize the memory allocations that have already been made, so only future memory allocation will be randomized. This means that every application will have the same shared memory address locations, and an exploit in one app has potential to leak base memory addresses locations for all other applications.

Why does Android use this fork only model?

It’s mainly just an optimization that helps reduce application startup time and memory usage. Every application running on Android runs within it’s own ART VM instance. It isn’t a cheap operation create a new ART VM every time an application needs to launch, and on lower powered mobile devices the startup time can be noticeable. The Zygote process is used to solve this problem. When an application wants to run, the Zygote process forks itself. Forking causes the Zygote process to be cloned into a new process and because of this the new process shares the same pre-loaded Android framework resources and is immediately ready to run application specific code.

Digging into the code

Let’s take a look at some of the code that makes this exec-based spawning model possible in CopperheadOS. This code lives in CopperheadOS’s forked version of Android’s platform_frameworks_base.

The majority of the code for this feature can be found in

The Zygote process registers a socket (/dev/socket/zygote) on startup that is used for listening to requests to start new applications. When a request is received, in stock Android it would call ZygoteInit.zygoteInit() to perform the standard fork only model, but this has been replaced with a call to ExecInit.execApplication().

@@ -795,8 +795,11 @@ private Runnable handleChildProc(Arguments parsedArgs, FileDescriptor[] descript
            // Should not get here.
            // Should not get here.
            throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
            throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
        } else {
        } else {
-            return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,
+            ExecInit.execApplication(parsedArgs.niceName, parsedArgs.targetSdkVersion,
-                    null /* classLoader */);
+                    VMRuntime.getCurrentInstructionSet(), parsedArgs.remainingArgs);
+            // Should not get here.
+            throw new IllegalStateException("ExecInit.execApplication unexpectedly returned");

In ExecInit.execApplication() we see that exec system call being made that doesn’t happen in stock Android. It uses execcv() to run app_process (a binary used to start java code on Android) telling it to execute

public static void execApplication(String niceName, int targetSdkVersion,
            String instructionSet, int debugFlags, String[] args) {
        int niceArgs = niceName == null ? 0 : 1;
        int baseArgs = 6 + niceArgs;
        String[] argv = new String[baseArgs + args.length];
        if (VMRuntime.is64BitInstructionSet(instructionSet)) {
            argv[0] = "/system/bin/app_process64";
        } else {
            argv[0] = "/system/bin/app_process32";
        argv[1] = "/system/bin";
        argv[2] = "--application";
        if (niceName != null) {
            argv[3] = "--nice-name=" + niceName;
        argv[3 + niceArgs] = "";
        argv[4 + niceArgs] = Integer.toString(targetSdkVersion);
        argv[5 + niceArgs] = Integer.toString(debugFlags);
        System.arraycopy(args, 0, argv, baseArgs, args.length);

        try {
            Os.execv(argv[0], argv);
        } catch (ErrnoException e) {
            throw new RuntimeException(e);

This will kick off the main() method which then runs execInit(), which is almost the same as the original call to ZygoteInit.zygoteInit() that got replaced. It has a few minor differences:

Verifying exec-based spawning works

Let’s try to see if we can see the difference between memory allocations for processes on stock Android vs CopperheadOS.

Stock Android memory allocation

Let’s look memory allocations for dialer and calendar apps on stock Android and do a comparison. Because stock Android only forks processes and doesn’t use exec - we see that the majority of memory regions are identical between two separate processes.

$ adb shell 'su 0 cat /proc/$(pidof' > stock-dialer.output
$ adb shell 'su 0 cat /proc/$(pidof' > stock-calendar.output
$ total_lines=$(wc -l < stock-dialer.output | awk '{print $1}')
$ common_lines=$(comm -1 -2 stock-dialer.output stock-calendar.output | wc -l | awk '{print $1}')
$ echo "common memory regions: ${common_lines}/${total_lines}"
common memory regions: 1317/1452

If we restart the dialer app, the majority of memory regions remain identical.

$ adb shell 'su 0 kill -9 $(pidof'
$ adb shell 'su 0 cat /proc/$(pidof' > stock-dialer-restarted.output
$ total_lines=$(wc -l < stock-dialer.output | awk '{print $1}')
$ common_lines=$(comm -1 -2 stock-dialer.output stock-dialer-restarted.output | wc -l | awk '{print $1}')
$ echo "common memory regions: ${common_lines}/${total_lines}"
common memory regions: 1324/1452

CopperheadOS memory allocation

Let’s look at memory allocations for dialer and calendar apps on CopperheadOS this time and do a comparison. CopperheadOS forks and execs processes - so we can see that the majority of memory regions differ between two processes.

$ adb shell 'su 0 cat /proc/$(pidof' > copperhead-dialer.output
$ adb shell 'su 0 cat /proc/$(pidof' > copperhead-calendar.output
$ total_lines=$(wc -l < copperhead-dialer.output | awk '{print $1}')
$ common_lines=$(comm -1 -2 copperhead-dialer.output copperhead-calendar.output | wc -l | awk '{print $1}')
$ echo "common memory regions: ${common_lines}/${total_lines}"
common memory regions: 88/1609

If we restart the dialer app, the majority of memory regions change again, which aligns with what we expected from a fork and exec model.

$ adb shell 'su 0 kill -9 $(pidof'
$ adb shell 'su 0 cat /proc/$(pidof' > copperhead-dialer-restarted.output
$ total_lines=$(wc -l < copperhead-dialer.output | awk '{print $1}')
$ common_lines=$(comm -1 -2 copperhead-dialer.output copperhead-dialer-restarted.output | wc -l | awk '{print $1}')
$ echo "common memory regions: ${common_lines}/${total_lines}"
common memory regions: 88/1609


Today we looked at the exec-based spawning feature of CopperheadOS, how it is implemented, why it is useful and proved that it is working as expected.

SSL Pinning Workaround for Android Uber Apps

August 31, 2016
ssl mitm android uber notes

Bruteforce Attack on Bitcoin Brainwallets

August 8, 2013
bitcoin cryptocurrency bruteforce security python