Looney Tunables 로컬 권한 상승(CVE-2023-4911) 워크숍(교육 목적으로만)
컴퓨팅에서 동적 링커는 라이브러리 내용을 영구 저장소에서 RAM으로 복사하고 점프 테이블을 채우고 포인터를 재배치하여 실행 파일이 실행될 때 필요한 공유 라이브러리를 로드하고 연결하는 운영 체제의 일부입니다.
예를 들어, openssl 라이브러리를 사용하여 md5 해시를 계산하는 프로그램이 있습니다.
$ head md5_hash.c
#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>
ld.so는 바이너리를 구문 분석하고 <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)
보시다시피 /lib/x86_64-linux-gnu/libcrypto.so.3 에서 필요한 암호화 라이브러리를 찾습니다. 프로그램 시작 중에 이 라이브러리의 코드를 프로세스 RAM에 넣고 이 라이브러리에 대한 모든 참조를 연결합니다.
프로그램이 시작되면 이 로더는 먼저 프로그램을 검사하여 필요한 공유 라이브러리를 결정합니다. 그런 다음 이러한 라이브러리를 검색하여 메모리에 로드하고 런타임에 실행 파일과 연결합니다. 이 과정에서 동적 로더는 함수 및 변수 참조와 같은 기호 참조를 확인하여 프로그램 실행을 위한 모든 것이 설정되었는지 확인합니다. 역할을 고려할 때 동적 로더는 로컬 사용자가 set-user-ID 또는 set-group-ID 프로그램을 시작할 때 높은 권한으로 코드가 실행되므로 보안에 매우 민감합니다.
튜너블은 애플리케이션 작성자와 배포 관리자가 워크로드에 맞게 런타임 라이브러리 동작을 변경할 수 있도록 하는 GNU C 라이브러리의 기능입니다. 이는 다양한 방식으로 수정될 수 있는 스위치 세트로 구현됩니다. 이를 수행하는 현재 기본 방법은 GLIBC_TUNABLES 환경 변수를 콜론으로 구분된 이름=값 쌍의 문자열로 설정하는 것입니다. 예를 들어, 다음 예에서는 malloc 검사를 활성화하고 malloc 트림 임계값을 128바이트로 설정합니다.
GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
--list-tunables를 동적 로더에 전달하여 최소값과 최대값으로 모든 조정 가능 항목을 인쇄합니다.
$ /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)
실행 초기에 ld.so는 __tunables_init()를 호출하여 환경(라인 279)을 살펴보고 GLIBC_TUNABLES 변수(라인 282)를 검색합니다. 찾은 각 GLIBC_TUNABLES에 대해 이 변수의 복사본을 만들고(라인 284), 이 복사본을 처리하고 정리하기 위해 parse_tunables()를 호출하고(라인 286), 마지막으로 원본 GLIBC_TUNABLES를 이 정리된 복사본으로 대체합니다(라인 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 }
parse_tunables()(tunestr)의 첫 번째 인수는 곧 삭제될 GLIBC_TUNABLES 복사본을 가리키는 반면, 두 번째 인수(valstring)는 원래 GLIBC_TUNABLES 환경 변수(스택에 있음)를 가리킵니다. GLIBC_TUNABLES("tunable1= aaa:tunable2=bbb"
형식이어야 함)의 복사본을 삭제하기 위해, parse_tunables()는 tunestr에서 모든 위험한 튜너블(SXID_ERASE 튜너블)을 제거하지만 SXID_IGNORE 및 NONE 튜너블은 유지합니다(라인 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 }
불행하게도 GLIBC_TUNABLES 환경 변수가 "tunable1=tunable2=AAA" 형식인 경우(여기서 "tunable1" 및 "tunable2"는 SXID_IGNORE 조정 가능 항목입니다(예: "glibc.malloc.mxfast")):
Parse_tunables()에서 "while(true)"의 첫 번째 반복 중에 전체 "tunable1=tunable2=AAA"가 tunestr(라인 221-235)에 복사되어 tunestr을 채웁니다.
247-248행에서 p는 증가되지 않습니다(203-204행에서 ':'가 발견되지 않았기 때문에 p[len]은 '