how heap load to memory & what is brk & how ASLR work - heap

이글을 읽고와야 이해가 된다. 이글에서 heap 영역 이전까지의 영역이 어떻게 설정되는지 다룬다.

그리고 이제 메인바이너리 영역이 끝나면 본격적으로 heap(brk) 영역이 시작되는데 그전에

왜 brk라고 불리는지 알아보자면

일단 메인바이너리의 끝 부분을 데이터 세그먼트의 끝(=program break) 라고 한다.

이 값을 당연히 mm_struct 구조체에서 담고 있을 것이고 이를 확인해보니

C
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에서 끝 부분에

C
		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 값에 넣고 있다.

그리고

C
	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이 적용될 차례이다.

C
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); 다음과 같이 다시 재설정을 한다.

C
unsigned long arch_randomize_brk(struct mm_struct *mm)
{
	return randomize_page(mm->brk, 0x02000000);
}
C
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이 자란다고 하는것이다.