该漏洞是 linux 内核(5.12.4版本之前)的近场通信 (NFC) 子系统中的释放后使用 (UAF)。但是,为了介绍,我们将在5.15版本中使用它。
Si 是一个由以下内容构成的套接字:
socket(AF_NFC, SOCK_STREAM, NFC_SOCKPROTO_LLCP)
然后假设我们将 Si 绑定到地址 A,Si 将加载一个指向类型为:nfc_llcp_local 的对象L的指针。但是,如果 Si 的绑定失败,则L将被释放,而 Si到 L 仍然被定义。因此,将来取消引用 Sito L 的计算会将内核转换为一个奇怪的状态。
当L在绑定失败时被释放时,补丁会删除指向L的指针Si。更多信息请点击这里。https://www.openwall.com/lists/oss-security/2021/05/11/4
io_uring
输入/输出(I/O)可以同步或异步执行。对于前者,当我们请求一个I/O操作时,我们等待它完成后再继续其他工作。对于后者,我们可以同时执行其他任务。假设异步I/O是同步I/O的超集。
io_uring机制实现了异步I/O,在建立一个io_uring实例之后,我们得到了两个相关的环形缓冲区:一个用于提交,一个用于完成。每个提交都是对内核执行 I/O 操作的请求。完成是从 I/O 请求返回状态和数据的对象。我们从提交队列条目数组中检索一个对象,使用操作码和相关数据对其进行初始化,然后发出系统调用 io_uring_enter 进行提交。
我们还可以使用标志 IORING_SETUP_SQPOLL 设置 io_uring 实例,这使内部内核线程能够轮询提交队列以获取新的提交。这通过确保始终有一个内核线程等待执行请求来减少系统调用开销。当这个内核线程从内核堆中的对象加载它的凭证时,我们将利用这一点。
Userfaultfd
一种系统调用,使用户能够创建文件描述符以在用户空间中实现按需分页。我们通过处理在 copy_from_user 和 copy_to_user 中产生的保护漏洞,广泛使用这个系统调用来管理内核堆。
msgsnd 和 msgrcv
支持消息队列进程间通信的系统调用。它们分别是消息发送和消息接收。
页面漏洞
通常,当线程尝试访问尚未与物理帧关联的虚拟内存地址时,内存管理单元会发出页面漏洞。在这种情况下,内核可以通过为指定页面创建新的页表条目来解决故障。然而,在这个更一般的上下文中,我们也指由尝试访问以某些方式保护的内存引起的一般保护漏洞,即写入只读内存。这是因为 userfaultfd 可以配置为处理“缺页”漏洞和保护漏洞。
假设运行环境
1.重新引入了包含漏洞的内核5.15版本。请注意,io_ring_ctx 在 5.12版本中不能立即使用,因为它是从 kmalloc-4k 发布的。但也许我们可以通过以下方式实现更多与缓存无关的原语:a) 泄漏或猜测 io_ring_ctx 对象的地址,b) 将其锻造成一个列表,该列表随后由 kfree 在其所有节点上执行“破坏”,或覆盖一个指针在L中,它也被传递给 kfree。
2.sysctl 旋钮 vm.unprivileged_userfaultfd=1。
3.默认情况下或通过用户命名空间使用 CAP.NET_RAW 功能。
战略概述
策略是泄漏和覆盖io_ring_ctx类型的对象R。这样,我们就可以控制R的sq_creds字段。请注意,之所以选择R的类型,是因为R和L类型的对象类似地从kmalloc-2k发出。
$R to$ sq_creds替换提交队列轮询器(SQP)线程执行io_uring请求时的当前凭证。如果sq_creds指向一个结构credit对象$C$,使$C$具有uid、gid等设置为$0$,那么SQP线程将执行I/O请求,就像调用用户是根用户一样。
因此,通过向io_uring实例(由受控的R管理)发出请求,我们可以作为非特权用户读写磁盘上的文件。这里的演示只是读取/etc/shadow。但要获得根权限,只需编辑/etc/passwd以进入一个根shell。
组件
我们使用以下组件和技术:
三个 NFC 套接字:S1、S2 和 S3;
六个线程:main、X、Y、Z、T 和 H;
msgsnd + msgrcv 用于泄漏 R。
userfaultfd + setxattr 用于写入 R 并确保 R 在利用期间和使用之后不会被另一个线程重新分配。
背景
使用三个 NFC 接口,我们可以释放L三次。但是,每次释放之后,我们需要覆盖L的某些部分,我们称之为local header,以确保随后的释放不会导致崩溃。具体来说,我们将 refcount 设置为 1,这样在下一次释放时不会将 refcount 设置为 $-1$,从而引发警告或导致进程崩溃。此外,我们确保L的第一个 list_head 字段定义明确。
使用线程 X、Y、Z,我们继续协调:再次分配 L,将L保存在内存中,从L读取并写入 L。这样,L就用R 表示,因为我们将曾经用于Si的相同对象重新分配给L为R。目的是通过我们的利用逻辑证明 L≡R。
用户使用setxattr 技术分配一个 kvalue 对象,其大小和内容由用户确定。它允许我们从内核对象开始写入。然而,当我们从用户空间完成对kvalue的写入后,kvalue被释放。
msgsnd + msgrcv 技术也是已知的,msgsnd 分配一个 msg_msg 对象,该对象充当标头,后跟消息文本,其长度和内容由用户确定。如果我们希望 msgrcv 正常工作,msg_msg 对象字段应该保持良好定义。因此,我们不能从内核对象的开头开始写。另一方面,msgrcv 将消息文本复制到用户空间,然后释放 msg_msg 对象。用户确定消息文本的长度以及接收和释放 msg_msg 的时间。
我们使用 userfaultfd 通过 setxattr 在内核上下文中暂停线程。我们使用该技术的两种变体。当读取用户空间地址时,会出现一个页面错误。另一个问题是在写入用户空间地址时出现页面错误。它们分别对应于copy_from_user和copy_to_user。前者与setxattr关联,后者与msgrcv关联。
“解除阻塞线程 X”通常意味着在内核上下文中处理由线程 X 引起的页面漏洞。一个线程不能处理它自己的页面漏洞,而是将它委托给层次结构中更上层的线程。但是,不一定是下一个,即线程 Z 解除线程 main 和线程 Y 的阻塞,而线程 Y 解除线程 X 的阻塞。
如果我们正在处理 setxattr 页面漏洞,那么“处理页面漏洞”意味着我们使用 ioctl 为 userfaultfd 子系统提供一个新缓冲区。然后这允许继续写入 kvalue (L) ,然后释放 kvalue (L)。同样,对于 msgrcv 页面漏洞,我们取消保护缓冲区并继续读取。建议读者查看上面链接的 Vitaly Nikolenko 的解释。这将解释我们在下一节中说的“特定偏移处的页面漏洞”时的意思。
方法描述
首先,我们关闭 S1,释放 L。然后我们分配 7 msgsnd 缓冲区,删除额外的分配。现在L是免费的,我们可以使用 setxattr 重新分配它。我们传递给 setxattr 的值是受内存保护的,因此在某个偏移量处读取将导致页面漏洞。使用 userfaultfd 我们注册相应的范围,以便线程 X 可以捕获页面漏洞。偏移量是我们自己为L类型定义的标头的大小。我们需要覆盖L的标头,以便再次释放它(当我们关闭 S2 时)不会导致早期崩溃。该标头仅包含一个 list_head 和一个 refcount 字段,我们将其计数器值设置为 1。
其次,从线程 X 中,我们从主线程中捕获 setxattr 中发出的页面漏洞。现在我们在主线程中做同样的事情,不同之处在于,我们为一个新值注册一个新范围,我们希望线程 Y 捕获下一个页面漏洞,并且页面漏洞发生的偏移量是 struct msg_msg +8。我们仍然用我们的标头覆盖 L。
第三,从线程 Y,我们捕获由 X 执行的 setxattr 中发出的前一个页面漏洞。然而,这一次在我们关闭 S3,第三次释放L之后,我们再次通过 msgsnd 分配 L。然后我们创建一个写入保护缓冲区并将其注册到 userfaultfd,我们的用户空间接收器 msg_msg 位于写入保护缓冲区的负偏移处。我们将 msg_msg 对象的地址传递给 msgrcv。当 msgrcv,特别是 store_msg 写入 msg_msg 的偏移量时,将发出另一个页面漏洞。此偏移量由 struct msg_msg 标头的大小决定。
此时,main 暂停以将L固定。我们稍后通过处理 main 中的 setxattr 发出的页面漏洞来解除对 main 的阻塞,这将释放L以重新分配为 R。此外,Y 会暂停以将L固定到位。Y 的延续会将 L(此时为 R)中的数据泄漏到用户空间,然后 Y 将通过解决 X 发出的 setxattr 页面漏洞来继续线程 X。最后,当 Y 解决 X 的页面漏洞时,X 会暂停并继续写入L(R)。
第四,从线程 Z 中,我们捕获由 Y 执行的 msgrcv 中发出的页面漏洞。
现在 X 通过处理它的页面漏洞来解除对 main 的阻塞。这将继续 setxattr 写入L并随后第四次释放 L。然后我们通过 io_uring_setup 系统调用分配一个 io_ring_ctx 对象,因此 Lequiv R$。最后,Z 尝试获取一个锁,如果成功,它将导致它创建一个新线程 T,并使用 setxattr 再次分配 R。但是,此时,线程 Y 持有锁,因此 Z 暂停尝试获取它。
第五,Z 通过处理写入保护页漏洞来解除对线程 Y 的阻塞。这首先将 R 复制到用户空间。但是通过这样做,它也释放了R。因此,我们放弃上述锁定,确保 Z 继续。当 Z 继续时,它使用带有一个值的setxattr,当读取该值时会导致第一个字节出现页面漏洞。线程 T 捕捉到这个页面漏洞并且不处理它。因此,R 不能用于其他线程的重新分配和潜在的损坏。
在我们有了R$的数据之后,我们将当前的R 定位到用户空间缓冲区的sq_creds偏移量78(缩放因素 sizeof(uint64))。在读取泄露的 sq_creds 后,我们从中减去 176 。目的是让R 到sq_creds指向当前struct credt对象的“后面”。然后我们将新值写回我们的用户空间缓冲区。最后,我们通过处理 setxattr 发出的页面漏洞来解除对线程 X 的阻塞。这会将我们的用户空间缓冲区写入 R,维护除我们操作的 sq_creds 之外的所有字段。但是通过这样做,我们也释放了 R。所以我们再次使用 setxattr 并在 H 中捕获页面漏洞,方法与 T 中相同。
最后,我们从线程 T 中直接使用 open 获取目录“/etc/”的文件描述符。然后我们向 io_uring 实例发出 openat 请求。检索完成队列条目后,我们有了一个文件描述符“/etc/shadow”。然后我们发出一个读取请求,将“/etc/shadow”的内容复制到用户空间缓冲区中。
最终结果如下:
参考及来源:https://ruia-ruia.github.io/NFC-UAF/