[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.
- 서론
- Double Free Bug
- Mitigation for Tcache DFB
- Tcache Duplication
- Q&A
- 마치며
free
함수로 청크를 해제하면, ptmalloc2는 이를 tcache나 bins에 추가하여 관리합니다.
그리고 이후에 malloc
으로 비슷한 크기의 동적할당이 발생하면, 이 연결리스트들을 탐색하여 청크를 재할당해줍니다.
이 매커니즘에서, 해커들은 free
로 해제한 청크를 free
로 다시 해제했을 때 발생하는 현상에 주목했습니다.
tcache와 bins를 'free list'라고 통칭한다면,
free list의 관점에서 free
는 청크를 추가하는 함수, malloc
은 청크를 꺼내는 함수입니다.
그러므로, 임의의 청크에 대해 free
를 두 번 이상 적용할 수 있다는 것은, 청크를 'free list'에 여러 번 추가할 수 있음을 의미합니다.
청크가 free list에 중복해서 존재하면 "청크가 duplicated"됐다고 표현하는데,
해커들은 duplcated free list를 이용하면 임의 주소에 청크를 할당할 수 있음을 밝혀냈습니다.
이렇게 할당한 청크의 값을 읽거나 조작함으로써 해커는 임의 주소 읽기 / 쓰기를 할 수 있습니다.
위와 같은 이유로, 같은 청크를 중복해서 해제할 수 있는 코드는 보안상의 약점으로 분류되어 'Double Free Bug'라고 불립니다.
'Double Free Bug(DFB)'는 같은 청크를 두 번 해제할 수 있는 버그를 말합니다.
ptmalloc2에서 발생하는 버그 중 하나이며, 공격자에게 임의 주소 쓰기 / 읽기, 임의 코드 실행, 서비스 거부 등의 수단으로 활용될 수 있습니다.
저번에 배운 Dangling Pointer는 DFB를 유발하는 대표적인 원인입니다.
코드 상에서 Dangling Pointer가 생성되는지, 그리고 이를 대상으로 free
를 호출하는 것이 가능한지 살피면 DFB가 존재하는지 가늠할 수 있습니다.
DFB를 활용하면, duplicated free list를 만드는 것이 가능한데, 이는 청크와 연결리스트의 구조때문입니다.
ptmalloc2에서, free list의 각 청크들은 fd
와 bk
로 연결됩니다.
fd
는 자신보다 이후에 해제된 청크를, bk
는 이전에 해제된 청크를 가리킵니다.
그런데, 해제된 청크에서 fd
와 bk
값을 저장하는 공간은 할당된 청크에서 데이터를 저장하는데 사용됩니다.
그러므로 만약 어떤 청크가 free list에 중복해서 포함된다면, 첫번째 재할당에서 fd
와 bk
를 조작하여 free list에 임의 주소를 포함시킬 수 있습니다.
초기에는 Double Free에 대한 검사가 미흡하여 DFB가 있으면 트리거할 수 있었습니다.
특히, tcache와 관련해서는 얼마전까지도 보호 기법이 전무하여 Double Free의 쉬운 먹잇감이었습니다.
그러나, 최근에는 관련한 보호 기법이 glibc
에 구현되면서, 이를 우회하지 않으면 같은 청크를 두 번 해제하는 즉시 프로세스가 종료됩니다.
// 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
}
위의 예제는 같은 청크를 두 번 해제하는 코드입니다.
컴파일하고 실행하면 Double Free가 감지되어 비정상적으로 종료됩니다.
먼저, double free를 탐지하기 위해 key
포인터가 tcahce_entry
에 추가되었습니다.
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
에 대응되는 값은 없습니다.
tcache_put
은 해제한 청크를 tcache에 추가하는 함수입니다.
tcache_put
함수는 해제되는 청크의 key
에 tcache
라는 값을 대입하도록 변경되었습니다.
여기서 tcache
는 tcache_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]);
}
tcache_get
은 tcache에 연결된 청크를 재사용할 때 사용하는 함수입니다.
아래의 코드를 보면, tcache_get
함수는 재사용하는 청크의 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;
}
_int_free
은 청크를 해제할 때 호출되는 함수입니다.
아래의 코드를 보면, 재할당하려는 청크의 key
값이 tcache
라면 Double Free가 발생했다고 보고 ㅍ로그램을 abort시킵니다.
그 외의 보호 기법이 없으므로, 20번째 줄의 조건문만 통과하면 Double 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
먼저, 청크 할당 직후에 중단점을 설정합니다.
$ gdb -q double_free
pwndbg> disass main
0x0000000000001189 <+0>: endbr64
0x000000000000118d <+4>: push rbp
0x000000000000118e <+5>: mov rbp,rsp
0x0000000000001191 <+8>: sub rsp,0x10
0x0000000000001195 <+12>: mov edi,0x50
0x000000000000119a <+17>: call 0x1090 <malloc@plt>
0x000000000000119f <+22>: mov QWORD PTR [rbp-0x8],rax
...
pwndbg> b *main+22
Breakpoint 1 at 0x0x119f
pwndbg> r
heap
명령어로 청크들의 정보를 조회합니다.
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x291
Allocated chunk | PREV_INUSE
Addr: 0x555555559290
Size: 0x61
Top chunk | PREV_INUSE
Addr: 0x5555555592f0
Size: 0x20d11
이 중, malloc(0x50)
으로 생성한 chunk
의 주소는 0x555555559290
입니다.
해당 메모리 값을 덤프하면, 아무런 데이터가 입력되지 않았음을 알 수 있습니다.
pwndbg> x/4gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000061
0x5555555592a0: 0x0000000000000000 0x0000000000000000
이후의 참조를 위해 청크를 gdb
변수로 정의하겠습니다.
pwndbg> set $chunk=(tcache_entry *)0x5555555592a0
chunk
를 해제할 때까지 실행하고, 청크의 메모리를 출력하겠습니다.
pwndbg> b *main+62
Breakpoint 2 at 0x5555555551c7
pwndbg> c
pwndbg> print *chunk
$1 = {
next = 0x0,
key = 0x555555756010
}
chunk
의 key
값이 0x555555756010
로 설정되었습니다.
이 주소의 메모리 값을 확인하면,
해제한 chunk
의 주소 0x5555555592a0
가 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가 발생합니다.
앞의 분석 기법을 통해 알 수 있듯,
if (__glibc_unlikely (e->key == tcache))
만 통과하면 tcache 청크를 Double Free 시킬 수 있습니다.
다시 말해, 해제된 청크의 key
값을 1비트만이라도 바꿀 수 있으면, 이 보호 기법을 우회할 수 있습니다.
아래의 코드는 tcahce에 적용된 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
에 중복 연결되어 연속으로 재할당되는 것을 확인할 수 있습니다.
-
-
[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.