이번 글을 시작으로 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()
의 호출 과정을 간단하게 정리하였습니다.
- __libc_malloc() 호출
- _malloc_hook이 NULL이 아닌지 검증
- NULL이 아닐 경우 _malloc_hook의 값을 함수 포인터로 지정하여 실행
- NULL일 경우 _int_malloc() 호출 과정으로 이동
- _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 값이 NULL이 아닌지 검사를 진행합니다. 기본적으로 NULL 값으로 초기화 되어있기 때문에 NULL이 아닌 경우에 대해서는 Hook Overwrite에서 설명을 진행하겠습니다.
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()
함수가 호출되었습니다. 실제 구현 코드의 양이 매우 많기에 임의로 구분지어 분석을 진행하였습니다.
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 상에서 효율성을 위한 목적으로 사용 가능한 함수로 자세한 내용은 링크에서 참조할 수 있습니다.