stack canary 분석

1. 서론

32/64bit 환경에서 Stack Canary 기법이 걸려있는 것을 많이 볼 수 있다.

여러 pwnable 문제만 봐도 Stack Canary + Heap Exploit이 거의 대다수를 이루고 있다.

근데 여태까지 Stack Canary가 Mitigation인 것만 알고 있었고 어떤 식으로 동작을 하는지 분석을 해본적이 없었다.

뭐.. 심심하기도 해서 이렇게 분석한 것을 글로 남긴다.

2. 본론

먼저 아래와 같은 소스코드를 컴파일보자.

#include <stdio.h>
int main(){
    char buf[1024];
    puts("KuroNeko~");
    gets(buf);
    return 0;
}

[64bit 기준]

32bit 컴파일 : gcc -o canary canary.c -m32

64bit 컴파일 : gcc -o canary canary.c

이렇게 컴파일을 한 뒤, gdb를 통해 카나리를 얻고 체크하는 부분의 어셈블리는 아래의 그림과 같다.

[그림 1] 32bit 바이너리
[그림 2] 64bit 바이너리

32bit는 gs:0x14, 64bit는 QWORD PTR fs:0x28을 통해 canary를 얻어오고 함수가 끝나기 전에 xor을 통해서 검사를 진행한다.

일단, 32bit 기준으로 분석을 해보자.

linux에서 gs 레지스터는 TCB(Task Control Block)의 head 구조체를 참조하게 된다.

Link

typedef struct
{
  void *tcb;		/* Pointer to the TCB.  Not necessarily the
			   thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;		/* Pointer to the thread descriptor.  */
  int multiple_threads;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  int gscope_flag;
#ifndef __ASSUME_PRIVATE_FUTEX
  int private_futex;
#else
  int __unused1;
#endif
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[5];
} tcbhead_t;

우리가 여기서 주목해야할 것은 0x14 offset에 stack_guard인데, 이 값이 바이너리에서 사용되는 Canary값이 되게 된다. 그렇다면 stack_guard가 어떤 식으로 값이 쓰여지는지 확인하기 위해 glibc repository에서 검색을 해봤다. 아래의 링크를 참조하자

Link

#define THREAD_SET_STACK_GUARD(value) \
  THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
#define THREAD_COPY_STACK_GUARD(descr) \
  ((descr)->header.stack_guard						      \
   = THREAD_GETMEM (THREAD_SELF, header.stack_guard))

위와 같이 THREAD_SETMEM이란 Macro를 통해서 stack_guard에 값을 쓰고 있는데, THREAD_SETMEM은 아래와 같다.

Link

# define THREAD_SETMEM(descr, member, value) \
  ({ if (sizeof (descr->member) == 1)					      \
       asm volatile ("movb %b0,%%gs:%P1" :				      \
		     : "iq" (value),					      \
		       "i" (offsetof (struct pthread, member)));	      \
     else if (sizeof (descr->member) == 4)				      \
       asm volatile ("movl %0,%%gs:%P1" :				      \
		     : "ir" (value),					      \
		       "i" (offsetof (struct pthread, member)));	      \
     else								      \
       {								      \
	 if (sizeof (descr->member) != 8)				      \
	   /* There should not be any value with a size other than 1,	      \
	      4 or 8.  */						      \
	   abort ();							      \
									      \
	 asm volatile ("movl %%eax,%%gs:%P1\n\t"			      \
		       "movl %%edx,%%gs:%P2" :				      \
		       : "A" (value),					      \
			 "i" (offsetof (struct pthread, member)),	      \
			 "i" (offsetof (struct pthread, member) + 4));	      \
       }})

보다싶이, 멤버 변수의 크기에 따라서 처리를 해주고 있는 모습을 볼 수 있다.

인자로 넣어진 값들은 THREAD_SLEF, header.stack_guard, value인데, THREAD_SELF 는 pthread 구조체이고 header.stack_guard의 offset 을 구해서 value을 쓰고 있는 것을 볼 수 있다.

