security/가디언

tcache safe linking

민사민서 2023. 8. 26. 18:20

https://github.com/bminor/glibc/tree/master

 

GitHub - bminor/glibc: Unofficial mirror of sourceware glibc repository. Updated daily.

Unofficial mirror of sourceware glibc repository. Updated daily. - GitHub - bminor/glibc: Unofficial mirror of sourceware glibc repository. Updated daily.

github.com

여기서 git clone 및 git pull 해서 립씨 파일 살펴봐도 되고

 

https://elixir.bootlin.com/glibc/latest/source/malloc/malloc.c#L3946

 

malloc.c - malloc/malloc.c - Glibc source code (glibc-2.38.9000) - Bootlin

/* Malloc implementation for multiple threads without lock contention. Copyright (C) 1996-2023 Free Software Foundation, Inc. Copyright The GNU Toolchain Authors. This file is part of the GNU C Library. The GNU C Library is free software; you can redistrib

elixir.bootlin.com

이번 문제 풀면서는 이 온라인 사이트를 참고했다

 

tcache bins에 담기는 chunk는 아래와 같은 형태이다

typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  uintptr_t key;
} tcache_entry;

 

tcache bins이 아래와 같은 상황일 때

chunk의 next 포인터가 무언가 조작(?)이 되어있는 모습이다

pointer hijacking을 막기 위해 next pointer에 safe linking / safe unlinking이 적용되어 있는 상황이다

 

libc 코드에서 살펴보면 

static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache_key;

  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

- tcache에 chunk 집어넣을 때 PROTECT_PTR() 매크로를 거치고

static __always_inline void *
tcache_get_n (size_t tc_idx, tcache_entry **ep)
{
  tcache_entry *e;
  if (ep == &(tcache->entries[tc_idx]))
    e = *ep;
  else
    e = REVEAL_PTR (*ep);

  if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");

  if (ep == &(tcache->entries[tc_idx]))
      *ep = REVEAL_PTR (e->next);
  else
    *ep = PROTECT_PTR (ep, REVEAL_PTR (e->next));

  --(tcache->counts[tc_idx]);
  e->key = 0;
  return (void *) e;
}

- tcache에서 chunk를 꺼내올 때 REVEAL_PTR() 매크로를 거쳐 next 포인터를 변환해 가져온다

 

/* Safe-Linking:
   Use randomness from ASLR (mmap_base) to protect single-linked lists
   of Fast-Bins and TCache.  That is, mask the "next" pointers of the
   lists' chunks, and also perform allocation alignment checks on them.
   This mechanism reduces the risk of pointer hijacking, as was done with
   Safe-Unlinking in the double-linked lists of Small-Bins.
   It assumes a minimum page size of 4096 bytes (12 bits).  Systems with
   larger pages provide less entropy, although the pointer mangling
   still works.  */
#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

- 다음 chunk의 포인터 주소와 (p->next 주소 >> 12)  값을 xor 해서 masking을 한다

- 물론 heap 주소는 ASLR에 의해 랜덤하게 매핑되므로 어느정도의 보안 효과가 있겠지만 무척이나 취약하다

 

 

공략 1) 만약 free chunk의 data를 읽어올 수 있다면 [UAF 취약점]

* PROTECT_PTR() 매크로에서 next ptr과 xor하는 값을 편하게 masking key라고 하자

 

* 알다싶이 tcache chunk chain의 마지막은 NULL ( next pointer = 0x0 ) 으로 끝난다

0x30 [  3]: 0x804b200 —▸ 0x804b1d0 —▸ 0x804b1a0 —▸ 0x0

 

* 즉 tcache chunk chain의 마지막 chunk의 next 포인터는 masking key이다 ( a xor a = 0 이므로). 이걸 읽어옴

=> heap 영역 내에 chunk들은 서로 가까이 분포할 것이므로 상위 5바이트 주소가 동일할 것이라고 가정 가능

=> masking key가 모든 chunk에서 동일할 것

 

공략 2) 만약 tcache chunk의 next pointer 한 개만 읽어올 수 있다면

* tcache chunk chain의 마지막 chunk아 아니라 중간 chunk의 next pointer 한 개만 읽어와도 공략 가능

Guardian 정재영 선배릠이 알려주심

 

* x86-64에서 하위 48bit만 가상메모리 주소로 사용되는데, 그 48bit를 12bit씩 쪼개서 생각해보자

* heap chunk 간 상위 36bit가 동일하다고 가정한다 (서로 가까이 분포할 것이므로)

* masking key는 다음 chunk 주소 >> 12 한 값과 동일할 것이다

 

32bit에서도 잘 적용되더라

0x804b1d0이 실제 next chunk의 주소이다