Heap은 프로그램 실행 중 동적으로 메모리를 할당하기 위한 영역이다.
프로그램이 실행되기 전에는 정확한 크기를 알 수 없는 데이터를 저장하기 위해 사용된다.
대표적으로 다음 함수들을 통해 Heap 메모리를 사용한다.
malloc
calloc
realloc
free
ex)
int *p = malloc(sizeof(int));
이 코드는 실행 중에 Heap 영역에 4바이트 메모리를 할당하고 그 주소를 반환한다.
Heap은 프로그램이 실행되는 동안 필요할 때마다 메모리를 할당하고 해제할 수 있기 때문에 매우 유연한 메모리 영역이다.
하지만 메모리를 직접 관리해야 하기 때문에 다음과 같은 문제가 발생할 수 있다.
이러한 취약점들이 Heap Exploit의 주요 공격 포인터가 된다.
프로그램이 실행되면 프로세스의 가상 메모리는 일반적으로 다음과 같은 구조를 가진다.
High Address
+------------------+
| Stack |
| | //함수 호출 시 자동 할당
| |
+------------------+
| Heap |
| | //동적 할당
| |
+------------------+
| BSS | //정적 변수 및 전역 변수
+------------------+
| Data | //초기화된 전역 변수
+------------------+
| Text | //프로그램 코드 영역
+------------------+
Low Address
각 영역의 특징은 다음과 같다.
프로그램의 실행 코드가 저장되는 영역이다.
일반적으로 읽기 전용으로 설정되어 있어 코드 변조를 방지한다.
ex)
main()
printf()
system()
초기화된 전역 변수와 정적 변수가 저장되는 영역이다.
ex)
int a = 10;
프로그램 시작 시 메모리에 로드된다.
초기화되지 않은 전역 변수와 정적 변수가 저장되는 영역이다.
ex)
int a;
이 변수는 실행 시 자동으로 0으로 초기화된다.
프로그램 실행 중 동적으로 메모리를 할당하기 위해 사용되는 영역이다.
Heap은 낮은 주소에서 높은 주소 방향으로 증가한다.
ex)
malloc()
new
Heap은 프로그램이 실행되는 동안 계속 크기가 변할 수 있으며 메모리 관리 라이브러리(glibc)가 이를 관리한다.
함수 호출과 관련된 정보가 저장되는 영역이다.
대표적으로 다음 정보들이 저장된다.
Stack은 높은 주소에서 낮은 주소 방향으로 증가한다.
| 구분 | Stack | Heap |
|---|---|---|
| 할당 방식 | 자동 할당 | 동적 할당 |
| 관리 | 컴파일러가 관리 | 프로그래머가 관리 |
| 속도 | 빠름 | 상대적으로 느림 |
| 크기 | 제한적 | 비교적 큼 |
| 해제 | 함수 종료 시 자동 해제 | free() / delete 필요 |
| 저장 데이터 | 지역 변수, 함수 호출 정보 | 동적으로 생성된 데이터 |
리눅스에서 malloc은 대부분 glibc allocator(ptmalloc)를 사용한다.
Heap 메모리는 단순히 계속 할당되는 것이 아니라
Chunk라는 단위로 관리된다.
기본 동작 흐름
malloc -> heap chunk 생성 -> free list에 저장 -> 재사용
즉, free()된 메모리는 바로 OS로 반환되는 것이 아니라 재사용을 위해 allocator 내부에 저장된다.
이 때문에 Heap Exploit에서는 free된 chunk 구조를 조작하는 공격이 많이 사용된다.
Heap 메모리는 Chunk 단위로 관리된다.
glibc 내부 구조
struct malloc_chunk {
size_t prev_size;
size_t size;
struct malloc_chunk* fd;
struct malloc_chunk* bk;
}
각 필드의 의미는 다음과 같다.
이전 Chunk의 크기를 저장한다.
이전 Chunk가 사용중인 경우(PREV_INUSE) 면 이 값은 사용되지 않는다.
현재 Chunk의 크기를 저장한다.
단순히 크기만 저장되는 것이 아니라 하위 비트에는 flag 정보가 포함된다.
대표 flag
PREV_INUSE
IS_MMAPPED
NON_MAIN_ARENA
ex)
0x21
실제 의미
0x20 = chunk size
0x1 = PREV_INUSE
free된 chunk가 bin에 들어갈 때 다음 chunk를 가리키는 포인터
free된 chunk가 이전 chunk를 가리키는 포인터
+------------------+
| prev_size |
+------------------+
| size |
+------------------+
| user data |
| |
| |
+------------------+
사용자가 malloc()으로 받은 메모리는 실제로는 metadata(prev_size, size) 뒤에 위치한 user data 영역이다.
이 metadata를 overwrite하면 Heap exploit이 가능하다.
malloc()을 호출하면 사용자는 단순히 메모리 주소만 받지만, 실제로 Heap에서는 metadata와 user data가 함께 존재하는 구조로 관리된다.
+------------------+
| prev_size |
+------------------+
| size |
+------------------+
| user data | ← malloc이 반환하는 주소
| |
| |
+------------------+
여기서 prev_size와 size 두 영역이 metadata다.
예를 들어 이러한 코드가 있다고 하자.
char *p = malloc(0x20);
메모리 구조는 다음과 같다.
0x555555559000 prev_size
0x555555559008 size
0x555555559010 user data ← p
즉
p = metadata + 0x10
이 된다.
[metadata][user data]
↑ ↑
prev_size malloc이 반환하는 주소
size
이런 느낌
Heap allocator(glibc)는 메모리를 관리하기 위해 각 chunk에 대한 정보를 저장해야 한다.
대표적으로 다음 정보를 관리한다.
그래서 metadata에 다음 값들이 들어간다.
prev_size
size
fd
bk
Heap exploit의 핵심은 이 metadata를 조작하는 것이다.
예를 들어 Heap Overflow가 발생하면
[chunk1 user data] → overflow
↓
chunk2 metadata overwrite
size 조작, fd 조작, bk 조작이 가능해지고 여러 공격이 가능해진다.