포너블/Heap

Tcache function

K0n9 2023. 9. 22. 10:43

이번에는 Ubuntu GLIBC 2.27-3ubuntu1.2 기준 Tcache에 관한 함수들을 설명하겠다. 

 

tcache_entry

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

tcache chunk의 list를 만드는 요소이다. next 포인터로 연결리스트를 관리한다.


tcache_perthread_struct

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

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

 


tcache_put

(tcache안에 넣을 때 호출되는 함수)

static __always_inline 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]);
}

포인터 e에 chunk의 fd부분을 반환한다.

assert로 tc_idx가 최대 크기를 넘는지 검사한다

e→next에는 tcache→entries[tc_idx]를 넣는다. 

tcache→entires[tc_idx] 즉 제일 위에있는거는 e로 갱신

tcache→counts를 1 증가

정리하자면 해제 요청이 들어왔을 때 tcache→entries 에 해제된 힙 청크의 주소를 추가하는 과정이다.


아래는 glibc 2.26 _int_malloc의 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→count[tc_idx] < mp_.tcache_count && (pp = *fb) != NULL

tcache_count 는 .tcache_count = TCACHE_FILE_COUNT로 인해 7이란 값으로 초기화되어있다. 즉 동일한 크기의 tcache→entries는 7개의 청크만 관리한다는 것을 알 수 있고 위의 코드는 그것을 검증하는 것이다.

이렇게 tcache에 삽입을 할 때 힙 청크의 크기만 검증하고, Double Free에 대한 검증은 존재하지 않는다.

 


tcache_get

(tcache안의 chunk를 제거 할 때)

static __always_inline 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;
}

e에다가 가장 위에거 저장하고

assert로 오류검출

tcache→entries[tc_idx] 를 e→next ,즉 가장위의 다음 chunk를 저장, 다시말해 2번째 애를 가장 최근에 들어온 얘로함

그리고 tcache→counts를 1 감소

그리고 제거된 chunk를 반환

정리하자면 할당 요청이 들어왔을 때 tcache→entries 에 해제된 힙 청크의 주소를 반환하는 과정이다.


아래는 glibc 2.26 _libc_malloc의 tcache_put 함수가 호출되는 과정이다.

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 함수가 호출된다. 해당 코드에는 별 다른 예외 처리가 존재하지 않기 때문에 공격이 쉽다.

 


tcache_thread_shutdwon

(tcache를 완전히 종료시킬때)

static void
tcache_thread_shutdown (void)
{
  int i;
  tcache_perthread_struct *tcache_tmp = tcache;

  if (!tcache)
    return;

  /* Disable the tcache and prevent it from being reinitialized.  */
  tcache = NULL;
  tcache_shutting_down = true;

  /* Free all of the entries and the tcache itself back to the arena
     heap for coalescing.  */
  for (i = 0; i < TCACHE_MAX_BINS; ++i)
    {
      while (tcache_tmp->entries[i])
			{
			  tcache_entry *e = tcache_tmp->entries[i];
			  tcache_tmp->entries[i] = e->next;
			  __libc_free (e);
			}
    }

  __libc_free (tcache_tmp);
}

tcache_tmp 포인터에 tcache를 우선 저장한다

만약 tcache가 이미 0이면 그냥 return한다

그게 아니면 우선 tcache에 null을 저장하고

tcache_shutting_down 를 true 로 한다

그 다음은 tcache의 모든 bin의 모든 chunk를 free하는 과정이다. 대충 tcache_get을 모든 bin의 모든 chunk에 한다고 보면 될거같다.

그리고 __libc_free 를 실행한다.

 


tcache_init

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);

  /* In a low memory situation, we may not be able to allocate memory
     - in which case, we just keep trying later.  However, we
     typically do this very early, so either there is sufficient
     memory, or there isn't enough memory to do non-trivial
     allocations anyway.  */
  if (victim)
    {
      tcache = (tcache_perthread_struct *) victim;
      memset (tcache, 0, sizeof (tcache_perthread_struct));
    }

}

우선 tcache_shutting_down을 체크하여 tcache가 꺼져있으면 그냥 return 하여 아무것도 하지 않는다. tcache가 꺼져있지 않으면 arena_get과 _int_malloc으로 할당된 주소를 가져온다.(만약 안되면 재시도 한다.) 그리고 unlock도 해준다. 그리고 memset으로 메모리를 초기화 해준다.

정리하자면 tcache_perthread_struct 구조체를 힙 영역에 할당하고 초기화하는 역할을 한다.

해당 구조체는 힙 페이지의 맨 첫 부분에 할당된다.

 

 

 

 

 

<틀린 부분이 있다면 비난과 욕설을 해주세요>