SLUB에서의 heap hardening 기법 중 freelist hardening 에 대해 알아볼 것이다.
SLUB의 경우, singly linked list (이면서 FIFO) 구조를 가지는 freelist를 가지고 있다.
linux kernel의 경우에도 freelist의 next 포인터의 난독화 기법을 적용하는데, 소스코드를 보면 아래와 같다.
static inline freeptr_t freelist_ptr_encode(const struct kmem_cache *s,
void *ptr, unsigned long ptr_addr)
{
unsigned long encoded;
#ifdef CONFIG_SLAB_FREELIST_HARDENED
encoded = (unsigned long)ptr ^ s->random ^ swab(ptr_addr);
#else
encoded = (unsigned long)ptr;
#endif
return (freeptr_t){.v = encoded};
}
여기서 swab()
매크로는 endian 변환을 위한 매크로에 해당한다.
즉, 원하는 ptr
을 얻기 위해서는 encoded ^ s->random ^ swab(ptr_addr)
을 해야 한다.
freelist의 마지막 원소에 주목해보자.
마지막 원소의 next
는 NULL
에 해당한다.
즉, encoded된 값은 NULL ^ s->random ^ swab(ptr_addr)
이다.
만약 UAF를 이용해 각 slot의 next pointer 값들을 읽어올 수 있는 상황을 고려해 보자.
freelist에서 마지막 직전의 slot을 1, 마지막 slot을 2라고 해 보자.
그러면
slot1->next
= address of slot2 ^ random ^ swab(address of slot1 + next offset)
이고,
slot2->next
= NULL ^ random ^ swab(address of slot2 + next offset)
이 된다.
여기서 slot1->next ^ slot2->next
를 하게 된다면 결과는
address of slot2 ^ swab(address of slot1 + next offset) ^ swab(address of slot2 + next offset)
이 된다.
만약 slot1과 slot2은 인접한 메모리에 있을 것이고, 예를 들어 같은 페이지에 있다고 하면 xor 후 하위 12비트를 제외한 나머지는 0일 것이다.
이를 통해 slot의 주소를 leak할 수 있게 된다.
static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
unsigned long freeptr_addr = (unsigned long)object + s->offset;
#ifdef CONFIG_SLAB_FREELIST_HARDENED
BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif
freeptr_addr = (unsigned long)kasan_reset_tag((void *)freeptr_addr);
*(freeptr_t *)freeptr_addr = freelist_ptr_encode(s, fp, freeptr_addr);
}
exploit 코드를 작성하면서 자꾸 double-free 시에 커널 패닉이 일어나길래 찾아보니 CONFIG_SLAB_FREELIST_HARDENED
가 설정된 경우에는 바로 연속해서 double-free가 일어날 경우 에러를 탐지하는 코드가 있는 것을 알게 되었다..