일단, 그럼 THREAD_SET_STACK_GUARD Macro가 어디서 사용되는지 검색을 해보면 libc_start_main함수에서 사용하는 것을 볼 수 있다.

Link

  /* Set up the stack checker's canary.  */
  uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
# ifdef THREAD_SET_STACK_GUARD
  THREAD_SET_STACK_GUARD (stack_chk_guard);
# else
  __stack_chk_guard = stack_chk_guard;
# endif

_dl_setup_stack_chk_guard함수를 통해서 얻은 값을 canary로 설정하게 되는 것을 볼 수 있으니 _dl_setup_stack_chk_guard함수를 찾아보면 아래와 같다.

Link

static inline uintptr_t __attribute__ ((always_inline))
_dl_setup_stack_chk_guard (void *dl_random)
{
  union
  {
    uintptr_t num;
    unsigned char bytes[sizeof (uintptr_t)];
  } ret = { 0 };

  if (dl_random == NULL)
    {
      ret.bytes[sizeof (ret) - 1] = 255;
      ret.bytes[sizeof (ret) - 2] = '\n';
    }
  else
    {
      memcpy (ret.bytes, dl_random, sizeof (ret));
#if BYTE_ORDER == LITTLE_ENDIAN
      ret.num &= ~(uintptr_t) 0xff;
#elif BYTE_ORDER == BIG_ENDIAN
      ret.num &= ~((uintptr_t) 0xff << (8 * (sizeof (ret) - 1)));
#else
# error "BYTE_ORDER unknown"
#endif
    }
  return ret.num;
}

인자로 받은 dl_random의 NULL 여부를 통해서 reg.bytes에 값을 작성하게 되는데, union이므로 값을 같이 사용한다.

그리고 최하위 1byte를 NULL로 만들어준다. 이 때, 인자로 받았던 _dl_random은 전역변수로 정의 되어있고 찾아보면 아래와 같은 소스코드에서 작성된 것을 볼 수 있다.

Link

extern void *_dl_random attribute_hidden attribute_relro;

_dl_random이 어디서 값이 쓰여지는지 확인을 해본 결과 아래의 소스코드에서 작성되는 것을 볼 수 있다.

Link

int _dl_clktck;

void internal_function _dl_aux_init (ElfW(auxv_t) *av)
{
  int seen = 0;
  uid_t uid = 0;
  gid_t gid = 0;

  _dl_auxv = av;
  for (; av->a_type != AT_NULL; ++av)
    switch (av->a_type)
      {
      case AT_PAGESZ:
	GLRO(dl_pagesize) = av->a_un.a_val;
	break;
      case AT_CLKTCK:
	GLRO(dl_clktck) = av->a_un.a_val;
	break;
      case AT_PHDR:
	GL(dl_phdr) = (void *) av->a_un.a_val;
	break;
      case AT_PHNUM:
	GL(dl_phnum) = av->a_un.a_val;
	break;
      case AT_HWCAP:
	GLRO(dl_hwcap) = (unsigned long int) av->a_un.a_val;
	break;
#ifdef NEED_DL_SYSINFO
      case AT_SYSINFO:
	GL(dl_sysinfo) = av->a_un.a_val;
	break;
#endif
#if defined NEED_DL_SYSINFO || defined NEED_DL_SYSINFO_DSO
      case AT_SYSINFO_EHDR:
	GL(dl_sysinfo_dso) = (void *) av->a_un.a_val;
	break;
#endif
      case AT_UID:
	uid ^= av->a_un.a_val;
	seen |= 1;
	break;
      case AT_EUID:
	uid ^= av->a_un.a_val;
	seen |= 2;
	break;
      case AT_GID:
	gid ^= av->a_un.a_val;
	seen |= 4;
	break;
      case AT_EGID:
	gid ^= av->a_un.a_val;
	seen |= 8;
	break;
      case AT_SECURE:
	seen = -1;
	__libc_enable_secure = av->a_un.a_val;
	__libc_enable_secure_decided = 1;
	break;
      case AT_RANDOM:
	_dl_random = (void *) av->a_un.a_val;
	break;
# ifdef DL_PLATFORM_AUXV
      DL_PLATFORM_AUXV
# endif
      }
  if (seen == 0xf)
    {
      __libc_enable_secure = uid != 0 || gid != 0;
      __libc_enable_secure_decided = 1;
    }
}

