what is safe-linking in heap & bypss

와.. 난 왜 이걸 지금 알았는지 모르겠다. 말이 되나?

바로 2.32+ 부터 도입된 safe-linking 기능이다. fastbin과 tcache에만 적용된다 이 둘은 단일 연결리스트이기 때문이다. 나머지 small/large/unsorted는 원형 이중 연결 시스트로써 다른 강한 무결성 검사를 통해 보호한다.

해당 보안기법은

다음과 같이 우리가 이전에 흔히 아는 free chunk 에는 fd에 직접적으로 포인터가 존재하였다. fastbin도 tcache도 이는

이 포인터를 노출시켜, heap leak이 될 수도 있고 또한 dup나 poisioning 같은 공격으로 공격을 하기가 너무 쉬웠다.

따라서, safe-linking 이라는 보호기법으로 이 fd ( next ) 필드를 보호를 하기 시작한다.

정확히 말하면 xor 난독화를 사용하는데 코드를 보면서 설명을 하자면,

C
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_put 이다. tcache bin에 넣는 함수인데 chunk를 가져와서

e->key = tcache_key; 로 key 필드에 값을 집어놓고 ( 자세한 건 이글 참조 )

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]); 로 next 필드에 값을 집어넣어야 하는데 일반적으로는 tcache->entries[tc_idx]) 이 현재 tcache bins 최상위에 있는 tcache free chunk를 가리키고 있는 값을 바로 썼지만, 이제는 PROTECT_PTR 매크로로 감싸서 값을 집어넣는다. 그리고 앞에는 &e->next 으로 현재 next 필드에 시작 주소 값이 들어간다.

해당 매크로는

C
#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

다음과 같다. ( ( next 필드의 시작 주소 >> 12 ) XOR 다음청크주소 ) 으로 계산이 된다. 즉, 이제는 next 필드의 주소를 알아야 poisiong 혹은 dup에 정확한 주소를 넣을 수 있고 → 이는 heap leak이 필수라는 의미가 된다.

조심해야 할게 >> 12 값도 항상 생각을 해야한다.

당연히 복호화는

C
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");
  tcache->entries[tc_idx] = REVEAL_PTR (e->next);
  --(tcache->counts[tc_idx]);
  e->key = 0;
  return (void *) e;
}
C
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

다음과 같이 xor을 한번 더 하면 되기에 쉽다.

C
 if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
      /*
	If TRIM_FASTBINS set, don't place chunks
	bordering top into fastbins
      */
      && (chunk_at_offset(p, size) != av->top)
#endif
      ) {

    if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
			  <= CHUNK_HDR_SZ, 0)
	|| __builtin_expect (chunksize (chunk_at_offset (p, size))
			     >= av->system_mem, 0))
      {
	bool fail = true;
	/* We might not have a lock at this point and concurrent modifications
	   of system_mem might result in a false positive.  Redo the test after
	   getting the lock.  */
	if (!have_lock)
	  {
	    __libc_lock_lock (av->mutex);
	    fail = (chunksize_nomask (chunk_at_offset (p, size)) <= CHUNK_HDR_SZ
		    || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
	    __libc_lock_unlock (av->mutex);
	  }

	if (fail)
	  malloc_printerr ("free(): invalid next size (fast)");
      }

    free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);

    atomic_store_relaxed (&av->have_fastchunks, true);
    unsigned int idx = fastbin_index(size);
    fb = &fastbin (av, idx);

    /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;

    if (SINGLE_THREAD_P)
      {
	/* Check that the top of the bin is not the record we are going to
	   add (i.e., double free).  */
	if (__builtin_expect (old == p, 0))
	  malloc_printerr ("double free or corruption (fasttop)");
	p->fd = PROTECT_PTR (&p->fd, old);
	*fb = p;
      }
    else
      do
	{
	  /* Check that the top of the bin is not the record we are going to
	     add (i.e., double free).  */
	  if (__builtin_expect (old == p, 0))
	    malloc_printerr ("double free or corruption (fasttop)");
	  old2 = old;
	  p->fd = PROTECT_PTR (&p->fd, old);
	}

fastbin 에서도 마찬가지로

C
	p->fd = PROTECT_PTR (&p->fd, old);

다음과 같이 포인터를 ( &p->fd 의 주소 >> 12 ) 와 xor 해서 난독화를 통해 보호한다.

이 역시 heap 영역의 주소이므로 역시 leak을 해야한다.

how to bypass

일단 당연히 heap 주소 영역을 leak 할 수 있으면, 이게 첫번째이고

난독화된 포인터 주소만 읽을 수 있으면 → 난독화된 전에 주소를 알 수 있다면 역계산을 통해 (주소 >> 12) 값을 알 수 있다. 이 주소 >> 12 값을 알 수 있으면 해당 근처 heap은 모두 같은 값이므로 다른 free chunk에서도 사용할 수 있다.

그리고 이상하게도

가장 오래된, 맨 아래(tail)에 있는 청크의 fd ( next )는 그 다음이 NULL이므로

fd ( next ) = (pos >> 12) ^ 0 = pos >> 12 가 성립되어 해당 필드에 그냥 pos >> 12 값이 노출이 되어 있다. 즉, 이 부분을 읽을 수 있으면 그냥 이후에는 이 값만 xor 해서 poisionging 을 하면 되는 것이다