Este README.md es una traducción del blog de David. David encontró los CVE's 1015 y 1016 en el kernel de Linux. Puedes visitar su página web para leer el documento original.
Aquí te dejo sus redes sociales:
发布于 2022 年 4 月 2 日。
在最新版 Ubuntu 和 RHEL 的默认配置中,这些问题应该都是可利用的。我针对 Arch Linux 的 5.16-rc3 内核版本编写了 CVE-2022-1015 的概念验证(PoC)。
本文面向对 Linux 内核的功能和安全性有基本了解的人。我试图让本文对缺乏网络栈知识的人友好,以便向所有读者开放。
以下是一份阅读指南:
2 月中旬,Google 安全计划宣布将继续其 kCTF 漏洞奖励计划,为能够在 nsjail 沙箱中从无特权进程将权限提升至 root 用户的 Linux 内核漏洞提供从 31,337 美元到 91,337 美元不等的奖励。
作为一个贫穷的学生,这显然引起了我的注意。这是我第一次尝试寻找“现实世界”的漏洞,但在和我的团队一起玩 CTF 的冒险中,我已经从安全角度熟悉了 Linux 内核。在数小时几乎毫无进展(但对 Linux 有了更多了解)之后,我设法在 nf_tables 模块中发现了一些漏洞。
遗憾的是,最终我意识到这个模块并不在 Google kCTF 的规则范围内(因此这两个漏洞没有为我赢得任何奖励)。但显然,我仍然报告了它们,并为 CVE-2022-1015 编写了一个 LPE(本地权限提升)漏洞利用。
好了,你已经决定要在 Linux 中找一些漏洞。现在呢?Linux 是一个巨大的项目,很容易只见树木不见森林(过于关注细节而失去对真正重要事物的整体视野)。更糟糕的是,许多部分没有文档,你需要阅读大量代码才能理解正在发生的事情。
我首先尝试详细了解 Linux 的安全模型。找到一个 bug 是一回事;找到一个好 bug 则是另一回事。毕竟,并非所有 bug 都是同等的:
CAP_SYS_ADMIN 或 CAP_NET_ADMIN。
/proc/config.gz 访问。模块可以编译为内置(=y),也可以(=m)编译并在运行时加载。/proc/modules 和 /proc/kallsyms,但并不总是可靠,因为模块可以在内核中动态加载(例如 request_module)。这些限制帮助我们了解可以在哪些文件系统边界中寻找漏洞。我认为花时间规划对目标目标的攻击是个好主意。
我已经从上面这一点吸取了教训。如前所述,nf_tables 模块并未加载到 kCTF 给我们的实例中。我本可以一开始就注意到这一点,从而避免失望 :p。另一方面,如果早点注意到,你现在可能就不会读这篇博客了,我想事情最终发展得还不错。
关于为何 COS(Google 的容器优化 Linux 分支)没有 nf_tables 的解释可以在这里和这里找到。
在评估了上述各点之后,我决定开始的最佳途径可能是查看网络源代码。那里许多有趣的功能需要 CAP_NET_ADMIN,但如前所述,这实际上不是问题。相反,我怀疑需要特殊权限的组件通常安全性较低,因为内核开发者可能有一种虚假的安全感。
我还努力选择了一个我想要更多了解的文件系统;这样,即使你找不到任何 bug,你仍然可以学到很多有趣的东西。
我研究了许多网络文件系统,但没有发现任何重要的东西。在浏览 net/ 子目录后,我遇到了 nf_tables 模块。它看起来有点复杂,所以我决定花点时间去了解它。
Netfilter(net/netfilter)是内核中一个相当大的网络文件系统子系统。简而言之,netfilter 在整个网络模块中放置钩子(hook),其他模块可以注册处理程序。当到达某个钩子时,控制权被交给这些处理程序,它们可以操作各自的网络数据包结构。处理程序可以接受、丢弃和修改数据包。
在花了几个小时浏览 nf_tables API(net/netfilter/nf_tables_api.c)以准确了解其工作原理之后,我决定检查用户发送的记录的逻辑验证,并发现了一些可疑的行为。在思考我是否疯了之后,我写了一个小的 PoC(概念验证)来尝试触发我发现的漏洞:一个被称为 OOB(越界)的漏洞,允许在栈内存上进行读写。
在找到过滤内核地址的方法后,控制指针就变得相当容易了。经过一些 ROP(面向返回编程) 之后,带有 root 权限的 shell 成为了现实。
每当表达式的 init 例程需要解析来自用户 netlink 消息的记录时,会根据是源寄存器还是目标寄存器来调用 nft_parse_register_load 或 nft_parse_register_store 例程。我添加了一些注释:```c
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
/* Given a netlink attribute and the length
* that is required to read the requested data,
* write a register index to `sreg` or return
* an error on failure. */
u32 reg;
int err;
reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
return err;
/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;
}
static unsigned int nft_parse_register(const struct nlattr attr) { / Convert a register to an index in nft_regs */
unsigned int reg;
/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));
switch (reg) {
/* If it's 0 to 4 inclusive,
* it's an OG 16-byte register and we need to
* multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
/* Else we subtract 4, since we need to account
* for the OG registers above. */
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
/* So supplied values of 1, 2, 3, 4 map to
* OG 16-byte registers, with indices 4, 8,
* 12, 16
* Supplied values of 5, 6, 7 overlap the verdict,
* 8,9,10,11 overlap with OG register 1
* 12,13,14,15 overlap with OG register 2
* etc. */
}
static int nft_validate_register_load(enum nft_registers reg, unsigned int len) { /* We can never read from the verdict register, * so bail out if the index is 0,1,2,3 */ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL;
/* Invalid operation, bail out */
if (len == 0)
return -EINVAL;
/* If there would be an OOB access whenever
* `reg` is taken as index and `len` bytes are read,
* bail out.
* sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
return -ERANGE;
return 0;
}
`*_store` 变体几乎完全相同,只是它们允许在某些条件下写入 *verbdict*。
在检查了最后的验证之后,这里确实有些不对劲:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
这看起来似乎是一个 integer overflow,你们不这么认为吗?如果我们能让 reg 包含某个乘以 4 后当加上 len 时会产生 overflow 的值,我们就可以满足这些条件。在 nft_parse_register_load 中,reg 的最后一个有效字节仍然被写入指针 u8 *sreg,落入我们的 nft_expr 中,随后被用作索引。```c
*sreg = reg;
¿De verdad podemos? `reg` es un `enum nft_registers` en la validación de la rutina, de todas formas. Podemos pasar valores que tengan un rango entre `0x00000001` hasta `0xfffffffb` inclusive, el rango de `nft_parse_register`; pero ¿será `reg` un valor de 32 bits en `nft_validate_register_load`? Se sabe que los compiladores podrían encoger los *enum types* si un tipo más pequeño puede representar todos los valores. Vamos a obtener una segunda opinión.
Obtenido del manual de GCC:```
The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).
Normally, the type is unsigned int if there are no negative values
in the enumeration, otherwise int. If -fshort-enums is specified,
then if there are negative values it is the first
of signed char, short and int that can represent all the values,
otherwise it is the first of unsigned char, unsigned short and unsigned int
that can represent all the values.
On some targets, -fshort-enums is the default; this is determined by the ABI.
TL;DR? 取决于 ABI 和可能的优化程度。我无法找到任何具体证据表明此选项在 Linux 构建中默认启用。