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

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的[我们对差点感染全世界的xz Utils后门的了解][goodin1]和Sam James的[xz-utils后门FAQ (CVE-2024-3094)][thesamesam]要点。我们不需要在这里全部重述,因此就本文而言,以下是一个非常粗略的回顾:
## 为什么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
这些补丁从未进入可移植 OpenSSH,因为可移植 OpenSSH 团队["对依赖 libsystemd 不感兴趣"][djmdjm]。它们也从未进入上游 OpenSSH,因为 OpenBSD 不需要支持 SystemD。
### 关于“关注点分离”的担忧
这看起来似乎无害,但它是一个更大问题的缩影,在开源领域尤其如此,特别是在 Linux 中:操作系统的关键组件由互不相识、互不沟通的人们开发。
* 为 SystemD 修补 OpenSSH 的人是否知道(或关心)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`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/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 最终将根据你 CPU 的古老程度打印 "AVX2 is present" 或 "AVX2 is missing"。
虽然 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/HEAD/dynamic_linking.md#relro)提供的保护变得[毫无意义][binarly-io]。
这一点很重要,因为RELRO宣称自己是保护动态加载符号完整性的一种方式。从用户的角度(作为编译器和链接器的用户)来看,这违反了[最小惊讶原则][pola]:任何一个理性的人都不会期望*加载一个动态库*会破坏旨在*保护动态库*的安全特性。

### 它并非总是必要的
有多种其他方法可以处理这种情况。它们各有不同的权衡,但都比IFUNC简单得多。所有这些方法都比IFUNC更具可移植性,更容易理解,也更难被利用。
> "Ifunc简直是进行运行时微架构特定代码选择的极其愚蠢的方式。"
>
> -- [Rich Felker](https://hachyderm.io/@dalias/112952237145378821),
> [musl](https://musl.libc.org)的维护者。
#### 全局函数指针
IFUNC之所以有吸引力,是因为它允许开发人员以*声明式*而非*命令式*的方式表达函数选择。但用命令式的方式来做实际上并不那么困难。考虑[`static_pointer.c`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/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 的 [一篇简单的 `LD_PRELOAD` 教程][catonmat]。)
#### 每个特性组合对应独立二进制
你到底需要支持多少个不同的 CPU 特性组合?实际上有多少种组合存在?
乍一看,这像一个组合爆炸问题。amd64 ISA 有几十种不同的向量算术、虚拟化和安全扩展。但这些特性在实际中并不是独立出现的。例如,没有 CPU 具备 AVX-512 却不具备 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`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/ifunc/main.c) 和 [`speed_demo/pointer`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/pointer/main.c)。这两个程序完成相同的工作(递增一个静态计数器),但递增函数的解析方式不同:前者利用 GNU IFUNC,后者依赖普通的函数指针。
整体逻辑如下:
1. 调用一个解析器函数来确定使用哪个递增函数。
2. 将结果记录在某处(GOT 或函数指针)。
3. 调用该递增函数数十亿次,以估算其成本。
作为对照,还有 [`speed_demo/fixed`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/fixed/main.c),它执行相同的递增工作,但不使用任何动态解析函数。这有助于估计运行时中多少部分用于函数调用,多少部分仅仅是加法操作。
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 亿次所花的时间大约是调用函数指针的 2 倍。
这在实际应用中有影响吗?完全没有。值得优化的函数远比我们分析的“加一”函数昂贵得多。这里之所以有趣,是因为 GNU IFUNC 声称能提升性能,但似乎却比函数指针带来更多成本。
#### 其他技术的性能
还有其他技术比 ifunc 更慢。请看 `super_rigorous_speed_demo`,它引入了另外两个实验:[`speed_demo/upfront`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/upfront/main.c) 和 [`speed_demo/always`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/always/main.c)。
`speed_demo/upfront` 的行为类似于 `speed_demo/pointer`,但它将 CPU 特性检查的结果存储在全局变量中,而不是维护一个函数指针。这仍然需要先运行一个“解析器”函数,根据这些全局变量的值来决定使用哪个实现。这种技术比 ifunc 慢,但也比存储函数指针更安全:函数指针可以被设置为任意值,而布尔标志则不能。因此,能够修改这些变量的攻击者只能让程序*变慢*,但不能让程序行为*变得不同*。
`speed_demo/always` 被设计为最慢的技术——它在每次需要实现时都检查所有必要的 CPU 特性,并即时选择一个。有趣的是,这种技术并不比其他技术慢很多。在只需要检查单个 CPU 特性的情况下,它仅比 ifunc 略慢。
| 测试 | 最低 | 最高 | 平均 |
|---------|--------|--------|--------|
| fixed | 5.02 | 5.70 | 5.37 |
| pointer | 6.40 | 7.02 | 6.66 |
| ifunc | 8.56 | 11.11 | 9.64 |
| upfront | 9.24 | 9.41 | 9.33333|
| always | 10.07 | 10.56 | 10.2333|
## 结论
GNU IFUNC 是 gcc/ld.so 的一个小众特性,在用于 CVE-2024-3094 之前很少人知道。它有着不明显的陷阱和不足的文档。通过让链接器在 `main` 之前、在进程映像的关键部分初始化和保护之前运行任意代码,它破坏了编程中最基本的假设之一:仅仅加载一个库的行为并不会*改变*你的程序。
IFUNC 的性能提升是真实的,但并不比替代方案显著更好。通过单一二进制文件为多 CPU 优化的部署方式非常吸引人,但这可以通过更简单的技术(如函数指针)实现。
我认为 gcc 默认应禁用 IFUNC。启用它应该需要一个令人警惕的标志,例如 `--enable-cve-2024-3094`。任何在 libc 之外使用它的人都应提供严格且论证充分的分析,证明没有其他解决方案适用。

