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)
{
void *tcbp = _dl_allocate_tls_storage ();
if (tcbp == NULL)
_dl_fatal_printf ("\
cannot allocate TLS data structures for initial thread\n");
GL(dl_initial_dtv) = GET_DTV (tcbp);
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; \
\
_head->self = _thrdescr; \
\
\
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)
{
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
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;
_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 매크로
#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는 해당 매크로의 선언부로,
tcphead_t 구조체
typedef struct
{
void *tcb;
dtv_t *dtv;
void *self;
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
void *__private_tm[4];
void *__private_ss;
} tcbhead_t;
할당된 TLS 영역은 tcphead_t 구조체로 구성되어 있는데,
stack_guard는 스택 카나리의 값을 가지는 멤버 변수이다.
따라서 THREAD_SET_STACK_GUARD는 TLS + 0x28 위치에 생성된 카나리 값을 삽입하는 매크로이다.
3.3 디버깅
#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