我看到你们这些在[橙色网站][hn]上的家伙在找我麻烦。我将在下面做出一些精选回应,但首先我提出一个挑战:我将向第一个能够在不使用 ifunc 的情况下演示此攻击的人送出我自己的 500 美元。我真心感兴趣,并愿意为这种启发付费。Fork 这个仓库并提交一个包含你可用 PoC 的 PR,如果你赢了,我会私下询问你的邮寄地址。现在开始回应:
这是找错树了。
孩子,我就住在错误的树上,我想对谁吠就对谁吠。
这对漏洞利用并非必要,
你对漏洞利用才不必要!
如果我们想增加针对以 root 身份运行的任意代码的保护,总是有 selinux。
一旦加载,此攻击不需要跨越任何进一步的系统调用边界。所以是的,我们本可以限制一个“额外”的 root 会话,但我们仍然会有不请自来的客人进入机器!
这是什么,比尔博的生日派对??除了参加派对事务外不得入内!!
- IFUNC 并不是在 main 之前运行代码的唯一方式。
但它是一种不必要的在内存保护设置之前运行代码的方式。
- 他们提出的替代方案可以说更不安全,因为函数指针将在进程的整个生命周期内保持可写,
我们可以用 mprotect 来临时应对!参见上面 修改 LD_PRELOAD 小节之前的最后一句。
是的,这篇博客被误导了。
抱歉,这篇博客是无引导的。所有这些恶作剧都是我自己做的!没有人骗我变得这么愚蠢。
IFUNC 应该由[客户端]软件本身实现,
@CountWSS 💯 没错
一系列明显的流程失败来自 Github 维护者直到……
这是我将认真回应的唯一一点:
我认为,认为这件事始于他的错误,对 xz-utils 维护者来说极其不公平,对社区来说也相当危险。它始于没有人愿意帮助维护这个项目。攻击者依赖 ifunc 作为技术漏洞,并将我们对 xz-utils 的集体疏忽作为社会漏洞。我认为,将 Collin 先生的行为视为除了英勇的、多年如一日的社区服务奉献之外的任何东西,都是可耻的。
而且 Bruce Schneier 同意我的观点,所以……很遗憾,你的论点完蛋了。
语言可能比需要的更严厉了一些
你不会相信我的朋友们先让我把这篇改得温和了多少。
Linux 发行版不应该如此自视甚高,以至于期望 OpenBSD 遵从并适应他们的烂摊子
@debazel!!!我喜欢。
完全是胡说八道。
好吧,那部分说得准确。
为什么你不应该为 [CVE-2024-3094][nvd] 责怪 xz-utils。也 看看我的 ETSA Talk!

CVE-2024-3094,更常被称为“xz-utils 后门”,是全球网络安全的一次险胜。如果这次攻击没有在关键时刻被 [Andres Freund][freund] 发现,我们星球上的大多数 SSH 服务器早就开始向这次攻击背后的势力授予 root 访问权限了。
不幸的是,太多分析都集中在[恶意代码][JiaT75]是如何进入 xz-utils 仓库的。相反,我想论证的是,关键开源软件中的两个长期设计决策才使这次攻击成为可能:[将 OpenSSH 链接到 SystemD][biebl],以及 [GNU IFUNC][sourceware] 的存在。
开始之前:本文的许多讨论涉及 Linux 上动态链接的复杂细节。如果你需要复习,请查看 dynamic_linking.md。
有大量优秀的文章概述了 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?
简短的回答是:他们不得不这么做。OpenSSH 是由
OpenBSD 社区开发、为 OpenBSD 社区服务的,他们根本
不在乎 Linux。[Portable OpenSSH][mindrot] 项目是一个
尽力而为的补丁集合,用通用 POSIX 组件替换 OpenBSD 特有的
组件,并在适用的情况下加入一些平台特定的
代码。在实践中,SSH 的软件供应链最终看起来
大致是这样的:```mermaid
flowchart TD
subgraph OpenBSD Folks
A[OpenBSD]
B[OpenSSH]
H[improvements]
end
B-->A
A-->H
H-->B
B-->C
C[Portable OpenSSH]
subgraph Debian Folks
D[Debian SSH]
G[improvements]
end
C-->D
D-->G
G-->C
subgraph Fedora Folks
J[Fedora SSH]
K[improvements]
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 Patches|D
这些补丁从未进入 Portable OpenSSH,因为 Portable OpenSSH 的开发者们[“对依赖 libsystemd 不感兴趣”][djmdjm]。它们也从未进入上游 OpenSSH,因为 OpenBSD 根本不需要支持 SystemD。
### 关于“关注点分离”的担忧
这看起来无害,但它反映了开源领域,尤其是 Linux 中一个更大的问题:操作系统的关键组件由互不相识、互不交流的人开发。
* 为 SystemD 修补 OpenSSH 的人是否知道(或关心)libsystemd 依赖于 xz-utils?
* SystemD 的开发者是否知道(或关心)xz-utils 已经开始使用 ifunc?
* OpenSSH 的开发者是否知道(或关心)ifunc 是什么?在 OpenBSD 上它显然不是什么常见的东西。
从某种意义上说,这种沟通上的断裂是开源的一个特性:我可以根据我的需求调整你的工作,而不必为此打扰你。但它也可能导致一定程度的间接性,使得关键的设计假设(例如传统的动态链接过程)无法得到维护。
[康威定律][conway]的一个明显推论是:如果你在交付你的组织架构图,你也在交付那些存在于组织架构图裂缝中的 bug。这里没有任何一个人或团队真正犯了错误,但事后看来,很明显攻击者察觉到 Debian/Fedora SSH 的左手并不知道 xz-utils 的右手在做什么。
## GNU IFUNC *本应* 做什么?
它允许你在运行时决定要使用某个函数的哪个版本。它通过给你一个机会来运行**任意代码**,从而影响链接器如何解析符号。

### 检测 CPU 特性
假设你有一个必须在各种 x86 CPU 上运行的应用程序。根据当前 CPU 的具体特性,你可能希望为同一任务使用不同的算法。IFUNC 最初的想法是允许程序在第一次调用某个函数时检查 CPU 特性,此后使用最适合该 CPU 的实现。
看看 [`cpu_demo.c`](https://github.com/robertdfrench/ifuncd-up/blob/trunk/code/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
// Print Green text to the Terminal
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);
}
// Print plain text to a file 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;
// Ask the kernel whether stdout is a file or a tty
int result = ioctl(STDOUT_FILENO, TCGETS, &term);
if (result == 0) {
// stdout is a terminal
return print_to_tty;
} else {
// stdout is not a terminal
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
通过允许在全局偏移表仍然可写时运行任意代码,[RELRO](https://github.com/robertdfrench/ifuncd-up/blob/trunk/dynamic_linking.md#relro) 所提供的保护[变得毫无意义][binarly-io]。
这一点值得注意,因为 RELRO 宣称自己是保护动态加载符号完整性的一种方式。从用户的角度来看(你,作为编译器和链接器的用户),这违反了[最小惊讶原则][pola]:没有理性的人会期望*加载一个动态库*竟然会破坏一个旨在*保护动态库*的安全功能。

### 它并不总是必要的
还有多种其他方式来处理这种情况。它们各有不同的权衡,但都比 IFUNC 简单得多。所有这些都比 IFUNC 更具可移植性,更容易理解,也更难被利用。