boot_init_stack_canary()

<kernel v5.0>

Stack Overflow Protector

Stack Overflow 감지를 위해 커널은 다음과 같이 두 가지 방법을 지원한다.

  • 스케줄 시마다 스택 종단의 매직값 체크
    • 생성되는 태스크용 커널 스택마다 끝 부분에 매직 값을 기록하여 매 스케줄 시 유실 여부를 체크한다.
  • 매 함수 호출시마다 스택 가드(canary) 설치 및 체크
    • 함수의 호출 시마다 부트업 타임에 결정된 stack canary 값을 사용하여 스택에 가드를 설치하고, 함수가 종료되면 그 때 스택 가드의 유실 여부를 체크한다.
    • 이 기능은 gcc의 도움을 받아 사용할 수 있다.

 

방법 1) 스케줄 시마다 스택 종단의 매직값 체크

STACK_END_MAGIC 값 설치

다음 그림과 같이 task를 fork할 때마다 태스크용 커널 스택이 만들어진다. 이 때 스택 overflow를 감지하기 위해 태스크용 커널 스택의 끝에 STACK_END_MAGIC 값을 설치한다.

 

set_task_stack_end_magic()

kernel/fork.c

void set_task_stack_end_magic(struct task_struct *tsk)
{
        unsigned long *stackend;

        stackend = end_of_stack(tsk);
        *stackend = STACK_END_MAGIC;    /* for overflow detection */
}

 

end_of_stack() – for ARM32

include/linux/sched/task_stack.h

/*
 * Return the address of the last usable long on the stack.
 *
 * When the stack grows down, this is just above the thread
 * info struct. Going any lower will corrupt the threadinfo.
 *
 * When the stack grows up, this is the highest address.
 * Beyond that position, we corrupt data on the next page.
 */
static inline unsigned long *end_of_stack(struct task_struct *p)
{
#ifdef CONFIG_STACK_GROWSUP
        return (unsigned long *)((unsigned long)task_thread_info(p) + THREAD_SIZE) - 1;
#else
        return (unsigned long *)(task_thread_info(p) + 1);
#endif
}

태스크용 커널 스택의 끝 위치를 반환한다. 스택 진행 방향에 따라 끝 위치가 달라지는데 ARM32와 ARM64의 경우 아래 방향으로 자라난다.

 

end_of_stack() – for ARM64

include/linux/sched/task_stack.h

#ifdef CONFIG_THREAD_INFO_IN_TASK
static inline unsigned long *end_of_stack(const struct task_struct *task)
{
        return task->stack;
}
}

태스크용 커널 스택의 끝 위치를 반환한다.

  • ARM64의 경우 태스크 구조체 내부에 thread_info 구조체를 포함시키도록 CONFIG_THREAD_INFO_IN_TASK 커널 옵션을 사용한다.

 

Stack Overflow 감지

스케줄 할때와 premption 포인트가 호출될 때마다 스택의 overflow 감지를 한다. 감지된 경우 panic 메시지를 출력한다.

 

schedule_debug()

kernel/sched/core.c

