EYEN

canary가 할당되는 경로 확실히 알기 본문

Wargame/pwn

canary가 할당되는 경로 확실히 알기

EYEN 2023. 2. 25. 17:00

일단 카나리 관련 토픽을 찾는 게 힘들더라구요.
그래서 드림핵 내용인 canary 생성과정에 대해 정리해봤습니다. 
 
예제입니다

#include <unistd.h>
int main() {
  char buf[8];
  read(0, buf, 32);
  return 0;
}

 
문자열 버퍼를 8만큼 할당해주고, 32만큼 입력받아서 buf에 저장하는 것을 볼 수 있습니다. 
 

두 부분이 카나리에 관련된 부분입니다.
 

main+8에서 fs:0x28에서 8바이트를 가져와서 rax에 넣고,
main+17에서 rax의 값을 rbp-0x8에 8바이트만큼 넣는 것을 볼 수 있습니다. 그리고 이때 rax의 값은 첫바이트가 널바이트인 8바이트 데이터임을 알 수 있습니다.
 
나중에 main+50에서 rbp-0x8에 저장된 카나리를 rcx로 복사하고,
main+54에서 rcx와 fs:0x28의 값을 xor하여 같지 않으면 stack_chk_fail을 호출합니다.
 
이제 진짜 중요한 카나리 생성과정을 보실 텐데요.
카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장되고, 각 함수마다 프롤로그와 에필로그에서 이 값을 참조한다.
TLS에 카나리값이 저장되는 과정을 보겠다.
 
fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알 수 있다. 
그러나 리눅스에서 fs의 값은 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있다. gdb에서 다른 레지스터의 값을 출력하듯 info register fs나 print $fs와 같은 방식으로는 값을 알 수 없다.
 
그래서 여기서는 fs의 값을 설정할 때 호출되는 arch_prctl(int code,unsigned long addr) 시스템 콜에 중단점을 설정하여 fs가 어떤 값으로 설정되는지 조사하겠다. 
 
이 시스템 콜을 arch_prctl(ARCH_SET_FS,addr)의 형태로 호출하면 fs의 값은 addr로 설정된다.
gdb에는 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 catch라는 명령어가 있다. arch_prctl에 catchpoint를 설정하고 실습에 사용했던 canary를 실행하겠다. 
 
catchpoint에 도달했을 때 rdi값이 0x1002인데 이 값은 ARCH_SET_FS의 상숫값이다. rsi의 값이 0x7fffff7fdb4c0이므로, 이프로세스는 TLS를 0x7ffff7fd4c0에 저장할 것이며, fs는 이를 가리키게 될 것이다.
 

 
카나리가 저장될 fs+0x28(0x7ffff7fdb4c0+0x28)의 값을 보면, 아직 어떠한 값도 설정되어 있지 않은 것을 확인할 수 있다.

TLS의 주소를 알았으므로, gdb의 watch명령어로 TLS+0x28에 값을 쓸 때 프로세스를 중단시켜보면
watch는 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어이다.
watch *(0x7ffff7fdb4c0+0x28)

이때 rax 값이

TLS+0x28의 값을 조회하면
 

main에서 mov rax, QWORD PTR fs:0x28를 실행하고 rax값을 확인해보면 security_init 에서 설정한 값과 같은 것을 확인할 수 있다.
 
 
TLS란
thread local storage로, 스레드의 저장공간을 의미한다. ELF 바이너리에서 섹션을 나눠서 데이터를 관리한다. 
이와 달리 TLS영역은 스레드의 전체의 전역 변수를 저장하기 위한 공간으로, 로더에 의해 할당된다.
 
로더에서 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 매크로의 인자로 전달한다.
 
dl_allocate_tls_storage에서 할당한 TLS영역을 FS로 초기화하는 TLS_INIT_TP 매크로
어셈블리를 통해 구현한 코드를 살펴보면, arch_prctl 시스템 콜의 첫 번째 인자로 ARCH_SET_FS, 두번째 인자로 할당한 TLS 주소가 전달되는 것을 확인할 수 있다. arch_prctl 시스템 콜의 ARCH_SET_FS는 프로세스의 FS세그먼트 레지스터를 초기화하는 작업을 수행하는 명령어이다. 따라서 FS세그먼트 레지스터는 두번째 인자에 있는 TLS주소에 따라 TLS 영역을 가리키게 된다.

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

 
master canary
스택 버퍼를 사용하는 모든 함수에서 같은 카나리 값을 사용하기 때문에 카나리를 한번 알아낸다면 다른 함수에서도 쓸 ㅅ ㅜ있다. 
 
이 security_init 함수는 TLS영역에 랜덤한 카나리 값을 삽입하는 함수이다.
_dl_setup_stack_chk_guard 함수는 커널에서 생성한 랜덤한 값을 가지는 포인터인 _dl_random을 인자로 카나리를 생성한다.

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을 보자.
이 함수는 공용체 변수인 ret에 커널에서 생성한 랜덤한 값을 갖는 dl_random의 데이터를 복사한다. 이후 바이너리의 바이트 오더링에 따라 AND 연산을 수행하여 리틀 엔디언의 경우 복사한 값의 첫 바이트를 null로 변환한다. 
 

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

uintptr: 포인터의 주소를 저장하는 것(부호 없는 버전)
 
이렇게 카나리가 생성되면, 이 값을 THREAD_SET_STACK_GUARD 매크로의 인자로 전달해 호출한다. 

#define THREAD_SET_STACK_GUARD(value) \
  THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

이것은 매크로 선언부로, THREAD_SETMEM 매크로를 통해 두 번째 인자인 header.stack_guard 위치에 value를 삽입한다.
할당된 TLS영역은 tcphead_t 구조체로 구성되어 있는데, stack_guard는 스택 카나리의 값을 가지는 멤버변수이다. 따라서 THREAD_SET_STACK_GUARD는 TLS+0x28 위치에 생성된 카나리 값을 삽입하는 매크로이다. 

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;

'Wargame > pwn' 카테고리의 다른 글

시스템 보안 11주차  (0) 2024.11.12
겨울방학 ctf 프로젝트 basic_rop_x64 발표자료  (0) 2023.02.26
Dreamhack-basic_rop_x64  (0) 2023.02.17
Dreamhack-stack canary/ r2s  (0) 2023.01.29
Dreamhack-basic_exploitation_000  (0) 2023.01.29