本案例研究是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]
| CVSS v3.1 指标 | 值 |
|---|---|
| 攻击向量 (AV) | 本地 |
| 所需权限 (PR) | 无 |
| 用户交互 (UI) | 无 |
| 机密性 (C) | 高 |
| 完整性 (I) | 高 |
| 可用性 (A) | 高 |
表 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 利用