how ASLR work - stack

해당 글 부터 읽어오면 된다 ( load_elf_binary 까지의 과정이 정리되어 있음 )

load_elf_binary

C
static int load_elf_binary(struct linux_binprm *bprm)
{
	struct file *interpreter = NULL; /* to shut gcc up */
	unsigned long load_bias = 0, phdr_addr = 0;
	int first_pt_load = 1;
	unsigned long error;
	
	
	...( 중략 )

실제로 ELF 바이너리를 메모리에 적재시키는 메인 코드이다. 당연히 어마어마한 길이를 자랑한다. 우리는 그중 ASLR 부분만 보자면,

C
	retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
				 executable_stack);

이 부분이다.

여기서 보면 randomize_stack_top(STACK_TOP) 다음과 같은 코드가 보이는데 일단 STACK_TOP 은

C
#define STACK_TOP		TASK_SIZE_LOW

→

C
#define TASK_SIZE_LOW		(test_thread_flag(TIF_ADDR32) ? \
					IA32_PAGE_OFFSET : DEFAULT_MAP_WINDOW)

→

C
#define DEFAULT_MAP_WINDOW	((1UL << 47) - PAGE_SIZE)

즉, STACK_TOP 은 x86-64에서 ((1UL << 47) - PAGE_SIZE) 다음과 같은데

PAGE_SIZE 는

C
#define PAGE_SHIFT		12
#define PAGE_SIZE		(_AC(1,UL) << PAGE_SHIFT)

즉, 1UL << 12 와 같다.

그럼 ((1UL << 47) - 1UL << 12 ) 이걸 직접 계산해보면,

0x7FFFFFFFF000 이다. 상당히 익숙한 숫자가 나온다.

따라서, 다시 돌아가서

randomize_stack_top(STACK_TOP) 는

randomize_stack_top(0x7FFFFFFFF000) 가 되고

randomize_stack_top 함수는

C

unsigned long randomize_stack_top(unsigned long stack_top)
{
	unsigned long random_variable = 0;

	if (current->flags & PF_RANDOMIZE) {
		random_variable = get_random_long();
		random_variable &= STACK_RND_MASK;
		random_variable <<= PAGE_SHIFT;
	}
#ifdef CONFIG_STACK_GROWSUP
	return PAGE_ALIGN(stack_top) + random_variable;
#else
	return PAGE_ALIGN(stack_top) - random_variable;
#endif
}

다음과 같다.

여기서 STACK_RND_MASK 가

C
#ifdef CONFIG_X86_32

#define __STACK_RND_MASK(is32bit) (0x7ff)
#define STACK_RND_MASK (0x7ff)

#define ARCH_DLINFO		ARCH_DLINFO_IA32

/* update AT_VECTOR_SIZE_ARCH if the number of NEW_AUX_ENT entries changes */

#else /* CONFIG_X86_32 */

/* 1GB for 64bit, 8MB for 32bit */
#define __STACK_RND_MASK(is32bit) ((is32bit) ? 0x7ff : 0x3fffff)
#define STACK_RND_MASK __STACK_RND_MASK(mmap_is_ia32())

다음과 같이 정리되어 있다. 따라서 64비트 일때는 0x3fffff 이 된다.

PAGE_SHIFT 는

C
#define PAGE_SHIFT		12

12 의 값을 가진다.

따라서 랜덤값을 불러오고 ( 최대 (0x3fffff << 12) = 16 GiB 정도의 랜덤값)

0x3fffff000 정도의 주소 마스크 안에 랜덤값을 stack_top 인 0x7FFFFFFFF000 에서

C
return PAGE_ALIGN(stack_top) - random_variable;

다음과 같이 뺌으로써 최종적인 stack_top 을 반환하게 된다. 실제 실험을 해보자면,

C
// gcc -O2 -fno-pie -no-pie -o test test.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>

static void show_stack_addr(const char *tag) {
    volatile int local = 42; // 최적화 방지용 volatile
    printf("%s: &local = %p\n", tag, (void*)&local);
}

