Daily Heap #6
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()
의 호출 과정을 간단하게 정리하였습니다.
- __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
변수의 선언을 완료한 뒤 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
Author And Source
이 문제에 관하여(Daily Heap #6), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@insp3ct0r_/Daily-Heap-6
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
Author And Source
이 문제에 관하여(Daily Heap #6), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@insp3ct0r_/Daily-Heap-6저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)