Linux 内核是操作系统中一个动态且不断变化的核心,它不断增强新 功能和新能力。这种持续的演进无意中扩大了潜在的攻击面,使得检测关键漏洞的出现变得越来越困难。
这篇博文回顾了过去,带我们回到了eBPF成为内核安全研究领域焦点的时代。在这篇更新中,我们讲述了发现 eBPF 验证程序中的漏洞CVE-2023-2163的过程、我们的根本原因分析过程以及我们为修复该问题所做的工作。
语境
eBPF 是一种技术,使您能够在运行时扩展 Linux 内核的功能,而无需编写复杂的内核模块或承担导致系统崩溃的风险。
它通过允许您加载用自定义字节码编写的程序来实现这一点,该程序将经过安全性验证,然后在发生某个事件时执行(例如, 在进行特定的系统调用时)。
这种工作模式大大增加了内核的攻击面,因为它允许在高权限级别执行任意代码。由于存在这种风险,程序 在加载之前 必须经过验证。这确保满足所有 eBPF安全假设。由复杂代码组成的验证器负责此任务。
鉴于验证程序是否可以安全执行 这一任务的难度,eBPF 验证器中发现了许多漏洞。当其中一个漏洞被利用时,通常会导致本地特权提升漏洞(或容器化环境中的容器逃逸)。虽然验证器的代码已经经过了广泛的审计,但随着 eBPF 中添加新功能以及验证器的复杂性增加,这项任务也变得更加困难。
模糊测试器可以自动审核复杂的 eBPF 验证器代码,从而提供帮助。在阅读了Simon Scannell关于 eBPF 模糊测试的文章后,我们 Google 决定创建一个“合适的”eBPF 模糊测试器;能够生成大量语法有效的程序。因此, Buzzer 诞生了。
蜂鸣器 指针 算法策略
我们使用了Buzzer,这是 eBPF 的一款新型模糊测试工具。Buzzer 可以配置为使用策略,即尝试触发逻辑错误的方法。为了满足本研究的范围,我们创建了一个 指针算法 策略,其工作原理如下:
-
如果在用户空间中没有观察到写入的值,我们可以断定可能发生了越界写入
Buzzer 发现的漏洞(CVE-2023-2163)存在于 eBPF 路径修剪逻辑中,我们成功编写了一个可用作容器逃逸和 LPE 的漏洞利用程序。
eBPF 路径修剪
eBPF 验证器的主要目的之一是确保加载到内核的程序可以安全运行。验证程序的安全性并非易事,尤其是涉及分支时。
当验证器遇到某种if情况并且不确定寄存器将具有哪些值时,它会尝试模拟所有可能状态的执行。让我们来看以下示例:
图 1. 模拟所有可能状态执行的示例
在块 (1) 中,对 r6 的模数运算使验证器不确定寄存器可以取哪些值。因此,当它进入块 (2) 中的条件时,它并不确定会采用哪个分支,并且必须模拟所有可能分支的执行。在这种情况下,可能的路径是:
-
1:2:3:4:5:6
-
1:2:3:4:6
-
1:2:4:5:6
-
1:2:4:6
正如预期的那样,在 eBPF 程序中引入大量条件跳转会导致执行流程在运行时采用的路径数量呈指数级增长。这会对将程序加载到内核的性能产生负面影响。
为此,eBPF 开发人员提出了“路径修剪”策略。如果对于给定状态(寄存器和堆栈槽可能采用的值),验证器可以保证它已经证明了等效状态可以安全地到达退出指令,那么它就不会再费心去进一步探索它。
漏洞
为了帮助验证者更有效地确定何时修剪程序中的路径,引入了“精确跟踪”的概念。
简而言之:如果寄存器涉及指针算术运算,作为常量传递给辅助函数等,它将被标记为“精确”,并且验证器将被迫探索该寄存器所涉及的所有状态。
让我们回到前面的图/示例,它准确地展示了 CVE-2023-2163 的工作原理:
-
验证器首先探索路径 1:2:3:4:5:6,因为它首先采用的是错误分支。
-
此时,验证器假定在 Epilogue 中有一个指针算术运算和一个退出,因此将 r6 标记为精确。
-
当验证者尝试探索其他状态时,它将在区块 (2) 中确定所有后续路径都是等价的。这是因为它无法将 r9 标记为对 r6 的精度有贡献。
换句话说:在修复错误之前,验证者假设 r9 不会影响 r6 的准确性,并且它已经推断出它可以安全地通过之前的路径使用 r6 到达出口。因此,它将其余状态视为等效状态并对其进行修剪。
事实上,这正是 Buzzer 找到的测试用例。程序在运行时最终采用的路径是 1:2:4:6,在 6 中,我们可以使用 r6 执行指针算术运算,验证程序认为 r6 是 0,但实际上它是一个不同的数字。
开发
总结一下:
-
验证者负责确保所有程序都是安全的
-
为了做到这一点,验证者有时必须探索程序可能采取的所有路径
-
由于从性能角度来看这可能代价高昂,因此某些路径被认为“等同于”其他已探索的路径,因此不会被探索(修剪)
-
CVE-2023-2163 是验证程序中的一个错误,由于不正确的“精确”跟踪,一条路径被错误地认为是安全的
如何利用此漏洞?
安全研究往往建立在巨人的肩膀上。在这种情况下, @chompie和 @_manfp的工作 对于此漏洞的开发都至关重要。
该代码可在Google 安全研究存储库中找到 。在这里,我们回顾了用于实现任意读/写的技术,以及如何使用这些技术实现 LPE 和容器逃逸。
高层计划如下:
-
实现任意读/写
-
按照 Manfred Paul采取的步骤 查找进程凭据并修补 uid 和fs_struct 指针以提升权限
让我们深入了解一下如何实现此漏洞的任意读/写:
“Uno no es ninguno”(一即无)
第一步是让损坏的寄存器的值为 1。通过手动分析,我们注意到在运行时,r6 的值为 0x400(记住:验证程序认为这是 0)。因此,我们的 eBPF 漏洞利用程序中的第一条指令是以下移位,它产生所需的 1 值:
r6 >>= 10
eBPF 中的堆栈溢出
现在,r6 被认为是 0,但实际上为 1,我们可以破坏 eBPF 堆栈中的指针以实现任意读/写。这可以通过辅助函数 bpf_skb_load_bytes_relative来完成。为此,请考虑以下堆栈布局:
堆栈偏移 | 价值 |
---|---|
-8 | 0xCAFE (任意标量值 1) |
-16 | 0xBACA (任意标量值 2) |
-24 | PTR 到 eBPF 的映射 |
-32 | 指向堆栈的偏移量 -8 |
-40 | skb_load_bytes_relative 函数的缓冲区 |
然后,如果我们执行以下 eBPF 指令:
// Put a ptr to skb (network packet) in r1
r1 = ptr_to_packet
// Set offset = 0
r2 = 0
// Set to = stack_ptr - 40
r3 = r10 - 40
// Set len = corrupted_register (has value 1).
// Verifier thinks len = 0, in reality len = 1.
r4 = r6
// len = len + 8, verifier thinks len = 8 so it deems it safe, in reality len = 9
r4 += 8
// Set start_header = 1
r5 = 1
BPF_FUNC_skb_load_bytes_relative(r1, r2, r3, r4, r5)
...由于 r6 为 1,因此调用skb_load_bytes_relative将写入 9 个字节的内存而不是 8 个。换句话说:它将破坏堆栈中 -32 偏移值的第一个字节。
任意读写并击败 KASLR
正常情况下,执行以下代码片段后:
r1 = *(r10 - 32)
r2 = *(r1 - 0)
r3 = *(r1 - 8)
…验证器会期望 r1 保存指向偏移量为 -8 的堆栈区域的指针。因此,根据我们的堆栈布局,r2 将保存值 0xCAFE,r3 将保存值 0xBACA。
然而,在使用正确的值破坏堆栈指针之后,我们会在运行时出现这样的情况:r2 保存值 0xBACA,并且 r2 包含指向我们的 eBPF 映射的指针。
这是通过以下算法实现的:
-
设置字节 = 0
-
发送一个网络数据包,其中数据[9] = {字节,字节,字节,字节,字节,字节,字节,字节,字节}
-
在 eBPF 中:存储 R2 和 R3 的值
-
在用户空间中:读取 R2 和 R3 的值,如果 R2 == 0xBACA,我们就知道 R3 是 map 指针的泄漏
-
如果 R2 != 0xBACA,则设置字节 = (字节 + 1) % 256 并转到 2)
一旦我们泄露了 eBPF 映射指针的值,我们就击败了 KASLR。
按照相同的策略,可以实现任意读/写,只不过我们不是覆盖连续堆栈区域的单个字节,而是用所需的指针覆盖整个字节。然后,我们可以简单地执行 eBPF 指令来从损坏的指针进行读/写:在验证者看来,我们在 BPF 堆栈中操作,而实际上内核才是我们的游乐场!
从地图泄漏到root shell
从 这里开始,我们的漏洞利用与 Chompie 的漏洞利用没有太大区别 (事实上,很多代码都是从她的那里借来的),粗略的算法如下:
-
init_pid_ns在kstrtab中迭代搜索字符串“ ”
-
在 ksymtab 中找到引用在 1 中找到的字符串的符号,完成此步骤后,我们将获得init_pid_ns结构的地址, 该结构应包含进程凭据
-
遍历基数树,直到找到一个条目,其 comm 字段与我们的可执行漏洞利用名称相匹配
-
为什么不直接使用 PID?因为如果我们在容器内运行,这不是一个可靠的启发式方法
-
将 uid 修补为 0 和fs_struct指针;使用 PID 1 引用的相同指针修补后者,可以保证如果我们在容器内运行,漏洞利用现在将观察主机的文件系统
-
执行系统(“/bin/bash”)并享受我们的root shell!
以下是作为容器逃逸运行的漏洞的屏幕截图:
图 2. 以容器逃逸形式运行的漏洞
值得注意的是,我们在 GitHub 上发布的代码仅适用于某些版本的 Linux 的容器转义。它只能作为 Ubuntu 和其他发行版的 LPE,因为它们对我们覆盖的数据结构使用的偏移量不同。这留给读者练习,以调整代码,使其在任何 Linux 发行版下都能工作。
修复
有关根本原因分析以及修复 CVE-2023-2163 的补丁的更多详细信息,请参见 此处。简而言之,修复包括将影响精确寄存器的操作中的不精确寄存器标记为精确。
我们不清楚这是否会对 eBPF 验证器的性能产生影响。但可以确定的是,我们一直在运行相同的指针算法模糊测试策略,但未能发现进一步的问题。
原文始发于微信公众号(Ots安全):深入研究 CVE-2023-2163:我们如何发现并修复 eBPF Linux 内核漏洞
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论