Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
ifuncd-up — GNU IFUNC 是 CVE-2024-3094 背后的真正元凶 | Kitploit
工具/GitHubGitHub/robertdfrench/ifuncd-up
漏洞分析动态代码分析 (DAST)漏洞利用逆向工程二进制分析供应链安全论文与研究学习与教育
GitHubrobertdfrench/ifuncd-up

ifuncd-up

GNU IFUNC 是 CVE-2024-3094 背后的真正元凶

查看仓库
6013个月前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
网站

回应黑客新闻

我看到你们这些家伙在[橙色的网站][hn]上找我的茬。下面我将做出一些精选回应,但首先我提出一个挑战:我将向第一个能够不用ifunc演示这次攻击的人发送我自己的500美元。我真的很感兴趣,并且愿意花钱获得启发。Fork这个仓库并提交一个包含可行PoC的PR,如果你赢了,我会私下联系你要邮寄地址。现在开始回应:

这完全是找错了方向。

小子,我就住在错误的方向上,我想朝谁吼就朝谁吼。

这对攻击来说无关紧要,

你才对攻击无关紧要!

如果我们想增加对以root身份运行的任意代码的保护,总有selinux。

一旦加载,这次攻击不需要跨越任何进一步的系统调用边界。所以是的,我们可以限制一个“额外”的root会话,但我们仍然会让不速之客留在机器上!

这是什么,比尔博的生日派对??非派对业务不得入内!!

  1. IFUNC远不是在main之前运行代码的唯一方式。

但它是在内存保护设置好之前运行代码的一种不必要的方式。

  1. 他们提出的替代方案可以说更不安全,因为函数指针在进程的整个生命周期中保持可写,

我们可以用mprotect来即兴发挥!请参见修改LD_PRELOAD小节上方的最后一句话。

是的,这篇博客方向错误。

抱歉,这篇博客是没有方向的。所有这些恶作剧都是我自己干的!没有人骗我变得这么蠢。

IFUNC应该由[客户端]软件本身来实现,

@CountWSS 💯 没错

一系列明显的流程失败 从Github维护者到……

这是我唯一会认真回应的观点:

我认为把这件事看成始于xz-utils维护者的失误,对他而言极其不公平,对社区也非常危险。它始于没人他妈在乎帮助维护这个项目。攻击者依赖ifunc作为技术漏洞,而我们集体对xz-utils的忽视则作为社会漏洞。我认为把柯林斯先生的行为看作除了一种英雄式的、长达数年的社区服务奉献之外任何东西,都是可耻的。

另外,Bruce Schneier同意我的观点,所以……为你感到遗憾,你的论点没戏了。

语言可能比必要的更严厉。

你不会相信我朋友让我先把这个淡化到什么程度。

Linux发行版不应该自视过高,以至于期望OpenBSD屈服并适应他们的烂摊子。

@debazel!!! Me gusta.

这纯属胡说八道。

好吧,那部分说得对。

IFUNC 搞砸了

为什么你不应该因[CVE-2024-3094][nvd]而责怪xz-utils。另外来看看我的ETSA演讲!

我觉得IFUNC搞砸了

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的[我们对差点感染全世界的xz Utils后门的了解][goodin1]和Sam James的[xz-utils后门FAQ (CVE-2024-3094)][thesamesam]要点。我们不需要在这里全部重述,因此就本文而言,以下是一个非常粗略的回顾:

  • 一些Linux发行版修改OpenSSH以依赖SystemD
  • SystemD依赖xz-utils,后者使用了GNU IFUNC
  • 因此,xz-utils最终出现在OpenSSH的地址空间中
  • 这允许ifunc修改SSH服务器中的代码```mermaid flowchart TD G["GNU IFUNC"] A["OpenSSH (OpenBSD)"] B["Portable OpenSSH
    (Linux / macOS / etc)"] C[OpenSSH + IFUNC] D[xz-utils] E["SystemD (Linux)"] A -->|Remove OpenBSD specifics| B B -->|Add SystemD specifics| C D --> E E --> C C --> F["Mayhem"] G --> D
root@kitploit:~
## 为什么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

root@kitploit:~
这些补丁从未进入可移植 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 应该做什么?
它允许你在运行时决定要使用某个函数的哪个版本。它通过给你运行**任意代码**的机会来影响链接器如何解析符号来实现这一点。

![](https://assets.kitploit.com/production/public/readmes/31477/ea52603b42c9b38ff0f8f693b09c5d5c0d107a4175f3be4e1e48b39c9ca3eb55.png)



### 检测 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;

root@kitploit:~
// 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; }

root@kitploit:~
这不是IFUNC的真正用途,但它展示了可能性:你可以在任何使用你声明
的IFUNC的程序中的`main`之前运行任意代码。





## IFUNC可能是个坏主意
![](https://assets.kitploit.com/production/public/readmes/31477/6d420d0d1b4f4c49a3ba8a4b2103d7f79d284c9d8fbda4449e6e6d0afa5b3cfa.png)

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]:任何一个理性的人都不会期望*加载一个动态库*会破坏旨在*保护动态库*的安全特性。

![](https://assets.kitploit.com/production/public/readmes/31477/ad252f1cb2937e3f8b168ece1e3f15455dcb0d7647b8715dff7435f3babc0e1c.png)



### 它并非总是必要的
有多种其他方法可以处理这种情况。它们各有不同的权衡,但都比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

root@kitploit:~
(如果你不熟悉 `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 之外使用它的人都应提供严格且论证充分的分析,证明没有其他解决方案适用。

![是的,所有共享库](https://assets.kitploit.com/production/public/readmes/31477/039631e9f0441a38341d4f2299c0666f63a731c02cb2239257afd7613455461c.png)

<!-- 参考文献 -->
[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
下载工具