
GNU IFUNC ist der wahre Schuldige hinter CVE-2024-3094
Ich sehe, ihr Witzbolden auf [der orangen Webseite][hn] macht mir das Leben schwer. Ich werde ein paar ausgewählte Antworten unten geben, aber zuerst stelle ich eine Herausforderung: Ich werde 500 $ meines eigenen Geldes an die erste Person senden, die diesen Angriff ohne ifunc demonstrieren kann. Ich bin aufrichtig interessiert und bereit, für die Erleuchtung zu zahlen. Forke dieses Repo und reiche einen PR mit Ihrem funktionierenden PoC ein. Ich werde privat Ihre Postanschrift erfragen, wenn Sie gewinnen. Nun zu den Antworten:
Das ist am falschen Baum.
Junge, ich lebe im falschen Baum, ich kann anbellen, wen ich will.
Es war nicht wesentlich für den Exploit,
Du bist nicht wesentlich für den Exploit!
Es gibt immer SELinux, wenn wir Schutz vor beliebigem Code hinzufügen wollen, der als root läuft.
Einmal geladen, musste dieser Angriff keine weiteren Syscall-Grenzen überschreiten. Also ja, wir hätten eine "Bonus"-Root-Sitzung einschränken können, aber wir hätten immer noch ungebetene Gäste auf der Maschine gehabt!
Was ist das, Bilbos Geburtstag?? Kein Eintritt außer in Partysachen!!
- IFUNC ist kaum der einzige Weg, Code vor main auszuführen.
Aber es ist ein unnötiger Weg, Code auszuführen, bevor der Speicherschutz eingerichtet ist
- Die Alternative, die sie präsentieren, ist wohl weniger sicher, weil der Funktionszeiger für die Lebensdauer des Prozesses beschreibbar bleibt,
Wir können mit mprotect improvisieren! Siehe den letzten Satz oben im Unterabschnitt Ändern von LD_PRELOAD.
Ja, dieser Blog ist fehlgeleitet.
Entschuldigung, dieser Blog war ungeleitet. Ich habe all diese Scherze selbst gemacht! Niemand hat mich zu dieser Dummheit verleitet.
IFUNC sollte von der [Client-]Software selbst implementiert werden,
@CountWSS 💯 verdammt ja
eine Reihe von offensichtlichen Prozessfehlern vom Github-Maintainer über ...
Dies ist der einzige Punkt, auf den ich ernsthaft antworten werde:
Ich halte es für außergewöhnlich unfair gegenüber dem xz-utils-Maintainer und ziemlich gefährlich für die Gemeinschaft, dies als beginnend mit einem Fehler seinerseits zu betrachten. Es begann damit, dass sich niemand einen Deut darum scherte, bei der Wartung dieses Projekts zu helfen. Der Angreifer verließ sich auf ifunc als technische Schwachstelle und unsere kollektive Vernachlässigung von xz-utils als soziale Schwachstelle. Ich halte es für beschämend, die Handlungen von Herrn Collins als etwas anderes zu sehen als einen heldenhaften, jahrelangen Einsatz für den Gemeinschaftsdienst.
Auch Bruce Schneier stimmt mir zu also... tut mir leid für euch, euer Argument ist im Eimer.
Die Sprache war vielleicht härter als nötig
Ihr werdet nicht glauben, wie sehr mich meine Freunde gezwungen haben, dies vorher abzuschwächen.
Linux-Distributionen sollten nicht so hohe Meinungen von sich haben, dass sie erwarten, dass OpenBSD sich anpasst und ihrem Chaos anpasst
@debazel!!! Me gusta.
Was für ein kompletter Quatsch.
Okay, der Teil stimmt.
Warum ihr aufhören solltet, xz-utils für [CVE-2024-3094][nvd] die Schuld zu geben. Schaut euch auch meinen ETSA-Vortrag an!

