[Dreamhack] Master Canary: 1 - Master Canary

securitykss·2023년 2월 27일
0

Pwnable 강의(dreamhack)

목록 보기
45/58

이 글은 https://dreamhack.io/lecture/courses/266 을 토대로 작성한 글입니다.

1. Introduction

예전 SSP(Stack Smashing Pointer)에 대해서 알아봤었다.

이는 스택 버퍼 오버플로우로부터 반환 주소를 보호하는 기법으로, 스택 버퍼가 존재하는 함수 내부에서

임의로 생성된 값을 버퍼에 삽입힌다.

이번에는 스택 카나리 값이 존재하는 TLS(Thread Local Storage)에 대해 자세하게 알아보면서, Canary에 대해 깊게 알아보자.

2. Thread Local Storage

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

ELF 바이너리를 보면, 각각의 목적을 가진 섹션에서 데이터를 관리한다.

코드를 실행하기 위한 .text(.code), 초기화되지 않은 전역 변수를 위한 .data 영역 등이 있다.

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

2.1 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;
}

2.2 SET_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;     \
  })

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

어셈블리를 통해 구현한 코드를 보면,

arch_prctl 시스템 콜의 첫 번째 인자로 ARCH_SET_FS,

두 번째 인자로 할당한 TLS 주소가 전달되는 것을 확인할 수 있다.

arch_prctl 시스템 콜의 ARCH_SET_FS는 프로세스의 FS 세그먼트 레지스터를 초기화하는 작업을 수행한다.

따라서 FS 세그먼트 레지스터는 TLS 영역을 가리키게 된다.

3. Mater Canary

스택 버퍼를 사용하는 모든 함수에서 같은 카나리 값을 사용한다.

이런 특징 때문에 임의 함수에서 메모리 릭으로 카나리를 알아낼 수 있다면,

다른 함수에서 발생하는 스택 버퍼 오버플로우에서 카나리를 덮어쓰고 실행 흐름을 조작할 수 있었따.

SSP 동작 원리를 보면, 버퍼를 사용하는 함수의 프롤로그에서 FS: 0x28예 위치하는 값을 가져와 RBP의 바로 앞에 (RBP-0x8) 삽입한다.

FS 세그먼트 레지스터는 앞서 arch_prctl 시스템 콜을 통해

_dl_allocate_tls_storage에서 할당한 주소로,

모든 함수가 해당 주소에서 값을 가져오기 때문에 같은 카나리 값을 사용하는 것이다.

이렇게 TLS 주소에 0x28 바이트 만큼 떨어진 주소에 위치한 랜덤한 값을

마스터 카나리(Mater Canary)라고 한다.

_dl_setup_stack_chk_guard 함수는 커널에서 생성한 랜덤한 값을 가지는 포인터인

_dl_random을 인자로 카나리를 생성한다.

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

3.1 카나리 값 생성

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

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

함수 코드를 보면, 공용체 변수인 ret에 커널에서 생성한 랜덤한 값을 갖는 dl_random의 데이터를 복사한다.

이후 바이너리의 바이터 오더링(Byte Oredering)에 따라 AND 연산을 수행하는데,

리틀 엔디안의 경우 복사한 값의 첫 바이트를 NULL로 변환한다.

카나리의 첫 바이트가 NULL인 이유는 위 함수를 분석하면서 알 수 있다.

3.2 카나리 값 삽입

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)

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

THREAD_SET_STACK_GUARD는 해당 매크로의 선언부로,

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

tcphead_t 구조체

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;

할당된 TLS 영역은 tcphead_t 구조체로 구성되어 있는데,

stack_guard는 스택 카나리의 값을 가지는 멤버 변수이다.

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

3.3 디버깅

// 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 주소에 있는 마스터 카나리를 확인해 볼 수 있다.

마치며

Thread Local Storage (TLS): 스레드의 저장 공간을 의미, 모든 스레드가 참조할 수 있는 전역 변수를 저장하는데에 쓰임

Master Canary: TLS 주소에 0x28 바이트 만큼 떨어진 주소에 위치한 랜덤한 값, 모든 함수에서 해당 값을 스택 카나리로 사용함

Reference

https://dreamhack.io/lecture/courses/266

profile
보안 공부를 하는 학생입니다.

0개의 댓글