GNU IFUNC 才是 CVE-2024-3094 真正的罪魁祸首
摘要
本文认为,GNU IFUNC 以及将 OpenSSH 链接到 SystemD 的设计决策,才是 CVE-2024-3094 xz-utils 后门漏洞得以实施的主要促成因素,而非恶意代码本身。
暂无内容
查看缓存全文
缓存时间: 2026/05/08 08:28
robertdfrench/ifuncd-up 来源: https://github.com/robertdfrench/ifuncd-up # IFUNC’d up 为何你不应再将 [CVE-2024-3094][nvd] 归咎于 xz-utils。同时也请查看我的 ETSA 演讲 (https://www.youtube.com/watch?v=7j8ZCDGwSr8)! 我认为 IFUNC’d up CVE-2024-3094,也就是更为人熟知的“xz-utils 后门事件”,是全球网络安全的一次侥幸逃脱。如果此次攻击未能被 [Andres Freund][freund] 及时识破,地球上绝大多数 SSH 服务器将会开始向攻击者授予 root 访问权限。不幸的是,目前的分析过于集中在 [恶意代码][JiaT75] 是如何潜入 xz-utils 代码库的。相反,我想指出,正是关键开源软件中两个长期存在的设计决策使得此次攻击成为可能:[将 OpenSSH 链接到 SystemD][biebl],以及 [GNU IFUNC][sourceware] 的存在。 开始之前:本文大量讨论了 Linux 动态链接的细节。如果你需要回顾基础知识,请查阅 dynamic_linking.md。 ## CVE-2024-3094 快速回顾 网络上有很多优秀的文章概述了 xz-utils 后门的宏观细节,例如 Dan Goodin 的 [What we know about the xz Utils backdoor that almost infected the world][goodin1] 和 Sam James 的 [FAQ on the xz-utils backdoor (CVE-2024-3094)][thesamesam] gist。我们无需在此重复所有内容,因此,出于本文目的,这里做一个非常粗略的回顾: * 某些 Linux 发行版修改 OpenSSH 以依赖 SystemD * SystemD 依赖 xz-utils,而 xz-utils 使用了 GNU IFUNC * 因此,xz-utils 最终进入了 OpenSSH 的地址空间 * 这使得 ifunc 能够修改 SSH 服务器中的代码 mermaid flowchart TD G["GNU IFUNC"] A["OpenSSH (OpenBSD)"] B["Portable OpenSSH(Linux / macOS / 等)"] C[OpenSSH + IFUNC] D[xz-utils] E["SystemD (Linux)"] A -->|移除 OpenBSD 特定内容| B B -->|添加 SystemD 特定内容| C D --> E E --> C C --> F["混乱"] G --> D ## 为什么 Linux 发行版要修改 OpenSSH? 简短的回答是:他们不得不这么做。OpenSSH 是由 OpenBSD 社区为 OpenBSD 社区开发的,他们对 Linux 并不关心。[Portable OpenSSH][mindrot] 项目是一个尽力而为的补丁集合,旨在用通用的 POSIX 组件替换 OpenBSD 特定的组件,并在适用时添加一些平台特定的代码。在实践中,SSH 的软件供应链看起来大致如下: mermaid flowchart TD subgraph OpenBSD 群体 A[OpenBSD] B[OpenSSH] H[改进] end B-->A A-->H H-->B B-->C C[Portable OpenSSH] subgraph Debian 群体 D[Debian SSH] G[改进] end C-->D D-->G G-->C subgraph Fedora 群体 J[Fedora SSH] K[改进] end C-->J J-->K K-->C OpenBSD 版本的 OpenSSH 是所有其他版本的上游,大多数改进都来自 OpenBSD 社区内部。这些变更会流向下游的 Portable OpenSSH 项目,该项目试图以不特定于 OpenBSD 的方式重新实现新功能。这使得 SSH 能够在 Linux、macOS、FreeBSD 甚至 Windows 等平台上运行。但这还没完。某些操作系统在 Portable OpenSSH 提供的功能之上进行了进一步的定制。例如,Apple 为 ssh-add 添加了 [--apple-use-keychain][keith] 标志,以帮助其集成到 macOS 密码管理器中。在 CVE-2024-3094 的案例中,Fedora 和 Debian 为其 OpenSSH 分支维护了自己的 [SystemD 补丁][biebl],以修复 [围绕 sshd 重启的竞态条件][schmidt]。因此,SSH 的实际供应链开始看起来像这样: mermaid flowchart TD A[OpenSSH] B[Portable OpenSSH] C[Debian SSH] D[Fedora SSH] A-->B B-->C B-->D C<-->|SystemD 补丁|D 这些补丁从未进入 Portable OpenSSH,因为 Portable OpenSSH 的开发人员表示 [“对依赖 libsystemd 不感兴趣”][djmdjm]。它们也从未进入上游 OpenSSH,因为 OpenBSD 没有支持 SystemD 的需求。 ### 关于“关注点分离”的担忧 这看起来似乎无害,但它是开源领域,尤其是 Linux 中一个更大问题的示例:操作系统的关键组件由互不认识、也不互相沟通的人开发。 * 为 OpenSSH 打 SystemD 补丁的人知道(或关心)libsystemd 依赖 xz-utils 吗? * SystemD 的人知道(或关心)xz-utils 开始使用 ifunc 吗? * OpenSSH 的人知道(或关心)ifunc 的存在吗? 至少在 OpenBSD 上肯定不是这样。在某种意义上,这种沟通的断裂是开源的一个特征:我可以根据我的需求适应你的工作,而不必打扰你。但这也可能导致一种间接性,使得关键的设计假设(如传统的动态链接过程)无法得到维持。[康威定律][conway] 的一个明显推论是:如果你在交付你的组织结构图,你也在交付潜藏在你组织结构图裂缝中的漏洞。没有人或团队在这里真正犯了错误,但事后看来,很明显攻击者意识到 Debian/Fedora SSH 的“左手”并不知道 xz-utils 的“右手”在做什么。 ## GNU IFUNC 应该 做什么? 它允许你在运行时决定使用哪个版本的某个函数。它通过提供一个运行任意代码的机会来实现这一点,从而影响链接器如何解析符号。 ### 检测 CPU 特性 假设你有一个应用程序必须在各种 x86 CPU 上运行。根据当前 CPU 的具体特性,你可能倾向于对同一任务使用不同的算法。IFUNC 最初的想法是允许程序在第一次调用函数时检查 CPU 特性,此后使用该 CPU 最合适的实现。看看 cpu_demo.c: c void print_cpu_info() __attribute__((ifunc ("resolve_cpu_info"))); void print_avx2() { printf("AVX2 is present.\n"); } void print_nope() { printf("AVX2 is missing.\n"); } static void* resolve_cpu_info(void) { __builtin_cpu_init(); if (__builtin_cpu_supports("avx2")) { return print_avx2; } else { return print_nope; } } int main() { print_cpu_info(); return 0; } 这个程序展示了 IFUNC 最常见的用法:它询问 CPU 是否支持某些特性,并根据支持的特性提供函数的不同实现。在这种情况下,我们的函数 print_cpu_info 最终会打印 “AVX2 is present” 或 “AVX2 is missing”,具体取决于你的 CPU 有多古老。 ### 探测进程环境 虽然 IFUNC 旨在探测 CPU 能力,但没有什么能阻止你在解析器中运行更复杂的代码。例如,tty_demo.c 展示了如何根据 STDOUT 是文件还是终端来加载不同的函数实现: c // 在终端上打印绿色文本 void print_to_tty(const char *message) { const char *green_start = "\033[32m"; const char *color_reset = "\033[0m"; printf("%sTTY: %s%s\n", green_start, message, color_reset); } // 在文件中打印纯文本 void print_to_file(const char *message) { printf("FILE: %s\n", message); } void print_message(const char *message) \ __attribute__((ifunc("resolve_print_function"))); void (*resolve_print_function(void))(const char *) { struct termios term; // 询问内核 stdout 是文件还是 tty int result = ioctl(STDOUT_FILENO, TCGETS, &term); if (result == 0) { // stdout 是终端 return print_to_tty; } else { // stdout 不是终端 return print_to_file; } } int main() { print_message("Hello, World!"); return 0; } 这并不是 IFUNC 的预期用途,但它展示了可能性:你可以在任何使用了你声明的 IFUNC 的程序中,在 main 之前运行任意代码。 ## IFUNC 可能是一个糟糕的想法 GNU IFUNC 难以实现,难以正确使用,而且(作为一种所谓的性能优化工具)并不比替代方案快多少。正如我们在 CVE-2024-3094 中看到的那样,它也是软件供应链攻击中非常强大的工具。IFUNC 在 GNU C 库中广泛使用,这可能没问题。那是为其最初开发的团队,他们与实际实现 IFUNC 的编译器和链接器团队联系紧密。他们最能理解权衡利弊,而且有很多 libc 函数受益于特定于 CPU 的实现。我认为我们应该将 IFUNC 视为 glibc 的内部接口,并避免在其他应用程序中使用它。 ### 使用过于复杂,难以保证安全 ifunc 完全太难用了。有太多的 [角落案例][nagy],而 [官方文档][gnu-cfa] 非常 [匮乏][sourceware]。这给用户造成了采用 ifunc 很简单的误导印象。即使在 ifunc 可用几年后,所宣传的接口 [仍不起作用][agner]。GCC 开发人员称其为 [一个错误][odonell],并考虑添加警告以补偿 IFUNC 的脆弱性: > 使 IFUNC 稳健所需的 glibc 解决方案尚未到位, > 因此我们应该尽我们所能警告用户它可能会出问题。 这不仅仅是 IFUNC 的问题。Apple Mach-O 有一个类似的功能叫做 .symbol_resolver,他们表示 [“后悔添加它”][rjmccall]。 ### 它削弱了 RELRO 通过允许在全局偏移表(GOT)仍可写时运行任意代码,RELRO 提供的保护被 [证明无效][binarly-io]。这一点值得注意,因为 RELRO 宣称自己是保护动态加载符号完整性的一种方式。从用户角度来看(作为编译器和链接器的用户),这违反了 [最小惊讶原则][pola]:没有合理的人会期望加载动态库会破坏旨在保护动态库的安全特性。 ### 并非总是必要 有多种其他方法可以处理这种情况。它们各自有不同的权衡,但都比 IFUNC 简单得多。所有这些方法都比 IFUNC 更具可移植性,更容易理解,也更难被利用。 > “Ifunc 只是一种极其愚蠢的运行时微架构特定代码选择方式。” > > – Rich Felker (https://hachyderm.io/@dalias/112952237145378821), > musl 维护者 (https://musl.libc.org)。 #### 全局函数指针 IFUNC 具有吸引力,因为它允许开发人员以声明式而非命令式的方式表达函数选择。但实际上,以命令式方式这样做并不难。考虑 static_pointer.c,它在运行时解析全局函数指针: c static int (*triple)(int) = 0; int triple_sse42(int n) { return 3 * n; } int triple_plain(int n) { return n + n + n; } void print_fifteen() { int fifteen = triple(5); printf("%d\n", fifteen); } int main() { __builtin_cpu_init(); if (__builtin_cpu_supports("sse4.2")) { triple = triple_sse42; } else { triple = triple_plain; } print_fifteen(); return 0; } 这真的糟糕到我们需要链接器中的特殊技巧来避免它吗?这种方法的一个缺点是函数指针 triple 在运行时是可写的,而 IFUNC+RELRO 将确保 GOT 中的 ifunc 地址在解析后是不可变的。然而,通过一些额外的努力,我们可以使用 [mprotect(2)][mprotect] 将这些指针标记为只读。 #### 修改 LD_PRELOAD 如果你知道你的代码需要什么 CPU 特性,并且为每种情况都有一个单独的动态库副本,那么你可以通过使用 $LD_PRELOAD 指定正确的库来实现相同的效果,如下所示: bash #!/bin/bash if (cat /proc/cpuinfo | grep flags | grep avx2 > /dev/null); then LD_PRELOAD=./myfunc_avx2.so ./my_app else LD_PRELOAD=./myfunc_normal.so ./my_app fi (如果你不熟悉 LD_PRELOAD,请查看 catonmat 的 [“A Simple LD_PRELOAD Tutorial”][catonmat]。) #### 每个特性组合单独的二进制文件 你真的需要支持多少种独特的 CPU 特性组合?实际上存在多少种?表面上看,这看起来像是一个组合爆炸。amd64 ISA 有数十种不同的向量算术、虚拟化和安全扩展。但这些特性在野外并不是独立出现的。例如,没有拥有 AVX-512 的 CPU 缺乏 SSE4.2 或 AES-NI。了解你的应用程序需要哪些 CPU 特性,以及哪些特性在真实芯片上同时出现,可以帮助你确定需要分发多少个不同的二进制文件。数量可能没有你预期的那么多。大多数包管理器允许你在安装时运行脚本;你可以在单个 rpm 或 deb 文件中分发多个二进制文件,并使用安装时逻辑为宿主 CPU 选择最佳的一个。 ### 速度优势并不明显 > 长期以来,对我来说显而易见的是,即使 ifunc 有性能优势, > 这也只有在整个函数调用非常短,以至于调用开销占总体时间的 > 显著部分时才成立。 > > – Rich Felker (https://hachyderm.io/@dalias/113074264762553873), > musl 维护者 (https://musl.libc.org)。 鉴于 ifunc 通常的借口是性能相关的,我想看看 ifunc 本身 会造成多少开销。毕竟,任何值得优化的函数可能都会被频繁调用,因此函数调用的开销值得注意。为了弄清楚这一点,我设计了一个实验,在一个紧密循环中反复调用一个动态解析的函数。看看 speed_demo_ifunc.c 和 speed_demo_pointer.c。这两个程序都做相同的工作(递增静态计数器),但递增器函数以不同方式解析:前者利用 GNU IFUNC,后者依赖普通的旧函数指针。总体逻辑如下: 1. 调用解析器函数以确定使用哪个递增器。 1. 在某个地方记录这个答案(在 GOT 中,或作为函数指针)。 1. 调用此递增器函数几十亿次,以估计其成本。 作为对照,还有 speed_demo_fixed.c,它执行相同的递增器工作,但没有动态解析的函数。这有助于估计运行时的一部分 dedicated to 函数调用,另一部分仅是做加法。Makefile 目标 rigorous_speed_demo 对每个程序进行多次运行,并生成有关其性能的一些简单统计数据。这些数字当然会根据你的硬件而变化,但 fixed 测试应作为比较的基线。 | 结果 | 低 | 高 | 平均 | |———–|——|——|—––| | fixed | 2.93 | 4.20 | 3.477 | | ifunc | 9.50 | 10.56| 9.986 | | pointer | 6.23 | 7.44 | 6.791 | 我们在这里看到的是,与使用普通函数指针相比,ifunc 有不可忽视的开销。平均而言,在我的硬件上,调用 ifunc 函数 20 亿次所花费的时间大约是调用函数指针 20 亿次的两倍。这在现实生活中重要吗?绝对不重要。值得优化的函数比我们要分析的“加一”函数昂贵得多。这之所以有趣,是因为 GNU IFUNC 声称是性能的一大助力,但却似乎比函数指针产生更多的成本。 #### 其他技术的性能 还有其他技术比 ifunc 慢。看看 super_rigorous_speed_demo,它引入了其他实验:speed_demo_upfront.c 和 speed_demo_always.c。speed_demo_upfront.c 的行为类似于 speed_demo_pointer.c,不同之处在于它将 t
相似文章
Linux内核中因单个错误字符导致的高危漏洞
Linux内核中一个错误的字符引入了一个use-after-free漏洞(CVE-2026-53111),允许非特权用户在Debian和Ubuntu系统上将权限提升至root;该漏洞已修复并移植回旧版本。
FUSE readdir缓存中的越界写入导致非特权用户提权至root (CVE-2026-31694)
Linux内核FUSE readdir缓存中的一个漏洞(CVE-2026-31694)允许无特权的本地用户通过越界写入的方式提权至root。该漏洞影响许多桌面发行版,可通过挂载FUSE文件系统进行利用。
RefluXFS: Linux内核XFS文件系统中的本地权限提升至root漏洞 (CVE-2026-64600)
Qualys和Anthropic披露了CVE-2026-64600,这是Linux内核XFS文件系统中的一个竞态条件漏洞,允许本地权限提升至root,影响超过1600万个系统,且无内核日志输出,能够在重启后持续存在。
@jedisct1: epoll UAF
对 Linux 内核 epoll 子系统中的一个释放后使用(UAF)漏洞的详细分析,该漏洞通过切换到 RCU 修复,以及作者在现代设备上尝试利用该漏洞失败的经过。
GhostLock:一个在全部Linux发行版中存在15年的栈UAF漏洞
GhostLock(CVE-2026-43499)是一个存在15年的Linux内核栈释放后使用漏洞,影响所有发行版,可导致本地权限提升与容器逃逸。文中给出了详细的利用技术。