本案例研究是ECE 9069课程“黑客入门”的一个作业成果:https://whisperlab.org/introduction-to-hacking/
CVE-2022-0185 是一个基于堆的缓冲区溢出漏洞,存在于 Linux 内核文件系统上下文(Filesystem Context)功能中的 legacy_parse_param 函数验证所提供的参数长度的方式中。一个非特权本地用户(如果启用了非特权用户命名空间,则不需要;否则需要命名空间的 CAP_SYS_ADMIN 权限),能够打开一个不支持文件系统上下文 API(从而回退到传统处理)的文件系统,可以利用此漏洞提升他们在系统上的权限。[1]
在该漏洞被报告后,已发布补丁修复此错误:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=722d94847de2
https://ubuntu.com/security/CVE-2022-0185#impact-score
发现者提供了一份详细的写入文章:https://www.hackthebox.com/blog/CVE-2022-0185:_A_case_study
在本仓库中,我将解释重现此漏洞的基本步骤和相关背景信息。此外,如果你有任何不清楚的地方,可以给我发邮件:[email protected],我很乐意回答问题。
CVE-2022-0185 漏洞于 2022 年 2 月 11 日发布,CVSS 3.x 基础评分为 8.4(高)。[1] 该漏洞是一个基于堆的缓冲区溢出,由无符号整数下溢引起。
该漏洞在 Linux v5.1 内核中引入,影响所有内核版本高于 5.1 的 Linux 发行版。例如,Ubuntu 20.04 LTS (focal) 曾受此漏洞影响。然而,补丁已发布,自版本 5.4.0-96.109 起可用。[3]
利用此漏洞允许非特权本地用户提升其在系统上的权限,可能危及整个系统。[1][2] 以下是 CVSS 评分的详细分析: 基础评分:8.4,表示需要立即关注的重要安全风险。 影响评分:5.9,表明如果被利用,可能造成实质性损害。高机密性、完整性和可用性值促成了这一评分。 可利用性评分:2.5,表明相对较高的可利用性。本地攻击向量、高完整性和高可用性值促成了这一评分。
表 1.1 和表 1.2 提供了关于这些评分及其组件的更多信息。
| CVSS v3.1 严重性 | 值 |
|---|---|
| 基础评分 | 8.4 高 |
| 影响评分 | 5.9 |
| 可利用性评分 | 2.5 |
表 1.1 CVSS 严重性评分[1]
表 1.2 CVSS 向量[1]
现代计算机中有两种整数类型:有符号和无符号。有符号数的表示通常涉及一种称为二进制补码的运算。[4] “二进制补码使用具有最高位值的二进制数字作为符号,指示二进制数是正还是负。”[4]
引入二进制补码会将减法计算转化为加法,从而简化 CPU 的设计和实现。生成一个整数的二进制补码涉及三个步骤:[4]
图 2.1.1.1 以图表形式展示了转换过程,并附有一个实际示例,将“-6”转换为其二进制补码格式。

图 2.1.1.1 将“-6”转换为二进制补码
图 2.1.1.2 展示了将“-6”的二进制补码与“+6”相加的过程。这说明了如何使用二进制补码将加法作为减法的替代。

图 2.1.1.2 使用二进制补码进行加法
从第 2.1.1 节中,我们已经理解了什么是二进制补码。现在,让我们看看计算机中无符号数下溢的场景。在现代计算机中,使用无符号数时,最高有效位不被视为符号位;相反,它是无符号数本身的一部分。这意味着在使用无符号数执行减法时必须谨慎,因为它可能导致一种称为无符号数下溢的情况。[5]
图 2.1.2.1 展示了对于 8 位无符号数,从 5 中减去 6 的情况。由于无符号数回绕,最终结果是 255。当下溢发生在条件语句中时,有可能破坏该语句的功能。

图 2.1.2.1 无符号数下溢
在 Linux 内核中,Slab 分配器是一种内存管理机制,用于高效分配和释放小块内存。它通过维护多个 Slab 缓存来提供性能,每个缓存包含固定大小的内存块。通常,kmalloc-32 分配 32 字节内存,它是一个 kmalloc-32 slab;而 kmalloc-4k 分配 4096 字节内存,它是一个 kmalloc-4k slab。[6]
此外,Linux 内核中的 slab 分配通常涉及从内核堆内存区域内的连续地址空间分配内存。该连续地址由内核管理,用于分配各种内核对象和数据结构。图 2.2.1.1 显示了 Linux 内核内存中 slab 的布局。