int main(int argc, char **argv, char **envp) {
    // 스택 예시 1: main의 지역 변수
    int a = 1;
    printf("main: &a     = %p\n", (void*)&a);

    // 스택 예시 2: 다른 함수의 지역 변수
    show_stack_addr("func");

    // 힙/텍스트 비교용 (참고)
    void *heap = malloc(16);
    printf("heap: ptr    = %p\n", heap);
    printf("text: &main  = %p\n", (void*)&main);
    printf("argv: %p, envp: %p (둘 다 스택 상단 근처)\n", (void*)argv, (void*)envp);

    free(heap);
    pause(); // /proc/$(pid)/maps 확인용: 필요 없으면 지워도 됨
    return 0;
}

다음과 같은 간단한 스택 확인 c 코드를 작성뒤 ( NO-PIE 로 ) 실행을 해보면,

C
main: &a     = 0x7ffd38847090  // 로컬 변수 a 위치
func: &local = 0x7ffd38847094
heap: ptr    = 0x5d1b6b0
text: &main  = 0x4010d0

다음과 같은 결과가 나오는데

0x7FFFFFFFF000 에서 0x3fffff000 의 마스크를 가진 랜덤값을 뺀 스택 위치가 나오는 것을 알수 있다.

그렇다면 왜 PIE가 켜져있으면 어떻까?

아마 경험을 해보았겠지만, 역시

C
main: &a     = 0x7ffd3159efac
func: &local = 0x7ffd3159ef64
heap: ptr    = 0x5dbe305126b0
text: &main  = 0x5dbe0ac35225
argv: 0x7ffd3159f0e8, envp: 0x7ffd3159f0f8 (둘 다 스택 상단 근처)

스택은 0x7ff~ 대역에 만들어진다.

다만 heap이나 code와 전역변수는 PIE의 영향을 받는다.

setup_arg_pages

C
int setup_arg_pages(struct linux_binprm *bprm,
		    unsigned long stack_top,
		    int executable_stack)
{
	unsigned long ret;
	unsigned long stack_shift;
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma = bprm->vma;
	struct vm_area_struct *prev = NULL;
	unsigned long vm_flags;
	unsigned long stack_base;
	unsigned long stack_size;
	unsigned long stack_expand;
	unsigned long rlim_stack;
	struct mmu_gather tlb;
	struct vma_iterator vmi;

#ifdef CONFIG_STACK_GROWSUP
	/* Limit stack size */
	stack_base = bprm->rlim_stack.rlim_max;

	stack_base = calc_max_stack_size(stack_base);

	/* Add space for stack randomization. */
	stack_base += (STACK_RND_MASK << PAGE_SHIFT);

	/* Make sure we didn't let the argument array grow too large. */
	if (vma->vm_end - vma->vm_start > stack_base)
		return -ENOMEM;

	stack_base = PAGE_ALIGN(stack_top - stack_base);

	stack_shift = vma->vm_start - stack_base;
	mm->arg_start = bprm->p - stack_shift;
	bprm->p = vma->vm_end - stack_shift;
#else
	stack_top = arch_align_stack(stack_top);
	stack_top = PAGE_ALIGN(stack_top);

	if (unlikely(stack_top < mmap_min_addr) ||
	    unlikely(vma->vm_end - vma->vm_start >= stack_top - mmap_min_addr))
		return -ENOMEM;

	stack_shift = vma->vm_end - stack_top;

	bprm->p -= stack_shift;
	mm->arg_start = bprm->p;
#endif

	if (bprm->loader)
		bprm->loader -= stack_shift;
	bprm->exec -= stack_shift;

	if (mmap_write_lock_killable(mm))
		return -EINTR;

	vm_flags = VM_STACK_FLAGS;

	/*
	 * Adjust stack execute permissions; explicitly enable for
	 * EXSTACK_ENABLE_X, disable for EXSTACK_DISABLE_X and leave alone
	 * (arch default) otherwise.
	 */
	if (unlikely(executable_stack == EXSTACK_ENABLE_X))
		vm_flags |= VM_EXEC;
	else if (executable_stack == EXSTACK_DISABLE_X)
		vm_flags &= ~VM_EXEC;
	vm_flags |= mm->def_flags;
	vm_flags |= VM_STACK_INCOMPLETE_SETUP;

	vma_iter_init(&vmi, mm, vma->vm_start);

	tlb_gather_mmu(&tlb, mm);
	ret = mprotect_fixup(&vmi, &tlb, vma, &prev, vma->vm_start, vma->vm_end,
			vm_flags);
	tlb_finish_mmu(&tlb);

	if (ret)
		goto out_unlock;
	BUG_ON(prev != vma);

	if (unlikely(vm_flags & VM_EXEC)) {
		pr_warn_once("process '%pD4' started with executable stack\n",
			     bprm->file);
	}

	/* Move stack pages down in memory. */
	if (stack_shift) {
		ret = shift_arg_pages(vma, stack_shift);
		if (ret)
			goto out_unlock;
	}

	/* mprotect_fixup is overkill to remove the temporary stack flags */
	vm_flags_clear(vma, VM_STACK_INCOMPLETE_SETUP);

	stack_expand = 131072UL; /* randomly 32*4k (or 2*64k) pages */
	stack_size = vma->vm_end - vma->vm_start;
	/*
	 * Align this down to a page boundary as expand_stack
	 * will align it up.
	 */
	rlim_stack = bprm->rlim_stack.rlim_cur & PAGE_MASK;

	stack_expand = min(rlim_stack, stack_size + stack_expand);

#ifdef CONFIG_STACK_GROWSUP
	stack_base = vma->vm_start + stack_expand;
#else
	stack_base = vma->vm_end - stack_expand;
#endif
	current->mm->start_stack = bprm->p;
	ret = expand_stack_locked(vma, stack_base);
	if (ret)
		ret = -EFAULT;

out_unlock:
	mmap_write_unlock(mm);
	return ret;
}