CVE-2024-3094, besser bekannt als "Die xz-utils-Hintertür", war ein Beinahe-Unfall für die globale Cybersicherheit. Wäre dieser Angriff nicht im letzten Moment von [Andres Freund][freund] entdeckt worden, hätten die meisten SSH-Server unseres Planeten der Partei hinter diesem Angriff Root-Zugriff gewährt.
Leider hat sich zu viel Analyse darauf konzentriert, wie [bösartiger Code][JiaT75] seinen Weg in das xz-utils-Repo fand. Stattdessen möchte ich argumentieren, dass zwei langjährige Designentscheidungen in kritischer Open-Source-Software diesen Angriff ermöglicht haben: [das Verknüpfen von OpenSSH mit SystemD][biebl] und die Existenz von [GNU IFUNC][sourceware].
Bevor ihr beginnt: Vieles dieser Diskussion befasst sich mit den Feinheiten
des dynamischen Linkens unter Linux. Falls ihr eine Auffrischung benötigt, schaut euch
dynamic_linking.md an.
Es gibt tonnenweise gute Aufsätze, die die Details auf hoher Ebene der xz-utils-Hintertür umreißen, wie Dan Goodins [Was wir über die xz-Utils-Hintertür wissen, die fast die Welt infiziert hätte][goodin1] und Sam James' [FAQ zur xz-utils-Hintertür (CVE-2024-3094)][thesamesam]-Gist. Wir müssen das hier nicht alles neu aufwärmen, also für die Zwecke dieses Artikels hier eine sehr grobe Zusammenfassung:
## Warum modifizieren Linux-Distributionen OpenSSH?
Die kurze Antwort ist, dass sie es müssen. OpenSSH wird von der
OpenBSD-Community für die OpenBSD-Community entwickelt, und sie kümmern
sich einen Dreck um Linux. Das [Portable OpenSSH][mindrot]-Projekt ist eine
Sammlung von Patches, die nach bestem Wissen und Gewissen OpenBSD-spezifische
Komponenten durch generische POSIX-Komponenten und, wo anwendbar,
plattformspezifischen Code ersetzen. Die Software-Lieferkette für SSH sieht in
der Praxis etwa so aus:```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
OpenBSDs Version von OpenSSH ist der Upstream für alles andere, und die meisten Verbesserungen daran stammen aus der OpenBSD-Community. Diese Änderungen fließen downstream zum Portable OpenSSH-Projekt, das versucht, neue Funktionen auf eine Weise zu implementieren, die nicht OpenBSD-spezifisch sind. Dies ermöglicht SSH auf Plattformen wie Linux, macOS, FreeBSD und sogar Windows zu arbeiten.
Aber damit nicht genug. Einige Betriebssysteme passen SSH noch weiter
an, über das hinaus, was Portable OpenSSH bietet. Beispielsweise fügt Apple
das [--apple-use-keychain][keith] Flag zu ssh-add hinzu, um die Integration
mit dem macOS-Passwortmanager zu unterstützen.
Im Fall von CVE-2024-3094 haben Fedora und Debian ihre eigenen
[SystemD-Patches][biebl] für ihre Forks von OpenSSH gepflegt, um einen
[Race Condition um sshd-Neustarts][schmidt] zu beheben. Die tatsächliche Lieferkette
für SSH sah also wie folgt aus:```mermaid
flowchart TD
A[OpenSSH]
B[Portable OpenSSH]
C[Debian SSH]
D[Fedora SSH]
A-->B
B-->C
B-->D
C<-->|SystemD Patches|D
Diese Patches wurden nie in Portable OpenSSH aufgenommen, da die Portable OpenSSH-Leute ["nicht daran interessiert waren, eine Abhängigkeit von libsystemd einzugehen"][djmdjm]. Und sie wurden nie in das Upstream-OpenSSH aufgenommen, da OpenBSD keine Notwendigkeit sieht, SystemD zu unterstützen.
### Bedenken zur "Trennung der Zuständigkeiten"
Das scheint harmlos genug, ist aber ein Beispiel für ein viel größeres Problem in der Open Source, insbesondere unter Linux: Kritische Komponenten des Betriebssystems werden von Menschen entwickelt, die sich nicht kennen und nicht miteinander sprechen.
* Wussten die Leute, die OpenSSH für SystemD patchten, dass libsystemd von xz-utils abhängt (oder kümmerte es sie)?
* Wussten die SystemD-Leute, dass xz-utils begonnen hatte, ifunc zu verwenden (oder kümmerte es sie)?
* Wussten die OpenSSH-Leute, dass ifunc eine Sache war (oder kümmerte es sie)? Es ist sicherlich keine Sache auf OpenBSD.
In gewisser Weise ist dieser Kommunikationsbruch ein Merkmal von Open Source: Ich kann deine Arbeit an meine Bedürfnisse anpassen, ohne dich damit belästigen zu müssen. Aber es kann auch zu einem Grad der Indirektion führen, der verhindert, dass kritische Designannahmen (wie ein traditioneller dynamischer Verknüpfungsprozess) eingehalten werden.
Die offensichtliche Folgerung aus [Conways Gesetz][conway] ist, dass du, wenn du dein Organigramm auslieferst, auch die Fehler auslieferst, die in den Ritzen deines Organigramms leben. Keine einzelne Person oder kein Team hat hier wirklich einen Fehler gemacht, aber im Nachhinein ist klar, dass die Angreifer erkannt haben, dass die linke Hand von Debian/Fedora SSH nicht wusste, was die rechte Hand von xz-Utils tat.
## Was soll GNU IFUNC *eigentlich* tun?
Es ermöglicht dir, zur Laufzeit zu bestimmen, welche Version einer Funktion du verwenden möchtest. Dies geschieht, indem dir die Möglichkeit gegeben wird, **beliebigen Code** auszuführen, um zu beeinflussen, wie der Linker Symbole auflöst.

