PWNABLE] tcache bin attack - glibc-2.30

노션으로 옮김·2020년 4월 21일
1

skills

목록 보기
25/37
post-thumbnail

개요

이전에 공부한 기법으로 로컬 환경(glibc-2.30)에서 ctf 문제를 풀어보니 동작하지 않았다.

버전 차이가 얼마 안났기에 패치됐을 거라고 생각을 못했는데, 직접 디버깅 및 소스를 확인해보니 약간의 차이가 있었고 그것을 정리하는 글이다.


glibc-2.30

Check double freed chunk

glibc-2.30malloc.c를 보면

#if USE_TCACHE

/* We overlay this structure on the user-data portion of a chunk when
   the chunk is stored in the per-thread cache.  */
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
{
  uint16_t counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

tcache_entrykey라는 요소가 생겼다.

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;

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

tcache_put()이 수행될 때, e->key의 값을 tcache로 설정한다.

#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))
	  {
	    tcache_entry *tmp;
	    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
	    for (tmp = tcache->entries[tc_idx];
		 tmp;
		 tmp = tmp->next)
	      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_put()이 호출되기 전에 실행되는 코드를 보면, double free bug를 검사하는 코드가 추가되었음을 알 수 있다.

앞서 봤듯이 tcache_put()에서 e->keytcache로 설정했다.
만약 이미 free된 청크라면 값이 일치하므로

if (__glibc_unlikely (e->key == tcache))

이 조건문이 실행될 것이다.
조건문 안에서는 tcache bin entries에 대해 루프를 돌며 현재 청크와 동일한 청크가 있는지 검사하게 된다.

만약 double free된 청크라면 청크가 이미 bin에 존재할 것이고
malloc_printerr()가 호출되면서 에러가 발생할 것이다.

Check prev chunk's size

이전에 off-by-one을 이용하여 libc 주소를 leak할 때 다음과 같은 방법을 사용했었다.

1.chunk_AAA, chunk_BBB, chunk_CCC를 선언한다.
2.chunk_AAA를 free시킨다.
3.chunk_BBB를 오버플로우시켜 chunk_CCCprev_inuse를 0으로 설정하고, prev_sizechunk_AAAchunk_BBB의 크기를 합친 값으로 설정한다.
4.이제 chunk_CCC를 free시키면 A,B,C가 모두 병합된다.
5.chunk_AAA를 다시 할당해서 chunk_AAA에 위치한 fd,bkchunk_BBB로 옮긴다.
6.chunk_BBB를 프린트하여 libc 값을 읽는다.

chunk_BBB를 free된 것처럼 속이기 위해 chunk_Cprev_size값만 수정하면 됬었다.

하지만, 다음과 같이 코드가 변경되었다.

기존의 코드를 보면

    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }

병합할 때 chunk_CCCprev_inuse만 검사하여, free된 청크일 경우 바로 병합시켰다.

그러나 glibc-2.30의 코드를 보면

    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
      unlink_chunk (av, p);
    }

병합되는 이전 청크의 current_size를 검사한다.
chunk_AAA의 크기를 검사하는 것이다.

위 상황에서

chunk_AAA는 0x120, chunk_BBB는 0x120, chunk_CCC는 오버플로우 되어 0x100이라고 할 때

chunk_CCCprev_size를 0x240으로 설정하는 것 뿐만 아니라
cuhnk_AAA의 사이즈를 0x240으로 변경해줘야 병합된다.

0개의 댓글