hogbal 2022. 12. 6. 11:30

해당 페이지는 dreamhack.io 사이트의 강의를 참고해 정리하고 이해못한 내용을 추가로 작성해둔것이니 참고사이트의 사이트를 참고하시기바랍니다.

glibc는 계속해서 업데이트가되고 있다 아래의 내용은 glibc 2.26 버전의 내용이고 glibc 2.29부터는 달라지게 된다.

tcacheglibc 2.26 버전 이상부터 적용된 기술이다. tcacheThread local caching)의 약자로 멀티 스레드 환경에서 메모리 할당 속도를 높이기 위해 적용되었다. 그렇기 때문에 보안이 취약하다.

우선 malloc 함수를 통해 동적 할당 요청이 들어오면 __libc_malloc 함수가 호출된다.__libc_malloc 함수는 MAYBE_INIT_TCACHE 매크로를 호출하여 tcache_init 함수를 호출한다.

#define MAYBE_INIT_TCACHE() \
  if (__glibc_unlikely (tcache == NULL)) \
    tcache_init();
void * __libc_malloc (size_t bytes)
{
    ...
    MAYBE_INIT_TCACHE ();
}

tcache_init 함수에서는 tcache_perthread_struct 구조체를 힙영역에 할당하고 초기화하는 역할을 하게 된다. 해당 구조체는 힙 페이지의 맨 첫부분에 할당된다.

static void
tcache_init(void)
{
  mstate ar_ptr;
  void *victim = 0;
  const size_t bytes = sizeof (tcache_perthread_struct);
  if (tcache_shutting_down)
    return;
  arena_get (ar_ptr, bytes);
  victim = _int_malloc (ar_ptr, bytes);
  if (!victim && ar_ptr != NULL)
    {
      ar_ptr = arena_get_retry (ar_ptr, bytes);
      victim = _int_malloc (ar_ptr, bytes);
    }
  if (ar_ptr != NULL)
    __libc_lock_unlock (ar_ptr->mutex);
  if (victim)
    {
      tcache = (tcache_perthread_struct *) victim;
      memset (tcache, 0, sizeof (tcache_perthread_struct));
    }
}

tcache_perthread_struct 구조체는 tcache_entry를 관리하기 위해 사용된다. 이전 버전에서 할당된 힙은 main_arena가 관리했지만 glibc 2.26 버전 이후부터 할당된 tcache의 힙은 tcache_perthread_struct가 관리하게 된다.

typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

tcache_entry 구조체의 멤버 변수인 next 포인터는 tcache->entries의 연결 리스트를 관리한다.

typedef struct tcache_entry
{
  struct tcache_entry *next;
} tcache_entry;

tcache_put

tcache_put 함수에서는 해제 요청이 들어오면 tcache->entries에 해제된 힙 청크의 주소를 추가한다. 아래의 코드는 tcache_put 함수가 호출되는 과정이다.

# define TCACHE_MAX_BINS        64
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
#if USE_TCACHE
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)         
{
    mchunkptr tc_victim;
    /* While bin not empty and tcache not full, copy chunks over.  */
    while (tcache->counts[tc_idx] < mp_.tcache_count && (pp = *fb) != NULL)
    {
        REMOVE_FB (fb, tc_victim, pp);
        if (tc_victim != 0)
        {
            tcache_put (tc_victim, tc_idx);
        }
}

청크의 크기를 이용해 tc_idx를 계산한 후 tcache->counts[tcidx] < mp_.tcache_count 조건을 만족한다면 tcache_put 함수를 호출하는 것을 확인할 수 있다.
mp_ 구조체를 살펴보면 tcache_count 멤버는 .tcache_count = TCACHE_FILL_COUNT로 인해 7이란 값으로 초기화되어 있다.
해당 코드를 통해 동일한 크기의 tcache->entries는 7개의 힙 청크만 관리한다는 것을 알 수 있고, fastbinsmallbin 크기의 청크가 해제되면 먼저 tcache가 관리한다.
free함수가 호출되어 해제된 힙 청크의 주소를 tcache->entries에 삽입할 때에는 해당 힙 청크의 크기만 검증하고, Double Free에 대한 검증은 존재하지 않는다.

static void tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

tcache_get

tcache_get 함수에서는 저장되어 있는 tcache->entries에서 힙 청크를 가져온다. 이후 가져온 힙 청크의 next 포인터를 tcache->entries에 삽입하고 힙 청크의 주소를 반환한다. 다음은 tcache_get 함수가 호출되는 과정이다.

size_t tbytes = request2size (bytes);
size_t tc_idx = csize2tidx (tbytes);
MAYBE_INIT_TCACHE ();
if (tc_idx < mp_.tcache_bins
    && tcache
    && tcache->entries[tc_idx] != NULL)
{
    return tcache_get (tc_idx);
}

요청이 들어온 사이즈에 맞는 tcache_entry가 존재한다면 tcache_get 함수가 호출된다.

static void *tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  return (void *) e;
}

참고사이트

-https://learn.dreamhack.io/16#76