tcache duplication
1. tcache?
1. tcache
glibc 2.26 부터 tcache 가 도입되면서 0x20 ~ 0x410의 크기의 청크는 main arena가 아닌 tcache에서 관리되기 시작했다
tcache는 크기별로 최대 7개까지 저장할 수 있고 가득 찬 이후에는 청크가 fastbin이나 다른 bin으로 이동하게 된다
tcache는 보안 검사가 많이 생략되어 있어 공격자들에게 힙 익스플로잇 도구로 사용된다
glibc 2.27 의 tcache 는 double free에 대한 검증 로직이 없는 것으로 알려져 있지만
실습 환경인 glibc 2.27 의 버전은 Ubuntu GLIBC 2.27-3ubuntu1.6 으로 이후 버전에서 도입된 검증 로직이 적용되었다
→ 사실상 glibc 2.26 까지만 tcache dup가 가능하다
2. 예제 코드
// Name: df2.c
// Compile: gcc df2.c -o df2
#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가 감지되어 프로그램이 종료되는 것을 볼 수 있다
2. tcache 코드 분석 (glibc 2.27)
1. tcache_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 청크들이 갖는 구조이다
double free를 탐지하기 위해 key 포인터가 추가된 것을 확인할 수 잇다
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라는 구조체 변수를 가리킨다
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을 대입하도록 변경됐다
4._init_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
_int_free은 청크를 해제할 때 호출되는 함수이다
재할당하려는 청크의 key값이 tcache이면 Double Free가 발생했다고 보고 프로그램을 abort시킨다
그 외의 보호 기법은 없다
→ 조건문만 통과하면 Double Free를 일으킬 수 있다
3. 동적 분석
1. key 확인
malloc 으로 heap을 할당한 뒤 break 걸고 확인해보자
0x56013b208250에 청크가 할당되었다
0x61 의 size 로 data 에 아무것도 들어가있지 않은 것을 볼 수 있다
gdb에서 변수를 설정한다
청크를 free 한 뒤 확인해보면
key 값이 0x556f495d1010 인 것을 알 수 있다
chunk에서 fd 다음 필드가 key인 것을 알 수 있다
key 값을 확인해보자
해제한 청크의 주소 0x556f495d1250이 entry에 저장되어 있는 것을 확인할 수 있다
→ 이는 해제된 청크들이 tcache_perthread_struct 내부의 bin entry에 연결되기 때문이다
2. 우회 방법
+ /* 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;
+ }
}
_init_free 함수에서 if (__glibc_unlikely (e->key == tcache)) 만 우회하면 tcache 청크를 double free 시킬 수 있다
→ 해제된 chunk의 key 를 1bit라도 바꿀 수 있다면 우회할 수 있다
4. tcache dup
1. 우회 코드
// 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);
// chunk + 8 = key 필드 위치
*(char *)(chunk + 8) = 0xff;
*(char *)(chunk + 8) = 0xff;
free(chunk);
printf("First allocation: %p\n", malloc(0x20));
printf("Second allocation: %p\n", malloc(0x20));
return 0;
}
해제된 청크의 key 값을 1byte 바꾸는 코드이다
2. 동적 분석
malloc으로 heap을 할당한 뒤 break를 걸고 확인해보자
size는 0x31 로 data 가 비어있는 것을 볼 수 있다
첫 번째 free를 한 뒤 chunk를 확인해보자
key 는 0x55ee7ef30010인 것을 알 수 있다
두 번째 free를 하기 직전 chunk를 확인해보자
key 값이 0x55ee7ef300ff 로 변조된 것을 볼 수 있다
free를 진행하면 정상적으로 되는 것을 볼 수 있다
해제된 청크의 fd가 해제된 청크를 가리키고 있다
→ 오류 없이 double free가 발생했다
tcache의 double free 방지 코드를 우회하여 double free가 된 것을 볼 수 있다
'Hacking > Pwnable' 카테고리의 다른 글
DFB(Double Free Bug) (0) | 2025.05.19 |
---|---|
UAF (Use After Free) (0) | 2025.05.07 |
Heap - ptmalloc2 (glibc) (0) | 2025.04.03 |
Stack Pivoting (0) | 2025.04.02 |
SROP (x64) (0) | 2025.03.26 |