日期:2021 年 7 月 20 日,星期二 12:36:11 +0000 发件人:Qualys 安全咨询 <[email protected]>致:“[email protected]”<oss-security@。 ..ts.openwall.com>主题:CVE-2021-33909:Linux 文件系统层中的 size_t-to-int 漏洞Qualys 安全公告红杉:Linux 文件系统层的深层根源 (CVE-2021-33909)======== ================================================== ================内容================================== ======================================总结分析利用概览利用细节缓解确认时间线========== ================================================== ==============总结==================================== ==================================我们在 Linux 内核的文件系统中发现了 size_t-to-int 转换漏洞layer:通过创建、挂载和删除总路径长度超过1GB的deepdirectory结构,非特权本地攻击者可以将10字节的字符串“//deleted”写入exac的偏移量tly -2GB-10B 在 vmalloc()ated 内核缓冲区的开头下方。我们成功地利用了这种不受控制的越界写入,并在 Ubuntu 20.04、Ubuntu 20.10、Ubuntu 21.04、Debian 11 和Fedora 34 工作站;其他 Linux 发行版肯定是易受攻击的,并且很可能被利用。我们的利用需要大约 5GB 的内存和 1M 的 inode;我们将在不久的将来发布它。此公告附有一个基本的概念证明(崩溃程序),可从以下网址获得:https://www.qualys.com/research/security-advisories/ 据我们所知,此漏洞于 2014 年 7 月引入(Linux 3.16 ) 通过提交 058504ed ("fs/seq_file: fallback to vmallocallocation").================================== ======================================分析========== ================================================== ============Linux 内核的seq_file 接口产生包含记录序列的虚拟文件(例如,/proc areseq_files 中有很多文件,记录通常是行)。每个记录必须适合 aseq_file 缓冲区,因此可以根据需要通过在第 242 行将其大小加倍(seq_buf_alloc() 是 kvmalloc() 的简单包装器)来扩大该缓冲区:---------------- -------------------------------------------------- ------ 168 ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter) 169 { 170 struct seq_file *m = iocb->ki_filp->private_data; ... 205 /* 抓取缓冲区,如果我们没有缓冲区 */ 206 if (!m->buf) { 207 m->buf = seq_buf_alloc(m->size = PAGE_SIZE); ... 210 } ... 220 // 获取缓冲区中的非空记录 ... 223 while (1) { ... 227 err = m->op->show(m, p); ... 236 if (!seq_has_overflowed(m)) // 得到它 237 goto Fill; 238 // 需要更大的缓冲区... 240 kvfree(m->buf); ... 242 m->buf = seq_buf_alloc(m->size <<= 1); ... 246 }-------------------------------------------- ---------------------------这种大小乘法本身并不是漏洞,因为m->size 是一个size_t(一个无符号的64 位整数,在 x86_64 上),并且系统会在乘法溢出整数 m->size 之前很久就耗尽内存。不幸的是,这个 size_t 也被传递给 size 参数是 int(有符号的 32 位整数)而不是 size_t 的函数.例如,show_mountinfo() 函数(在第 227 行调用以格式化 /proc/self/mountinfo 中的记录)调用 seq_dentry()(在第 150 行),它调用 dentry_path()(在第 530 行),它调用 prepend( )(在第 387 行):------------------------------------------- -----------------------------135 static int show_mountinfo(struct seq_file *m, struct vfsmount *mnt)136 {...150 seq_dentry(m, mnt->mnt_root, " \t\n\\");------------------------------ ----------------------------------------- 523 int seq_dentry(struct seq_file *m, struct dentry *dentry, const char *esc) 524 { 525 char *buf; 526 size_t size = seq_get_buf(m, &buf); ... 529 if (size) { 530 char *p = dentry_path(dentry, buf, size);--------------------------- ---------------------------------------------380 char *dentry_path( struct dentry *dentry, char *buf, int buflen)381 {382 char *p = NULL;...385 if (d_unlinked(dentry)) {386 p = buf + buflen;387 if (prepend(&p, &buflen, " //deleted", 10) != 0)-------------------------------------- --------------------------------- 11 static int prepend(char **buffer, int *buflen, const char * str, int namelen) 12 { 13 *buflen -= namelen; 14 if (*buflen < 0) 15 返回 -ENAMETOOLONG; 16 *缓冲区-=名称长度; 17 memcpy(*buffer, str, namelen);--------------------------------------- ---------------------------------结果,如果无特权的本地攻击者创建、挂载和删除深层目录总路径长度超过 1GB 的结构,如果攻击者 open()s 和 read()s /proc/self/mountinfo,则:- 在 seq_read_iter() 中,vmalloc() 化了 2GB 缓冲区(第 242 行),并且show_mountinfo() 被调用(第 227 行);- 在 show_mountinfo() 中,seq_dentry() 用空的 2GB 缓冲区调用(第 150 行);- 在 seq_dentry() 中,dentry_path() 用 2GB 大小调用(第 530 行) ;- 在 dentry_path() 中,因此 int buflen 为负(INT_MIN,-2GB),p 指向 vmalloc()ated 缓冲区下方 -2GB 的偏移量(第 386 行),并调用 prepend()(第 387 行) ;- 在 prepend() 中,*buflen 减少了 10 个字节,变成了一个大但正整数(第 13 行),*buffer 减少了 10 个字节并指向 vmalloc()ated 缓冲区下方 -2GB-10B 的偏移量(第 16 行),10 字节字符串“//deleted”是 wri tten 越界(第 17 行)。========================================== ==============================开发概述================ ================================================== ======1/ 我们mkdir() 一个总路径长度超过1GB 的深层目录结构(大约1M 个嵌套目录),我们将它绑定挂载到一个非特权用户命名空间,然后rmdir() 它。2/ 我们创建一个线程vmalloc() 生成一个小的 eBPF 程序(viaBPF_PROG_LOAD),在我们的 eBPF 程序通过内核 eBPF 验证程序验证之后,但在内核进行 JIT 编译之前,我们阻塞了这个线程(通过 userfaultfd 或 FUSE)。3/ 我们打开( ) /proc/self/mountinfo 在我们的非特权用户命名空间中,并开始 read() 我们的绑定挂载目录的长路径,从而将字符串“//deleted”写入到 a 开头下方恰好 -2GB-10B 的偏移量vmalloc()ated buffer.4/ 我们安排这个“//deleted”字符串覆盖我们经过验证的 eBPF 程序的指令(从而使安全 c hecks of the kernel eBPF verifier),并将这种不受控制的越界写入转换为信息披露,并转换为有限但受控的越界写入。5/我们将这种有限的越界写入转换为任意读写内核内存,通过重用 Manfred Paul 漂亮的 btf andmap_push_elem 技术:https://www.thezdi.com/blog/2020/4/8/cve-2020-8835-linux-kernel-privilege-escalation-via-improper- ebpf-program-verification6/ 我们使用这个任意读取来定位 modprobe_path[] 缓冲区内核内存,并使用任意写入将这个缓冲区的内容(默认为“/sbin/modprobe”)替换为我们自己的可执行文件的路径,从而============================================================================================================================================================================================ ==========================开发细节====================== ================================================== =a/ 我们创建一个总路径长度超过1GB的目录:理论上,我们需要创建超过1GB/256B=4M的嵌套目录tories(NAME_MAX 为 255);实际上,show_mountinfo() 将我们长目录中的每个 '\\' 字符替换为 4 字节字符串“\\134”,因此我们只需要创建 1M 嵌套目录。b/ 我们填充所有大 vmalloc 漏洞:我们在几个非特权用户命名空间中绑定挂载 (MS_BIND) 长目录的各个部分,并且 vmalloc() 通过 read()ing /proc/self/mountinfo 消耗大 seq_file 缓冲区。例如,我们 vmalloc() 消耗了 768MB我们的exploit.c/中的大缓冲区我们 vmalloc() 使用了两个 1GB 缓冲区和一个 2GB 缓冲区(通过在三个不同的用户命名空间中绑定挂载我们的长目录,以及通过 read()ing/proc/self/mountinfo),我们检查“//deleted”确实写入了我们 2GB 缓冲区开头下方的 -2GB-10B 偏移量(即,我们第一个 1GB 缓冲区开头上方的 8182B——“XXX”的保护页):“//deleted " | 4KB v 1GB 4KB 1GB 4KB 2GB-----|---|---+-------------|---|----------- ------|---|-----------------| ... |XXX| seq_file 缓冲区 |XXX| seq_file 缓冲区 |XXX| seq_file 缓冲区 |-----|---|---+-------------|---|--------------- --|---|-----------------| | | | | \----<----<----<----<----<----<----<----/ 8182B -2GB-10Bd/ 我们都填小的 vmalloc 漏洞:我们 vmalloc() 通过发送 () 大量 NETLINK_USERSOCK 消息来处理各种小套接字缓冲区。例如,wevmalloc() 在我们的exploit.e/中占用了256MB 的小缓冲区。我们创建了1024 个用户空间线程;每个线程开始将 eBPF 程序加载到内核中,但是(通过 userfaultfd 或 FUSE)我们阻止内核空间中的每个线程(在第 2101 行),在我们的 eBPF 程序实际上被 vmalloc() 化之前(在第 2162 行):------ -------------------------------------------------- ----------------2076 static int bpf_prog_load(union bpf_attr *attr, union bpf_attr __user *uattr)2077 {....2100 /* 从用户空间复制 eBPF 程序许可证 */ 2101 if (strncpy_from_user(license, u64_to_user_ptr(attr->license),....2161 /* 简单的 bpf_prog 分配 */2162 prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt)-------GFP_USER); -------------------------------------------------- ---------------f/ 我们 vfree() 我们的第一个 1GB seq_file 缓冲区(其中“//deleted”被写入越界),我们立即解除对所有 1024 个线程的阻塞;我们的 BPF 程序是vmalloc() 插入我们刚刚 vfree()d 的 1GB 孔中: 4KB 1GB 4KB 1GB 4KB 2GB-----|---|-----------------| ---|-----------------|---|-----------------| ... |XXX| eBPF 程序|XXX| seq_file 缓冲区 |XXX| seq_file 缓冲区 |-----|---|-----------------|---|--------------- --|---|-----------------|g/ 接下来,(再次通过 userfaultfd 或 FUSE)我们在 eBPF 程序之后阻塞我们的一个线程(atline 12795)已通过内核 eBPFverifier 验证,但在内核 JIT 编译之前:-------------------------------- ----------------------------------------12640 int bpf_check(struct bpf_prog **prog, union bpf_attr *attr,12641 union bpf_attr __user *uattr)12642 {.....12795 print_verification_stats(env);------------------------ -----------------------------------------------h/ 最后,我们用一个越界的“//deleted”字符串(再次通过我们的2GB seq_file缓冲区)覆盖了这个eBPF程序的一条指令,因此使内核eBPF验证器的安全检查无效:“//deleted”| 4KB v 1GB 4KB 1GB 4KB 2GB-----|---|---+-------------|---|----------- ------|---|-----------------| ... |XXX| eBPF 程序 |XXX| seq_file 缓冲区 |XXX| seq_file 缓冲区 |-----|---|---+-------------|---|--------------- --|---|-----------------| | | | | \----<----<----<----<----<----<----<----/ 8182B -2GB-10B 首先,我们改造这个不受控制的 eBPF 程序损坏为信息泄露。我们的第一个未损坏的 eBPF 程序被内核 eBPF 验证器认为是安全的(“storage”和“control”是两个基本的 BPF_MAP_TYPE_ARRAY,通过 BPF_MAP_LOOKUP_ELEM 和 BPF_MAP_UPDATE_ELEM 从用户空间可读和可写):- BPF_LD_IMM64_RAW,加载我们的存储地址存储映射(驻留在内核空间中,我们不知道其地址)到 eBPF 寄存器 BPF_REG_2;- BPF_MOV64_IMM(BPF_REG_2, 0) 立即将 BPF_REG_2 的内容(我们存储映射的地址)替换为常量值 0;- BPF_LD_IMM64_RAW(BPF_REG_3, BPF_PSEUDO_MAP_VALUE, control) 将我们控制映射的地址加载到 BPF_REG_3 中;- BPF_STX_MEM(BPF_DW, BPF_REG_3, BPF_REG_2, 0) 将 BPF_REG_2 的内容存储到我们的控制映射中(我们的常量值 eBPF.0)程序损坏用 8 字节字符串“已删除”覆盖指令 BPF_MOV64_IMM(BPF_REG_2, 0),该字符串转换为指令 BPF_ALU32_IMM(BPF_LSH, BPF_REG_5, 0x74):a NO P(“无操作”),因为我们的程序没有使用 BPF_REG_5。因此,我们没有将常量值 0 存储到我们的控制映射中:相反,我们存储并公开了存储映射的地址。(此信息公开使我们能够大大减少我们的漏洞利用中硬编码内核偏移量的数量:我们的 Ubuntu 20.04在 Ubuntu 20.10、Ubuntu 21.04、Debian 11 和 Fedora 34 上开箱即用。)其次,我们将不受控制的 eBPF 程序损坏转化为有限但受控的越界写入。我们的第二个未损坏的 eBPF 程序也被内核 eBPF 验证器认为是安全的(“损坏”是 a3*64KB BPF_MAP_TYPE_ARRAY):- BPF_LD_IMM64_RAW(BPF_REG_4, BPF_PSEUDO_MAP_VALUE,corruption) 将我们损坏的映射的地址加载到 BPF_REGADD_4_BPF_4BPF_4BPF_4BPF_4B , 3*64KB/2) 将 BPF_REG_4 指向我们损坏映射的中间;- BPF_ALU64_IMM(BPF_SUB, BPF_REG_4, 3*64KB/4) 将 BPF_REG_4 指向我们损坏映射的第一季度;- BPF_LD_IMM64_RAW(BPF_REG_3, BPF_REG_3, BPF_MAPF_MAP)将我们的控制映射的地址加载到 BPF_REG_3;- BPF_LDX_MEM(BPF_H, BPF_REG_7, BPF_REG_3, 0) 从我们的控制映射加载一个变量 16 位偏移到 BPF_REG_7;- BPF_ALU64_REG(BPF_ADD, BPF_REG_4, 添加 BPF_REG_7) -bit 偏移)到 BPF_REG_4,因此它安全地指向我们损坏的映射的边界内(因为 BPF_REG_7 在 [0,64KB] 范围内)。但是,我们的 eBPF 程序损坏覆盖了指令 BPF_ALU64_IMM(BPF_ADD, BPF_REG_4, 3*64KB /2) 与 str ing "deleted", 翻译成 BPF_ALU32_IMM(BPF_LSH, BPF_REG_5, 0x74) (a NOP). 因此,下面的 BPF_ALU64_IMM(BPF_SUB, BPF_REG_4, 3*64KB/4) 指向 BPF_REG_4 并允许我们从边界读取并写入内核空间中损坏映射之前的结构 bpf_map。最后,我们通过重用 Manfred Paul 的 btfand map_push_elem 技术,将这种有限的越界读写转换为内核内存的任意读写:- 使用任意内核读取我们找到符号“__request_module”,因此找到函数 __request_module(),反汇编这个函数,并从“if (!modprobe_path[0])”的指令中提取 modprobe_path[] 的地址。 - 使用任意内核编写我们用我们自己的可执行文件的路径覆盖 modprobe_path[](默认为“/sbin/modprobe”)的内容,并调用 request_module()(通过创建一个 netlink 套接字),它以 root 身份执行 modprobe_path,从而执行我们自己的可执行文件.================================ ========================================缓解措施========== ================================================== ==============重要提示:以下缓解措施仅阻止我们特定的漏洞利用(但可能存在其他漏洞利用技术);要彻底修复此漏洞,必须修补内核。- 将 /proc/sys/kernel/unprivileged_userns_clone 设置为 0,以防止攻击者在用户命名空间中挂载长目录。但是,攻击者可能会通过 FUSE 挂载一个长目录;我们还没有完全探索这种可能性,因为我们无意中在 systemd 中偶然发现了 CVE-2021-33910:如果攻击者 FUSE-mount 一个长目录(超过 8MB),那么 systemd 会耗尽其堆栈,崩溃,从而导致整个操作崩溃系统(内核崩溃)。- 将 /proc/sys/kernel/unprivileged_bpf_disabled 设置为 1,以防止攻击者将 eBPF 程序加载到内核中。然而,攻击者可能会破坏其他 vmalloc() 化对象(例如,线程堆栈),但我们尚未调查这种可能性。====================== ================================================== =鸣谢================================================ ========================我们感谢 PaX 团队回答了我们关于 Linux 内核的许多问题。我们还要感谢 Manfred Paul、Jann Horn、Brandon Azad、SimonScannell 和 Bruce Leidl 的利用和文章:https://www.thezdi.com/blog/2020/4/8/cve-2020-8835-linux -kernel-privilege-escalation-via-improper-ebpf-program-verification https://googleprojectzero.blogspot.com/2016/06/exploiting-recursion-in-linux-kernel_20.html https://googleprojectzero.blogspot.com /2020/12/an-ios-hacker-tries-android.html https://scannell.io/posts/ebpf-fuzzing/ https://github.com/brl/grlh 感谢红帽产品安全部和linux的成员[email protected] 和 [email protected] 在此协调披露方面的工作。我们还要感谢 Mitre 的 CVE 分配团队。最后,我们感谢 Marco Ivaldi 的持续支持。================================ ==========================================时间线======== ================================================== ==============2021-06-09:我们将针对 CVE-2021-33909 和 CVE-2021-33910 的建议发送给 Red Hat 产品安全(这两个漏洞密切相关,并且 systemd-安全邮件列表由 Red Hat 托管。2021-07-06:我们发送了我们的建议,Red Hat 将他们编写的补丁发送到了 [email protected] 邮件列表。2021-07-13:我们发送了我们针对 CVE-2021-33909 的建议和 Red Hat 将他们编写的补丁发送到 [email protected] 邮件列表。2021-07-20:协调发布日期(UTC 时间下午 12:00)。查看“text/plain”类型的附件“CVE-2021-33909-crasher.c”(6904 字节)