what is tcache key ( tcache dup mitigation ) & bypass
알다시피 원래 tcache는 하나의 취약한 부분중 하나였다. 그중 하나의 취약점이
tcache dup ( double free ) 였다. 이는 너무나 큰 취약점이었으며, 이를 mitigation하기 위해서 2.29 버전 부터 key의 개념이 생기기 시작한다.
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;tcache_entry 구조체의 기본 구조는 다음 tcache_entry 구조체의 주소를 나타내는 next와 key로 각각 8 바이트씩 16바이트이다. 이 위에 헤더 부분은 당연히 속도, 효율 위주인 tcache에서 같이 붙어있는 하나의 데이터일뿐 전혀 신경쓰지를 않는다.
pwndbg> x/6gx 0x4052d0 -0x10
0x4052c0: 0x0000000000000000 0x0000000000000031
0x4052d0: 0x0000000000000405 0xfe4406d6c47c42ca
0x4052e0: 0x0000000000000000 0x0000000000000000위에 free 된 tcache chunk를 보면 key 부분의 8바이트 난수가 들어가 있다.
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache_key;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
이는 e->key = tcache_key; 이 부분에서 들어가게 되는데
static void
tcache_key_initialize (void)
{
if (__getrandom (&tcache_key, sizeof(tcache_key), GRND_NONBLOCK)
!= sizeof (tcache_key))
{
tcache_key = random_bits ();
#if __WORDSIZE == 64
tcache_key = (tcache_key << 32) | random_bits ();
#endif
}
}
다음과 같은 함수에서 난수를 가져와서 만든다.
해당 key는 프로세스의 전역으로 하나만 만들어진다. 즉, 모든 tcache가 저 key를 공유하게된다.
그럼 어떻게 dup를 판단하냐고 할 수 있는데 생각보다 간단하다
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
mchunkptr nextchunk;
중략
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache_key))
{
tcache_entry *tmp;
size_t cnt = 0;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next), ++cnt)
{
if (cnt >= mp_.tcache_count)
malloc_printerr ("free(): too many chunks detected in tcache");
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
}
if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}다음은 _int_free 함수 즉, free 함수의 초반 진행인데
if (__glibc_unlikely (e->key == tcache_key)) 으로 key 필드가 key와 같으면, glibc 입장에서는 어? 얘 왜 key 필드의 우리상수가 있어? 예전에 한번 tcache bin 에 들어 왔던 얘인데? 라고 판단해서 검사 루틴을 실행한다.
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next), ++cnt)
{
if (cnt >= mp_.tcache_count)
malloc_printerr ("free(): too many chunks detected in tcache");
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}그리고 이제 다음과 같이 같은 크기의 tcache bin을 돌면서 같은 포인터가 있으면 double free 오류를 내서
malloc_printerr 함수 내에서 프로세스를 죽여버린다.
how to bypass?
따라서 우리는 해당 key값을 다른 값으로 바꿀 수 있다면, 저 검사 루틴을 거치지 않고 free 입장에서는 전에는 key 필드에 일반데이터가 있는 클린한 영역인줄 알고 free를 하게 해준다. 따라서 dup가 가능해진다