一个内核漏洞详解:容器逃逸
该漏洞是由 Andy Nguyen (theflow@)发现,于 2021 年 07 月 16 日发布。换句话说,该漏洞已经存在了 15 年之久,都没有被人发现。该漏洞将允许本地用户通过用户名空间获取权限提升,和容器逃逸。
1.漏洞概述
简述: Linux 内核模块 Netfilter 中存在一处权限提升漏洞,在 64 位系统 上为 32 位进程 处理 setsockopt IPT_SO_SET_REPLACE(或 IP6T_SO_SET_REPLACE)时,如果内核选项 CONFIG_USER_NS 、CONFIG_NET_NS 被开启,则攻击者可以通过该漏洞实现权限提升,以及从 docker、k8s 容器中实施容器逃逸。
2.漏洞环境和样例
2.1.环境准备
Linux 发行版:Ubuntu 20.04.2 LTS
Linux kernel 版本:5.8.0-48-generic
如图:
2.2.利用样例
3 漏洞原理
在 Linux 内核代码 net/netfilter/x_tables.c 中的 xt_compat_target_from_user 函数内的第 1129 行,在使用 memset()时并未对传进来的参数进行校验,会导致在写 0 时,越过堆的大小,导致“堆溢出”,从而破坏堆与堆之间的边界。
而写超的 0 的内核内存,刚好又在应用层程序(恶意程序)通过堆喷技术拿到感知控制。通过搜索,可以找到被写超的内核内存;恶意程序在应用层建立与这段内核内存的联系,把要执行代码 commit_creds(prepare_kernel_cred(NULL))封装到回调函数内;利用内核机制在内核内存中建立与回调机制使用关系,再把刚才封装的函数地址更新到内核内存中。当主动释放这段内核内存,触发回调函数,从而调用到 commit_creds()函数把权限升级到 root 权限。
4 前导技术知识点
4.1 UAF (Use-After-Free)
是一个与程序运行期间不正确使用动态内存相关的漏洞利用方式。
程序在释放内存位置后,系统不会马上回收内存。指针指向的那段内存依然存在,并且内容没有被清除,攻击者可以利用该错误来入侵该程序。(本次漏洞重点使用。通过 UAF,可以达到同一块内存在不同的操作对象之间交换控制管理权)
4.2 SMAP & SMEP
SMAP(Supervisor Mode Access Prevention):管理模式访问保护–禁止内核 CPU 访问用户空间的数据.
SMEP(Supervisor Mode Execution Prevention):管理模式执行保护–禁止内核 CPU 执行用户空间的代码(并不会因为你权限高就能访问/执行低权限的资源,你的就是你的,我的就是我的).
SMEP 和 SMAP 导致我们不能像从前那样,利用恶意进程提权到内核权限后,扭头去执行布置在用户态的恶意 shellcode,用户态 shellcode 注入也不好使了(Linux 内核漏洞防御机制).
4.3 KASLR
KASLR(kernel address space layout randomization)也就是内核地址空间布局随机化。KASLR 技术允许 kernel image 加载到 VMALLOC 区域的任何位置。当 KASLR 关闭的时候,kernel image 都会映射到一个固定的链接地址。
对于黑客来说是透明的,因此安全性得不到保证。KASLR 技术可以让 kernel image 映射的地址相对于链接地址有个偏移。如果 bootloader 支持每次开机随机生成偏移数值,那么可以做到每次开机 kernel image 映射的虚拟地址都不一样。
因此,对于开启 KASLR 的 kernel 来说,不同的产品的 kernel image 映射的地址几乎都不一样。因此在安全性上有一定的提升(Linux 内核漏洞防御机制)。
4.4 rop 链攻击
rop(Return Oriented Programming)。X86 架构下,函数调用规则是,当刚跳转到其他函数去执行时,从被调用者的视角看:栈顶是返回地址,紧接着是自己的参数。
被调用者会对栈空间进行一系列操作,保存寄存器和存储临时变量,但在即将退出时会清理自己消耗的栈空间,以使其回到自己被调用前的栈空间,保持栈平衡;
最后,被调用者以 ret 指令结尾,ret 指令将栈顶地址传递到 RIP 寄存器 ,随后代码也就跳转到之前栈顶存放的返回地址处;
rop 链即是基于以上这个简单的原理,在代码空间中寻找以 ret 结尾的代码片段或函数(代码片段称为 Rop gadgets),组合可以实现拓展可写栈空间、写入内存、shell 等功能,依靠 ret 将 代码执行权 紧握在自己的手里。
4.5 一段代码的运行
在计算机体系中,所有的行为逻辑,都是要以“程序”为媒介得以执行。当然,这也包含了所有“恶意行为”在内的病毒、木马等待。
而“程序”的本质是通过代码来对“现实行为”在计算机内的行为仿真(程序没有“恶意”和“非恶意”之分,恶意的是写这份代码的人心);既然是代码,也是要遵照计算机体系的约定。
4.5.1 代码运行的条件
在 Linux 系统中所有的行为都是被“权限”所规范起来。
换句话说,程序想要被运行起来就需要“获得”相应的权限。而在 Linux 系统权限是被严格的划分。我们可以从 2 个维度(应用层/内核层)来对权限进行了解。
4.5.2 应用层(Ring 3)
也就是平时我们操作 Linux 界面。在应用层中是以用户为单位进行管理,又划分成 2 类:特权用户(也就是 root),普通用户。
Linux 限定了普通用户的行为,也包含普通用户的程序;在普通用户模式下,有很多系统级的接口、功能,都是不能使用的;基本也就是只能使用主机的“计算能力”,不能使用到“管理能力”。
4.5.4 内核层(Ring 0)
操作系统是对物理硬件的抽象,而内核层是操作系统的核心。
在内核层中,没有像应用层那样,做权限区分,任何执行代码都是一样的权限。
而内核层相对于用户层,拥有的特权是最高。
4.5.5 漏洞利用逻辑
根据前面提到的,想要运行代码,需要 2 个条件:
1、代码在内存中
2、装载代码的内存拥有执行权限
一个普通用户的程序,正常情况下只能通过高权限的授权,来提升自己的权限,除此别无他法。
而 CVE-2021-22555 漏洞可以通过“内核特权”来打破这一规则。
5 漏洞利用步骤
5.1 堆喷:建立线性的内核堆内存
通过系统提供的消息队列功能,建立大量的消息队列,并往每个消息队列里面传入消息(每个消息大小:4096)。这些消息会缓存到内核当中。
缓存在内核,也就意味着有大量的内存占用。而当有大量的内存申请使用,内核的内存管理模块为了减小内存碎片的问题,会把相邻的内存返回给内存使用者。
也就是说,这些缓存的消息,在内存中是连续存在,紧挨着的。
如图:
这样还不够,再次往各个消息队列发送消息,这次发送的消息大小是 1024。而在消息队列中的消息是用链表的方式来连接的,在内存中的布局如图:
这里面的选用 4096 大小作为第一条消息,是为了适配漏洞代码 memset() 做格式化的大小。
而选用 1024 大小作为每二条消息,是为了利用上漏洞后,当内核内存使用,与后续的回调机制匹配。
5.2 漏洞触发 & UAF 攻击
现在读出部分各个消息队中的 4096 大小的消息,其目的是为了释放在内核内存中缓存的内存。在读出的消息的同时内存也得到释放。
接下应用层利用 socket 触发漏洞函数。触发代码:
setsockopt(fd, SOL_IP, IPT_SO_SET_REPLACE, &data, sizeof(data))
data 的大小通过人为的方式来构造,其中 sizeof(data) 的大小,会传到漏洞语句。造成堆写 0 溢出,踩到相邻的内存。
内核层对应的漏洞函数 xt_compat_target_from_user 被触发时,会在内核层申请内核内存。根据 UAF 攻击原理,xt_compat_target_from_user 函数内申请的内存刚好会是上面消息队在内核层释放的内存块。
而该内存块相连的是其它的 4096 大小的消息内存块。当漏洞函数被触发,写超的内存,就会写到相邻的 4096 消息内。
4096 消息在应用层可以通过遍历,来找出被踩的那块内核消息块。
在溢出写 0 的长度是在应用层可以人为的控制,而这里,我们设计溢出的长度为 2 个字节。
根据消息结构体在内核层中的定义,溢出写 0 的 2 个字节刚好会覆盖消息结构体内的指向下一条 1024 消息的指针的后 2 位。就会导致被溢出踩的 4096 消息会指向另一个 1024 消息,而这条 1024 同时会被另一个 4096 的消息指向。
也就是同一个 1024 消息被 2 个 4096 消息指向,为后续的 UAF 利用提供了条件。
如图:
5.2.1 一点题外话
该漏洞的利用开始就是从写超 2 个字节的堆内存,而该漏洞的发现者也因此获取得了 10000$的奖励。而在作者后续的文章说明中,也是很幽默地把该点写到文章的标题上。
如图:
5.3 绕过内核 SMAP 机制
其实目的是为了把权限升级的相关的代码传到内核层让,让内核层的权限去运行权限升级的代码,来提升用户层用户的权限。
绕过 SMAP 的基础,就是要知道内核内存的地址。知道地址的基础,把我们要提升权限的代码以 ROP 攻击链的方式写到该内核内存当中。
通过对消息队的遍历,找到了被双重引用的 1024 消息,而目前还不知道该被双重引用的 1024 消息的地址。
不知道该消息的内核内存地址,是没办法绕过 SMAP 机制,同时在后面作为 ROP 链的媒介内存,在不知道内存地址的情况下,是没办法使用的。
接下来就是需要知道该消息的内核内存地址了。
好在可以通过消息结构(struct msg_msg)的 m_ts 字段,再利用 copy_msg()函数
来读取出被双重指向的 1024 消息的相邻消息。(设置被双重指向的 1024 消息的 m_ts 字段大于 DATALEN_MSG(4096 - sizeof(struct msg_msg)) ),再通过该相邻消息找出被双重指定的 1024 消息的内核内存地址,现在我们得到了内核内存地址,就可以把我们的 ROP 链的代码写到该内核内存地址上,从而达到绕过 SMAP 机制。
5.4 解除内核消息队列的限制
在内核的消息队列,也就只是消息结构类型(struct msg_msg),并且还需要保证里面的元素的合法性。换句话说,我们就不能使用消息队列的对象来做内核内存的修改对象,因为没有可塑性。
需要找一种有可塑造的内核结构消息来指向该内核消息队列内存地址。
这里使用 struct sk_buff 结构体类型, sk_buff 就没有 struct msg_msg 的内核限制。
利用双重指向的 1024 消息的其中一个引用,来释放该 1024 消息。再使用 struct sk_buff 类型的消息来堆喷 1024 大小,正常情况下,通过堆喷会找到刚释放出被双重指定的 1024 消息。而这样做的目的是为了通过另一种控制的方式(sk_buff)来指向了该内核内存地址。
到目前为止,我们拿到了一段内核内存,大小 1024,并且还一个 struct sk_buff 类型的指针可以对它进行操作。
(为了方便大家理解,再次解释一下:该步骤的目的是为了把同一块内存的控制管理权限转交给 struct sk_buff 结构;因为在原消息结构 struct msg_msg 类型在内核中是不允许对结构体做超范围的操作)
5.5 找一个有回调机制的结构
也就是找到一个结构体内有函数指针的对象。
这里选用 struct pipe_buffer 结构对象,该结构体中的 const struct pipe_buf_operations *ops;刚好可以用来存放回调函数。
该结构体刚好占 1024 字节,同时也很容易使用 pipe()函数来制成。
5.6 绕过 KASLR & SMEP
调用 pipe()申请 struct pipe_buffer 结构时,ops 字段的内容会默认填充为 anon_pipe_buf_ops,而内容又存在内核的 .data 段内:
因为在内核中的.data 和.text 段之间的偏移固定,我们可以计算内核程序代码基地址。
这就又要用到堆喷技术了,在上面我们提到有一段内核内存被 sk_buff 指向,同时还被另一个消息队列给指针。
利用消息队列释放该段内核内存。随后,调用大量的 pipe()函数来实现堆喷,找回刚才被释放的内核内存。
这样同一块内核内存就被 pipe 和 sk_buff 同时指向,同时具有操作权。
而 const struct pipe_buf_operations *ops 中有一个名为 release 的回调函数,会在 pipe 被闭时实调用到。
利用内核内存部署上我们的 ROP 攻击链(也就是我们要提升权限的函数),把并 ROP 链的触发写到 ops 的 release 内。
ROP 链代码:
最后关闭 pipe 会调用 release,也是会执行到我们的权限提升函数,得到 root 权限。
到此已完成了漏洞的利用。再利用 root 权限返回一个 root 权限的 shell。
6 修复建议
请尽快升级 Linux 内核到安全版本,如下:
Linux Kernel 5.12(b29c457a6511435960115c0f548c4360d5f4801d),5.10.31, 5.4.113, 4.19.188, 4.14.231, 4.9.267, 4.4.267.
临时修补建议:
RedHat 建议,用户可通过以下命令禁止非特权用户执行 CLONE_NEWUSER、CLONE_NEWNET 来执行此漏洞:
来之:LFAPAC
原文:https://mp.weixin.qq.com/s/DjWCuBv5iegZBAgIrjfKA
评论