### Erkennung von CPU-Funktionen
Angenommen, du hast eine Anwendung, die auf einer Vielzahl von x86-CPUs laufen muss. Abhängig von den spezifischen Funktionen der aktuellen CPU möchtest du möglicherweise verschiedene Algorithmen für dieselbe Aufgabe verwenden. Die ursprüngliche Idee hinter IFUNC war es, Programmen zu ermöglichen, beim ersten Aufruf einer Funktion die CPU-Funktionen zu überprüfen und danach eine Implementierung zu verwenden, die für diese CPU am besten geeignet ist.
Wirf einen Blick auf [`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;
}
Dieses Programm zeigt die häufigste Verwendung von IFUNC: Es fragt die CPU, ob sie bestimmte Funktionen unterstützt, und stellt eine andere Implementierung einer Funktion bereit, je nachdem, welche Funktionen unterstützt werden. In diesem Fall wird unsere Funktion print_cpu_info entweder „AVX2 ist vorhanden“ oder „AVX2 fehlt“ ausgeben, je nachdem, wie alt Ihre CPU ist.
Während IFUNC für das Abfragen von CPU-Funktionen gedacht ist, hindert Sie nichts daran, in Ihren Resolvern komplexeren Code auszuführen. Beispielsweise zeigt tty_demo.c, wie Sie eine andere Funktionsimplementierung laden können, je nachdem, ob STDOUT eine Datei oder ein Terminal ist:```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; }
Dies ist nicht wirklich die beabsichtigte Verwendung von IFUNC, aber es zeigt, was möglich ist: Man kann in jedem Programm, das einen von Ihnen deklarierten IFUNC verwendet, beliebigen Code vor `main` ausführen.
## IFUNC ist wahrscheinlich eine schlechte Idee

