본문 바로가기

System Hacking

Malloc(1) - chunk (glibc의 ptmalloc2)

1. Memory Allocator

  • 메모리 관리를 위해 사용되는 Allocator에는 dlmalloc, ptmalloc2, jemalloc, tcmallc, libumem 등 다양한 종류의 메모리 할당자가 존재한다.
  • 본 글에서는 "GNU C Library"의 메모리 할당자인 ptmalloc2에 대한 내용이다.

1-1. ptmalloc2

  • dlmalloc코드를 기반이고 멀티 스레드에서 사용되도록 확장되었다.
  • 한 번에 두 개 이상의 메모리 영역을 활성화하여, 멀티 스레드 애플리케이션을 효율적으로 처리할 수 있다.
  • 복수 스레드가 동시에 malloc을 호출하면, 각 스레드는 별도의 힙 세그먼트가 생성되고 해당 힙을 유지 보수하는 데이터 구조도 분리되어 메모리에 할당된다.
  • 즉, ptmalloc2를 사용하면 서로 다른 스레드가 서로 간섭하지 않고 서로 다른 메모리 영역에 접근할 수 있다.

2. Chunk

  • Malloc에 메모리할당을 요청하면, 넓은 메모리의 영역(heap)을 다양한 크기의 덩어리로 나누는데, 이를 Chunk라고 한다.
  • Chunk에는 In-use Chunk, Free Chunk, Top Chunk, Last Remainder가 존재한다.
  • 32bit에서는 8byte단위로 할당되며, 64bit에서는 16byte로 할당된다.
  • 일반적으로 malloc을 호출하고 반환되는 주소를 이용해, 데이터를 입력하는데 이는 청크의 시작 부분이 아닌 페이로드 주소이다.
  • 페이로드 바로 위에는 meta-data를 포함하는 청크 헤더가 존재한다. 이는 현재 청크의 사이즈가 몇이고 인접한 청크의 사이즈는 몇인지에 대해 알 수 있으며, 해당 chunk가 사용 중인지 해제되었는지에 대해 알 수 있다.
  In-use chunk    응용프로그램에서 할당받아 사용중인 덩어리이다.
  Free chunk    응용프로그램에서 시스템에 반환한 덩어리이다.
  Top chunk    Arena의 가장 상단에 있는 덩어리이다.
  Last Remainder chunk    Allocator는 메모리 할당할 때, Free chunk중에서 사용가능한 chunk를 확인하는데, 
  만약 일치하는 chunk가 없고, 요청된 크기보다 큰 chunk가 있다면 해당 덩어리를 분할한다.
  이때, 분할되고 남은 덩어리를 "Last Remainder chunk"라고 한다.
   
   Arena란?

   - 각각의 스레드가 서로 간섭하지 않고, 서로 다른 메모리 영역에서 액세스할 수 있게 도와주는 힙 영역을 의미한다.
   - 단일 스레드 경우는 하나의 Arena를 가지지만, 멀티 스레드는 하나 이상의 Arena를 갖고 있으며 서로 다른 Arena안에
     존재하는 각각의 스레드는 정지하지 않고 힙 작업을 수행한다.
   - 자원고갈 문제로, 각각의 스레드마다 자신의 Arena를 갖고 있는 것은 아니며, 32bit 또는 64bit 시스템 Core의 갯수에 따라
     Arena의 개수가 제한되어 있다. 
   - 제한된 크기만큼 Arena 개수가 증가하여 더 늘릴 수 없다면 여러 스레드가 하나의 Arena안에서 공유하며 힙작업을 한다.

3. Struct of malloc_chunk

  • malloc()은 각 chunk를 관리하기 위해, malloc_chunk구조체인 mchunkptr을 선언한다.
  • malloc_chunk구조체는 페이로드 위에 있는 meta-data를 의미한다.

malloc.c
0.17MB

983	/* Forward declarations.  */
984	struct malloc_chunk;
985	typedef struct malloc_chunk* mchunkptr;