static inline void schedule_debug(struct task_struct *prev)
{
#ifdef CONFIG_SCHED_STACK_END_CHECK
        if (task_stack_end_corrupted(prev))
                panic("corrupted stack end detected inside scheduler\n");
#endif

(...생략...)

 

___might_sleep()

kernel/sched/core.c

void ___might_sleep(const char *file, int line, int preempt_offset)
{
       (...생략...)

        if (task_stack_end_corrupted(current))
                printk(KERN_EMERG "Thread overran stack, or stack corrupted\n");

       (...생략...)

 

task_stack_end_corrupted()

include/linux/sched/task_stack.h

#define task_stack_end_corrupted(task) \
                (*(end_of_stack(task)) != STACK_END_MAGIC)

태스크의 커널 스택 마지막 위치에 STACK_END_MAGIC이 유실된지 여부를 반환한다.

 


2) 매 함수 호출시마다 스택 가드(canary) 설치 및 체크

stack 가드를 사용하기 전에 먼저 가드에 사용할 canary 값을 정해야 하는데 이 값은 부트업 타임에 결정한다.

 

stack canary 초기화

gcc가 지원하는 스택 프로텍터 기능에서 사용할 canary 값을 초기화한다.

  • 함수의 진입시 stack에 canary 값을 기록하고 함수를 빠져나올 때 기록해 두었던 stack canary 값을 확인하여 stack 영역이 침범되었는지 확인할 수 있는 기능을 사용할 때 사용된다.

 

boot_init_stack_canary() – ARM32

arch/arm/include/asm/stackprotector.h

/*
 * Initialize the stackprotector canary value.
 *
 * NOTE: this must only be called from functions that never return,
 * and it must always be inlined.
 */
static __always_inline void boot_init_stack_canary(void)
{
        unsigned long canary;

        /* Try to get a semi random initial value. */
        get_random_bytes(&canary, sizeof(canary));
        canary ^= LINUX_VERSION_CODE;

        current->stack_canary = canary;
#ifndef CONFIG_STACKPROTECTOR_PER_TASK
        __stack_chk_guard = current->stack_canary;
#else
        current_thread_info()->stack_canary = current->stack_canary;
#endif
}

stack overflow 검출을  위해 gcc가 지원하는 스택 프로텍터 기능에서 사용할 canary 값을 초기화한다.

  •  __always_inline:
    • 100% inline화 할 수 있도록 컴파일러에 요청.
  • current:
    • 커널 스택의 마지막에 thread_info가 담기고 thread_info->task는 task_struct를 가리킨다. 즉 current->는 현재 task의 task_struct 구조체 정보를 가리킨다.

 

boot_init_stack_canary() – ARM64

arch/arm64/include/asm/stackprotector.h

/*
 * Initialize the stackprotector canary value.
 *
 * NOTE: this must only be called from functions that never return,
 * and it must always be inlined.
 */
static __always_inline void boot_init_stack_canary(void)
{
        unsigned long canary;

        /* Try to get a semi random initial value. */
        get_random_bytes(&canary, sizeof(canary));
        canary ^= LINUX_VERSION_CODE;
        canary &= CANARY_MASK;

        current->stack_canary = canary;
        if (!IS_ENABLED(CONFIG_STACKPROTECTOR_PER_TASK))
                __stack_chk_guard = current->stack_canary;
}

stack overflow 검출을  위해 gcc가 지원하는 스택 프로텍터 기능에서 사용할 canary 값을 초기화한다.

 

get_random_bytes()

drivers/char/random.c

void get_random_bytes(void *buf, int nbytes)
{
        static void *previous;

        warn_unseeded_randomness(&previous);
        _get_random_bytes(buf, nbytes);
}
EXPORT_SYMBOL(get_random_bytes);

@buf에 @nbytes만큼 random 값을 만들어 온다.

 

_get_random_bytes()

drivers/char/random.c

/*
 * This function is the exported kernel interface.  It returns some
 * number of good random numbers, suitable for key generation, seeding
 * TCP sequence numbers, etc.  It does not rely on the hardware random
 * number generator.  For random bytes direct from the hardware RNG
 * (when available), use get_random_bytes_arch(). In order to ensure
 * that the randomness provided by this function is okay, the function
 * wait_for_random_bytes() should be called and return 0 at least once
 * at any point prior.
 */
static void _get_random_bytes(void *buf, int nbytes)
{
        __u8 tmp[CHACHA_BLOCK_SIZE] __aligned(4);

        trace_get_random_bytes(nbytes, _RET_IP_);

        while (nbytes >= CHACHA_BLOCK_SIZE) {
                extract_crng(buf);
                buf += CHACHA_BLOCK_SIZE;
                nbytes -= CHACHA_BLOCK_SIZE;
        }

        if (nbytes > 0) {
                extract_crng(tmp);
                memcpy(buf, tmp, nbytes);
                crng_backtrack_protect(tmp, nbytes);
        } else
                crng_backtrack_protect(tmp, CHACHA_BLOCK_SIZE);
        memzero_explicit(tmp, sizeof(tmp));
}

@buf에 @nbytes만큼 random 값을 만들어 온다.

  • 랜덤 값을 얻는 방법은 아키텍처 및 시스템마다 구현이 다르지만 특별한 방법을 사용하지 않는 경우 기본적으로 drivers/char/random.c를 사용한다.

 

스택 가드의 설치 및 체크

gcc에서 -fstack-protector-all 옵션을 사용할 때에만 동작하며 각 함수에서 canary 값을 기록하고 비교하는 루틴이 자동으로 추가된다.

  • buffer를 overflow 시키는 공격 등을 통해 stack overflow를 시도 시 stack-smashing-detected 가 출력된 후 태스크가 중단된다.
    • 보안 공격을 받아 변조된채 프로그램이 계속 진행되지 않도록 막을 수 있다.

 

참고