CVE-2022-25636 Netfilter防火墙模块提权漏洞

CVE-2022-25636 Netfilter防火墙模块提权漏洞

CVE-2022-25636漏洞描述

Linux 内核 5.4 到 5.6.10 中的 net/netfilter/nf_dup_netdev.c 允许本地用户由于堆越界写入而获得特权。这与 nf_tables_offload 有关。

该漏洞跟踪为CVE-2022-25636(CVSS 评分:7.8),影响 Linux 内核版本 5.4 至 5.6.10,是内核中 netfilter 子组件中堆越界写入的结果。这个问题是由 Sophos 的高级威胁研究员 Nick Gregory发现的。

红帽在 2022 年 2 月 22 日发布的公告中表示: “此漏洞允许在系统上拥有用户帐户的本地攻击者访问越界内存,从而导致系统崩溃或权限提升威胁。” DebianOracle LinuxSUSEUbuntu也发布了类似的警报。

具体来说,CVE-2022-25636 与对框架硬件卸载功能的错误处理问题有关,本地攻击者可能将其武器化以导致拒绝服务 (DoS) 或可能执行任意代码。

“尽管在处理硬件卸载的代码中,当针对没有卸载功能(例如 lo)的网络设备时,这是可以实现的,因为在规则创建失败之前触发了错误。” 格雷戈里。“此外,虽然 nftables 需要 CAP_NET_ADMIN,但我们可以取消共享到一个新的网络命名空间,以作为(通常)非特权用户来获取它。”

“这可以毫不费力地转化为内核 [面向返回的编程]/本地特权升级,因为越界写入的值之一是指向 net_device 结构的指针,”Gregory 补充道。

CVE-2022-25636 的发现和利用

几周前,我发现并报告了 CVE-2022-25636 – Linux 内核中的一个越界写入堆。该漏洞可用于实现内核代码执行(通过 ROP),提供完整的本地权限提升、容器逃逸,无论您想要什么。

在这篇文章中,我介绍了发现和利用漏洞的整个过程(至少在某种程度上我所做的)——从最初的“看起来很奇怪”到正常工作的 LPE。

这是一篇很长的文章,但希望这对其他人(尤其是那些对内核开发较新的人)有用,以了解我的过程是什么样的。

最后,如果您只是在这里了解漏洞利用细节并且不希望我发现它的背景故事,请随意跳过 head。

寻虫

几周前的一个晚上,我很无聊。我本来可以参与一些其他项目,但似乎没有一个特别有趣,所以我决定进行一些随机(内核)代码审查。在过去的几年中,我在 netfilter 内核子系统中看到了一些值得注意的错误(也许最值得注意的是CVE-2021-22555),所以我决定开始寻找那里。这是一个广泛可用的相对复杂的子系统——完美的目标。

什么是netfilter?

正如该项目的网站所说, Netfilter “支持数据包过滤、网络地址 [和端口] 转换 (NA[P]T)、数据包日志记录、用户空间数据包队列和其他数据包处理”。您之前可能在不知道的情况下与 netfilter 进行过交互!曾经用于iptables阻止服务器上的入站流量,或将 Linux 机器配置为具有 NAT 的路由器?所有的数据包处理都是由 netfilter 在内核中完成的。

我过去做过很多事情iptables,但除此之外我不熟悉 netfilter 提供的其他任何东西(而且绝对不知道它是如何工作的),所以我点击了一些文件子系统的主要源目录,以尝试获得一席之地。

我从顶部开始查看一些(似乎是)协议解析器。解析重要的数据总是容易出错,所以它感觉是一个很好的起点。我最终专注于从用户空间(通过 netlink 套接字)获取配置输入的代码部分,因为虽然数据包处理中的错误会很有趣,但解码器仍然必须通过用户空间中的某些配置“激活”第一名。

编者注:也许值得再看一下这些文件,因为syzkaller没有对这些文件进行太多报道,所以也许有一些东西潜伏着……

无论如何,在经历nf_conntrack_ftp.c了其他一些没有看到太多有趣的事情之后,我正在滚动寻找其他“类型”的代码并看到nf_dup_netdev.c. 实际上,当我看到它并想“如果某些内容重复时可能存在一些引用计数错误”时,我实际上正要单击其他文件,所以我决定在那里查看。

