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 난독화를 사용하는데 코드를 보면서 설명을 하자면,
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 필드에 시작 주소 값이 들어간다.
해당 매크로는
#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 값도 항상 생각을 해야한다.
당연히 복호화는
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;
}#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)다음과 같이 xor을 한번 더 하면 되기에 쉽다.
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 에서도 마찬가지로
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 을 하면 되는 것이다