1. Tcache(Thread local Caching)
glibc 2.26 버전 이후 힙 메모리 관리 매커니즘
-> 주어진 라이브러리 버전을 보고 Tcache 여부 판단한다.
힙 메모리 관리 속도 향상 위해 추가 -> 이전 버전보다 공격이 쉽다.
malloc() 함수로 동적 할당 요청이 들어오면 __libc_malloc 호출된다.
__libc_malloc 함수: MAYBE_INIT_TCACHE 매크로 호출해 tcache_init 함수 호출한다.
#define MAYBE_INIT_TCACHE() \
if (__glibc_unlikely (tcache == NULL)) \
tcache_init();
void * __libc_malloc (size_t bytes)
{
...
MAYBE_INIT_TCACHE ();
}
tcache_init 함수: tcache_perthread_struct 구조체를 힙 영역에 할당 후 초기화하는 역할. 구조체는 힙 페이지의 맨 첫 부분에 할당한다.
tcache_perthread_struct 구조체: tcache_entry 관리. glibc 2.26 버전 이후 할당된 tcache의 힙 관리.
(이전 버전에서는 할당된 힙을 main_arena가 관리했다.)
tcache_entry 구조체: 멤버 변수 next 포인터는 tcache->entries의 연결 리스트 관리한다.
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
fastbin과 차이점
(1) tcache는 fd가 fd를 가리킨다. (fastbin에서는 prev_size를 가리킨다.)
(2) tcache는 병합이 발생하지 않으므로 prev_size, prev_inuse_bit 설정하지 않는다.
(3) tcache는 힙에 있는 tcache_perthread_struct가 관리하고, fastbin은 libc에 존재하는 main_arena가 관리한다.
2. tcache_put 함수
static 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]);
}
- 해제 요청 들어오면 tcache->entries(tcache list)에 해제된 힙 청크의 주소를 추가한다.
- 동일한 크기의 tcache->entries는 7개의 힙 청크만 관리한다. -> 8번째 free 청크는 fastbin 또는 smallbin에 들어간다.
- 새로 들어온 청크의 next에 기존에 존재하던 청크의 주소를 넣는다.
- _init_malloc과 _init_free에서 호출된다.
- fastbin과 smallbin 크기의 청크 해제 시 먼저 tcache가 관리한다.
★ free 함수가 호출되면 _init_free가 호출되고, 해제된 힙 청크 주소를 tcache->entries에 삽입할 때 힙 청크의 크기와 유효한 주소인지만 검증하고 Double Free에 대한 검증이 없다.
3. tcache_get 함수
static 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;
}
- 저장되어 있는 tcache->entries(tcache list)에서 힙 청크를 가져온다.
- 가져온 힙 청크의 next 포인터를 tcache->entries에 삽입 후 힙 청크 주소를 반환한다.
- _libc_malloc과 _init_malloc에서 호출된다.
- 요청된 크기에 맞는 tcache_entry 존재하면 tcache_get 함수가 호출된다.
예외처리가 존재하지 않아 공격이 쉽다.
_libc_mallc: malloc 호출 시 가장 먼저 호출되고, _libc_malloc에서 _init_malloc을 호출한다.
★ fastbin처럼 fake chunk size를 맞출 필요가 없다.
4. 예시
// gcc -o tcache_ex tcache_ex.c -no-pie
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *ptr = malloc(0x10);
char *ptr2 = malloc(0x20);
char *ptr3 = malloc(0x30);
free(ptr);
free(ptr2);
free(ptr3);
ptr = malloc(0x10);
ptr2 = malloc(0x20);
ptr3 = malloc(0x30);
}
힙 페이지 시작 부분(0x602000)을 확인해보면, 0x251 크기를 가진 힙 청크가 존재한다.
malloc 호출 시 tcache_init 함수를 통해 tcache_perthread_struct 구조체가 힙 영역에 할당된 것이다.
(1) 첫 번째 청크 해제
첫 번째 free 함수 호출한 직후 모습이다.
첫 번째 청크 해제 후, tcache_count가 1 증가했다.
tcache_entry의 0번 인덱스에 해제된 청크의 주소가 적힌다.
(2) 두 번째 청크 해제
두 번째 힙 청크는 첫 번째 청크와 다른 tcache_entry 인덱스를 가지고 있다.
두 번째 청크 해제 시 tcacahe_count가 증가하고, tcache_entry에 두 번째 청크에 대한 포인터가 적힌다.
(3) 세 번째 힙 청크 해제
세 번째 힙 청크 해제된 후에도 tcache_count가 증가하고, tcache_entry에 세 번째 청크에 대한 포인터가 적힌다.
따라서 각 크기마다 tcache_entry와 tcache_count가 관리되는 것을 확인할 수 있다.
(4) 첫 번째 tcache_entry가 관리하는 크기의 청크 재할당
첫 번째 tcache_entry의 tcache_count가 감소되어 0이 되었다.
첫 번째 포인터 위치에 재할당되었으므로 tcache_entry에서 포인터가 지워진 것을 확인할 수 있다.
5. 예시2
// gcc -o tcache_escape tcache_escape.c -no-pie
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *ptr[8];
int i;
for(i=0;i<=7;i++) {
printf("malloc !\n");
ptr[i] = malloc(0x20);
}
for(i=0;i<=7;i++) {
printf("free!\n");
free(ptr[i]);
}
}
tcache는 7개의 힙 청크만 관리하므로 7개까지는 tcache_entry에 들어가지만, 나머지 해제된 1개는 fastbin에 들어가게 된다.
fastbin dup (0) | 2020.06.10 |
---|---|
Tcache Dup (0) | 2020.06.10 |
Master canary (0) | 2020.06.08 |
Stack Frame & 함수 호출 규약 (32bit, 64bit) (0) | 2020.05.30 |
[Back to Basic] Linux exploitation & Mitigation #1 (0) | 2020.04.06 |