
demo CVE-2019-2215 (Bad Binder) for Android Q
This repository is a small test project for researching the vulnerability
CVE-2019-2215 (Bad Binder) and writing a working prototype of an exploit for Android with
a simple graphical interface in Kotlin/Jetpack Compose.
In the README I:
The repository has a GitHub Actions workflow configured that, on every push/PR, builds the project
with the command ./gradlew assembleDebug and publishes the finished badbinder-debug.apk as an artifact.
You can download it like this:
badbinder-debug-apk
with the built APK.This is done for convenience, if you just want to test the application without setting up a local environment.
CVE-2019-2215 is a Use-After-Free (UAF) in the Binder IPC subsystem of the Android kernel.
Simplified:
struct binder_thread that describes a thread
performing Binder calls;waitqueue);remove_wait_queue,
which opens a classic UAF scenario;I made a more detailed theoretical breakdown based on the materials from:
The assignment recommends using an AVD with Android 10.0 (Q) x86_64 image.
I did the following:
/dev/binder device exists.adb shell
ls -l /dev/binder
At this stage I ran into an unpleasant fact:
currently, the current AVD images already come with a patched kernel where
CVE-2019-2215 is fixed. That is, you won't actually be able to get root on a modern official emulator
— the exploit fails at later stages or simply doesn't give privilege escalation.
In the end, I use the AVD as a simulator to reproduce the exploit logic:
addr_limit,This is an important nuance: all code and report below are educational, not "combat".
I made a small Android application:
Main steps:
Created a regular project in Android Studio (Kotlin, minimum API Android 10).
Added NDK and CMake.
Added a native file with the exploit (that same cve-2019-2215.c with functions
leak_task_struct, overwrite_addr_limit, etc.).
In CMakeLists.txt added the build of libcve-2019-2215.so.
In MainActivity:
init {
System.loadLibrary("cve-2019-2215")
}
external fun runNativeExploit(): String
external fun setNativeLogger(logger: NativeLogger)
On the Kotlin side, I made an ExploitViewModel that implements the
NativeLogger interface and puts all messages into a StateFlow<List<String>>. The UI subscribes
to this flow and displays the log in a "terminal".
When the activity starts, I call setNativeLogger(viewModel), so that the native code gets
an object to which it can send strings.
Build and install the application:
./gradlew installDebug
Start the AVD and the application itself.
On the screen I see a "terminal" and a button RUN EXPLOIT.
When pressed:
runNativeExploit() in a background thread.On a real vulnerable kernel, I would expect to see something like at the end:
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...
On the current Android 10 emulator this, of course, does not happen, but everything else —
leak of task_struct, attempt to overwrite addr_limit, calculation of cred and kernel_base —
works as a "scenario", which is what was required for the assignment.
Below is the logical scheme of the exploit with reference to specific C functions.
The high-level plan is:
struct binder_thread object and use it to
leak the address of task_struct of my process (leak_task_struct).addr_limit field in task_struct (overwrite_addr_limit) — this removes the
restriction between user-space and kernel-space addresses for subsequent
copy_to_user / copy_from_user.arb_read / arb_write).cred of the current process and the kernel base (verifying),
then:
selinux_enforcing = 0),cred fields to become root and get the full set of capabilities
(runNativeExploit).In parallel, I integrated a JNI logger, so that all these stages are visible directly in the UI.
task_struct (leak_task_struct)Key function:
void leak_task_struct() {
android_log("[*] Starting leak_task_struct...");
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
ret = sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
assert(ret >= 0);
...
}
What the function does:
Pins the thread to CPU 0 (sched_setaffinity) to make kernel allocator behavior
more predictable. This improves UAF exploit stability.
Opens /dev/binder, creates an epoll descriptor:
fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
The binder descriptor is registered in epoll:
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
Prepares an array struct iovec iov_buffers[IOVEC_N] and allocates memory:
spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Here it is important that the lower 32 bits of the address are zero:
if (((long) spinner & 0xffffffff) != 0) {
android_log("[!] mmap returned wrong address!");
return;
}