Atelier Looney Tunables sur l'élévation des privilèges locaux (CVE-2023-4911) (à des fins éducatives uniquement)
En informatique, un éditeur de liens dynamique est la partie d'un système d'exploitation qui charge et relie les bibliothèques partagées nécessaires à un exécutable lors de son exécution, en copiant le contenu des bibliothèques du stockage persistant vers la RAM, en remplissant les tables de sauts et en déplaçant les pointeurs.
Par exemple, nous avons un programme qui utilise la bibliothèque openssl pour calculer le hachage md5 :
$ head md5_hash.c
#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>
ld.so analyse le binaire et essaie de trouver la bibliothèque liée à <openssl/md5.h>
$ 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)
Comme nous pouvons le voir, il trouve la bibliothèque de chiffrement nécessaire dans /lib/x86_64-linux-gnu/libcrypto.so.3 Lors du démarrage du programme, il place le code de cette bibliothèque dans la RAM du processus et lie toutes les références à cette bibliothèque.
Lorsqu'un programme est lancé, ce chargeur examine d'abord le programme pour déterminer les bibliothèques partagées dont il a besoin. Il recherche ensuite ces bibliothèques, les charge en mémoire et les relie à l'exécutable au moment de l'exécution. Au cours du processus, le chargeur dynamique résout les références de symboles, telles que les références de fonctions et de variables, garantissant que tout est défini pour l'exécution du programme. Compte tenu de son rôle, le chargeur dynamique est très sensible à la sécurité, car son code s'exécute avec des privilèges élevés lorsqu'un utilisateur local lance un programme set-user-ID ou set-group-ID.
Les paramètres réglables sont une fonctionnalité de la bibliothèque GNU C qui permet aux auteurs d'applications et aux responsables de distribution de modifier le comportement de la bibliothèque d'exécution pour l'adapter à leur charge de travail. Ceux-ci sont implémentés sous la forme d’un ensemble de commutateurs pouvant être modifiés de différentes manières. La méthode par défaut actuelle pour ce faire consiste à utiliser la variable d'environnement GLIBC_TUNABLES en la définissant sur une chaîne de paires nom=valeur séparées par des deux-points. Par exemple, l'exemple suivant active la vérification malloc et définit le seuil de suppression de malloc sur 128 octets :
GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
Passer --list-tunables au chargeur dynamique pour imprimer tous les paramètres avec des valeurs minimales et maximales :
$ /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)
Au tout début de son exécution, ld.so appelle __tunables_init() pour parcourir l'environnement (à la ligne 279), en recherchant les variables GLIBC_TUNABLES (à la ligne 282) ; pour chaque GLIBC_TUNABLES qu'il trouve, il fait une copie de cette variable (à la ligne 284), appelle parse_tunables() pour traiter et nettoyer cette copie (à la ligne 286), et enfin remplace le GLIBC_TUNABLES original par cette copie nettoyée (à la ligne 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 }
Le premier argument de parse_tunables() (tunestr) pointe vers la copie qui sera bientôt nettoyée de GLIBC_TUNABLES, tandis que le deuxième argument (valstring) pointe vers la variable d'environnement GLIBC_TUNABLES d'origine (dans la pile). Pour nettoyer la copie de GLIBC_TUNABLES (qui devrait être de la forme "tunable1= aaa:tunable2=bbb"
), parse_tunables() supprime tous les paramètres dangereux (les paramètres SXID_ERASE) de tunestr, mais conserve les paramètres SXID_IGNORE et NONE (aux lignes 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 }
Malheureusement, si une variable d'environnement GLIBC_TUNABLES est de la forme "tunable1=tunable2=AAA" (où "tunable1" et "tunable2" sont des réglages SXID_IGNORE, par exemple "glibc.malloc.mxfast"), alors :
lors de la première itération de "while (true)" dans parse_tunables(), l'intégralité de "tunable1=tunable2=AAA" est copiée sur place dans tunestr (aux lignes 221-235), remplissant ainsi tunestr ;
aux lignes 247-248, p n'est pas incrémenté (p[len] est '