这是一个很短的文件,但第 67 行

entry = &flow->rule->action.entries[ctx->num_actions++];

对我来说有两个特别的原因:

  1. 它正在递增ctx->num_actions并将其用作数组的索引,而没有任何边界检查
  2. 索引 ( ctx->num_actions) 和数组本身 ( flow->rule->action.entries) 是两个完全不同的变量的结构成员,没有明显的相关性。也就是说,这条线等价于a->b[x->y]看起来可能比 更“可疑”的线a->b[a->c]

当然,这些原因都没有使这成为一个明确的错误(但),但它确实“闻起来”,这促使我们进行更多的挖掘。

它是一个错误吗?

我有几个直接的问题:

  • 什么决定了action.entries数组的大小?
  • 怎么nft_fwd_dup_netdev_offload称呼?什么控制它被调用的次数?
  • 何时/如何ctx初始化?

此时我也意识到这是在nft_fwd_dup_netdev_offload. 即使这个错误是真实存在的,它也可能只能在具有支持数据包处理卸载的网络接口卡 (NIC) 的系统上访问,这种情况非常罕见(而且非常昂贵)。它仍然是一个错误,但可能不是世界上最有趣的错误。

拉起 x-refsnft_fwd_dup_netdev_offload表明它是在and s的.offload处理程序中调用的。查看struct 成员的参考资料(这在 Elixir 中确实不是一次愉快的体验……),我发现这个用法回答了上述所有问题,但只有一个问题:dupfwd nft_expr_typeoffload

ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);

...

while (nft_expr_more(rule, expr)) {
  if (!expr->ops->offload) {
    err = -EOPNOTSUPP;
    goto err_out;
  }
  err = expr->ops->offload(ctx, flow, expr);
  if (err < 0)
    goto err_out;

  expr = nft_expr_next(expr);
}
  • 怎么nft_fwd_dup_netdev_offload称呼?: 它被间接称为nft_flow_rule_create.
  • nft_fwd_dup_netdev_offload什么控制调用多少次?:卸载处理程序(因此nft_fwd_dup_netdev_offload对于 fwd/dup 表达式)在规则中具有一个的每个表达式都被调用。没有其他检查。
  • 何时/如何ctx初始化?:对于创建的每个规则,上下文都是零初始化的,并且相同的实例被传递给每个卸载处理程序。

然而,比所有这些更重要的是,最有趣的问题的答案就在上面:

expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {
  if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
    num_actions++;

  expr = nft_expr_next(expr);
}

...

flow = nft_flow_rule_alloc(num_actions);

我们看到,对于规则中的每个表达式,仅当表达式中设置了某个位 ( )num_actions时,计数器才会递增。快速检查and表达式的定义,它们都没有设置。事实上,只有一种用途:在表达式类型中(这里)。NFT_OFFLOAD_F_ACTIONops->offload_flagsdupfwdNFT_OFFLOAD_F_ACTIONNFT_OFFLOAD_F_ACTIONimmediate

在这一点上,我非常有信心有一个错误。据我所知,唯一可以阻止它的是,如果有一些强制执行,每个 dup/fwd 规则都有一个立即数。

检查可利用性

由于不熟悉如何与 nftables “交谈”,我四处搜索了一些关于 nftables 表/链定义​​的外观以及如何安装它的示例。 一个邮件列表帖子特别有用,因为它包含所需的一切,包括如何设置offload到达错误所需的标志(因为这个检查)。

table netdev filter_test {
  chain ingress {
    type filter hook ingress device eth0 priority 0; flags offload;
    ip daddr 192.168.0.10 tcp dport 22 drop
  }
}

拿着那个样本,我开始玩 nftables 看看是否/如何解决这个错误。

首先,我在flow_rule_alloc(负责创建我们的action.entries数组)上设置了一个 kprobe,并使用 fetcharg 来显示num_actions参数:sudo kprobe-perf -F 'p:flow_rule_alloc num_actions=%di:u32'。这立即失败了,因为(至少在 Ubuntu 上)nftables 是一个延迟加载的内核模块,因此实际上还没有加载代码。哎呀。快速运行后nft -a mailing_list.nft(即使命令本身失败,也会强制加载内核模块),我实际上可以设置 kprobe。

