[하루한줄] CVE-2025-4802 : GLIBC의 정적 setuid 바이너리에서 발생하는 임의 라이브러리 경로 취약점

URL

Target

  • 2.27부터 2.38까지의 GNU C Library를 사용하는 환경

Explain

background

일반적으로 리눅스에서 setuid/setgid 권한이 적용된 바이너리를 실행하면 커널은 execve() 내부에서 secure execution이라는 특수 모드를 활성화합니다. 이 과정에서 커널은 bprm->secureexec = 1을 설정[1]하고 ELF 보조 벡터에 AT_SECURE = 1 값을 삽입[2]합니다.

linux-6.17.9/security/commoncap.c

int cap_bprm_creds_from_file(struct linux_binprm *bprm, const struct file *file)
...
	/* Check for privilege-elevated exec. */
	if (id_changed ||
	    !uid_eq(new->euid, old->uid) ||
	    !gid_eq(new->egid, old->gid) ||
	    (!__is_real(root_uid, new) &&
	     (effective ||
	      __cap_grew(permitted, ambient, new))))
		bprm->secureexec = 1;                                                                                         // [1]
...

linux-6.17.9/fs/binfmt_elf.c

static int
create_elf_tables(struct linux_binprm *bprm, const struct elfhdr *exec,
		unsigned long interp_load_addr,
		unsigned long e_entry, unsigned long phdr_addr)
...
	NEW_AUX_ENT(AT_SECURE, bprm->secureexec);                                                                       // [2]
...

AT_SECURE1이면 glibc는 secure mode로 전환하여 내부적으로 __libc_enable_secure = 1을 설정[3]하고 LD_LIBRARY_PATH, LD_PRELOAD, LD_AUDIT과 같은 위험한 환경 변수를 무시[4]하게 됩니다.

glibc-2.35/elf/dl-support.c

void
_dl_aux_init (ElfW(auxv_t) *av)
...
      case AT_SECURE:
	seen = -1;
	__libc_enable_secure = av->a_un.a_val;                                                                          // [3]
	__libc_enable_secure_decided = 1;
	break;
...

glibc-2.35/elf/rtld.c

static void
dl_main (const ElfW(Phdr) *phdr,
	 ElfW(Word) phnum,
	 ElfW(Addr) *user_entry,
	 ElfW(auxv_t) *auxv)
...
  dl_main_state_init (&state);
...

static void
dl_main_state_init (struct dl_main_state *state)
{
  audit_list_init (&state->audit_list);
  state->library_path = NULL;
  state->library_path_source = NULL;
...

static void
process_envvars (struct dl_main_state *state)
...
	case 12:
	  /* The library search path.  */
	  if (!__libc_enable_secure                                                                                     // [4]
	      && memcmp (envline, "LIBRARY_PATH", 12) == 0)
	    {
	      state->library_path = &envline[13];
	      state->library_path_source = "LD_LIBRARY_PATH";
	      break;
	    }
...

해당 필터링은 프로세스의 초기화 루틴에서 수행되며 이로 인해 일반 사용자가 setuid 바이너리에 임의의 라이브러리 경로를 전달함으로써 root 권한을 획득하는 경로가 차단됩니다.

root cause

해당 취약점의 경우 glibc의 __libc_enable_secure 값을 이용한 환경 변수 필터링 로직이 동적 로더의 초기화 함수인 dl_main() 에서만 적용되어 있었기 때문에 발생하였습니다.

정적 링크된 바이너리는 동적 로더를 사용하지 않기 때문에 _dl_non_dynamic_init() 함수를 통해 라이브러리 경로를 설정[5]하는데 해당 함수에서는 __libc_enable_secure 값에 따른 환경 변수 필터링이 미흡했습니다.

glibc-2.35/elf/dl-support.c

void
_dl_non_dynamic_init (void)
{
...
  /* Initialize the data structures for the search paths for shared
     objects.  */
  _dl_init_paths (getenv ("LD_LIBRARY_PATH"), "LD_LIBRARY_PATH",                                                  // [5]
		  /* No glibc-hwcaps selection support in statically
		     linked binaries.  */
		  NULL, NULL);
...

이로 인해 커널이 secure execution 모드를 활성화하더라도 dlopen()등의 함수 호출에서 환경 변수 기반의 경로를 그대로 사용하게 되어 공격자가 LD_LIBRARY_PATH 환경 변수에 제어 가능한 디렉터리를 설정하면 setuid 바이너리가 공격자의 라이브러리를 로드할 수 있게 됩니다.

patch

패치 커밋 5451fa962cd0a90a0e2ec1d8910a559ace02bba0에서 __libc_enable_secure 값을 통해 환경 변수를 필터링[6]한 후 라이브러리 경로를 로드[7]하도록 변경되었습니다.

glibc-2.39/elf/dl-support.c

void
_dl_non_dynamic_init (void)
{
  _dl_main_map.l_origin = _dl_get_origin ();
  _dl_main_map.l_phdr = GL(dl_phdr);
  _dl_main_map.l_phnum = GL(dl_phnum);

  /* Set up the data structures for the system-supplied DSO early,
     so they can influence _dl_init_paths.  */
  setup_vdso (NULL, NULL);

  /* With vDSO setup we can initialize the function pointers.  */
  setup_vdso_pointers ();

  if (__libc_enable_secure)                                                                                       // [6]
    {
      static const char unsecure_envvars[] =
	UNSECURE_ENVVARS
	;
      const char *cp = unsecure_envvars;

      while (cp < unsecure_envvars + sizeof (unsecure_envvars))
	{
	  __unsetenv (cp);
	  cp = strchr (cp, '\0') + 1;
	}
    }
...
  /* Initialize the data structures for the search paths for shared
     objects.  */
  _dl_init_paths (getenv ("LD_LIBRARY_PATH"), "LD_LIBRARY_PATH",                                                  // [7]
		  /* No glibc-hwcaps selection support in statically
		     linked binaries.  */
		  NULL, NULL);
...

Reference



본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.