[DH] Memory Corruption: Double Free Bug

정말 간만의 포너블 공부.. mac 사고 포너블 세팅을 미루다 미루다가 깃블로그 만든 김에 슈슈슉 해버리자 다짐했다.

free 함수로 청크 해제 -> ptmalloc2는 이를 tcache나 bins에 추가/관리 tcache나 bins를 어떠한 List라고 했을 때, free는 청크를 추가하는 함수, malloc은 청크를 꺼내는 함수이다. 임의의 청크에 대해 free는 두 번 적용이 가능하다. 왜냐면 프리했다가 말록한 거 다시 프리할 수 있으니까.

만약 청크가 중복되면 duplicated 되었다고 하는데, 이 duplicated free list를 활용하면 임의 주소에 청크를 할당할 수 있다고 한다(어떻게?) 그래서 이 임의 주소에 할당한 청크 값을 읽거나 작성하는 공격을 진행할 수 있다.

단순히 요약하자면, 같은 청크를 중복해서 해제할 수 있으면 DFB로 분류한다.

Double Free Bug

ptmalloc2에서 발생하는 버그이며, 공격자가 임의 주소 쓰기/읽기, 코드 실행 등으로 활용될 수 있다.

DFB는 보통 코드상에 아래 방법으로 살펴볼 수 있다.

  1. dangling pointer가 생성되는가?
  2. 이를 대상으로 Free 호출이 가능한가?

이 double free bug로는 duplicated free list 생성이 가능하다. ptmalloc2에서 Free list의 각 청크는 fd와 bk로 연결된다.

fd
자신보다 나중에 해제된 청크
bk
자신보다 먼저 해제된 청크

해제된 청크는 각각의 공간을 그냥 데이터 저장에만 사용한다. 때문에 어떤 청크가 Free list에 중복되어 포함된다면, 첫번째 재할당에서 fd와 bk를 조작하여 임의 주소를 삽입할 수 있다.

// Name: dfb.c
// Compile: gcc -o dfb dfb.c

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

int main() {
  char *chunk;
  chunk = malloc(0x50);

  printf("Address of chunk: %p\n", chunk);

  free(chunk);
  free(chunk); // Free again
}

위 코드에서 malloc으로 청크 꺼내오고, 이 청크 free를 두 번 진행한다.

Mitigation for Tcache DFB

static

패치된 Tcache의 코드를 살펴보자.

tcache_entry

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

해당 구조체에 double frees를 탐지하기 위한 key 포인터가 추가되었다. 이 구조체는 해제된 Tcache 청크들이 갖는 구조이다. fd가 Next로 대체된다.

tcache_put

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에 추가하는 함수가 tcache_put이다. 왼쪽에 +로 된 부분을 보면, e->key에는 tcache를 넣도록 되어 있다. 이때 tcache의 타입은 key의 포인터로 선언했던 tcache_perthread_struct이다.

tcache_get

tcache_get (size_t tc_idx)
   assert (tcache->entries[tc_idx] > 0);
   tcache->entries[tc_idx] = e->next;
   --(tcache->counts[tc_idx]);
+  e->key = NULL;
   return (void *) e;

tcache_get은 tcache에 연결된 청크를 재사용할 때 사용하는 함수이다. e->key를 Null로 설정한다.

_int_free

청크를 해제할 때는 위 _int_free 함수를 호출한다. 이부분이 중요한데, 청크의 Key값을 참조하여 tcache값이 담겨 있으면 DF로 취급하여 abort시킨다.

+       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 Duplication

위에서 살펴보았던 보호 기법을 우회하여 트리거하는 코드이다.

// Name: tcache_dup.c
// Compile: gcc -o tcache_dup tcache_dup.c

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

int main() {
  void *chunk = malloc(0x20);
  printf("Chunk to be double-freed: %p\n", chunk);

  free(chunk);

  *(char *)(chunk + 8) = 0xff;  // manipulate chunk->key
  free(chunk);                  // free chunk in twice

  printf("First allocation: %p\n", malloc(0x20));
  printf("Second allocation: %p\n", malloc(0x20));

  return 0;
}

free를 하고, chunk+8이 Key의 위치이기 때문에 이부분을 Null로 바꿔준다. 이후에 free를 하면 double free bug를 트리거할 수 있다.