nft -a mailing_list.nft这次真正运行会导致 kprobe 命中(尽管规则安装失败):

$ sudo nft -f mailing_list.nft
a.nf:1:1-2: Error: Could not process rule: Operation not supported
table netdev x {
^^
$ sudo kprobe-perf 'p:flow_rule_alloc num_actions=%di:u32'
Tracing kprobe flow_rule_alloc. Ctrl-C to end.
             nft-20137   [001] .... 1573655.306178: flow_rule_alloc: (flow_rule_alloc+0x0/0x60) num_actions=1

即使我flow_rule_alloc正在测试的虚拟机肯定没有能够进行硬件卸载的网络设备,也确实受到了打击!系统没有崩溃或任何事情,所以看起来错误的行为还没有受到打击,但这是一个很好的进展。

正是在这一点上,我意识到我从未将邮件列表中的示例更改为实际包含dup表达式。又来了。在将规则更改为ip daddr 192.168.0.10 dup to eth0虽然之后,我的系统令人讨厌地保持在非panicd 状态。

在继续之前,我还想在进入新用户和网络命名空间 ( )nft后尝试运行命令,看看是否有可能以非特权用户的身份访问它。果然是这样,使这个错误可能更加强大。unshareunshare -Urn

回到错误本身:翻阅nft手册页,我发现你可以通过-d netlink它,因为它显示了正在发送到内核的规则的“反汇编”,因此非常有用:

[ meta load protocol => reg 1 ]
[ cmp eq reg 1 0x00000008 ]
[ payload load 4b @ network header + 16 => reg 1 ]
[ cmp eq reg 1 0x0a00a8c0 ]
[ immediate reg 1 0x00000001 ]
[ dup sreg_dev 1 ]

由此,错误没有被触发的原因很明显:CLI 在dup(表示数据包应该复制到的设备)之前生成一个立即表达式,因此记帐是“工作”的。是否可以有一个dup没有前面的immediate?我找不到让 CLI 从这种反汇编格式安装规则的方法(所以不能强制它生成dup没有immediates 的 s),所以是时候更深入并手动创建要发送到子系统的数据包.

Golang 实现

我对 Go 既爱又恨的关系,但这是另一个博客。归根结底,它基本上是唯一一种拥有大型社区(因此有大量库可供选择)的语言,它的级别足够低,可以满足我的需要,但也足够高,不会让我想扔当我试图让某些东西工作时,我的电脑在窗外。所以我开始在 Go 中构建概念验证。

方便的是,谷歌有一个 gonftables ,它看起来是一个很好的起点,因为我可以手动构建规则。不幸的是,它并没有完全暴露我需要的所有东西(主要是关于设置卸载标志),当我发现这一点时,我已经花了几个小时围绕它进行构建,并且真的不想用 C 重写它。我拼凑起来一些非常糟糕的代码一起使用反射来覆盖要发送的私有消息数组,手动构造必要的链创建消息并翻转适当的位,等等等等。再过一个小时左右,我又回到了我开始的地方nft命令行界面。

我添加了另一个dup没有immediate之前的,运行它并……

没有发生太多事情。它以正常的“不允许操作”出错,但没有别的。所以至少它没有因为缺少即时消息而被拒绝,我猜这很好吗?

然后,几秒钟后,kaboom。内核惊慌失措,我的外壳死了。我们有一个错误!

有趣的来了。

操作

分析我们的错误实际上为我们提供了什么(在pahole获取结构偏移的帮助下),我们看到有 2 个越界写入:

entry = &flow->rule->action.entries[ctx->num_actions++];
entry->id = id;
entry->dev = dev;
  1. 在数组结束后立即写入enum flow_action_id id,写入值 4 或 5(取决于这是 afwddup)表达式
  2. struct net_device *dev超过数组末尾的 24 个字节的写入

至于所有内容的大小(在我的 Ubuntu 测试 VM 上,内核为 5.13),基本flow_rule结构是 32 字节,entry数组中的每个附加都是 80 字节。这意味着:

  • 如果我们的规则中没有立即数,则规则分配的大小将为 32,并将在 kmalloc-32 平板中分配
  • 一条规则给出大小为 112 的分配,落在 kmalloc-128 平板中
  • 两条规则给出大小为 192 的分配,落在 kmalloc-192 平板中
  • 等等

专注于dev指针写入,上述分配大小意味着写入将在下一个 32 或 192-slab 分配的偏移量 24 处,或者在下一个 128-slab 分配的偏移量 8 处。我通过 ‘s 的输出手动搜寻pahole任何有趣的结构,该结构在必要的偏移处有一个指针,但空手而归。我发现的所有东西要么在一个需要提升权限才能访问的子系统中,要么在一个“异国情调”的子系统中(可能不容易到达),或者在一个我觉得太脆弱而无法尝试登陆的子系统中(例如调度程序)。

长话短说,我把它放在一边,几天后又带着新鲜的眼睛回来了。

在阅读 Alexander Popov 关于另一个最近的内核错误的文章以寻找灵感时,我想到了一个想法:我们有能力导致这些越界写入中的多个,而不仅仅是一个(因为dup可以将多个 s 放入规则中)。因此,除了命中下一个 128-slab 分配的偏移量 8 之外,我们还可以命中该分配的偏移量 88,或第二次分配的偏移量 40,或第二次分配的偏移量 120,或者……

刚刚阅读了 Alexander 使用安全指针(在偏移量 40 处)登陆 akfree的那篇文章,利用路径变得显而易见。

我们所做的是:

  • 喷出一堆 System V 消息队列消息,导致内核分配大量msg_msg大小可控的结构。目前,我们关心的是登陆 kmalloc-128 平板
  • 释放其中一些
  • 添加 netlink 规则,使flow_rule分配有望落在刚刚释放的堆槽之一
  • 我们的 OOB 总共写了 3 次(即dup我们的规则中有 3 s 没有 no immediate),破坏
    • 堆上下一条消息的list_head.prev指针(偏移量 8)
    • 堆上下一条消息的内容中的一些随机数据(偏移量 88)
    • security堆上第二条下一条消息的指针(偏移量 40)
  • 查找和msgrcv第二条下一条消息,导致内核进入kfree()net_device因为它是一个net_device被写入的指针)
  • 分配更多消息,但这次在 kmalloc-4k 平板中,目标是登陆net_device刚刚释放的
  • 使内核在设备上执行某些操作,这将导致调用(现在受控制的)net_device.netdev_ops操作结构中的函数指针,从而为我们提供代码执行。阅读 from/proc/net/dev是对此的一个简单答案(导致netdev_ops->ndo_get_stats64称为),这是我最终使用的。

