본문 바로가기
Security/System Hacking

[Dreamhack System Hacking Advanced] STAGE 3

by 단월໒꒱ 2022. 9. 18.

 

Background: Master Canary

 

  스택 카나리에 대해서 깊게 파해쳐보자. 이해가 잘 안되면 기초과정의 Stack Canary 다시 보기..!!

 

 

 

Thread Local Storage

 

 Thread Local Storage

 

  Thread Local Storage (TLS)는 스레드의 저장 공간을 의미한다.

  ELF 바이너리를 살펴보면, 각각의 목적을 가진 섹션에서 데이터를 관리하고 있다. ex) .text, .data 등... 

  이와 달리 TLS 영역은 스레드의 전역 변수를 저장하기 위한 공간으로, 로더(Loader)에 의해서 할당된다.

 

  다음은 로더에서 TLS 영역을 할당하고 초기화하는 함수인 init_tls의 코드이다.

 

 

static void *
init_tls (void)
{
  /* Construct the static TLS block and the dtv for the initial
     thread.  For some platforms this will include allocating memory
     for the thread descriptor.  The memory for the TLS block will
     never be freed.  It should be allocated accordingly.  The dtv
     array can be changed if dynamic loading requires it.  */
  void *tcbp = _dl_allocate_tls_storage ();
  if (tcbp == NULL)
    _dl_fatal_printf ("\
cannot allocate TLS data structures for initial thread\n");
  /* Store for detection of the special case by __tls_get_addr
     so it knows not to pass this dtv to the normal realloc.  */
  GL(dl_initial_dtv) = GET_DTV (tcbp);
  /* And finally install it for the main thread.  */
  const char *lossage = TLS_INIT_TP (tcbp);
  if (__glibc_unlikely (lossage != NULL))
    _dl_fatal_printf ("cannot set up thread-local storage: %s\n", lossage);
  tls_init_tp_called = true;
  return tcbp;
}

 

 

  코드를 살펴보면, _dl_allocate_tls_storage 함수에서 TLS 영역을 할당하고 이를 tcbp에 저장한 뒤 TLS_INIT_TP 매크로의 인자로 전달하고 있다.

 

 

 SET_FS

 

  다음은 dl_allocate_tls_storage에서 할당한 TLS 영역을 FS로 초기화 하는 TLS_INIT_TP 매크로이다.

 

 

# define TLS_INIT_TP(thrdescr) \
  ({ void *_thrdescr = (thrdescr);                                              \
     tcbhead_t *_head = _thrdescr;                                              \
     int _result;                                                              \
                                                                              \
     _head->tcb = _thrdescr;                                                      \
     /* For now the thread descriptor is at the same address.  */              \
     _head->self = _thrdescr;                                                      \
                                                                              \
     /* It is a simple syscall to set the %fs value for the thread.  */              \
     asm volatile ("syscall"                                                      \
                   : "=a" (_result)                                              \
                   : "0" ((unsigned long int) __NR_arch_prctl),                      \
                     "D" ((unsigned long int) ARCH_SET_FS),                      \
                     "S" (_thrdescr)                                              \
                   : "memory", "cc", "r11", "cx");                              \
                                                                              \
    _result ? "cannot set %fs base address for thread-local storage" : 0;     \
  })

 

 

  어셈블리를 통해 구현한 코드를 살펴보면, arch_prctl 시스템 콜의 첫 번째 인자로 ARCH_SET_FS, 두 번째 인자로 할당한 TLS 주소가 전달되는 것을 확인할 수 있다.

  arch_prctl 시스템 콜의 ARCH_SET_FS는 프로세스의 FS 세그먼트 레지스터를 초기화하는 작업을 수행하는 명령어이다. 따라서 FS 세그먼트 레지스터는 TLS 영역을 가리키게 된다.

 

 

 

