how heap load to memory & what is brk & how ASLR work - heap
이글을 읽고와야 이해가 된다. 이글에서 heap 영역 이전까지의 영역이 어떻게 설정되는지 다룬다.
그리고 이제 메인바이너리 영역이 끝나면 본격적으로 heap(brk) 영역이 시작되는데 그전에
왜 brk라고 불리는지 알아보자면
일단 메인바이너리의 끝 부분을 데이터 세그먼트의 끝(=program break) 라고 한다.
이 값을 당연히 mm_struct 구조체에서 담고 있을 것이고 이를 확인해보니
unsigned long start_brk, brk, start_stack;구조체에 내에 다음과 같은 필드로 담고 있다. start_brk는 말그대로 항상 데이터 세그먼트의 끝(=program break) dm의 주소를 나타내고
brk 는 초기에는 start_brk값과 같겠지만, heap이 자라면 같이 커지면서, brk 영역에 끝을 나타낸다.
즉, heap을 관리하는 아레나 중 맨 처음아레나인 main_arena에서 맨 처음 heap 영역은 굳이 mmap으로 새로운 공간을 할당하지않고 이 brk 영역을 사용한다. 물론 새로운 아레나를 할당해야 하거나 너무 큰 할당이 일어나면 mmap이 사용되지만, main_arena 감당할 수 있으면 이 brk 영역이 사용된다.
그럼 어떻게 unsigned long start_brk, brk 이 주소들을 바꿀 수 있을까?
일단 brk와 sbrk 와 같은 시스템콜이 존재를 한다. ( sbrk는 glibc에서 존재하는 brk 레퍼 함수 )
- brk(addr) = “프로세스의 데이터 세그먼트 끝(=프로그램 break)을 addr로 설정해라”
→ 즉, addr에 절대값으로 넣어 brk 값을 바꿈
- sbrk(delta) = “현재 brk에서 delta만큼 위/아래로 이동해라”
→ 정수값 ( 음의 정수 도 가능 ) 을 넣어 brk 값에 더함
이런 시스템콜을 사용하여 우리가 malloc을 사용하면 내부적으로
- glibc malloc이 기본적으로
- 작은/중간 크기 할당 → sbrk/brk 증가시켜 이 영역 사용
- 큰 할당 / 추가 arena → mmap으로 별도 익명 영역 확보
- 이때 brk 쪽으로 확장된 anon 구간이 우리가 흔히 보는 [heap] 이다.
라고 생각하면 된다. 그럼 이제 다시 어떤식으로 이 brk 영역이 메모리에 올라가는지 확인을 해보면
PT_LOAD 타입의 세그먼트를 메모리에 올리는 loop에서 끝 부분에
error = elf_load(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
중략 .......
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if ((elf_ppnt->p_flags & PF_X) && end_code < k)
end_code = k;
if (end_data < k)
end_data = k;
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
if (k > elf_brk)
elf_brk = k;바로 이 부분인데. 자세히 보면 elf_brk 값을 정하는 과정인데 딱봐도 가장 큰 주소의
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; 을 구하고 있는 과정이다. 즉, p_memsz 을 더하는 것까지 생각하면 가장 마지막 세그먼트의 bss 섹션 까지 끝난 주소를 elf_brk 값에 넣고 있다.
그리고
e_entry = elf_ex->e_entry + load_bias;
phdr_addr += load_bias;
elf_brk += load_bias;
start_code += load_bias;
end_code += load_bias;
start_data += load_bias;
end_data += load_bias;
current->mm->start_brk = current->mm->brk = ELF_PAGEALIGN(elf_brk);elf_brk 에 load_bias 값을 더해서 최종적인 위치를 구해주고 아까 말했던 mm_struct 구조체의
start_brk 필드와 brk 필드를 정렬된 elf_brk 값으로 초기화한다.
그럼 이제 alsr이 적용될 차례이다.
mm = current->mm;
mm->end_code = end_code;
mm->start_code = start_code;
mm->start_data = start_data;
mm->end_data = end_data;
mm->start_stack = bprm->p;
if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
/*
* For architectures with ELF randomization, when executing
* a loader directly (i.e. no interpreter listed in ELF
* headers), move the brk area out of the mmap region
* (since it grows up, and may collide early with the stack
* growing down), and into the unused ELF_ET_DYN_BASE region.
*/
if (IS_ENABLED(CONFIG_ARCH_HAS_ELF_RANDOMIZE) &&
elf_ex->e_type == ET_DYN && !interpreter) {
mm->brk = mm->start_brk = ELF_ET_DYN_BASE;
}
mm->brk = mm->start_brk = arch_randomize_brk(mm);
#ifdef compat_brk_randomized
current->brk_randomized = 1;
#endif
}
alsr 활성화 비트가 켜져있고 커널의 변수가 randomize_va_space가 1보다 크면
mm->brk = mm->start_brk = arch_randomize_brk(mm); 다음과 같이 다시 재설정을 한다.
unsigned long arch_randomize_brk(struct mm_struct *mm)
{
return randomize_page(mm->brk, 0x02000000);
}
unsigned long randomize_page(unsigned long start, unsigned long range)
{
if (!PAGE_ALIGNED(start)) {
range -= PAGE_ALIGN(start) - start;
start = PAGE_ALIGN(start);
}
if (start > ULONG_MAX - range)
range = ULONG_MAX - start;
range >>= PAGE_SHIFT;
if (range == 0)
return start;
return start + (get_random_long() % range << PAGE_SHIFT);
}
인데
if (!PAGE_ALIGNED(start)) { range -= PAGE_ALIGN(start) - start; start = PAGE_ALIGN(start); } if (start > ULONG_MAX - range) range = ULONG_MAX - start;
이 부분은 사실 정렬 검증과 오버플로우 확인이고
range >>= PAGE_SHIFT; 여기서 부터 시작이다.
0x02000000 >>= 12 를 하니깐
range = 8192
return start + (get_random_long() % range << PAGE_SHIFT);
인데 정리를 해보면
새 brk = start + (random_page_index * 4096)
where random_page_index ∈ [0, 8191]
라고 보면 된다 특정 페이지 만큼 랜덤으로 더한 위치에 start_brk와 brk 값을 초기화 한다는 것
즉, 이게 끝이다. start_brk는 정해졌고 이제 우리가 malloc을 하면 단순히 내부적으로 brk 시스템콜이 사용되어서 brk 값이 늘어나면 우리가 heap이 자란다고 하는것이다.