这条链子非常好。只是为了强调一些好处:

  • 我们确切地知道哪个msg_msg指针list_head.prev被破坏(因此释放不安全),因为我们可以MSG_COPY将它移出队列(它不会触及下一个/上一个指针,因为它实际上没有被删除)并查看消息的内容是否有改变了。
  • 除了告诉我们哪个消息是“危险的”之外,这还会泄漏我们将要登陆的内核堆指针,使得启动 ROPing 变得微不足道(更多内容见下文)。
  • 我们也确切地知道哪条消息的security指针被覆盖了。我们可以添加第 4 个dup(复制后再次查看消息数据),也可以在mtype复制后查看消息。还记得 2 件事是如何写出边界的(4 或 5,以及指针)吗?碰巧 4 或 5 被写入消息mtype(偏移量 16),因此通过检查是否mtype从输入的任何值更改,我们可以判断我们是否有正确的消息。

到了晚上(也许熬夜有点太晚了……),我有了第一个概念的工作证明(POC)(在 ARM VM 不是 x86 中,因此有不同的寄存器等等)。

CVE-2022-25636 Netfilter防火墙模块提权漏洞

成功!

不过,在这方面又花了几个小时的时间,但我离代码执行还差得远。

由于某种原因,该漏洞利用非常不稳定(即成功率非常低)。我认为这是因为:

  1. 内核freelist 随机化比我想象的更有效
  2. Go 运行时在后台所做的所有事情都在搞乱内核堆。
  3. 系统上运行的其他事情导致零星kmalloc-128分配,抛出/用完空闲列表

我尝试将所有内容更改为从kmalloc-2048平板中计算出来(因为所有偏移数学仍然有效),但这似乎根本没有帮助。在这一点上,我可能应该花一些时间在内核调试器上,准确地跟踪 freelist 发生的事情,但我决定继续用 C 重写漏洞利用程序,看看是否有帮助。如果不出意外,它可能会使漏洞利用的后期阶段更容易使用,因为我不必尝试链接内核可以作为漏洞利用的最后阶段跳转到的其他东西。

