Android Binder Attack Matrix: Fuzzing Binder with Linux Kernel Library (LKL) (Article — 3)
Binder, a crucial IPC mechanism in Android, has been a focal point for security researchers due to its complex and extensive codebase. Syzkaller, a state-of-the-art fuzzer, has been instrumental in this domain, effectively fuzzing Binder for years. It operates by generating programs based on syscall descriptions, which are then executed on the target machine to uncover potential vulnerabilities. This approach led to the discovery of significant security issues, such as CVE-2019–2215, known as Bad Binder. Despite these efforts, Syzkaller has achieved approximately 25% line coverage, highlighting both its success and the ongoing need for advanced techniques like the Linux Kernel Library (LKL) to enhance fuzzing efficacy and uncover deeper, hidden bugs.
Binder Fuzzing Challenges
struct object {
char *x_ptr;
char *y_ptr;
};
a) Data Dependencies: Fuzzing Binder presents several unique challenges, particularly due to the intricate data dependencies inherent in its operations. Binder commands often involve complex scatter-gather data structures, such as BINDER_TYPE_PTR, which can reference memory in a non-contiguous manner. This complexity makes it difficult to generate meaningful and effective fuzzing inputs that accurately simulate real-world scenarios. Addressing these data dependencies is crucial for advancing the fuzzing process and ensuring comprehensive coverage of Binder’s codebase.
b) State Dependencies: Another significant challenge in fuzzing Binder lies in managing state dependencies. Binder operates as a synchronous IPC mechanism, which introduces constraints such as the inability to send transactions to oneself and the limitation of having multiple outstanding transactions at once. These constraints complicate the generation of fuzzing inputs that can effectively explore Binder’s various states and behaviors.
Moreover, some inputs in Binder fuzzing are highly dependent on the outcomes of previous IOCTL calls. For instance, transaction buffers require careful handling, and commands like BC_FREE_BUFFER depend on the proper sequence and state of preceding operations. This interdependence of inputs necessitates a sophisticated approach to fuzzing, ensuring that each step is accurately simulated to maintain the integrity and realism of the testing environment. Addressing these state dependencies is critical to uncovering deeper vulnerabilities and achieving more thorough code coverage.
c) Multi-process Coordination: Effective communication in Binder fuzzing requires meticulous setup, involving a Context Manager, as well as the establishment of Node and Ref structures. These components are essential for initiating and maintaining connections within the Binder framework. The Context Manager oversees the state and coordination of communication, while the Node and Ref setup ensures that connections are properly established and referenced. This setup process is crucial for accurately simulating real-world Binder interactions and ensuring the reliability and integrity of fuzzing operations.
LKL Overview
The Linux Kernel Library (LKL) is an innovative approach that compiles the Linux kernel into a user-space library. Implemented as a Linux arch-port, LKL allows developers to utilize kernel functionalities without the need for a traditional kernel environment. This approach contrasts with User-Mode Linux (UML), which runs the entire kernel as a user-space process. By leveraging LKL, developers gain the flexibility to integrate kernel features directly into user-space applications, streamlining various testing and development processes.
The foundational building blocks of LKL include several key components:
- Host Environment API: A portability layer that enables LKL to function across different host environments.
- Linux Kernel Code: The core kernel code is repurposed to operate within the LKL framework.
- LKL syscall API: This API is exposed to user-space applications, allowing direct interaction with the kernel functionalities provided by LKL.
Using LKL offers significant advantages, such as running kernel code without the overhead of launching a Virtual Machine. This capability is particularly beneficial for tasks like kernel unit testing and fuzzing, where efficiency and speed are crucial. By integrating LKL, developers can perform comprehensive kernel testing and security assessments within a user-space environment, enhancing the robustness and security of their applications.
Anatomy of LKL Fuzzer
The Linux Kernel Library (LKL) significantly enhances the ability to fuzz Linux kernel code within a user-space environment. By utilizing an in-process fuzzing engine like libFuzzer, LKL enables seamless and efficient fuzzing without the need for traditional kernel space, making it a powerful tool for security researchers and developers.
Advantages of LKL Fuzzer:
- High Fuzzing Performance: LKL provides high-performance fuzzing on x86_64 architectures, allowing for rapid identification of potential vulnerabilities.
- Ease of Custom Modifications: LKL’s user-space nature simplifies the process of making custom modifications, such as mocking hardware components or implementing a custom scheduler. This flexibility is crucial for testing specific scenarios and configurations that might be challenging to replicate in a standard kernel environment.
Limitations of LKL Fuzzer:
- No Symmetric Multiprocessing (SMP): LKL does not support SMP, which can limit the ability to test scenarios involving multiple processors.
- Architectural Differences: The differences between x86_64 and aarch64 architectures can lead to potential false positives and false negatives. These architectural discrepancies must be carefully managed to ensure accurate fuzzing results and avoid misleading conclusions about the presence or absence of vulnerabilities.
Using LKL from your C++ code
int ret = lkl_start_kernel(&lkl_host_ops, "mem=50M");
lkl_mount_fs("sysfs");
lkl_mount_fs("proc");
lkl_mount_fs("dev");
int binder_fd = lkl_sys_open("/dev/binder", O_RDWR | O_CLOEXEC, 0);
void *binder_map = lkl_sys_mmap(NULL, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE, binder_fd, 0);
struct lkl_binder_version version = { 0 };
ret = lkl_sys_ioctl(binder_fd, LKL_BINDER_VERSION, &version);
Fuzzing Harness
// Fuzz Data
client_1 {
binder_write {
binder_commands {
transaction {
binder_objects { binder { ptr: 0xbeef } }
}
}
}
}
client_2 {
binder_read { ... }
binder_write {
binder_commands { free_buffer { ... } }
}
}
client_3 { ... }
client_2 { ... }
client_3 { ... }
client_1 { ... }
The fuzzing harness is designed to simulate Inter-Process Communication (IPC) interactions between multiple clients, providing a realistic testing environment for the Binder framework. This involves setting up three clients, including one acting as the Context Manager, to orchestrate and manage the interactions. The harness generates a variety of IOCTL calls and data exchanges, ensuring that different communication patterns and edge cases are thoroughly tested. By simulating these complex interactions, the fuzzing harness helps identify potential vulnerabilities and robustness issues within the Binder IPC mechanism.
Randomized Scheduler
The fuzzing framework deterministically simulates thread interleaving based on the provided fuzz data, ensuring comprehensive coverage of concurrent execution paths. By inserting yield points strategically before and after synchronization primitives such as spin_lock
, spin_unlock
, mutex_lock
, and mutex_unlock
, the framework meticulously explores various thread interleaving scenarios. This approach allows the detection of subtle concurrency issues and race conditions that might be missed with non-deterministic testing methods, enhancing the overall robustness and reliability of the kernel code under test.
Conclusion
Stay tuned for the next article which would conclude the Android Binder Attack Matrix Series where we will discuss the results of the findings that were covered here. Cheers, Ladies & Gentlemen!