3-1. 구조체 malloc_chunk

  • 6개의 정보를 관리한다.
  • 이전 Chunk가 Free Chunk가 되면, 이전 Chunk의 크기가 "mchunk_prev_size"에 저장된다.
  • 반대로 이전 Chunk가 In-use chunk가 되면, 이전 Chunk의 사용자 데이터가 배치된다.
  • In-use Chunk의 크기는 "mchunk_size"에 저장되며, 필드 맨 끝 3bit는 flag정보를 나타낸다.
  • Free chunk는 크기와 히스토리에 따라 다양한 목록에 저장되는데, 이러한 목록들을 "bins"라고 한다.
    • fd, bk, fd_nextsize, bk_nextsize는 chunk가 Free Chunk일 경우에만 사용된다.
    • fd (Forward pointer) : 다음 Free Chunk의 포인터를 가진다.
    • bk (Backward pointer) : 이전 Free Chunk의 포인터를 가진다.
    • fd_nextsize, bk_nextsize : large bin에서 사용하는 포인터이다.
  • 모든 청크 크기는 MALLOC_ALIGNMENT(2 * sizeof(size_t))의 배수이다.
    • 32bit는 size_t의 크기가 4byte이기 때문에, chunk의 크기는 8의 배수가 된다.
984	struct malloc_chunk {
985	 
986	  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
987	  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */
988 
989	  struct malloc_chunk* fd;         /* double links -- used only if free. */
990	  struct malloc_chunk* bk;
991 
992	  /* Only used for large blocks: pointer to next larger size.  */
993	  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
994	  struct malloc_chunk* bk_nextsize;
995	};
  • mchunk_size필드 맨 끝 3bit는 플래그로 사용되며, 아래와 같이 정의된다.
  PREV_INUSE [P](0X1) 플래그   인접한 이전 청크가 사용중일 때 또는 free된 청크가 fastins에 들어가는 경우
  1로 세팅된다.  
  IS_MMAPPED [M](0X2) 플래그   chunk가 mmap()으로 할당받은 경우 1로 세팅되며, free될 경우 allocator는
  해당 청크가 사용하고 있는 영역(mmap으로 할당받은 영역을 mummap()을 통해
  바로 커널에 돌려주는데 사용된다.
  NON_MAIN_ARENA [A](0X4) 플래그   Main Arena가 아닌 Arena로부터 할당받은 경우 1로 세팅된다.
1221	/*
1222	   --------------- Physical chunk operations ---------------
1223	 */
1224	 
1225	 
1226	/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */
1227	#define PREV_INUSE 0x1
1228	 
1229	/* extract inuse bit of previous chunk */
1230	#define prev_inuse(p)       ((p)->size & PREV_INUSE)
1231	 
1232	 
1233	/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
1234	#define IS_MMAPPED 0x2
1235	
1236	/* check for mmap()'ed chunk */
1237	#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
1238	 
1239	 
1240	/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
1241	   from a non-main arena.  This is only set immediately before handing
1242	   the chunk to the user, if necessary.  */
1243	#define NON_MAIN_ARENA 0x4
1244	 
1245	/* check for chunk from non-main arena */
1246	#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)

3-2. In-use chunk

  • 할당자로부터 메모리를 할당받아 사용중인 메모리 덩어리를 의미한다.
  • 해당 chunk의 크기는 mchunk_size에 저장되며, 필드의 맨 끝 3bit는 flag정보를 나타낸다.

출처 : https://www.lazenca.net/pages/viewpage.action?pageId=51970061

 

(1) in-use chunk를 확인하기 위한 코드

  • malloc 크기가 136byte와 80byte인 메모리 할당을 요청하며, 이를 통해 반환된 포인터를 heap1, heap2에 저장한다.
  • free함수를 통해, heap1이 가르키는 메모리의 해제를 요청한다.
  • 이후, 다시 malloc 크기가 136byte인 메모리 할당을 요청하며, 이번에는 heap3에 반환된 포인터를 저장한다.
  • read함수를 이용하여, heap3이 가르키는 메모리에 데이터를 입력한다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
   
void main(){
    char *heap1 = malloc(136);
    char *heap2 = malloc(80);
 
    free(heap1);
 
    char *heap3 = malloc(136);
    read(STDIN_FILENO,heap3,136);
}

 

(2) gdb를 통해, 디버깅하여 메모리 변화를 확인한다.

  •  디버깅 코드

 

① 첫번째, malloc() 호출되기 전 상황이다.

  • 시스템은 Heap 공간이 필요한 경우에만 프로세스에 해당 공간을 맵핑하므로, malloc호출 후에 해당 프로세스에 Heap 공간이 매핑되는 것을 확인할 수 있다.

heap 할당전
heap 할당후

  • 할당자로부터 할당받은 첫 번째 heap의 포인터는 0x5555555592a0이다.
    • size(0x555555559298)에 0x91(145)가 저장되어 있다.
    • 본 바이너리는 64bit이기 때문에, 청크의 크기는 모두 16의 배수가 된다.
      따라서, 136(0x88)은 16의 배수가 아니라서, 해당 수와 가장 가까운 16의 배수인 144(0x90)가 청크의 크기이다.
    • 144에 PREV_INUSE [P](0x1)에 대한 플래그 값을 더해, 145(0x91)가 들어있는 것을 확인할 수 있다.
    • 할당 가능한 메모리의 크기가 0x555555559328에 저장된 것을 확인할 수 있다.

 

② 두번째 malloc() 호출된 후 상황이다.

  • 두번째 heap의 포인터는 0x555555559330이며, 청크의 크기는 0x61(97)임을 알 수 있다.
  • 하지만, 실제적으로 코드에서 요청했던 사이즈는 0x50(80)이다.
    이는 할당자가 청크 관리하기 위해, 필요한 메타데이터(fd, bk)를 저장하기 위해 요청된 크기 16byte를 더했기 때문이다.
  • 또한, 0x60에 PREV_INUSE [P](0x1)에 대한 플래그 값을 더해, 0x61이 들어있는 것을 확인할 수 있다.

 

③ free함수로 heap1인 첫번째 청크를 해제한 상황이다.

  • 0x5555555592a0 ~ 0x5555555592a8 메모리에 fd, bk값이 저장된다.

 

④ 세번째 malloc() 호출된 후 상황이다.

  • 세번째 heap의 포인터는 0x5555555592a0이며, 처음에 할당했던 heap1 포인터와 동일하다.
  • 이는 malloc() 함수가 메모리 효율성을 위해, free chunk를 관리하기 때문이다.
    따라서, 할당자는 메모리 할당 요청을 받으면, free chunk를 먼저 사용한다.
  • 다시 할당된 chunk는 이전에 저장된 데이터가 초기화되지 않고 그대로 존재한다.

  • 새로 할당받은 heap3메모리는 정상적으로 사용이 가능하며, 값을 입력하게 되면 이전에 저장되어있던 heap1의 값을 덮어쓰게 된다.

3-3. Free chunk

  • 할당자에게 반환된 메모리 덩어리를 의미한다.
  • chunk의 크기에 따라 fd, bk, fd_nextsize, bk_nextsize의 값이 해당 chunk내에 저장된다.
    • chunk의 크기가 최소 크기일 경우, fd_nextsize, bk_nextsize의 값을 저장할 수 없다.
    • Free chunk의 크기는 커지지 않으므로, chunk 크기가 최소의 경우는 prev_size, size, fd, bk값만 메모리에 저장된다.
    • fd_nextsize, bk_next size는 큰 블록에서만 사용된다.

출처:https://www.lazenca.net/pages/viewpage.action?pageId=51970061

 

(1) Free chunk를 확인하기 위한 코드

  • malloc에 128byte와 8byte 크기인 메모리를 각각 3개씩 할당 요청한다.
  • 그리고 128byte 크기인 메모리 3개를 해제한다.
#include <stdio.h>
#include <stdlib.h>
  
void main(){
        char *heap1 = malloc(128);
        char *tmp1 = malloc(8);
        char *heap2 = malloc(128);
        char *tmp2 = malloc(8);
        char *heap3 = malloc(128);
        char *tmp3 = malloc(8);
  
        free(heap1);
        free(heap2);
        free(heap3);
}

 

(2) gdb를 통해, 디버깅하여 메모리 변화를 확인한다.

  • 디버깅 코드
pwndbg> disassemble main
Dump of assembler code for function main:
   0x0000555555555169 <+0>:     endbr64
   0x000055555555516d <+4>:     push   rbp
   0x000055555555516e <+5>:     mov    rbp,rsp
   0x0000555555555171 <+8>:     sub    rsp,0x30
   0x0000555555555175 <+12>:    mov    edi,0x80
   0x000055555555517a <+17>:    call   0x555555555070 <malloc@plt>
   0x000055555555517f <+22>:    mov    QWORD PTR [rbp-0x30],rax
   0x0000555555555183 <+26>:    mov    edi,0x8
   0x0000555555555188 <+31>:    call   0x555555555070 <malloc@plt>
   0x000055555555518d <+36>:    mov    QWORD PTR [rbp-0x28],rax
   0x0000555555555191 <+40>:    mov    edi,0x80
   0x0000555555555196 <+45>:    call   0x555555555070 <malloc@plt>
   0x000055555555519b <+50>:    mov    QWORD PTR [rbp-0x20],rax
   0x000055555555519f <+54>:    mov    edi,0x8
   0x00005555555551a4 <+59>:    call   0x555555555070 <malloc@plt>
   0x00005555555551a9 <+64>:    mov    QWORD PTR [rbp-0x18],rax
   0x00005555555551ad <+68>:    mov    edi,0x80
   0x00005555555551b2 <+73>:    call   0x555555555070 <malloc@plt>
   0x00005555555551b7 <+78>:    mov    QWORD PTR [rbp-0x10],rax
   0x00005555555551bb <+82>:    mov    edi,0x8
   0x00005555555551c0 <+87>:    call   0x555555555070 <malloc@plt>
   0x00005555555551c5 <+92>:    mov    QWORD PTR [rbp-0x8],rax
   0x00005555555551c9 <+96>:    mov    rax,QWORD PTR [rbp-0x30]
   0x00005555555551cd <+100>:   mov    rdi,rax
   0x00005555555551d0 <+103>:   call   0x555555555060 <free@plt>
   0x00005555555551d5 <+108>:   mov    rax,QWORD PTR [rbp-0x20]
   0x00005555555551d9 <+112>:   mov    rdi,rax
   0x00005555555551dc <+115>:   call   0x555555555060 <free@plt>
   0x00005555555551e1 <+120>:   mov    rax,QWORD PTR [rbp-0x10]
   0x00005555555551e5 <+124>:   mov    rdi,rax
   0x00005555555551e8 <+127>:   call   0x555555555060 <free@plt>
   0x00005555555551ed <+132>:   nop
   0x00005555555551ee <+133>:   leave
   0x00005555555551ef <+134>:   ret
End of assembler dump.

 

① 첫번째 malloc() 호출된 후 상황이다.

  • heap의 어디 주소부터 할당되었는지 확인할 수 있다.

 

② 마지막 malloc할당을 끝마친 상황이다.

  • chunk 크기 128byte 메모리 3개가 각각 0x5555555592a0, 0x555555559350, 0x555555559400에 할당된 것을 확인할 수 있다.
  • chunk 크기 8byte 메모리 3개가 각각 0x555555559330, 0x5555555593e0, 0x555555559490에 할당된 것을 확인할 수 있다.

 

③ 첫번째 free() 호출된 후 상황이다.

  • heap1이 해제되면, 해당 chunk에 fd, bk가 저장된다.

 

④ 두번째 free() 호출된 후 상황이다.

  • heap2이 해제되면, 해당 chunk에 fd, bk가 저장된다.

 

⑤ 세번째 free() 호출된 후 상황이다.

  • heap3이 해제되면, 해당 chunk에 fd, bk가 저장된다.


# 참고주소

'System Hacking' 카테고리의 다른 글

Malloc(3) - Arena (glibc의 ptmalloc2)  (0) 2022.09.16
Malloc(2) - Bin (glibc의 ptmalloc2)  (0) 2022.09.14
Fake EBP  (0) 2022.08.03
RTC (Return To CSU)  (0) 2022.07.04
RTL (Return to Library)  (0) 2022.06.29