Android Kernel Adventures: Insights into Compilation, Customization and Application Analysis
This article marks the first in a series aimed at sharing my adventures, personal notes, and insights into the Android kernel. My focus will primarily be on how to modify it for a deeper analysis of the applications running on the system. It’s important to note that it’s been quite a while since my last experience with the Android kernel — the last time I compiled and ran a module on my old Samsung Galaxy. As expected, a lot has changed since then, and this will be a process of rediscovery and relearning as I write the upcoming articles. If I happen to make any mistakes or if you have any suggestions, feel free to reach out. Without further ado, let’s embark on this journey!
Given the increasing complexity of RASP (Runtime Application Self-Protection) solutions for Android applications, focusing efforts on deeper layers of the system, such as the Android kernel, emerges as a strategic approach for more effective security analysis. By concentrating on the kernel, there is no need to bypass protections implemented at the application layer, as RASP solutions are specifically designed to protect higher layers but do not cover the kernel. Moreover, understanding the inner workings of the Android kernel can significantly enhance the work of vulnerability researchers, providing valuable insights into vulnerabilities within the Android operating system.
According to Android’s documentation, the Android kernel consists of the Linux kernel along with Android-specific patches, forming what is known as Android Common Kernels (ACKs). Starting from kernel version 5.4 and onwards, ACKs are referred to as GKI (Generic Kernel Image) kernels. The GKI is a Google-certified boot image that contains a kernel built from the ACK source tree, designed to be written to the boot partition of Android devices. The GKI is part of a Google project aimed at addressing kernel fragmentation by separating the common core functionality of the kernel, maintained by Google, from the vendor-specific kernel build configuration that builds vendor kernel modules. The diagram below illustrates the architecture of Android, showing that everything runs on top of the Linux kernel.
In Android’s documentation, you can also find a list of supported kernels for each version of Android, categorized into Launch Kernels and Feature Kernels. The Launch Kernel is the valid kernel for the release of a device with a specific version of Android, while the Feature Kernel ensures the implementation of version-specific features, preventing these features from being backported to earlier kernel versions.
For our tests, we chose the kernel android13–5.15. The Android kernel branches are organized to reflect both the Android version and the corresponding kernel version, following the format ANDROID_RELEASE-KERNEL_VERSION, where ANDROID_RELEASE represents the Android version and KERNEL_VERSION indicates the kernel version. For example, the android13–5.15 kernel corresponds to kernel version 5.15 for Android 13. We used the following commands to download the Android kernel.
mkdir android-kernel && cd android-kernel
repo init -u https://android.googlesource.com/kernel/manifest -b common-android13-5.15
repo sync -c -j4
In the android-kernel directory, we can observe the presence of a file called WORKSPACE. This indicates that we are dealing with a Bazel project. Bazel is a free and open-source software tool developed by Google, designed for automating software build and testing processes. Directories that contain a WORKSPACE file are considered the root of a workspace, which is a directory tree containing the source code files of the software we are attempting to build. Instructions for installing Bazel can be found at this link.
Before we begin compiling the kernel, it is important to highlight that we will use Cuttlefish to test our kernel. The main difference between Goldfish (the emulator used in Android Studio) and Cuttlefish lies in their purpose and how they simulate the Android environment. Goldfish is optimized for application testing but is not ideal for testing the operating system, as it does not accurately replicate real hardware and has limitations, particularly during the boot process. On the other hand, Cuttlefish is a virtual platform designed to simulate real hardware more faithfully, making it the ideal choice for testing the Android operating system and kernel, as it provides a more accurate and representative simulation of the behavior of a physical device. You can find the installation instructions for Cuttlefish through this link.
The Android kernel build files use a framework called Kleaf, which, along with Bazel, is responsible for building the Android kernel and other related artifacts, such as boot images, kernel modules, and more. Although I’m not an expert in Kleaf or Bazel, my focus here will be to explain only the key code snippets necessary to understand how to build and modify the Android kernel.
Do you remember when I mentioned that the GKI splits the kernel into a kernel image maintained by Google and a module specific to the SoC and board, implemented by vendors? The kernel image files maintained by Google are stored in the common directory, while the files for the SoC and board-specific module are stored in the common-modules directory. These two kernels will be compiled separately, as we will see next.
Upon starting our analysis with the common/BUILD.bazel file, we identified the presence of the define_common_kernels macro, which is responsible for defining the common build targets for Android kernels. For the x86_64 kernel, the architecture we will use to build our kernel, the configuration file used is build.config.gki.x86_64.
load("//build/kernel/kleaf:common_kernels.bzl", "define_common_kernels", "define_db845c")
The configuration file build.config.gki.x86_64 uses three additional configuration files: build.config.common, build.config.x86_64, and build.config.gki. I won’t be able to go over all of them in detail, but there is an important point to note: within the build.config.gki file, you will encounter the execution of the check_defconfig command. If you plan to modify the default kernel configuration (as we will do shortly), it’s important to comment out this line. Otherwise, after changes are detected in the configuration file, the kernel build will fail.
###POST_DEFCONFIG_CMDS="check_defconfig"
Upon analyzing the common-modules/virtual-device/Build.bazel file, we found several kernel build targets. Since we will be building the kernel with support for a virtual x86_64 device, our focus will be on the following lines:
kernel_build(
name = "virtual_device_x86_64",
srcs = [":virtual_device_x86_64_common_sources"],
outs = [],
base_kernel = "//common:kernel_x86_64",
build_config = "build.config.virtual_device.x86_64",
module_outs = _virt_common_modules + [
# keep sorted
"test_meminit.ko",
"test_stackinit.ko",
],
)
We can see in this section of the script that the kernel configuration will be in the file build.config.virtual_device.x86_64. The content of the build.config.virtual_device.x86_64 file reveals that one of the configurations used in the kernel build is stored in the common/arch/x86/configs/gki_defconfig file. With this information, we can modify its contents to include additional features in the kernel. To test this, we will add support for ftrace, which is not enabled in the default kernel configuration.
PRE_DEFCONFIG_CMDS="KCONFIG_CONFIG=${ROOT_DIR}/${KERNEL_DIR}/arch/x86/configs/${DEFCONFIG} ${ROOT_DIR}/${KERNEL_DIR}/scripts/kconfig/merge_config.sh -m -r ${ROOT_DIR}/${KERNEL_DIR}/arch/x86/configs/gki_defconfig ${ROOT_DIR}/common-modules/virtual-device/virtual_device.fragment"
ftrace is a tracing and profiling framework integrated directly into the Linux kernel. It allows the observation and recording of the kernel’s function execution flow through both static probes (added during kernel compilation) and dynamic probes (injected at runtime). Using these probes, ftrace enables the capture of events related to process scheduling, interrupts, system calls, function latencies, I/O device interactions, and even specific functions executed within the kernel. To enable ftrace in the kernel, the following lines must be added to the kernel configuration file, gki_defconfig.
CONFIG_FUNCTION_TRACER=y
CONFIG_FUNCTION_GRAPH_TRACER=y
CONFIG_STACK_TRACER=y
CONFIG_DYNAMIC_FTRACE=y
Next, inside the android-kernel directory, we will execute the following commands to compile both kernels and the necessary modules for booting Cuttlefish.
tools/bazel build //common:kernel_x86_64_dist
tools/bazel run //common-modules/virtual-device:virtual_device_x86_64_dist
After compilation, we can verify that files such as bzImage and initramfs.img have been copied to the out/android13–5.15/dist directory. We will use these files to test the kernel on Cuttlefish.
With Cuttlefish properly configured, we can use the command below to test the newly compiled kernel. The parameters can be adjusted as needed.
HOME=${PWD} ./bin/launch_cvd -daemon -memory_mb 27000 -data_policy always_create -blank_data_image_mb 30000 -cpus 1 --kernel_path=/home/thiago/android-kernel/out/android13-5.15/dist/bzImage -initramfs_path=/home/thiago/android-kernel/out/android13-5.15/dist/initramfs.img
The image below shows a virtual device in Cuttlefish running our kernel, with ftrace working correctly.
Conclusion
In conclusion, this article outlined the essential steps for compiling and modifying the Android kernel, as well as performing tests in Cuttlefish to verify its functionality. This lays the foundation for customizing the Android kernel in a virtualized environment. In the upcoming articles, we will explore advanced kernel debugging techniques and demonstrate how to customize the kernel to gather detailed information about the applications running on the system.