GNU IFUNC ist schwierig zu implementieren, schwer richtig zu verwenden und (als angebliches Leistungswerkzeug) nicht viel schneller als Alternativen. Wie wir bei CVE-2024-3094 gesehen haben, ist es auch ein sehr mächtiges Werkzeug für Angriffe auf die Software-Lieferkette.
IFUNC wird intensiv in der GNU-C-Bibliothek verwendet, und das ist wahrscheinlich in Ordnung. Das sind die Leute, für die es ursprünglich entwickelt wurde, und sie sind eng mit den Compiler- und Linker-Teams verbunden, die IFUNC tatsächlich implementieren. Sie sind am besten in der Lage, die Kompromisse zu verstehen, und es gibt eine Menge libc-Funktionen, die von CPU-spezifischen Implementierungen profitieren. Ich denke, wir sollten IFUNC als eine interne Schnittstelle für glibc betrachten und seine Verwendung in anderen Anwendungen vermeiden.
### Es ist zu verwirrend, um es sicher zu verwenden
ifunc ist viel zu schwer zu verwenden. Es gibt zu viele [Grenzfälle][nagy] und die [offizielle Dokumentation][gnu-cfa] ist [spärlich][sourceware]. Das gibt Benutzern die irreführende Vorstellung, dass die Einführung von ifunc unkompliziert sei.
Selbst mehrere Jahre nachdem ifunc verfügbar wurde, funktionierte die beworbene Schnittstelle [nicht][agner]. GCC-Entwickler haben es [einen Fehler][odonell] genannt und erwogen, Warnungen hinzuzufügen, um die Fragilität von IFUNC auszugleichen:
> Die glibc-Lösungen, die erforderlich sind, um IFUNC robust zu machen, sind nicht vorhanden, und deshalb sollten wir tun, was wir können, um Benutzer zu warnen, dass es möglicherweise bricht.
Es ist nicht nur IFUNC. Apple Mach-O hat eine ähnliche Funktion namens `.symbol_resolver`, die sie ["hinzugefügt zu haben bereuen"][rjmccall].
### Es untergräbt RELRO
Indem es erlaubt, dass beliebiger Code ausgeführt wird, während die Global Offset Table noch beschreibbar ist, werden die durch [RELRO](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/dynamic_linking.md#relro) gewährten Schutzmaßnahmen [hinfällig][binarly-io].
Dies ist wichtig zu beachten, da RELRO sich selbst als eine Möglichkeit anpreist, die Integrität dynamisch geladener Symbole zu schützen. Aus Benutzersicht (Sie als Benutzer des Compilers und des Linkers) verstößt dies gegen das [Prinzip der geringsten Überraschung][pola]: Kein vernünftiger Mensch würde erwarten, dass *das Laden einer dynamischen Bibliothek* eine Sicherheitsfunktion gefährdet, die *dynamische Bibliotheken schützen* soll.

### Es ist nicht immer notwendig
Es gibt mehrere andere Möglichkeiten, mit dieser Situation umzugehen. Sie haben jeweils unterschiedliche Kompromisse, sind aber alle viel einfacher als IFUNC. Alle diese sind portabler als IFUNC, leichter zu verstehen und schwieriger auszunutzen.
> "Ifunc ist einfach eine absolut dumme Art, eine laufzeitmikroarchitektur-spezifische Codeauswahl durchzuführen."
>
> -- [Rich Felker](https://hachyderm.io/@dalias/112952237145378821), Betreuer von [musl](https://musl.libc.org).
#### Globale Funktionszeiger
IFUNC ist attraktiv, weil es Entwicklern erlaubt, die Funktionsauswahl *deklarativ* statt *imperativ* auszudrücken. Aber dies imperativ zu tun ist tatsächlich nicht so schwer. Betrachten Sie [`static_pointer.c`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/static_pointer.c), das einen globalen Funktionszeiger zur Laufzeit auflöst:```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;
}
Ist das wirklich so schlimm, dass wir spezielle Tricks im Linker brauchen, nur um es zu vermeiden?
Ein Nachteil dieses Ansatzes ist, dass der Funktionszeiger triple zur Laufzeit beschreibbar ist, während IFUNC+RELRO sicherstellen würde, dass die IFUNC-Adressen im GOT nach ihrer Auflösung unveränderlich sind. Mit etwas mehr Aufwand könnten wir jedoch [mprotect(2)][mprotect] verwenden, um solche Zeiger schreibgeschützt zu machen.
LD_PRELOADWenn Sie wissen, welche CPU-Funktionen Ihr Code benötigt, und Sie eine separate Kopie Ihrer dynamischen Bibliothek für jeden Fall haben, dann könnten Sie dasselbe erreichen, indem Sie die richtige Bibliothek mit $LD_PRELOAD angeben, wie folgt:```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
(Wenn dir `LD_PRELOAD` nicht vertraut ist, wirf einen Blick auf catonmats [„A Simple `LD_PRELOAD` Tutorial“][catonmat].)
#### Separate Binaries pro Feature-Kombination
Wie viele verschiedene CPU-Feature-Kombinationen musst du wirklich unterstützen?
Wie viele gibt es überhaupt?
Auf den ersten Blick sieht das nach einer kombinatorischen Explosion aus. Es gibt
Dutzende verschiedene Vektorarithmetik-, Virtualisierungs- und Sicherheits-
erweiterungen für die amd64-ISA. Aber diese Features treten in der Praxis nicht
unabhängig voneinander auf. Beispielsweise besitzt keine CPU, die AVX-512 hat,
nicht auch SSE4.2 oder AES-NI.
Zu wissen, welche CPU-Features deine Anwendung benötigt und
welche davon auf echten Chips gemeinsam vorkommen, kann dir helfen zu bestimmen,
wie viele verschiedene Binärdateien du ausliefern müsstest. Es sind vielleicht weniger,
als du denkst. Die meisten Paketmanager erlauben es, Skripte zur Installationszeit
auszuführen; du könntest mehrere Binärdateien in einer einzigen RPM- oder DEB-Datei
ausliefern und bei der Installation die beste für die jeweilige CPU auswählen.
### Es ist nicht viel schneller als Alternativen
> Was mir schon lange klar ist: Selbst wenn es einen Performance-Vorteil von ifunc gäbe,
> könnte dieser nur dann zum Tragen kommen, wenn der gesamte Funktionsaufruf so kurz ist,
> dass der Aufruf-Overhead einen signifikanten Anteil der Gesamtzeit ausmacht.
>
> – [Rich Felker](https://hachyderm.io/@dalias/113074264762553873),
> Betreuer von [musl](https://musl.libc.org).
Da die übliche Rechtfertigung für ifunc leistungsbezogen ist, wollte ich herausfinden,
wie viel Overhead *ifunc selbst* verursacht. Schließlich wird jede optimierungswürdige
Funktion wahrscheinlich häufig aufgerufen, sodass der Overhead des Funktionsaufrufs
beachtenswert ist.
Um das herauszufinden, habe ich ein Experiment entworfen, das eine *dynamisch aufgelöste*
Funktion in einer engen Schleife immer wieder aufruft. Wirf einen Blick auf
[`speed_demo/ifunc`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/ifunc/main.c) und
[`speed_demo/pointer`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/pointer/main.c). Diese Programme
machen dieselbe Arbeit (Inkrementieren eines statischen Zählers), aber die
Inkrementierungsfunktionen werden auf unterschiedliche Weise aufgelöst: Ersteres nutzt
GNU IFUNC, letzteres verlässt sich auf einfache Funktionszeiger.
Hier ist die allgemeine Logik:
1. Eine Resolver-Funktion aufrufen, um zu bestimmen, welcher Inkrementierer verwendet wird.
1. Diese Antwort irgendwo speichern (im GOT oder als Funktionszeiger).
1. Diese Inkrementierungsfunktion einige Milliarden Male aufrufen, um eine Schätzung
ihrer Kosten zu erhalten.
Als Kontrolle gibt es auch
[`speed_demo/fixed`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/fixed/main.c), das dieselbe Inkrementierungsarbeit
leistet, aber ohne dynamisch aufgelöste Funktionen. Dies kann verwendet werden, um eine
hilfreiche Schätzung zu erhalten, welcher Teil der Laufzeit auf den Funktionsaufruf
entfällt und welcher Teil nur die Addition selbst ist.
Das Makefile-Ziel `rigorous_speed_demo` führt mehrere Läufe jedes dieser Programme durch
und erstellt einfache Statistiken über ihre Leistung. Diese Zahlen ändern sich natürlich
je nach Hardware, aber der `fixed`-Test sollte als Basis für den Vergleich dienen.
| *Ergebnisse* | NIEDRIG | HOCH | DURCHSCHNITT |
|--------------|---------|--------|--------------|
| fixed | 2.93 | 4.20 | 3.477 |
| ifunc | 9.50 | 10.56 | 9.986 |
| pointer | 6.23 | 7.44 | 6.791 |
Was wir hier sehen, ist, dass ifunc einen nicht unerheblichen Overhead im Vergleich zu
einem einfachen Funktionszeiger hat. Im Durchschnitt braucht es auf meiner Hardware
etwa doppelt so lange, um eine ifunc-Funktion 2 Milliarden Mal aufzurufen, wie um einen
Funktionszeiger 2 Milliarden Mal zu invozieren.
Spielt das im echten Leben eine Rolle? Absolut nicht. Funktionen, die optimierungswürdig
sind, sind viel teurer als die „um eins erhöhen“-Funktionen, die wir hier analysieren.
Es ist nur interessant, weil GNU IFUNC behauptet, ein Segen für die Leistung zu sein,
aber offenbar mehr Kosten verursacht als Funktionszeiger.
#### Leistung anderer Techniken
Es gibt andere Techniken, die langsamer sind als ifunc. Wirf einen Blick auf das Ziel
`super_rigorous_speed_demo`, das zwei weitere Experimente ins Spiel bringt:
[`speed_demo/upfront`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/upfront/main.c) und
[`speed_demo/always`](https://github.com/robertdfrench/ifuncd-up/blob/HEAD/code/speed_demo/always/main.c).
`speed_demo/upfront` verhält sich ähnlich wie `speed_demo/pointer`,
außer dass es die Ergebnisse der CPU-Feature-Prüfungen in globalen Variablen speichert,
anstatt einen Funktionszeiger zu verwalten. Dies erfordert dennoch, dass zuerst eine
„Resolver“-Funktion läuft, um zu bestimmen, welche Implementierung basierend auf den
Werten dieser globalen Variablen verwendet wird. Diese Technik erweist sich als
langsamer als ifunc, ist aber auch sicherer als das Speichern von Funktionszeigern:
Während Funktionszeiger auf beliebige Werte gesetzt werden können, können boolesche
Flags das nicht. Ein Angreifer, der diese Variablen ändern kann, kann das Programm also
*verlangsamen*, aber nicht das Verhalten des Programms *ändern*.
`speed_demo/always` ist darauf ausgelegt, die langsamste Technik zu sein – es prüft
jedes Mal, wenn eine Implementierung benötigt wird, alle notwendigen CPU-Features und
wählt eine spontan aus. Interessanterweise ist diese Technik nicht signifikant
langsamer als alles andere. Sie ist nur geringfügig langsamer als ifunc in dem Fall,
wo wir nur ein einziges CPU-Feature prüfen müssen.
| TEST | NIEDRIG | HOCH | DURCHSCHNITT |
|---------|---------|--------|--------------|
| 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 |
## Fazit
GNU IFUNC ist eine Nischenfunktion von gcc/ld.so, von der nur wenige wussten, bevor sie
in CVE-2024-3094 verwendet wurde. Sie hat nicht offensichtliche Fallstricke und eine
unzureichende Dokumentation. Indem sie dem Linker erlaubt, vor `main`, also bevor
kritische Teile des Prozessabbilds initialisiert und geschützt sind, beliebigen Code
auszuführen, untergräbt sie eine der grundlegendsten Annahmen der Programmierung: Dass
das bloße Laden einer Bibliothek dein Programm nicht inhärent *ändert*.
Die Leistungsvorteile von IFUNC sind real, aber nicht sinnvoll besser als Alternativen.
Die Einfachheit, eine einzelne Binärdatei auszuliefern, die für mehrere CPUs optimiert
ist, ist sehr verlockend, kann aber mit einfacheren Techniken (wie Funktionszeigern)
erreicht werden.
Ich glaube, dass IFUNC in gcc standardmäßig deaktiviert sein sollte. Das Aktivieren
sollte ein furchteinflößendes Flag erfordern, wie `--enable-cve-2024-3094`. Von jedem,
der es außerhalb von libc verwendet, sollte erwartet werden, dass er ein
strenges, gut recherchiertes Argument liefert, dass keine alternative Lösung geeignet ist.

<!-- REFERENCES -->
[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