Workshop de escalonamento de privilégios locais Looney Tunables (CVE-2023-4911) (apenas para fins educacionais)
Na computação, um vinculador dinâmico é a parte de um sistema operacional que carrega e vincula as bibliotecas compartilhadas necessárias para um executável quando ele é executado, copiando o conteúdo das bibliotecas do armazenamento persistente para a RAM, preenchendo tabelas de salto e realocando ponteiros.
Por exemplo, temos um programa que usa a biblioteca openssl para calcular o hash md5:
$ head md5_hash.c
#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>
ld.so analisa o binário e tenta encontrar a biblioteca relacionada a <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)
Como podemos ver, ele encontra a biblioteca criptográfica necessária em /lib/x86_64-linux-gnu/libcrypto.so.3 Durante a inicialização do programa, ele coloca o código desta biblioteca na RAM do processo e vincula todas as referências a esta biblioteca.
Quando um programa é iniciado, este carregador primeiro examina o programa para determinar as bibliotecas compartilhadas necessárias. Em seguida, ele procura essas bibliotecas, carrega-as na memória e vincula-as ao executável em tempo de execução. No processo, o carregador dinâmico resolve referências de símbolos, como referências de funções e variáveis, garantindo que tudo esteja configurado para a execução do programa. Dada a sua função, o carregador dinâmico é altamente sensível à segurança, pois seu código é executado com privilégios elevados quando um usuário local inicia um programa set-user-ID ou set-group-ID.
Tunables são um recurso da Biblioteca GNU C que permite que autores de aplicativos e mantenedores de distribuição alterem o comportamento da biblioteca de tempo de execução para corresponder à sua carga de trabalho. Eles são implementados como um conjunto de opções que podem ser modificadas de diferentes maneiras. O método padrão atual para fazer isso é por meio da variável de ambiente GLIBC_TUNABLES, configurando-a como uma sequência de pares nome=valor separados por dois pontos. Por exemplo, o exemplo a seguir ativa a verificação de malloc e define o limite de corte de malloc para 128 bytes:
GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
Passando --list-tunables para o carregador dinâmico para imprimir todos os ajustáveis com valores mínimos e máximos:
$ /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)
Logo no início de sua execução, ld.so chama __tunables_init() para percorrer o ambiente (na linha 279), procurando por variáveis GLIBC_TUNABLES (na linha 282); para cada GLIBC_TUNABLES que encontra, ele faz uma cópia desta variável (na linha 284), chama parse_tunables() para processar e higienizar esta cópia (na linha 286) e, finalmente, substitui o GLIBC_TUNABLES original por esta cópia higienizada (na linha 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 }
O primeiro argumento de parse_tunables() (tunestr) aponta para a cópia de GLIBC_TUNABLES que será higienizada em breve, enquanto o segundo argumento (valstring) aponta para a variável de ambiente GLIBC_TUNABLES original (na pilha). Para limpar a cópia de GLIBC_TUNABLES (que deve estar no formato "tunable1= aaa:tunable2=bbb"
), parse_tunables() remove todos os ajustáveis perigosos (os ajustáveis SXID_ERASE) do tunestr, mas mantém os ajustáveis SXID_IGNORE e NONE (nas linhas 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 }
Infelizmente, se uma variável de ambiente GLIBC_TUNABLES estiver no formato "tunable1=tunable2=AAA" (onde "tunable1" e "tunable2" são ajustáveis SXID_IGNORE, por exemplo "glibc.malloc.mxfast"), então:
durante a primeira iteração do "while (true)" em parse_tunables(), todo o "tunable1=tunable2=AAA" é copiado no local para tunestr (nas linhas 221-235), preenchendo assim tunestr;
nas linhas 247-248, p não é incrementado (p[len] é '