触发与分析 Android 内核漏洞 CVE-2019-2215
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 访问权限的。最后,我们将了解如何通过补丁缓解此漏洞。
要通过触发此漏洞使内核崩溃,我们使用以下步骤:
观看以下视频:
[视频]
在本节(以及下一节)中,我们将通过静态和动态分析来理解崩溃发生的原因。
在这里,我们将分析内核代码(静态分析)以理解该问题。在 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); }
我们来看看 `trigger.cpp` 在做什么。
在 Android 中(与其他基于 Unix 的操作系统一样),我们有一些进程。我们运行的每个程序都会创建一个(或多个)进程,这些进程由操作系统(OS)管理。操作系统可以在它们之间切换(多任务处理),或者终止某个进程等。出于安全原因,进程默认彼此隔离。
在某些情况下,一个进程可能需要与另一个进程交换数据。这称为进程间通信(IPC)。在 Linux 中有多种方式可以让进程通信。Android 引入了一种名为 **'Binder'** 的特定 IPC 机制。Binder 是一个用于促进进程间通信的内核驱动程序。
在 Android 中,IPC 可以通过直接调用一些内核方法(其中大部分位于 drivers/binder.c)或使用高级实现(例如 Java)来完成。

为了使用 **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 结构体并添加到队列结构中。

通过调用 **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 的链表)。

#### 释放(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!!
我们创建了一个 event_poll,它包含一棵 红黑树(red_black_tree),每个节点是一个 ep_item,它有一个字段是 epoll_entry 列表,每个 epoll_entry 有两个指向 binder_thread 结构体的指针 (wait, whead).
通过调用 ioctl(),我们从内存中释放了 binder_thread,然后在退出时,这个结构体通过一个仍然可用的指针被访问了!
动态分析是通过实时执行数据来测试和评估程序;以在程序运行时发现错误。
步骤:
构建不带 KASan 的 Android 内核
使用新构建的内核启动模拟器
启动模拟器
构建漏洞触发器并将其推送到虚拟设备
在 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 操作前后发生的情况。
启动 adb shell 并运行触发 PoC
结果: