Daily Heap #6

14719 단어 heap memoryheap memory

Overview

이번 글을 시작으로 malloc() 함수와 free() 함수의 동작 방식에 대해 세부적으로 파헤쳐 볼 예정입니다. glibc의 version에 따라 세부 동작의 차이가 존재하기에 glibc 2.23, glibc 2.29, glibc 2.34 총 세 개의 version을 선정하였습니다.

위 버전을 선정한 기준으로는 가장 기초가 되는 version인 glibc 2.23, tcache와 일부 보호기법이 추가된 glibc 2.29, 작성일 기준 최근 배포판인 Ubuntu 21.10에서 사용되는 glibc 2.34로 선정하였습니다.


malloc() in glibc 2.23

먼저 malloc()의 호출 과정을 간단하게 정리하였습니다.

  1. __libc_malloc() 호출
  2. _malloc_hook이 NULL이 아닌지 검증
    • NULL이 아닐 경우 _malloc_hook의 값을 함수 포인터로 지정하여 실행
    • NULL일 경우 _int_malloc() 호출 과정으로 이동
  3. _int_malloc() 호출

__libc_malloc()함수의 구현 코드는 아래와 같습니다.

void *
__libc_malloc (size_t bytes)
{
  mstate ar_ptr;
  void *victim;

  void *(*hook) (size_t, const void *)
    = atomic_forced_read (__malloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0));

  arena_get (ar_ptr, bytes);

  victim = _int_malloc (ar_ptr, bytes);
  /* Retry with another arena only if we were able to find a usable arena
     before.  */
  if (!victim && ar_ptr != NULL)
    {    
      LIBC_PROBE (memory_malloc_retry, 1, bytes);
      ar_ptr = arena_get_retry (ar_ptr, bytes);
      victim = _int_malloc (ar_ptr, bytes);
    }    

  if (ar_ptr != NULL)
    (void) mutex_unlock (&ar_ptr->mutex);

  assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
          ar_ptr == arena_for_chunk (mem2chunk (victim)));
  return victim;
}

Hook과 관련된 함수는 "Hook Overwrite Exploit" 에서 다룰 기회가 있기 때문에 이 글에서는 자세한 설명을 진행하지 않았습니다.

내부 코드를 살펴보면 먼저 두 개의 변수를 선언하는 것을 확인할 수 있습니다. mstate ar_ptr의 경우 malloc_state 구조체를 참조하며 여기서는 해당 chunk를 할당할 "arena"의 ptr 값을 할당합니다.

void *victim 의 경우 chunk의 할당이 이루어진 뒤 반환된 'mem' 영역의 주소를 저장하는 변수로 이후에 설명할 코드에서도 주요 행동 대상이 되는 항목을 'victim' 으로 명명된 함수를 이용합니다.

Hook

변수의 선언을 완료한 뒤 hook 값이 NULL이 아닌지 검사를 진행합니다. 기본적으로 NULL 값으로 초기화 되어있기 때문에 NULL이 아닌 경우에 대해서는 Hook Overwrite에서 설명을 진행하겠습니다.

arena_get

arena_get() 의 동작은 arena.c 파일에 정의되어 있었으며 그 내용은 다음과 같습니다.

#define arena_get(ptr, size) do {                                        \
    ptr = thread_arena;                                                  \
    arena_lock (ptr, size);                                              \
} while (0)

전달받은 인자를 통해 다시 arena_lock() 매크로를 호출하였고 이를 통해 교착 상태를 방지하기 위한 과정이 진행됨을 유추할 수 있었습니다.

_int_malloc()

위의 과정을 통해 arena에 대한 사전 준비까지 마친 뒤에 실제 chunk를 할당하기 위한 _int_malloc() 함수가 호출되었습니다. 실제 구현 코드의 양이 매우 많기에 임의로 구분지어 분석을 진행하였습니다.

Variable Declaration

3318 static void *
3319 _int_malloc (mstate av, size_t bytes)
3320 {
3321   INTERNAL_SIZE_T nb;               /* normalized request size */
3322   unsigned int idx;                 /* associated bin index */
3323   mbinptr bin;                      /* associated bin */
3324 
3325   mchunkptr victim;                 /* inspected/selected chunk */
3326   INTERNAL_SIZE_T size;             /* its size */
3327   int victim_index;                 /* its bin index */
3328 
3329   mchunkptr remainder;              /* remainder from a split */
3330   unsigned long remainder_size;     /* its size */
3331 
3332   unsigned int block;               /* bit map traverser */
3333   unsigned int bit;                 /* bit map traverser */
3334   unsigned int map;                 /* current word of binmap */
3335 
3336   mchunkptr fwd;                    /* misc temp for linking */
3337   mchunkptr bck;                    /* misc temp for linking */
3338 
3339   const char *errstr = NULL;

먼저 자료형에 대해 설명하자면 'mbinptr', 'mchunkptr' 둘 다 typedef struct malloc_chunk* 로 선언되었습니다.

typedef struct malloc_chunk* mchunkptr;
typedef struct malloc_chunk *mbinptr;

'INTERNAL_SIZE_T'는 size_t 자료형과 같으며 이는 x86의 경우 4, x86-64의 경우 8 byte의 크기를 가집니다.

다음으로 요청된 size가 유효한 범위에 해당하는지, 사용 가능한 arena가 존재하는지 확인하는 구문이 존재합니다.

3341   /*
3342      Convert request size to internal form by adding SIZE_SZ bytes
3343      overhead plus possibly more to obtain necessary alignment and/or
3344      to obtain a size of at least MINSIZE, the smallest allocatable
3345      size. Also, checked_request2size traps (returning 0) request sizes
3346      that are so large that they wrap around zero when padded and
3347      aligned.
3348    */
3349 
3350   checked_request2size (bytes, nb);
3351 
3352   /* There are no usable arenas.  Fall back to sysmalloc to get a chunk from
3353      mmap.  */
3354   if (__glibc_unlikely (av == NULL))
3355     {
3356       void *p = sysmalloc (nb, av);
3357       if (p != NULL)
3358         alloc_perturb (p, bytes);
3359       return p;
3360     }

checked_request2size() 의 경우 define을 통해 macro로 정의되어 있으며 그 코드는 아래와 같습니다.

#define checked_request2size(req, sz)                             \
  if (REQUEST_OUT_OF_RANGE (req)) {                                           \
      __set_errno (ENOMEM);                                                   \
      return 0;                                                               \
    }                                                                         \
  (sz) = request2size (req);

요청된 크기가 유효한 범위 내에 존재할 경우 alignment를 위해 가공한 size를 반환합니다.

사용 가능한 arena가 존재하는지 확인하는 구문의 경우 인자로 전달받은 av 값이 NULL일 경우 sysmalloc() 을 호출하여 mmap() 으로부터 chunk를 가져옵니다. 이 과정에서 사용되는 __glibc_unlikely() 의 경우 Kernel 상에서 효율성을 위한 목적으로 사용 가능한 함수로 자세한 내용은 링크에서 참조할 수 있습니다.


Reference

좋은 웹페이지 즐겨찾기