0 views
들어가며
1. 서론

free함수로 청크를 해제하면, ptmalloc2는 이를 tcache나 bins에 추가하여 관리한다.- 그리고 이후에
malloc으로 비슷한 크기의 동적 할당이 발생하면, 이 연결 리스트들을 탐색하여 청크를 재할당해준다. - 이 메커니즘에서, 해커들은
free로 해제한 청크를free로 다시 해제했을 때 발생하는 현상에 주목했다.
- 그리고 이후에
- Tcache와 bins를 free list라고 통칭한다면, free list의 관점에서
free는 청크를 추가하는 함수,malloc은 청크를 꺼내는 함수이다.- 그러므로, 임의의 청크에 대해
free를 두 번이상 적용할 수 있다는 것은, 같은 청크를 free list에 여러 번 추가할 수 있음을 의미한다.
- 그러므로, 임의의 청크에 대해
- 청크가 free list에 중복해서 존재하면 청크가 duplicated 됐다고 표현하는데, 해커들은 duplicated free list를
이용하면 임의 주소에 청크를 할당할 수 있음을 밝혀냈다.
- 이렇게 할당한 청크의 값을 읽거나 조작함으로써 해커는 임의 주소 읽기 또는 쓰기를 할 수 있다.
- 자세한 방법과 원리에 대해서는 이번 강의와 다음 강의를 통해 천천히 설명하겠다.
- 이상의 이유로, 같은 청크를 중복해서 해제할 수 있는 코드는 보안상의 약점으로 분류되어 Double Free Bug (DFB)
라고 불린다.
- 이번 강의에서는 Double free bug가 발생하는 원인과 효과, 그리고 이를 막기 위해 도입된 보호 기법에 대해 알아보겠다.
- 힙에 대한 공격 기법은 연구되고 발전하면서 힙과 관련된 새로운 보호 기법들이 탄생하게 되었다.
- 그로 인해 Glibc 버전이 높아질수록 공격의 복잡도가 올라가며, 그만큼 공격 기법을 이해하기가 어려워진다.
- 따라서 본 강의에서는 실습의 편의상 Glibc 2.27 버전이 내장된 Ubuntu 18.04 64-bit 환경을 기준으로 설명한다.
2. 실습 환경 Dockerfile
2.1 실습 환경 Dockerfile
- Ubuntu 18.04 64-bit (Glibc 2.27) 실습 환경 구축을 위한 Dockerfile은 다음과 같다.
- 다른 버전의 우분투를 사용하는 경우, 이번 강의의 실습을 수행하는 과정이 원활하지 않을 수 있으니 반드시 우분투 18.04 64-bit 환경을 구축한 후 실습하시기를 바란다.
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 one_gadget
WORKDIR /root
-
위 내용을
Dockerfile이라는 이름의 파일로 저장한 후, 아래의 명령어로 이미지를 빌드하고 컨테이너를 실행한 후 셸을 켤 수 있다.$ IMAGE_NAME=ubuntu1804 CONTAINER_NAME=my_container; \ docker build . -t $IMAGE_NAME; \ docker run -d -t --privileged --name=$CONTAINER_NAME $IMAGE_NAME; \ docker exec -it -u root $CONTAINER_NAME bash
Double Free Bug
1. Double Free Bug
- Double Free Bug (DFB)는 같은 청크를 두 번 해제할 수 있는 버그를 말한다.
ptmalloc2에서 발생하는 버그 중 하나이며, 공격자에게 임의 주소 쓰기, 임의 주소 읽기, 임의 코드 실행, 서비스 거부 등의 수단으로 활용될 수 있다.
- dangling pointer는 Double free bug를 유발하는 대표적인 원인이다.
- 코드 상에서 dangling pointer가 생성되는지, 그리고 이를 대상으로
free를 호출하는 것이 가능한지 살피면 Double free bug가 존재하는지 가늠할 수 있다.
- 코드 상에서 dangling pointer가 생성되는지, 그리고 이를 대상으로
- Double free bug를 이용하면 duplicated free list를 만드는 것이 가능한데, 이는 청크와 연결리스트의 구조때문이다.
ptmalloc2에서, free list의 각 청크들은fd와bk로 연결된다.fd는 자신보다 이후에 해제된 청크를,bk는 이전에 해제된 청크를 가리킨다.
- 그런데, 해제된 청크에서
fd와bk값을 저장하는 공간은 할당된 청크에서 데이터를 저장하는 데 사용된다.- 그러므로 만약 어떤 청크가 free list에 중복해서 포함된다면, 첫 번째 재할당에서
fd와bk를 조작하여 free list에 임의 주소를 포함시킬 수 있다.
- 그러므로 만약 어떤 청크가 free list에 중복해서 포함된다면, 첫 번째 재할당에서

