힙에서 관리되는 메모리 단위를 청크라고 칭한다.
청크는 glibc 소스를 보면 다음과 같은 구조체로 정의되어 있다.
struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ /* double links -- used only if free. */ struct malloc_chunk* fd; struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ /* double links -- used only if free. */ struct malloc_chunk* fd_nextsize; struct malloc_chunk* bk_nextsize; }; typedef struct malloc_chunk* mchunkptr;
mchunk_size
는 현재 청크의 사이즈와 상태를 나타내는 플래그를 포함한 값이다.
여기서 플래그는 3bit를 이용하여 3개의 상태를 표현한다.
1. NON_MAIN_ARENA
2. IS_MMAPPED
mmap()
에 의해서도 힙 메모리가 할당될 수 있다. mmap()
으로 할당된 청크일 때 1로 설정되며, 이 때 다른 플래그 값은 의미가 없어진다.3. PREV_INUSE
이전 청크가 사용중일 때 1로 설정된다.
참고로 첫 번째로 할당된 청크의 prev_inuse
플래그 값은 항상 1이다.
예제를 통해 힙의 구조를 확인하자.
char *ptr = malloc(0x88);
char *ptr2 = malloc(0x28);
for (int i = 0; i < 0x88; i++) ptr[i] = 'A';
free(ptr);
위 코드를 실행하여 메모리 상태를 디버깅한다.
이제 첫 번째 청크인 ptr
을 free()
했을 때 메모리를 확인하자.
정리해보면
1.
malloc()
이 힙을 할당할 때, 데이터 영역 이전 16바이트(할당된 주소 - 0x10)가 헤더로 사용된다.
2. 헤더에는prev_size
와size+flag
가 있다.
3.prev_size
는 현재 청크의 앞에free
된 청크의 크기를 나타내며, 이전 청크가 사용중일 때는 이전 청크의 데이터영역으로 사용된다.
마지막으로 free 이후 저장된 fd, bk의 주소는 libc 안에 있는 arena에서 해당 청크가 저장된 bin의 주소이다.
gdb-peda$ p main_arena
$1 = {
mutex = 0x0,
flags = 0x1,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x6020c0,
last_remainder = 0x0,
bins = {0x602000, 0x602000, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>,
0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>,
0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bb8 <main_arena+152>,
0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bd8 <main_arena+184>,
여기서 bins
의 주소를 보면 0x602000으로 위에서 free된 청크를 나타낸다.
값이 두 개씩 쌍을 가지는 것은, double linked list이므로 각각 head와 tail을 나타내기 때문이며
head와 tail의 값이 0x602000으로 동일한 이유는 현재 free된 청크가 0x602000 하나이므로 둘 다 동일한 청크를 가리키고 있기 때문이다.
앞서 살펴본 것은 다른 블로그에 포스팅 된 내용이며, 구버전인 glibc-2.23
으로 진행된 내용이다.
최신 버전인 glibc 2.27
으로 실습하여 변화된 내용을 확인해보자.
root@j-VirtualBox:/work/tmp/pico/gho/heap# ldd test
linux-vdso.so.1 (0x00007ffe075f9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8209b4b000)
/lib64/ld-linux-x86-64.so.2 (0x00007f820a13e000)
root@j-VirtualBox:/work/tmp/pico/gho/heap# ls -al /lib/x86_64-linux-gnu/libc.so.6
lrwxrwxrwx 1 root root 12 2월 7 21:35 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.27.so
root@j-VirtualBox:/work/tmp/pico/gho/heap#
앞서 실습에서 사용했던 코드를 그대로 컴파일 하자.
char *ptr = malloc(0x88);
char *ptr2 = malloc(0x28);
for (int i = 0; i < 0x88; i++) ptr[i] = 'A';
free(ptr);
먼저 할당하고 난 뒤 메모리 상태이다.
gdb-peda$ x/30gx $rax-0x10
0x555555756250: 0x0000000000000000 0x0000000000000091
0x555555756260: 0x4141414141414141 0x4141414141414141
0x555555756270: 0x4141414141414141 0x4141414141414141
0x555555756280: 0x4141414141414141 0x4141414141414141
0x555555756290: 0x4141414141414141 0x4141414141414141
0x5555557562a0: 0x4141414141414141 0x4141414141414141
0x5555557562b0: 0x4141414141414141 0x4141414141414141
0x5555557562c0: 0x4141414141414141 0x4141414141414141
0x5555557562d0: 0x4141414141414141 0x4141414141414141
0x5555557562e0: 0x4141414141414141 0x0000000000000031
0x5555557562f0: 0x0000000000000000 0x0000000000000000
0x555555756300: 0x0000000000000000 0x0000000000000000
0x555555756310: 0x0000000000000000 0x0000000000020cf1
앞서 살펴본 내용과 동일하다.
이제 free(ptr)
를 실행해보자.
gdb-peda$ x/30gx 0x555555756250
0x555555756250: 0x0000000000000000 0x0000000000000091
0x555555756260: 0x0000000000000000 0x4141414141414141
0x555555756270: 0x4141414141414141 0x4141414141414141
0x555555756280: 0x4141414141414141 0x4141414141414141
0x555555756290: 0x4141414141414141 0x4141414141414141
0x5555557562a0: 0x4141414141414141 0x4141414141414141
0x5555557562b0: 0x4141414141414141 0x4141414141414141
0x5555557562c0: 0x4141414141414141 0x4141414141414141
0x5555557562d0: 0x4141414141414141 0x4141414141414141
0x5555557562e0: 0x4141414141414141 0x0000000000000031
0x5555557562f0: 0x0000000000000000 0x0000000000000000
0x555555756300: 0x0000000000000000 0x0000000000000000
0x555555756310: 0x0000000000000000 0x0000000000020cf1
0x555555756320: 0x0000000000000000 0x0000000000000000
0x555555756330: 0x0000000000000000 0x0000000000000000
내용이 다르다.
free된 청크에 fd
,bk
가 저장되있지도 않으며
두 번째 청크의 prev_size
가 free된 청크의 사이즈로 변경되지도 않았다.
이유는 glibc-2.26
부터 도입된 tcache bin 때문이다.
바뀐 버전에서는 위에서 할당해제된 사이즈의 청크는 우선적으로 tcache bin에 저장된다.
이후에 설명할 것이지만, tcache bin에는 fd
, bk
도 없고 병합을 하지 않기 때문에 prev_size
를 저장할 필요가 없다.
따라서 prev_size
가 표시되지 않았던 것이다.
정확한 이해를 위해선 bin에 대해 좀 더 알아볼 필요가 있다.
다음 포스팅을 확인하자.
https://syedfarazabrar.com/2019-10-12-picoctf-2019-heap-challs/
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/implementation/tcache/