what is tcache key ( tcache dup mitigation ) & bypass

알다시피 원래 tcache는 하나의 취약한 부분중 하나였다. 그중 하나의 취약점이

tcache dup ( double free ) 였다. 이는 너무나 큰 취약점이었으며, 이를 mitigation하기 위해서 2.29 버전 부터 key의 개념이 생기기 시작한다.

C
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에서 같이 붙어있는 하나의 데이터일뿐 전혀 신경쓰지를 않는다.

C
pwndbg> x/6gx 0x4052d0 -0x10
0x4052c0:	0x0000000000000000	0x0000000000000031
0x4052d0:	0x0000000000000405	0xfe4406d6c47c42ca
0x4052e0:	0x0000000000000000	0x0000000000000000

위에 free 된 tcache chunk를 보면 key 부분의 8바이트 난수가 들어가 있다.

C
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; 이 부분에서 들어가게 되는데

C
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를 판단하냐고 할 수 있는데 생각보다 간단하다

C
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 에 들어 왔던 얘인데? 라고 판단해서 검사 루틴을 실행한다.

C
  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가 가능해진다