Workshop „Looney Tunables Local Privilege Escalation“ (CVE-2023-4911) (nur für Bildungszwecke)
In der Informatik ist ein dynamischer Linker der Teil eines Betriebssystems, der die gemeinsam genutzten Bibliotheken lädt und verknüpft, die eine ausführbare Datei bei ihrer Ausführung benötigt, indem er den Inhalt der Bibliotheken vom persistenten Speicher in den RAM kopiert, Sprungtabellen füllt und Zeiger verschiebt.
Zum Beispiel haben wir ein Programm, das die OpenSSL-Bibliothek verwendet, um den MD5-Hash zu berechnen:
$ head md5_hash.c
#include
#include
#include
ld.so analysiert die Binärdatei und versucht, eine Bibliothek zu finden, die sich auf
$ ldd md5_hash
linux-vdso.so.1 (0x00007fffa530b000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f19cda00000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f19cd81e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f19ce032000)
Wie wir sehen können, findet es die erforderliche Krypto-Bibliothek unter /lib/x86_64-linux-gnu/libcrypto.so.3. Während des Programmstarts legt es den Code dieser Bibliothek in den Prozess-RAM und verknüpft alle Verweise auf diese Bibliothek.
Wenn ein Programm gestartet wird, untersucht dieser Loader zunächst das Programm, um die benötigten gemeinsam genutzten Bibliotheken zu ermitteln. Anschließend sucht es nach diesen Bibliotheken, lädt sie in den Speicher und verknüpft sie zur Laufzeit mit der ausführbaren Datei. Dabei löst der dynamische Lader Symbolreferenzen wie Funktions- und Variablenreferenzen auf und stellt so sicher, dass alles für die Programmausführung bereit ist. Aufgrund seiner Rolle ist der dynamische Lader äußerst sicherheitsempfindlich, da sein Code mit erhöhten Rechten ausgeführt wird, wenn ein lokaler Benutzer ein Set-User-ID- oder Set-Group-ID-Programm startet.
Tunables sind eine Funktion in der GNU C-Bibliothek, die es Anwendungsautoren und Distributionsbetreuern ermöglicht, das Verhalten der Laufzeitbibliothek an ihre Arbeitslast anzupassen. Diese werden als eine Reihe von Schaltern implementiert, die auf unterschiedliche Weise geändert werden können. Die aktuelle Standardmethode hierfür ist die Verwendung der Umgebungsvariablen GLIBC_TUNABLES, indem sie auf eine Zeichenfolge aus durch Doppelpunkte getrennten Name=Wert-Paaren festgelegt wird. Das folgende Beispiel aktiviert beispielsweise die Malloc-Überprüfung und legt den Malloc-Trimmschwellenwert auf 128 Byte fest:
GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
Übergeben von --list-tunables an den dynamischen Loader, um alle optimierbaren Werte mit Mindest- und Höchstwerten auszugeben:
$ /lib64/ld-linux-x86-64.so.2 --list-tunables
glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
glibc.elision.skip_lock_after_retries: 3 (min: 0, max: 2147483647)
glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.perturb: 0 (min: 0, max: 255)
glibc.cpu.x86_shared_cache_size: 0x100000 (min: 0x0, max: 0xffffffffffffffff)
glibc.pthread.rseq: 1 (min: 0, max: 1)
glibc.cpu.prefer_map_32bit_exec: 0 (min: 0, max: 1)
glibc.mem.tagging: 0 (min: 0, max: 255)
Ganz am Anfang seiner Ausführung ruft ld.so __tunables_init() auf, um durch die Umgebung zu laufen (in Zeile 279) und nach GLIBC_TUNABLES-Variablen zu suchen (in Zeile 282); Für jedes gefundene GLIBC_TUNABLES erstellt es eine Kopie dieser Variablen (in Zeile 284), ruft parse_tunables() auf, um diese Kopie zu verarbeiten und zu bereinigen (in Zeile 286) und ersetzt schließlich das ursprüngliche GLIBC_TUNABLES durch diese bereinigte Kopie (in Zeile 288). ):
// (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c)
269 void
270 __tunables_init ( char * * envp )
271 {
272 char * envname = NULL ;
273 char * envval = NULL ;
274 size_t len = 0 ;
275 char * * prev_envp = envp ;
...
279 while (( envp = get_next_env ( envp , & envname , & len , & envval ,
280 & prev_envp )) != NULL )
281 {
282 if ( tunable_is_name ( "GLIBC_TUNABLES" , envname )) // searching for GLIBC_TUNABLES variables
283 {
284 char * new_env = tunables_strdup ( envname );
285 if ( new_env != NULL )
286 parse_tunables ( new_env + len + 1 , envval ); //
287 /* Put in the updated envval. */
288 * prev_envp = new_env ;
289 continue ;
290 }
Das erste Argument von parse_tunables() (tunestr) zeigt auf die bald zu bereinigende Kopie von GLIBC_TUNABLES, während das zweite Argument (valstring) auf die ursprüngliche GLIBC_TUNABLES-Umgebungsvariable (im Stapel) zeigt. Um die Kopie von GLIBC_TUNABLES (die die Form „tunable1= aaa:tunable2=bbb"
haben sollte) zu bereinigen, entfernt parse_tunables() alle gefährlichen Tunables (die SXID_ERASE Tunables) aus tunestr, behält aber die Tunables SXID_IGNORE und NONE bei (in den Zeilen 221- 235):