- 초기에는 double free에 대한 검사가 미흡하여 Double free bug가 있으면 손쉽게 트리거할 수 있었다.
- 특히, glibc 2.26 버전부터 도입된 tcache는 도입 당시에 보호 기법이 전무하여 double free의 쉬운 먹잇감이 되었다.
- 하지만 시간이 흐르면서 관련한 보호 기법이
glibc에 구현되었고 이를 우회하지 않으면 같은 청크를 두 번 해제하는 즉시 프로세스가 종료된다.
1.1 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
}
- 위 코드는 같은 청크를 두 번 해제하는 예제 코드이다.
- 컴파일하고 실행하면 tcache에 대한 double free가 감지되어 프로그램이 비정상 종료되는 것을 확인할 수 있다.
$ ./dfb
Address of chunk: 0x55ce62641260
free(): double free detected in tcache 2
zsh: abort ./dfb
Mitigation for Tcache DFB
1. 정적 패치 분석
1.1 tcache_entry
- Tcache에 도입된 보호 기법을 분석하기 위해, 패치된 코드의 diff를 살펴보겠다.
- 먼저, 하단의 코드를 보면 double free를 탐지하기 위해
key포인터가tcache_entry에 추가되었음을 알 수 있다.
- 먼저, 하단의 코드를 보면 double free를 탐지하기 위해
typedef struct tcache_entry {
struct tcache_entry *next;
+ /* This field exists to detect double frees. */
+ struct tcache_perthread_struct *key;
} tcache_entry;
tcache_entry는 해제된 tcache 청크들이 갖는 구조다.- 일반 청크의
fd가next로 대체되고, LIFO 형태로 사용되므로bk에 대응되는 값은 없다.
- 일반 청크의
1.2 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_put은 해제한 청크를 tcache에 추가하는 함수다.- 상단의 코드를 보면
tcache_put함수는 해제되는 청크의key에tcache라는 값을 대입하도록 변경되었다. - 여기서
tcache는tcache_perthread라는 구조체 변수를 가리킨다.
- 상단의 코드를 보면
1.3 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에 연결된 청크를 재사용할 때 사용하는 함수다.- 상단의 코드를 보면
tcache_get함수는 재사용하는 청크의key값에 NULL을 대입하도록 변경되었다.
- 상단의 코드를 보면
1.4 _int_free
_int_free은 청크를 해제할 때 호출되는 함수다.- 하단의 코드의 20번째 줄 이하를 보면, 재할당하려는 청크의
key값이tcache이면 Double Free가 발생했다고 보고 프로그램을 abort시킨다.
- 하단의 코드의 20번째 줄 이하를 보면, 재할당하려는 청크의
_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
- 그 외의 보호 기법은 없으므로, 20번째 줄의 조건문만 통과하면 double free를 일으킬 수 있다.
2. 동적 분석
- 이번에는
gdb를 이용하여 보호 기법의 적용 과정을 동적 분석해보겠다.- 먼저, 청크 할당 직후에 중단점을 설정하고 실행한다.
$ gdb -q double_free
pwndbg> disass main
0x00005555555546da <+0>: push rbp
0x00005555555546db <+1>: mov rbp,rsp
0x00005555555546de <+4>: sub rsp,0x10
0x00005555555546e2 <+8>: mov edi,0x50
0x00005555555546e7 <+13>: call 0x5555555545b0 <malloc@plt>
0x00005555555546ec <+18>: mov QWORD PTR [rbp-0x8],rax
...
pwndbg> b *main+18
Breakpoint 1 at 0x5555555546ec
pwndbg> r
heap명령어로 청크들의 정보를 조회하면 다음과 같다.
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555756000
Size: 0x251
Allocated chunk | PREV_INUSE
Addr: 0x555555756250
Size: 0x61
Top chunk | PREV_INUSE
Addr: 0x5555557562b0
Size: 0x20d51
- 이 중
malloc(0x50)으로 생성한chunk의 주소는 0x555555756250 이다.- 해당 메모리 값을 덤프하면, 아무런 데이터가 입력되지 않았음을 확인할 수 있다.
pwndbg> x/4gx 0x555555756250
0x555555756250: 0x0000000000000000 0x0000000000000061
0x555555756260: 0x0000000000000000 0x0000000000000000
- 이후의 참조를 위해 청크를 gdb에서
chunk변수로 정의하고 넘어가겠다.
pwndbg> set $chunk=(tcache_entry *)0x555555756260
chunk를 해제할 때까지 실행하고, 청크의 메모리를 출력하면 다음과 같다.
pwndbg> disass main
0x0000555555554703 <+41>: call 0x5555555545a0 <printf@plt>
0x0000555555554708 <+46>: mov rax,QWORD PTR [rbp-0x8]
0x000055555555470c <+50>: mov rdi,rax
0x000055555555470f <+53>: call 0x555555554590 <free@plt>
0x0000555555554714 <+58>: mov rax,QWORD PTR [rbp-0x8]
pwndbg> b *main+58
Breakpoint 2 at 0x0000555555554714
pwndbg> c
pwndbg> print *$chunk
$1 = {
next = 0x0,
key = 0x555555756010
}
-
chunk의key값이 0x555555756010로 설정된 것을 확인할 수 있다. -
이 주소의 메모리 값을 조회하면, 해제한
chunk의 주소 0x555555756260가entry에 포함되어 있음을 알 수 있는데, 이는tcache_perthread에 tcache들이 저장되기 때문이다.print *(tcache_perthread_struct *)0x555555756010 $2 = { counts = "\000\000\000\000\001", '\000' <repeats 58 times>, entries = {0x0, 0x0, 0x0, 0x0, 0x555555756260, 0x0 <repeats 59 times>} } -
이 상태에서 실행을 재개하면
key값을 변경하지 않고, 다시free를 호출하므로, abort가 발생한다.
2.1 우회 기법
- 앞의 분석을 통해 알 수 있듯,
if (__glibc_unlikely (e->key == tcache))만 통과하면 tcache 청크를 double free 시킬 수 있다.
+ /* 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)) // Bypass it!
+ {
+ ...
+ if (tmp == e)
+ malloc_printerr ("free(): double free detected in tcache 2");
+ }
+ ...
+ if (tcache->counts[tc_idx] < mp_.tcache_count)
+ {
+ tcache_put (p, tc_idx);
+ return;
+ }
}
- 다시 말해, 해제된 청크의
key값을 1비트만이라도 바꿀 수 있으면, 이 보호 기법을 우회할 수 있다.
Tcache Duplication
1. Tcache Duplication
- 아래 코드는 tcache에 적용된 double free 보호 기법을 우회하여 Double free bug를 트리거하는 코드다.
// 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;
}
- 이를 컴파일하고, 실행한 결과는 다음과 같다.
$ ./tcache_dup
Chunk to be double-freed: 0x55d4db927260
First allocation: 0x55d4db927260
Second allocation: 0x55d4db927260
chunk가tcache에 중복 연결되어 연속으로 재할당되는 것을 확인할 수 있다.
Double Free Bug Lab
1. Q1

2. Q2

마치며
1. 마치며
- 이번 강의에서는 Double Free Bug (DFB)를 배우고, 관련된 보호 기법을 우회하는 방법에 대해 알아보았다.
- 그리고 이를 이용한 Tcache duplication에 대해서도 살펴보았다.
- 다음 강의에서는 임의 주소 쓰기, 임의 주소 읽기 등에 사용되는 Tcache poisoning 기법에 대해 배워보겠다.
- 이 공격 기법은 tcache duplication을 응용하므로, 이 강의의 내용을 숙지하고 학습하시기 바란다.
2. 키워드
- tcache_entry
- 해제된 tcache 청크를 나타내는 구조체다.
- 각 청크는
next라는 멤버 변수로 연결된다. - Double free 보호 기법이 적용되면서,
key라는 멤버 변수가 추가되었다.
- tcache_perthread_struct
- tcache를 처음 사용하면 할당되는 구조체다.
- Double Free Bug
- 한 청크를 두 번 해제할 수 있는 버그다.
- Tcache Duplication
- tcache에 같은 청크가 두 번 연결되는 것이다.
- Double free bug로 발생시킬 수 있으며, tcache poisoning으로 응용될 수 있다.
'Dreamhack'카테고리의 다른 글
Loading comments...