보다시피 auxv라고 하는 것을 사용하는 것을 볼 수 있는데, auxv는 Auxiliary Vectors의 약자로 kernel data를 user process에게 전달하는 메커니즘이다.

다른 것들도 중요하긴한데, 우리가 여기서 주목해야할 것은 AT_RANDOM type일 때 _dl_random 값을 넣어주는 것을 볼 수 있다는 점이다. 

auxv에서 얻어온 값을 _dl_random에 집어넣고 그 이후에 _dl_setup_stack_chk_guard함수에 의해서 우리가 자주 봐왔던 canary값을 만들어주게 된다.

auxv에 대해서 더 알고 싶다면 아래의 링크를 통해서 확인해보면 된다.

Link

위의 링크를 참조해보면, envp의 바로 뒤부터 auxv가 있는 것을 알 수 있고, 이 곳은 Program Loader에 의해서 작성된다.

그럼 이제, auxv의 AT_RANDOM type 에 어떤 값이 쓰여지는지 알아보기 위해 아래의 소스코드를 보자.

Link

...
  /*
   * Generate 16 random bytes for userspace PRNG seeding.
   */
  get_random_bytes(k_rand_bytes, sizeof(k_rand_bytes));
  u_rand_bytes = (elf_addr_t __user *)
           STACK_ALLOC(p, sizeof(k_rand_bytes));
  if (__copy_to_user(u_rand_bytes, k_rand_bytes, sizeof(k_rand_bytes)))
    return -EFAULT;

  /* Create the ELF interpreter info */
  elf_info = (elf_addr_t *)current->mm->saved_auxv;
  /* update AT_VECTOR_SIZE_BASE if the number of NEW_AUX_ENT() changes */
#define NEW_AUX_ENT(id, val) \
  do { \
    elf_info[ei_index++] = id; \
    elf_info[ei_index++] = val; \
  } while (0)

#ifdef ARCH_DLINFO
  /* 
   * ARCH_DLINFO must come first so PPC can do its special alignment of
   * AUXV.
   * update AT_VECTOR_SIZE_ARCH if the number of NEW_AUX_ENT() in
   * ARCH_DLINFO changes
   */
  ARCH_DLINFO;
#endif
  NEW_AUX_ENT(AT_HWCAP, ELF_HWCAP);
  NEW_AUX_ENT(AT_PAGESZ, ELF_EXEC_PAGESIZE);
  NEW_AUX_ENT(AT_CLKTCK, CLOCKS_PER_SEC);
  NEW_AUX_ENT(AT_PHDR, load_addr + exec->e_phoff);
  NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr));
  NEW_AUX_ENT(AT_PHNUM, exec->e_phnum);
  NEW_AUX_ENT(AT_BASE, interp_load_addr);
  NEW_AUX_ENT(AT_FLAGS, 0);
  NEW_AUX_ENT(AT_ENTRY, exec->e_entry);
  NEW_AUX_ENT(AT_UID, from_kuid_munged(cred->user_ns, cred->uid));
  NEW_AUX_ENT(AT_EUID, from_kuid_munged(cred->user_ns, cred->euid));
  NEW_AUX_ENT(AT_GID, from_kgid_munged(cred->user_ns, cred->gid));
  NEW_AUX_ENT(AT_EGID, from_kgid_munged(cred->user_ns, cred->egid));
  NEW_AUX_ENT(AT_SECURE, bprm->secureexec);
  NEW_AUX_ENT(AT_RANDOM, (elf_addr_t)(unsigned long)u_rand_bytes);
...

주석이 설명해주는 것처럼 커널에서 생성된 16 byte random bytes들을 생성 후, 유저영역으로 값을 복사하고 vector에 추가하는 것을 볼 수 있다.

하지만 전부 canary로 사용하는 것이 아닌 하위 4byte만 canary값으로 사용하는 것을 볼 수 있다.

