SYSTEM] Heap Basics - Memory structure

노션으로 옮김·2020년 4월 15일
1

Study

목록 보기
19/33
post-thumbnail

Heap

구조

힙에서 관리되는 메모리 단위를 청크라고 칭한다.

청크는 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

  • main arena에서 관리되는 청크일 때 1로 설정된다.

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);

위 코드를 실행하여 메모리 상태를 디버깅한다.

이제 첫 번째 청크인 ptrfree() 했을 때 메모리를 확인하자.

정리해보면

1. malloc()이 힙을 할당할 때, 데이터 영역 이전 16바이트(할당된 주소 - 0x10)가 헤더로 사용된다.
2. 헤더에는 prev_sizesize+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이므로 각각 headtail을 나타내기 때문이며

headtail의 값이 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_sizefree된 청크의 사이즈로 변경되지도 않았다.

이유는 glibc-2.26부터 도입된 tcache bin 때문이다.

바뀐 버전에서는 위에서 할당해제된 사이즈의 청크는 우선적으로 tcache bin에 저장된다.

이후에 설명할 것이지만, tcache bin에는 fd, bk도 없고 병합을 하지 않기 때문에 prev_size를 저장할 필요가 없다.
따라서 prev_size가 표시되지 않았던 것이다.


정확한 이해를 위해선 bin에 대해 좀 더 알아볼 필요가 있다.

다음 포스팅을 확인하자.

https://velog.io/@woounnan/SYSTEM-Heap-Basic-Bean


참조

https://syedfarazabrar.com/2019-10-12-picoctf-2019-heap-challs/

https://devel0pment.de/?p=688

https://krrr-1.tistory.com/23

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/implementation/tcache/

0개의 댓글