Master Canary

 

 Master Canary

 

  스택 버퍼를 사용하는 모든 함수에서 같은 카나리 값을 사용한다. 이 때문에 임의 함수에서 메모리 릭으로 카나리를 알아낼 수 있다면 다른 함수에서 발생하는 스택 버퍼 오버플로우에서 카나리를 덮어쓰고 실행 흐름을 조작할 수 있었다.

 

  FS 세그먼트 레지스터는 앞서 arch_prctl 시스템 콜을 통해 _dl_allocate_tls_storage에서 할당한 주소로, 모든 함수가 해당 주소에서 값을 가져오기 때문에 같은 카나리 값을 사용하는 것이다. 이렇게 TLS 주소에 0x28 바이트 만큼 떨어진 주소에 위치한 랜덤한 값을 마스터 카나리 (Master Canary)라고 한다.

 

  다음은 이전에 할당한 TLS 영역에 랜덤한 카나리 값을 삽입하는 security_init 함수이다.

 

 

static void
security_init (void)
{
  /* 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
  /* Set up the pointer guard as well, if necessary.  */
  uintptr_t pointer_chk_guard
    = _dl_setup_pointer_guard (_dl_random, stack_chk_guard);
#ifdef THREAD_SET_POINTER_GUARD
  THREAD_SET_POINTER_GUARD (pointer_chk_guard);
#endif
  __pointer_chk_guard_local = pointer_chk_guard;
  /* We do not need the _dl_random value anymore.  The less
     information we leave behind, the better, so clear the
     variable.  */
  _dl_random = NULL;
}

 

 

  여기에서 _dl_setup_stack_chk_guard 함수는 커널에서 생성한 랜덤한 값을 가지는 포인터인 _dl_random을 인자로 카나리를 생성한다.

 

 

 카나리 값 생성

 

  다음은 security_init 함수에서 처음으로 호출하는 _dl_setup_stack_chk_guard 함수이다.

 

 

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)));

 

  

  함수의 코드를 살펴보면, 공용체 변수인 ret에 커널에서 생성한 랜덤한 값을 갖는 dl_random의 데이터를 복사한다. 이후 바이너리의 바이트 오더링에 따라 AND 연산을 수행하는데, 리틀 엔디언의 경우 복사한 값의 첫 바이트를 NULL로 변환합니다.

 

 

 카나리 값 삽입

 

  _dl_setup_stack_chk_guard에서 카나리 값을 생성했다면, 해당 값을 THREAD_SET_STACk_GUARD 매크로의 인자로 전달해 호출한다.

 

다음은 해당 매크로의 선언부이다.

 

 

/* Set the stack guard field in TCB head.  */
#define THREAD_SET_STACK_GUARD(value) \
  THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

 

 

  THREAD_SETMEM 매크로를 통해 두 번째 인자인 header.stack_guard 위치에 value를 삽입한다.

 

  할당된 TLS 영역은 아래와 같은 tcbhead_t 구조체로 구성되어 있는데, stack_guard는 스택 카나리의 값을 가지는 멤버 변수이다.

 

 

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 __glibc_reserved1;
#endif
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[4];
  /* GCC split stack support.  */
  void *__private_ss;
} tcbhead_t;

 

 

  따라서 THREAD_SET_STACK_GUARD는 TLS + 0x28 위치에 생성된 카나리 값을 삽입하는 매크로이다.

 

 

 디버깅

 

  다음 코드를 컴파일 하고 디버깅을 통해 마스터 카나리를 찾아보자.

 

// Name: master_canary.c
// Compile: gcc -o master_canary master_canary.c -no-pie
#include <stdio.h>
#include <unistd.h>
int main()
{
	char buf[256];
	read(0, buf, 256);
}

 

 

 

 

  gdb에서는 FS 세그먼트 레지스터의 주소를 확인하는 명령어가 있는데, 이를 통해 TLS 주소에 있는 마스터 카나리를 확인해볼 수 있다.

 

 

 

댓글