<!-- 参考文献 -->
[aes-ni]: https://en.wikipedia.org/wiki/AES_instruction_set
[agner]: https://www.agner.org/optimize/blog/read.php?i=167
[biebl]: https://salsa.debian.org/ssh-team/openssh/-/commit/818791ef8edf087481bd49eb32335c8d7e1953d6
[binarly-io]: https://github.com/binarly-io/binary-risk-intelligence/tree/master/xz-backdoor
[catonmat]: https://catonmat.net/simple-ld-preload-tutorial
[conway]: https://en.wikipedia.org/wiki/Conway%27s_law
[djmdjm]: https://github.com/openssh/openssh-portable/pull/251#issuecomment-2027935208
[fr0gger]: https://infosec.exchange/@fr0gger/112189232773640259
[freund]: https://www.openwall.com/lists/oss-security/2024/03/29/4
[gnu-cfa]: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-ifunc-function-attribute
[goodin1]: https://arstechnica.com/security/2024/04/what-we-know-about-the-xz-utils-backdoor-that-almost-infected-the-world/
[hn]: https://news.ycombinator.com/item?id=48056749
[jasoncc]: https://jasoncc.github.io/gnu_gcc_glibc/gnu-ifunc.html#relocations-and-pic
[JiaT75]: https://github.com/tukaani-project/xz/commit/cf44e4b7f5dfdbf8c78aef377c10f71e274f63c0
[keith]: https://keith.github.io/xcode-man-pages/ssh-add.1.html#apple-use-keychain
[mindrot]: https://anongit.mindrot.org/openssh.git
[mprotect]: https://www.man7.org/linux/man-pages/man2/mprotect.2.html
[musl]: https://musl.libc.org
[nagy]: https://sourceware.org/legacy-ml/libc-alpha/2015-11/msg00108.html
[nvd]: https://nvd.nist.gov/vuln/detail/CVE-2024-3094
[odonell]: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70082#c0
[OpenSSH9.8p1]: https://www.openssh.com/releasenotes.html#9.8p1
[openssh-unix-dev]: https://marc.info/?l=openssh-unix-dev&m=171288895109872&w=2
[pola]: https://en.wikipedia.org/wiki/Principle_of_least_astonishment
[rjmccall]: https://reviews.llvm.org/D139163#3993795
[schmidt]: https://bugzilla.redhat.com/show_bug.cgi?id=1381997#c4
[sourceware]: https://sourceware.org/glibc/wiki/GNU_IFUNC
[thesamesam]: https://gist.github.com/thesamesam/223949d5a074ebc3dce9ee78baad9e27#design