위의 과정들을 종합해보면 결과적으로는 아래와 같이 동작하게 된다.

1. Program Loader에 의해서 kernel에서 생성된 u_rand_bytes를 user 영역에 복사 후, auxv에 설정해준다.

2. ld.so에서 AT_* 관련된 값들을 읽어들이면서 해당하는 값들을 셋팅한다.

3. 1번에서 설정된 u_rand_bytes(16 bytes)의 하위 4byte를 canary로 사용(32bit 기준)하는데 하위 1byte를 NULL로 만든 것을 사용한다.

  -> gs:0x14 (stack_guard)에 값을 설정

커널에서 랜덤값을 읽는 것과 ld.so에서 AT_* 관련 값들을 설정해주는 것을 제외하고 위의 과정이 맞는지 알아보기 위해 아래와 같은 코드를 작성했다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/unistd.h>
#include <asm/ldt.h>
#include <pthread.h>

#define GET_THREAD_AREA 244

void *test(){
        struct user_desc uinfo;
        pthread_t th;
        int status;

        uinfo.entry_number = 12;
        syscall(244, &uinfo);

        printf("baseaddr : %p\n", uinfo.base_addr);
        printf("Canary : %p\n", *(unsigned int *)(uinfo.base_addr + 0x14));

        while(1);

        return NULL;
}

int main(){
        struct user_desc uinfo;
        pthread_t th;
        int status;

        uinfo.entry_number = 12;
        syscall(244, &uinfo);

        printf("baseaddr : %p\n", uinfo.base_addr);
        printf("Canary : %p\n", *(unsigned int *)(uinfo.base_addr + 0x14));

        if(pthread_create(&th, NULL, test, NULL) < 0){
                puts("wtf...");
                return -1;
        }

        pthread_join(th, (void **)&status);

        return 0;
}

위의 소스코드를 간단하게 요약하자면, 프로그램은 시작되면 ld.so에서 set_thread_area syscall을 통해, 현재 thread의 속성(?)을 설정해주게 된다. 설정되었던 속성들을 얻어오기 위해서 get_thread_area syscall을 사용해야하는데 이 때, entry_number를 통해서 여러 정보들 중 하나(현재 스레드)를 골라 얻어오게 되는 원리다.
여기서 얻어온 user_desc구조체의 base_addr 변수의 값은 TCB(Thread Control Block)의 주소이며, gs 레지스터가 가지고 있는 주소와 동일하다. 즉, base_addr + 0x14는 gs:0x14와 동일한 결과를 가져온다는 것을 알 수 있으므로, 그 값을 읽어보기 위해 실행하면 아래와 같은 결과가 나온다.

[그림 3] get_thread_area를 통한 카나리 얻기

심심하니 재미로 canary가 있는 주소를 알아보도록 하자.

[그림 4] canary 값 검색

일단 위와 같이 0xf7de3714 주소에 카나리가 들어있는 것을 볼 수 있다. 그럼 이제 base_addr를 출력해주는 곳으로 이동해서 값과 어디에 위치하고 있는지 확인해보자.

[그림 5] base_addr (eax) 확인

보다시피 ubuntu 16.04 기준 ld-2.23.so에서는 libc 바로 이전 영역에 base_addr이 가리키고 있는 것을 볼 수 있다. 그리고 현재 eax 레지스터가 가지고 있는 값은 TCB(Thread Control Block)이므로 eax + 0x14를 하게 되면 위의 그림에서 봤던 카나리 값이 나오게 될 것이다.

[그림 6] canary값 확인

3. 결론

일단 32bit에서 어떻게 Canary가 작성되는지 코드 분석을 통해서 어떻게 카나리가 로드되는지 확인해봤는데,

언젠가 또 심심하면 커널에서 random value를 어떻게 생성하는 지 분석할 것 같다. 그래서 언제 다음 글이 올라올지 모르겠다.

* 틀린게 있다면 댓글로 알려주세요

관련 글

답글 남기기

이메일 주소는 공개되지 않습니다.