rosieblue
article thumbnail
728x90

배경지식 : Double Free Bug

[Heap] Exploitation : Double Free Bug

 

[Heap] Exploitation : Double Free Bug

Double Free Bug Double Free Bug(DFB)는 free(ptr1); free(ptr1);처럼 같은 메모리를 여러번 free했을 때 나타나는 버그이다. 이게 왜 문제인지는 아래에서 설명할 것임 free(ptr1); free(ptr1); free 함수는 포인터를 초

hannahsecurity.tistory.com

Duble Free Bug에 대해 모르는 분들은 위 글 꼭읽고 오기!

 

 

다시 한번 DFB에 대해 복습해보자. Double Free Bug은 free등으로 동일한 힙 메모리를 중복으로 해제했을 때 일어나는 문제점이었다.

중복으로 해제하면 freelist에 중복으로(0x1234->0x1234) 적히게 된다. 이후 재할당을 하면 해당 힙 메모리가 재할당되면서 freelist에도 동시에 남아있게 된다.(0x1234) 재할당을 통해 해당 메모리에 접근할 수 있기 때문에 청크의 fd(혹은 next)등을 조작하여 freelist에 새로운 요소를 추가하고 이를 다시 재할당한다면 임의의 메모리 주소에 데이터를 삽입하거나 수정할 수 있어 문제가 되었었다.

 

이때 freelist가 fastbin인경우는 저번에 다뤘으니 혹시 궁금하면 읽어보자 -> [Heap] Exploitation : fastbin dup & poisoning 

 

[Heap] Exploitation : fastbin dup & poisoning

이번 포스트는 [Heap] Exploitation : Double Free Bug을 알아야 읽을 수 있다. Fastbin dup & poisoning Fastbin dup fastbin dup은 double free bug을 통해 이미 할당된 청크에 다른 청크를 또 할당하는 기법이다. 아까 위에

hannahsecurity.tistory.com

오늘은 fastbin이 아니라 tcache에서 dfb가 일어나는 경우를 다룰 것이다.

 

 

이 글은 glibc2.27 버전 기준으로 작성되었다. 다른 버전으로 실습을 진행할 시 패치가 달라 안될 가능성이 높으므로 꼭 해당 환경에서 실습해주기! (ubuntu 18.04) -> 해당 버전 도커 설치

Mitigation for Tcache DFB

tcache에 대해서는 따로 포스트를 작성 중이다ㅠㅠ tcache에 대하여 간단히 말해서는 glibc 2.27이상부터 arena의 단점을 보완하기 위해 도입된 heap에서의 멀티스레딩을 위한 bin이라고 생각하면 된다. 각 스레드 별로 tcache가 존재하기 때문에 다른 thread와 충돌하지 않고 heap freelist를 관리할 수 있게 된다.

이제부터는 tcache에서 DFB를 막기 위해 도입합 mitigation 기법을 보도록 하겠다.

 

tcache_entry

tcahce_entry는 간단하게 청크와 비슷한 것이라고 보면된다. tcache에 들어가는 요소들을 tcache라고 부르는 것이다.

구조를 살펴보면 아래처럼 다음 tcache_entry를 가리키는 포인터인 next와, DFB를 막기 위해 도입된 key가 있다.

 

next는 일반 청크의 FD부분에 해당한다.

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

 

tcache에 들어갔을 때, (즉 free되었을 때) key에는 tcache(tcache_perthread 구조체의 주소)가 적히고,

다시 tcache에서 빠져나와 재할당되었을 때NULL 값이 적히게 된다. 이에 대해서는 아래에 실습을 통해 다시 확인하겠다.

 

tcache_put

tcache_put은 청크가 tcache에 들어갈 때 사용되는 함수이다. 즉 free될때 사용되는 것이다.

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

여기서 e->key=tcache라는 부분이 있는데, 이는 tcache에 들어온 청크의 key에는 tcache를 대입한다는 뜻이다.

위에서 말한 것처럼 이 tcache라는 변수는 tcache_perthread 구조체의 주소를 나타낸다. 

'key가 tcache'인 경우, free되어 tcache안에 들어있는 것으로 취급한다는 뜻이다.

그렇기 때문에 만약 free를 할 때 이 청크의 key값이 tcache라면 이미 free된 것을 의미하기 때문에 Double Free가 일어났다고 판단할 수 있게 되는 것이다.

 

tcache_get

tcache_get은 tcache에서 청크를 꺼낼 때 사용하는 함수다. 즉 재할당될 때 사용하는 것이다.

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

위에서 보면 e->key=NULL;를 해주고 있다. 따라서 재할당되면 key값이 NULL이 되는 것이다.

 

_int_free

_int_free는 청크를 해제할 때 호출하는 함수이다. 

_int_free (mstate av, mchunkptr p, int have_lock)
 #if USE_TCACHE
   {
     size_t tc_idx = csize2tidx (size);
-
-    if (tcache
-       && tc_idx < mp_.tcache_bins
-       && tcache->counts[tc_idx] < mp_.tcache_count)
+    if (tcache != NULL && tc_idx < mp_.tcache_bins)
       {
-       tcache_put (p, tc_idx);
-       return;
+       /* 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.  */
+         }
+
+       if (tcache->counts[tc_idx] < mp_.tcache_count)
+         {
+           tcache_put (p, tc_idx);
+           return;
+         }
       }
   }
 #endif

 

if (__glibc_unlikely (e->key == tcache)) 라는 조건문을 통과하면 DFB가 일어났다고 판단하고 프로그램을 종료시킨다. 왜냐하면 key가 tcache라는 것은 이미 해당 청크가 tcache에 속해있다는 것을 의미하기 때문이다. 하지만 key가 tcache가 아니기만 한다면, 즉 해제된 청크의 key값을 1bit만이라도 바꿀 수 있다면 바로 저 보호 기법을 우회할 수 있게 된다.

 

 

실습

실습 코드는 아래의 코드를 사용할 것이다. 그냥 말그대로 double free만 하는 간단한 프로그램이다.

#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를 한 후 청크(0x555b070f5260)의 정보를 출력해보았다. (참고로 청크 시작은 0x555b070f5250이지만, tcache_entry는 데이터부분부터 시작하므로 0x555b070f5260이기 때문에 0x555b070f52060으로 설정해주는 것을 유의하자!!!)

이처럼 set을 이용하면 저렇게 타입에 맞게 변수를 설정할 수 있다!!

key에는 0x555b070f5010라는 값이 적힌 것을 볼 수 있다.

아래처럼 메모리 덤프를 통해서도 확인 가능하다.

next(여기서는 0) 바로 다음 부분에 key가 적힌 부분을 볼 수 있다!

 

그러면 이 key(0x555b070f5010)가 무엇을 의미하는지 보자. 느낌상 힙메모리 안의 주소같아 보인다!

tcache명령어를 통해 출력된 결과와(peda에서는 안먹히고 기본 gdb에서만 tcache 명령어 사용가능), 0x555b070f5010의 메모리 내용을 출력한 것의 결과가 완전히 동일하다.

즉, key에 적힌 0x555b070f5010은 tcache (tcache_perthread_struct)의 주소와 같다는 것을 확인할 수 있다.

 

이상태에서 free를 다시 하면 key값이 tcache로 되어있으므로 아래처럼 프로그램이 종료되게 된다.

 

 

Tcache Duplication

아래는 key값을 바꿔서 위 mitigation을 우회하는 코드이다.

// 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;
}

 

*(char*)(chunk+8)=0xff라는 코드를 통해 데이터부분시작 위치 + 8byte 부분에 0xff를 넣어줬다. 이부분이 key가 시작하는 위치인데 한 바이트만 바뀌어도 보호 기법을 우회할 수 있다.

 

 

위처럼 double free가 성공한 것을 볼 수 있다.

profile

rosieblue

@Rosieblue

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!