War Against Heap - A New Security Check Bypass for TCache

4 minute read

Preface

Sad news for all pwners: a new security check for TCache is added recently. Emmmm, we already have over 10 exploit methods of heap in Shellphish’s how2heap. With additional check, heap challenge will continue being nightmare of pwners.

The new version(2.29) is not released yet, so it probably will be changed before release date. However, I still want to discuss some probable exploit methods against the future TChache.

Changed Code

Let’s see the diff:

--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -2967,6 +2967,8 @@ mremap_chunk (mchunkptr p, size_t new_size)
 typedef struct tcache_entry
 {
   struct tcache_entry *next;
+  /* This field exists to detect double frees.  */
+  struct tcache_perthread_struct *key;
 } tcache_entry;
 
 /* There is one of these for each thread, which contains the
@@ -2990,6 +2992,11 @@ tcache_put (mchunkptr chunk, size_t tc_idx)
 {
   tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
   assert (tc_idx < TCACHE_MAX_BINS);
+
+  /* Mark this chunk as "in the tcache" so the test in _int_free will
+     detect a double free.  */
+  e->key = tcache;
+
   e->next = tcache->entries[tc_idx];
   tcache->entries[tc_idx] = e;
   ++(tcache->counts[tc_idx]);
@@ -3005,6 +3012,7 @@ tcache_get (size_t tc_idx)
   assert (tcache->entries[tc_idx] > 0);
   tcache->entries[tc_idx] = e->next;
   --(tcache->counts[tc_idx]);
+  e->key = NULL;
   return (void *) e;
 }
 
@@ -4218,6 +4226,26 @@ _int_free (mstate av, mchunkptr p, int have_lock)
   {
     size_t tc_idx = csize2tidx (size);
 
+    /* Check to see if it's already in the tcache.  */
+    tcache_entry *e = (tcache_entry *) chunk2mem (p);
+
+    /* This test succeeds on double free.  However, we don't 100%
+       trust it (it also matches random payload data at a 1 in
+       2^<size_t> chance), so verify it's not an unlikely coincidence
+       before aborting.  */
+    if (__glibc_unlikely (e->key == tcache && tcache))
+      {
+       tcache_entry *tmp;
+       LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
+       for (tmp = tcache->entries[tc_idx];
+            tmp;
+            tmp = tmp->next)
+         if (tmp == e)
+           malloc_printerr ("free(): double free detected in tcache 2");
+       /* If we get here, it was a coincidence.  We've wasted a few
+          cycles, but don't abort.  */
+      }
+
     if (tcache
        && tc_idx < mp_.tcache_bins
        && tcache->counts[tc_idx] < mp_.tcache_count)

And here is the new protect mechanism:

 typedef struct tcache_entry
 {
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
 } tcache_entry;
 
// tcache_put
{
...
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
  /* Mark this chunk as "in the tcache" so the test in _int_free will detect a double free.*/
  e->key = tcache;
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
...
}

// tcache_get
{
...
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  e->key = NULL;
  return (void *) e;
...
}

// _int_free
{
...

  /* Check to see if it's already in the tcache.  */
  tcache_entry *e = (tcache_entry *) chunk2mem (p);
 
  /* This test succeeds on double free.  However, we don't 100%
  trust it (it also matches random payload data at a 1 in
  2^<size_t> chance), so verify it's not an unlikely coincidence*/
  if (__glibc_unlikely (e->key == tcache && tcache))
  {
    tcache_entry *tmp;
    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
    for (tmp = tcache->entries[tc_idx];
       tmp;
       tmp = tmp->next)
    if (tmp == e)
      malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence.  We've wasted a few
   cycles, but don't abort.  */
  }

...
}

The code is easy to understand. It adds following mitigation:

  • When user free a chunk to TCache, it
    • Check whether the chunk contains valid key(it acts like stack canary), if it does, libc:
      • Checks if the address is duplicated in tcache->entries array by looping
      • When it’s not duplicated, return malloc. Otherwise aborting the program.
    • Use pointer tcahce(which stores the tcache_perthread_struct) info as key for further security check
  • When user malloc from TCache, it
    • erase the key content to precent leaking

To summarize, we cannot add any chunks twice in TCache without modifying key, no matter subsequently or intermittently.(even the something like fastbin double free A->B->A)

New Techniques Against Mitigation

These are only proposes, they might be changed before glibc 2.29 released date.

Overwrite the key (Bypass)

This is the most obvious option, we can achieve it via overlapping chunks or unsorted bins attack. Any methods that can overwrite the key will be a great help.

TCache + Fastbin Double Free (Arbitrary R/W)

Now, we cannot free a chunk to cache directly. However, we can fill 6 chunks to a TCache list(while the max of a list is 7). Then free Chunk A to TCache.

The TCache is now full filled, and the program will use Fastbin instead. Since the fastbin won’t check key, we can double free Chunk A to fastbin.

In the final, we can overwrite bk to malloc an arbitrary chunk.

TCache + Unsorted Bin Double Free (Leak Heap)

The process is similar to above steps. Fill 6 chunks to TCache list.

Subsequently, put a chunk in size of small bin to TCache and unsorted bin.

After malloc our chunk. We can see bk address of unsorted bin (leaking Heap). The fk pointer will be erased because of e->key = NULL; in tcache_get. We need to recover it to a correct address or give up using unsorted bin.

Leaking the address of tcache (Leak glibc)

According to libc, this is the definition of tcache:

static __thread tcache_perthread_struct *tcache = NULL;

Since it’s created statically, its relative address is always constant, we have another leak choice.

Remember, when we free a chunk from TCache, its key will be erased. So we can only leak the chunks already in TCache.

Conclusion

Remember, this is not released yet and might be changed. But I still hope the whole passage will be useful in future attack. If you spark some great ideas or want to correct my article, don’t forget to comment in the bottom.

Leave a comment