重写

男孩,这是一场噩梦。有一个C 库可以“很好地”使用 nftables,但归根结底它是 C,所以没有什么是真正“好”的。在盯着 netlink 数据包的输出数小时strace试图找出我在 C 代码中遗漏了什么之后,我最终回到了我在 goland 的位置。如果您有兴趣,可以在我发布到 oss-security 邮件列表的复制器中找到与 nftables 交互所需的代码。

但它不再稳定。该死。

又搞砸了几天(主要是想弄清楚是否有特定的顺序来释放初始消息以最好地绕过 freelist 随机化),我达到了利用成功率约为 30% 的程度,这已经足够好了继续。我完全有可能在我的漏洞利用代码中遗漏了一些明显被破坏的东西,但是如果你对我可能遗漏内核方面的东西有任何想法,请给我发电子邮件或 DM – 我真的很想知道发生了什么。

已经在这方面花费了足够的时间,我决定放弃将其作为一个完整的漏洞利用。我只是想获得我的 root shell 并收工。我在我的测试虚拟机上禁用了 SMEP、SMAP、KPTI 和 KASLR,并组合了一个快速的“回调”(让我成为 root 并退出任何容器/命名空间),我可以直接从内核跳转到:

void *get_task(void) {
    void *task;
    asm volatile ("movq %%gs: 0x1fbc0, %0":"=r"(task));
    return task;
}

void *elevate(void *dev, void *storage) {
    void *c = ((void * (*)(int))(prepare_kernel_cred))(0);
    ((void (*)(void *))(commit_creds))(c);
    void *current = get_task();
    ((void (*)(void *, void *))(switch_task_namespaces))(current, (void *)init_nsproxy);
    return NULL;
}

基本上就是这样。减去整个“它只有 30% 的时间有效”,漏洞利用就完成了,经过几次尝试后我得到了我的 shell。

CVE-2022-25636 Netfilter防火墙模块提权漏洞

在您尝试破解密码哈希之前,这只是vagrant:P

旁注:ROP

虽然我最终没有在我的漏洞利用中实现它,但我们在 ROP 方面处于一个惊人的位置(使 SMEP/SMAP/KPTI 成为非问题)。由于内核堆地址net_device被泄露,我们知道我们的消息数据将在内存中的什么位置。然后,该指针可用于计算我们的假地址netdev_ops(将其放在消息中的其他位置),然后当内核调用从该 ops 结构中获取的函数时(以net_device(/our message) 作为第一个参数) ),我们可以给它一个简单的mov rsp, rdi; ret小工具的地址,以堆栈枢轴到我们的消息。从那里开始,一切皆有可能。

唯一缺少的是 KASLR 泄漏,但这并不是很大的障碍 🙂

代码?

在我写这篇博文的几周内,@Bonfee已经独立开发了一个漏洞利用并发布了它!

我还没有查看它们的全部实现,但它似乎使用了与我上面描述的类似的路径。但是,它还包括完整的 ROP 链和 KASLR 泄漏,使其比我的更完整。我建议你检查一下!https://github.com/Bonfee/CVE-2022-25636

CVE-2022-25636 poc&exp

github: https://github.com/Bonfee/CVE-2022-25636.zip

云中转网盘:

https://yzzpan.com/#sharefile=xR86760p_37287
解压密码:www.ddosi.org

这是我对CVE-2022-25636的poc.
我用内核针对 Ubuntu 21.10 对其进行了测试5.13.0-30
几乎~40%可以正常工作,在其他情况下,您可能会遇到内核恐慌。
该漏洞利用可能会破坏堆上的重要数据,在尝试不成功后最好重新启动

总结

这是一个非常有趣的错误发现和工作。从开始到结束,只用了不到一周的时间就找到、分类错误、弄清楚如何攻击它并构建漏洞利用。虽然并不新颖,但我们使用它获得的 OOB 写入原语也非常有趣,并且正如我们所见,它可以进行相当干净的利用。

我希望您喜欢阅读,当然也可以提出您可能遇到的任何问题。

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。