how ASLR work - stack
해당 글 부터 읽어오면 된다 ( load_elf_binary 까지의 과정이 정리되어 있음 )
load_elf_binary
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 부분만 보자면,
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);이 부분이다.
여기서 보면 randomize_stack_top(STACK_TOP) 다음과 같은 코드가 보이는데 일단 STACK_TOP 은
#define STACK_TOP TASK_SIZE_LOW→
#define TASK_SIZE_LOW (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : DEFAULT_MAP_WINDOW)→
#define DEFAULT_MAP_WINDOW ((1UL << 47) - PAGE_SIZE)즉, STACK_TOP 은 x86-64에서 ((1UL << 47) - PAGE_SIZE) 다음과 같은데
PAGE_SIZE 는
#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 함수는
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 가
#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 는
#define PAGE_SHIFT 1212 의 값을 가진다.
따라서 랜덤값을 불러오고 ( 최대 (0x3fffff << 12) = 16 GiB 정도의 랜덤값)
0x3fffff000 정도의 주소 마스크 안에 랜덤값을 stack_top 인 0x7FFFFFFFF000 에서
return PAGE_ALIGN(stack_top) - random_variable;다음과 같이 뺌으로써 최종적인 stack_top 을 반환하게 된다. 실제 실험을 해보자면,
// 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 로 ) 실행을 해보면,
main: &a = 0x7ffd38847090 // 로컬 변수 a 위치
func: &local = 0x7ffd38847094
heap: ptr = 0x5d1b6b0
text: &main = 0x4010d0다음과 같은 결과가 나오는데
0x7FFFFFFFF000 에서 0x3fffff000 의 마스크를 가진 랜덤값을 뺀 스택 위치가 나오는 것을 알수 있다.
그렇다면 왜 PIE가 켜져있으면 어떻까?
아마 경험을 해보았겠지만, 역시
main: &a = 0x7ffd3159efac
func: &local = 0x7ffd3159ef64
heap: ptr = 0x5dbe305126b0
text: &main = 0x5dbe0ac35225
argv: 0x7ffd3159f0e8, envp: 0x7ffd3159f0f8 (둘 다 스택 상단 근처)스택은 0x7ff~ 대역에 만들어진다.
다만 heap이나 code와 전역변수는 PIE의 영향을 받는다.
setup_arg_pages
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 로 할당 및 설정하는 함수이다.
이 함수를 보면,
#ifdef CONFIG_STACK_GROWSUP다음과 같이 전처리기를 사용하는데 알겠지만, 리눅스는 GROWSUP 이 아니라 GROWSDOWN이다. 따라서 해당 분기가 안되고
#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
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 범위 내에서 임의로 내린다.
이후에
return sp & ~0xf;으로 16바이트 경계로 정렬을 한다.
그럼 여기서 알 수 있는것은 프로세스가 시작할때 stack_base는 0x7fAABBC000 처럼 하위 12비트가 ( 1페이지 4096 ) 000으로 되어 있지만, 실제로 스택의 시작(최초의 rsp) 인 stack_top은 stack_base 에서 랜덤 오프셋을 빼서 0x7fAABBAAD0 처럼 하위 4비트를 제외한 모든 부분이 랜덤인 stack_top 을 만든다.