介绍
在我第一次研究 Linux 的BleedingTooth之后,我也想找到一个提权漏洞。我首先查看了 CVE-2016-3134 和 CVE-2016-4997 等旧漏洞,这些漏洞启发了我在 Netfilter 代码中memcpy()使用grep。memset()这导致我发现了一些错误的代码。
漏洞
当IPT_SO_SET_REPLACE或IP6T_SO_SET_REPLACE在兼容模式下调用时,需要CAP_NET_ADMIN在用户+网络命名空间中获得的能力,结构需要从用户转换为内核以及从 32 位转换为 64 位,以便由本机函数处理。自然,这注定容易出错。我们的漏洞在xt_compat_target_from_user()wherememset()被调用时使用了target->targetsize在分配期间没有考虑到的偏移量 - 导致一些字节越界写入:
targetsize不是由用户控制的,但可以通过名称(如,或)选择具有不同结构大小的TCPMSS不同TTL目标NFQUEUE。越大targetsize,我们可以改变的偏移量越大。但是,目标大小不得为 8 个字节对齐以实现pad > 0. 我发现最大的可能是NFLOG我们可以选择一个最大为 0x4C 字节的偏移量(可以通过在struct xt_entry_match和之间添加填充来影响偏移量struct xt_entry_target):
请注意,缓冲区的目标是分配的,GFP_KERNEL_ACCOUNT并且大小也可以不同:
不过,最小大小大于 0x100,这意味着可以分配此对象的最小平板是 kmalloc-512。换句话说,我们必须找到分配在 kmalloc-512 和 kmalloc-8192 之间的受害者来利用。
探索结构 msg_msg
使用 发送数据时msgsnd(),有效负载分为多个段:
struct msg_msg和的标题在哪里struct msg_msgseg:
第一个成员struct msg_msg是mlist.next指向队列中另一条消息的指针(不同于next因为这是指向下一个段的指针)。正如您接下来将学习的那样,这是一个完美的腐败候选人。
漏洞分析
首先,我们使用msgget(). struct msg_msg然后,我们使用 为每个消息队列发送一条大小为 4096(包括标头)的消息msgsnd(),我们将其称为主消息。最终,在大量消息之后,我们有一些是连续的:
接下来,我们使用以下命令为每个消息队列发送大小为 1024的辅助消息msgsnd():
最后,我们在主消息中创建一些漏洞(在我们的例子中每 1024 个),并触发易受攻击的setsockopt(IPT_SO_SET_REPLACE)选项,在最好的情况下,它将struct xt_table_info在其中一个漏洞中分配对象:
我们选择用零覆盖相邻对象的两个字节。假设我们与另一个主要消息相邻,我们覆盖的这些字节是指向辅助消息的指针的一部分。由于我们分配它们的大小为 1024 字节,因此我们有 1 - (1024 / 65536) 的机会重定向指针(我们失败的唯一情况是指针的两个最低有效字节已经为零)。
现在,我们可以期待的最好情况是被操纵的指针也指向一个次要消息,因为结果将是两个不同的主要消息指向同一个次要消息,这可能导致释放后使用:
但是,我们如何知道哪两个主要消息指向同一个辅助消息?为了回答这个问题,我们用在 [0, 4096) 中的消息队列的索引标记每条(主要和次要)消息。然后,在触发损坏后,我们遍历所有消息队列,使用msgrcv()with查看所有消息MSG_COPY并查看它们是否相同。如果主消息的标签与辅助消息的标签不同,则表示它已被重定向。在这种情况下,主消息的标签代表虚假消息队列的索引,即包含错误辅助消息的队列。,而错误次要消息的标签代表真实消息队列的索引。知道了这两个索引,实现 use-after-free 现在是微不足道的 - 我们即使用从真正的消息队列中获取辅助消息并因此释放它:msgrcv()
请注意,我们仍然在假消息队列中引用了已释放的消息。
绕过 SMAP
使用 unix 套接字(可以通过 轻松设置socketpair()),我们现在喷射大量大小为 1024 的消息并模仿struct msg_msg标头。理想情况下,我们能够回收先前释放的消息的地址:
请注意,它mlist.next是 41414141,因为我们还不知道任何内核地址(启用 SMAP 时,我们无法指定用户地址)。没有内核地址是至关重要的,因为它实际上会阻止我们再次释放块(稍后您将了解为什么需要这样做)。原因是在 期间msgrcv(),消息从循环列表的消息队列中取消链接。幸运的是,我们实际上处于实现信息泄露的有利位置,因为struct msg_msg. 即,该字段m_ts用于确定返回用户空间的数据量:
消息的原始大小只有1024-sizeof(struct msg_msg)字节,我们现在可以人为地增加到DATALEN_MSG=4096-sizeof(struct msg_msg). 因此,我们现在可以读取超出预期的消息大小并泄漏struct msg_msg相邻消息的标头。如前所述,消息队列被实现为一个循环列表,因此,mlist.next指向主消息。
知道主消息struct msg_msg的地址后,我们可以使用该地址重新制作假消息next(意味着它是下一个段)。然后可以通过读取多个字节来泄漏主要消息的内容。来自主消息DATALEN_MSG的泄露mlist.next指针揭示了与我们的假消息相邻的辅助消息的地址。从该地址中减去 1024,我们最终得到了假消息的地址。struct msg_msg
实现更好的免费使用后
struct msg_msg现在,我们可以用泄漏的地址重建假对象mlist.next和mlist.prev(意味着它指向自己),使假消息能够与假消息队列一起释放。
请注意,当使用 unix 套接字进行喷射时,我们实际上有一个struct sk_buff指向假消息的对象。显然,这意味着当我们释放假消息时,我们仍然有一个过时的引用:
这个陈旧struct sk_buff的数据缓冲区是一个更好的使用后释放场景,因为它不包含标题信息,这意味着我们现在可以使用它来释放平板上的任何类型的对象。相比之下,struct msg_msg只有当前两个成员是可写指针(需要取消链接消息)时,才能释放对象。
寻找受害者对象
攻击的最佳受害者是在其结构中具有函数指针的人。请记住,还必须为受害者分配GFP_KERNEL_ACCOUNT.
在与 Jann Horn 交谈时,他提出了struct pipe_buffer在 kmalloc-1024 中分配的对象(因此为什么辅助消息是 1024 字节)。struct pipe_buffer可以很容易地用它pipe()作为alloc_pipe_info()子例程进行分配:
虽然它不直接包含函数指针,但它包含一个指向struct pipe_buf_operations另一方面具有函数指针的指针:
绕过 KASLR/SMEP
当一个人写入管道时,struct pipe_buffer被填充。最重要的是,ops将指向anon_pipe_buf_ops位于 .data 段中的静态结构:
由于 .data 段和 .text 段之间的差异总是相同的,因此anon_pipe_buf_ops基本上可以让我们计算内核基址。
我们喷射大量struct pipe_buffer对象并回收陈旧struct sk_buff数据缓冲区的位置:
由于我们仍然有来自 的引用struct sk_buff,我们可以读取它的数据缓冲区,泄露 的内容struct pipe_buffer并显示 的地址anon_pipe_buf_ops:
有了这些信息,我们现在可以找到 JOP/ROP 小工具。请注意,当从 unix 套接字读取时,我们实际上也释放了它的缓冲区:
提升权限
struct pipe_buffer我们用一个假的来回收陈旧的,ops指向一个假的struct pipe_buf_operations。因为我们知道它的地址,所以这个假结构被种植在同一个位置,显然,这个结构应该包含一个恶意函数指针release。
漏洞利用的最后阶段是关闭所有管道以触发释放,这反过来将启动 JOP 链。找到 JOP gadgets 很困难,因此目标是尽快实现内核堆栈枢轴,以便执行内核 ROP 链。
内核ROP链
我们将 RBP 的值保存在内核中的某个暂存器地址,以便稍后恢复执行,然后调用commit_creds(prepare_kernel_cred(NULL))安装内核凭据,最后调用switch_task_namespaces(find_task_by_vpid(1), init_nsproxy)将进程 1 的命名空间切换到init进程之一。之后,我们恢复 RBP 的值并返回以恢复执行(将立即free_pipe_info()返回)。
转义容器并弹出根外壳
回到用户区,我们现在拥有更改 mnt、pid 和 net 命名空间的 root 权限,以逃离容器并脱离 kubernetes pod。最终,我们弹出一个 root shell。
漏洞复现
漏洞证明可在https://github.com/google/security-research/tree/master/pocs/linux/cve-2021-22555获得。
在易受攻击的机器上执行它会授予你 root 权限: