[포너블] Memory Corruption: Double Free Bug

Chris Kim·2024년 10월 25일

시스템해킹

목록 보기
26/33

출처: 드림핵 강의

0. 서론

임의의 청크에 대해 free를 두 번이상 적용할 수 있다는 것은. 같은 청크를 free list에 여러번 추가할 수 있음을 의미한다. 해커들은 free list에 중복해서 존재하는(duplicated) 청크를 통해 임의 주소에 청크를 할당할 수 있음을 밝혀냈다.
하지만 Glibc 버전이 높아질 수록 새로운 보호 기법과 공격 기법의 복잡도는 더욱 높아졌다. 이번에는 Glibc 2.27버전이 내장된 우분투 18.04 64bit 환경을 기준으로 설명한다.

Dockerfile

FROM ubuntu:18.04

ENV PATH="${PATH}:/usr/local/lib/python3.6/dist-packages/bin"
ENV LC_CTYPE=C.UTF-8

RUN apt update
RUN apt install -y \
    gcc \
    git \
    python3 \
    python3-pip \
    ruby \
    sudo \
    tmux \
    vim \
    wget

# install pwndbg
WORKDIR /root
RUN git clone https://github.com/pwndbg/pwndbg
WORKDIR /root/pwndbg
RUN git checkout 2023.03.19
RUN ./setup.sh

# install pwntools
RUN pip3 install --upgrade pip
RUN pip3 install pwntools

# install one_gadget command
RUN gem install elftools -v 1.1.3
RUN gem install one_gadget -v 1.9.0

WORKDIR /root

1. Double Free Bug

1.1 개념

DFB는 dangling pointer가 생성되는지, 그리고 이를 대상으로 free를 호출하는 것이 가능한지 살피면 DFB가 존재하는지 가늠할 수 있다. 이를 통해 duplicated free list를 만들 수 있다. 중복된 청크를 재할당에서 fd, bk를 조작, free list에 임의 주소를 포함시킬 수 있다.
tcache 도입 초기에는 double free가 잘 먹혔지만, 시간이 흐르면서 보호기법이 등장하고 이를 우회하지 않으면 청크를 두 번 해제하려는 즉시 프로세스가 종료된다.

1.2 Tcache Double Free

아래 코드는 같은 청크를 두 번 해제하는 예제 코드다.

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

이를 실행하면 비정상 종료가 이뤄진다.

2. Mitiation for Tcache DFB

2.1 정적 패치 분석

자세한 내용은 여기

2.1.1 tcache_enrty

하단 코드를 보면 key 포인터가 tcache_entry에 추가되었다. tcache_entry는 해제된 tcache 청크들이 갖는 구조다. 일반 청크의 fdnext로 대제되며, LIFO 구조를 가지므로 bk 대응 값은 없다.

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

2.1.2 tcache_put

이 함수는 해제한 청크를 tcache에 추가하는 함수다. 하단 코드를 보면 keytcache라는 값을 대입한다. tcachetcache_perthread라는 구조체 변수를 가리킨다.

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

2.1.3 tcache_get

tcache에 연결된 청크를 재사용할 때 사용하는 함수 e->key = NULL 을 실행하고 있다.

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

2.1.4 _int_free

_int_free 는 청크를 해제할 때 호출되는 함수로 하단 코드를 보면 재할당하려는 청크의 key값이 tcache면 Double Free가 발생한 것으로 판단하고 프로그램을 abort 한다. 이 부분 외에는 보호기법이 없으므로 이 부분만 우회하면 된다.

_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

2.2 동적 분석

청크 할당 직후의 청크 정보를 heap으로 조회해보자.

0x555555602250malloc(0x50)으로 할당된 청크의 주소다. 이 메모리 값을 덤프해보면 아무런 값도 없는 것을 확인할 수 있다.

이 청크를 chunk 변수로 정의하고 넘어가자.

청크 해제까지 프로그램을 실행하고 청크 메모리를 출력해보자. 그리고 key에 담긴 주소를 조회하면 chunk의 주소가 entry에 포함되어 있음을 알 수 있다. 이는 tcache_perthread에 tcache들이 저장되기 때문이다.

2.3 우회 기법

if (__glibc_unlikely (e->key == tcache))만 우회한다면 tcache 청크를 double free 시킬 수 있다. 즉 해제된 청크의 key값을 1비트만이라도 바꾸면 된다.

3. Tcache Duplication

3.1 DFB 트리거 코드

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

3.2 실행 결과


chunktcache에 중복 연결되어 연속으로 재할당 된다.

profile
회계+IT=???

0개의 댓글