图 2.2.1.1 Linux 中的 Slab 分配器 [7] (该图的作者是 https://leviathan.vip/)
如果你想使用自行编译的 Linux 内核重现该过程,请阅读以下 Markdown 文件以获取背景信息:
注意:
所有 Markdown 文件以及代码和脚本均位于此仓库的不同文件夹中,每个文件夹都附带自己的 Markdown 文件,在进行任何操作前请先阅读它!
在第 2.1 节中,我们解释了无符号下溢的工作原理。现在,我们将检查包含此漏洞的内核函数。
用户“clubby789”在内核函数 legacy_parse_param 中发现了一个漏洞。该函数主要负责解析传递给内核的参数。在 CVE-2022-0185 中,该函数在通过 fsopen 打开文件描述符后,再使用 fsconfig 函数将配置键值对传递给内核时被调用。legacy_parse_param 的简化版本如以下代码所示。[2]```c static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param) { struct legacy_fs_context *ctx = fc->fs_private; // [1] unsigned int size = ctx->data_size; // [2] size_t len = 0; int ret; [ ... ] switch (param->type) { case fs_value_is_string: len = 1 + param->size; // [3] case fs_value_is_flag: len += strlen(param->key); break; default: return invalf(fc, "VFS: Legacy: Parameter type for '%s' not supported", param->key); } if (len > PAGE_SIZE-2-size) return invalf(fc, "VFS: Legacy: Cumulative options too large"); // [4] [ ... ] if (!ctx->legacy_data) { ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); // [5] if (!ctx->legacy_data) return -ENOMEM; } ctx->legacy_data[size++] = ','; // [6] len = strlen(param->key); memcpy(ctx->legacy_data + size, param->key, len); size += len; if (param->type == fs_value_is_string) { ctx->legacy_data[size++] = '='; memcpy(ctx->legacy_data + size, param->string, param->size); size += param->size; } ctx->legacy_data[size] = '\0'; ctx->data_size = size; ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS; return 0; }
从上述代码片段可以看出,第[1]行和第[2]行设置了代码的上下文,而第[4]行包含了发生无符号下溢的语句。第[5]行处理堆slab分配,第[6]行和第[7]行负责将数据填充到分配的slab中。值得注意的是,第[6]行添加了一个逗号(',')作为单独的分隔符,并且还添加了一个等号('='),导致实际数据大小之外多了两个字节。
在第[4]行中,if语句内的变量包含PAGE_SIZE(设置为4096的宏)和size(一个无符号64位数)。当无符号数累计到4095时,会发生下溢,导致if语句始终评估为假。这允许对相邻slab进行越界写入。下溢是由于减去一个无符号数引起的,结果是4096 - 4095,得到一个无符号数18446744073709551615。[2]
`18446744073709551615`这个结果在以下文本中进行解释:```c
if (len > PAGE_SIZE-2-size) return invalf(fc, "VFS: Legacy: Cumulative options too large");
注意,这里 PAGE_SIZE 等于 4096 字节,而 2 对应添加的字符 , 和 =,用于分隔每个 key-value 对。问题在于 size 是一个 无符号值,因此当 size 达到 4095 时,表达式 PAGE_SIZE-2-size 将等于 有符号值: -1,但作为 无符号值: 18446744073709551615,这是由于二进制补码表示,如下图所示![3]

因此上述 if 语句始终为 false,这意味着剩余的数据会复制到我们分配的 slab 之外的堆区域!
在理解了无符号数下溢如何发生后,我们可以继续构建一个概念验证(POC)代码来演示该漏洞。
用户 “clubby789” 提供了一个详细的 POC 代码,如下代码片段所示。代码简洁明了;它首先打开一个名为 ext4 的文件描述符,然后多次使用 fsconfig 向内核填充数据。
这里需要注意两点:
在3.1.1节中,我们提到在观察到越界写入之前,必须填充4095字节的数据。考虑到每个周期仅填充35字节,我们需要执行操作117次(4095 / 35),然后才能观察堆内存以完成概念验证。
### 3.1.3 使用QEMU的POC
在本仓库:GitHub - dcheng69/CVE-2022-0185-Case-Study 中,我们提供了一个名为poc.sh的shell脚本,以方便调试过程。在开始设置之前,请阅读Poc文件夹中的markdown文件。
由于legacy_parse_param是一个内核函数,您需要调试内核函数。为此,您需要编译内核源码以获得必要的符号和源码。我们还提供了一个详细的markdown文件来指导您完成此过程。请参阅Compile_linux文件夹以了解更多细节。
在图3.1.3.1中,我们演示了在向内核堆填充4095字节数据后,通过利用无符号下溢成功触发了越界写入。此外,我们总共向一个kmalloc-4k slab填充了4130字节数据,成功破坏了相邻的slab。虽然在这个例子中相邻slab不包含任何信息(全为零),但我们可以精心构造代码来利用此特性写入恶意数据。我们将在3.2 利用部分演示如何实现这一点。

**图 3.1.3.1 使用QEMU的POC**
更多详情请参阅本仓库`Poc`文件夹下的 https://github.com/dcheng69/CVE-2022-0185-Case-Study/blob/main/Poc/poc.md!
## 3.2 利用
在了解此漏洞后,我们可以继续利用它进行漏洞利用,相关细节记录在`exploit-ubuntu`文件夹以及此markdown文件中:https://github.com/dcheng69/CVE-2022-0185-Case-Study/blob/main/explot-ubuntu/exploit.md
简而言之,我们编译了Ubuntu源码,获取deb文件,然后在虚拟机上测试以获得特定内核版本。然后我们根据从`System.map`文件中找到的信息,修改利用代码的偏移量以针对该内核版本。最后更新grub并重启进行利用!
### 3.2.1 利用概览
用户'clubby789'为我们提供了详细的利用代码。我们将先进行概述,然后通过图表解释几个关键概念。最后,我们将展示在虚拟机(VirtualBox)上运行Ubuntu的利用结果。
在演示了此漏洞的概念验证之后,我们现在可以继续进行利用。在图3.2.1.1中,展示了如何利用此漏洞的概览:
- 左侧部分侧重于获取Linux内核基地址。这是通过利用无符号下溢覆盖msg_msg结构的m_ts字段实现的,从而允许越界读取,访问先前喷射的内核结构。
- 右侧部分旨在获取root权限。这是通过利用无符号下溢覆盖msg_msg结构的next指针,使其指向modprobe_path实现的。然后我们触发缺页异常,调用我们构造的fuse代码,实现在内核空间中的任意写入。

**图 3.2.1.1 利用概览**
### 3.2.2 获取Linux内核基地址
如前所述,我们将在此部分利用无符号下溢覆盖msg_msg结构的m_ts字段,实现越界读取。通过用包含内核指针的结构喷射堆,我们有希望获得内存泄漏。
struct msg_msg是Linux内核中用于实现System V消息队列的数据结构。在本节中,我们关注struct msg_msg的内部结构,以及与发送、接收和分配消息相关的函数逻辑。如图3.2.2.1所示,这些是我们需要理解的函数。
发送消息的实现位于msg.c文件中,该文件将消息的最大长度定义为8192字节。在alloc_msg函数中,消息根据其长度被分割成多个段。如果消息长度加上消息头超过一页(4096字节),消息将被存储在由指针链接的几个段中。

**图 3.2.2.1 struct msg_msg 发送和接收**
在图3.2.2.2中,我们可以看到struct msg_msg作为消息头,占用0x30字节内存。如果消息中有剩余数据,它将存储在消息段中,并链接到struct msg_msgseg。因此,如果内核允许最大8192字节的消息,数据将最多存储在三个消息段中。

**图 3.2.2.2 struct msg_msg**
在图3.2.2.3中,我们展示了struct msg_msg的结构。从代码中我们知道,m_ts字段是我们需要覆盖以实现越界读取的字段。(如果需要,您可以在`res`文件夹下找到draw.io源文件!)

**图 3.2.2.3 struct msg_msg 结构**
现在我们已经了解了struct msg_msg的结构,需要学习如何获得内核泄漏。Linux内核具有内核地址空间布局随机化(KASLR)功能,这意味着内核代码在启动阶段被加载到一个随机地址。然而,内核起始点到任何函数地址的偏移量是恒定的,这使得我们可以通过特定操作,用包含特定内核函数的结构填充堆空间。通过减小偏移量,我们可以找到内核起始地址。
幸运的是,我们可以通过打开/proc/self/stat轻松地用seq_operations结构喷射堆,该结构位于kmalloc-32 slab中。seq_operations的定义如图3.2.2.4所示。

**图 3.2.2.4 用于内核泄漏的结构**
最后,整个过程如图3.2.2.5所示。我们首先用4095字节数据填充legacy_data,为覆盖做准备。然后,我们使用struct msg_msg构造消息。由于堆内存是连续分配的,构造的消息很可能与相邻的kmalloc-4k slab相邻。我们通过控制写入legacy_data的数据来覆盖m_ts字段。
接下来,我们用多个kmalloc-32 seq_operations结构喷射堆。然后我们从消息队列接收数据,触发越界读取。通过调整内核函数的偏移量,我们可以获得内核基地址。

**图 3.2.2.5 越界读取概览**
### 3.2.3 获取Root权限
与之前的分析类似,在这一部分,我们需要设置一个FUSE文件系统,允许我们的用户空间代码处理来自内核空间的缺页异常。同时,我们将覆盖msg_msgseg *next指针,使其指向modprobe_path,从而实现在内核空间中的任意写入。
我们先来看一下图3.2.3.1中展示的FUSE调用堆栈。一般来说,FUSE允许我们在用户空间实现一个文件系统。当有新操作时,系统会调用我们为FUSE定义的代码!

**图 3.2.3.1 FUSE 概览**
让我们分析如何使用FUSE在内核空间中实现任意写入。首先,考虑struct msg_msg的发送消息操作。该操作涉及将数据写入内核空间。如果我们构造的消息有两个段,就可以覆盖指针并写入任意内核地址。这一概念在图3.2.3.2中进行了说明。
此外,在检查向队列发送消息的逻辑后,我们知道该过程涉及将缓冲区从用户空间复制到内核空间。如果消息足够长,它们将逐段复制。如果我们能在此过程中触发缺页异常,就可以实现图3.2.3.2所示的场景。
幸运的是,FUSE提供了必要的功能。我们可以将一个页面映射到FUSE,当触发缺页异常时,系统会调用我们的FUSE读取函数来处理该缺页异常。通过暂停读取过程,直到无符号下溢覆盖了msg_msg_seg *next指针,然后恢复发送消息过程,我们就可以实现任意写入。整个过程如图3.2.3.3所示。

**图 3.2.3.2 使用发送消息**

**图 3.2.3.3 FUSE 和发送消息**
### 3.2.4 使用VirtualBox进行利用
按照markdown文件:https://github.com/dcheng69/CVE-2022-0185-Case-Study/blob/main/explot-ubuntu/exploit.md
编译完成后,您可以在图3.2.4.1中观察到利用的结果。

**图 3.2.4.1 VirtualBox 利用结果**
# 4. 漏洞缓解措施
## 4.1 官方补丁
在此漏洞被报告后,Linux及其许多发行版合并了补丁来修复此错误。[9] [10] [11]
在以下截图中,我们展示了Linus Torvalds合并的补丁。对此下溢问题的缓解措施很简单,就是将减法操作转换为加法操作。

# 5. 实际影响
在本报告中,我们演示了如何利用此漏洞入侵Ubuntu系统。此外,它还可能针对缺乏定期安全更新的旧系统。
关于Kubernetes,此漏洞可能导致权限提升、容器逃逸或拒绝服务攻击。
虽然新闻中没有报道此漏洞造成损失,但它强调了持续应用关键安全更新的重要性。报告此漏洞的研究人员 exemplifies 了我们都应努力维护的道德黑客实践。[12]
# 参考文献
[1] https://nvd.nist.gov/vuln/detail/CVE-2022-0185
[2] https://www.hackthebox.com/blog/CVE-2022-0185:_A_case_study
[3] https://ubuntu.com/security/CVE-2022-0185#impact-score
[4] [https://en.wikipedia.org/wiki/Two%27s_complement](https://en.wikipedia.org/wiki/Two's_complement)
[5]https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Unsigned-Overflow.html
[6] https://www.kernel.org/doc/gorman/html/understand/understand011.html
[7] https://leviathan.vip/
[8] https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html
[9] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=722d94847de2
[10] https://ubuntu.com/security/CVE-2022-0185
[11] https://access.redhat.com/security/cve/CVE-2022-0185
[12]https://jfrog.com/blog/the-impact-of-cve-2022-0185-linux-kernel-vulnerability-on-popular-kubernetes-engines/
[14] https://github.com/chenaotian/CVE-2022-0185?tab=readme-ov-file
[15] https://www.tutorialspoint.com/two-s-complement
| CVSS v3.1 指标 | 值 |
|---|
| 攻击向量 (AV) | 本地 |
| 所需权限 (PR) | 无 |
| 用户交互 (UI) | 无 |
| 机密性 (C) | 高 |
| 完整性 (I) | 高 |
| 可用性 (A) | 高 |