대강 이 함수를 정리한다면,

argv, envp 같은 인자/환경변수를 올려놓을 유저 스택 영역을 vm_area_struct 로 할당 및 설정하는 함수이다.

이 함수를 보면,

C
#ifdef CONFIG_STACK_GROWSUP

다음과 같이 전처리기를 사용하는데 알겠지만, 리눅스는 GROWSUP 이 아니라 GROWSDOWN이다. 따라서 해당 분기가 안되고

C
#else
	stack_top = arch_align_stack(stack_top);
	stack_top = PAGE_ALIGN(stack_top);

	if (unlikely(stack_top < mmap_min_addr) ||
	    unlikely(vma->vm_end - vma->vm_start >= stack_top - mmap_min_addr))
		return -ENOMEM;

	stack_shift = vma->vm_end - stack_top;

	bprm->p -= stack_shift;
	mm->arg_start = bprm->p;

다음과 같이 else 부터 시작된다.

여기서 보면 인자로 받은 stack_top을 arch_align_stack 함수와 PAGE_ALIGN 으로 정하는 것을 알 수 있다.

arch_align_stack

C
unsigned long arch_align_stack(unsigned long sp)
{
	if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
		sp -= get_random_u32_below(8192);
	return sp & ~0xf;
}

프로세스가 ADDR_NO_RANDOMIZE 플래그와

randomize_va_space (시스템 전역 설정 /proc/sys/kernel/randomize_va_space)

에 대한 검사를 한뒤에 만약 랜덤화가 활성화 되어 있으면,

get_random_u32_below 함수를 실행해서 해당 값을 기존 sp 에서 뺀다.

해당 함수는 스택 포인터를 최대 8KB 범위 내에서 임의로 내린다.

이후에

C
return sp & ~0xf;

으로 16바이트 경계로 정렬을 한다.

그럼 여기서 알 수 있는것은 프로세스가 시작할때 stack_base는 0x7fAABBC000 처럼 하위 12비트가 ( 1페이지 4096 ) 000으로 되어 있지만, 실제로 스택의 시작(최초의 rsp) 인 stack_top은 stack_base 에서 랜덤 오프셋을 빼서 0x7fAABBAAD0 처럼 하위 4비트를 제외한 모든 부분이 랜덤인 stack_top 을 만든다.