이번에는 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 구조체를 힙 영역에 할당하고 초기화하는 역할을 한다.
해당 구조체는 힙 페이지의 맨 첫 부분에 할당된다.
<틀린 부분이 있다면 비난과 욕설을 해주세요>
'포너블 > Heap' 카테고리의 다른 글
UAF (0) | 2023.11.07 |
---|---|
Tcache function(2.31) (0) | 2023.09.22 |
Tcache (0) | 2023.09.12 |
free 소스코드 분석(libc 2.27) (0) | 2023.08.28 |
malloc 소스코드 분석(libc 2.27) (0) | 2023.08.23 |