Glibc 2.27关于Tcache的增强保护

 

0x01 前言

在2020年09月10日,Ubuntu基金会发布了名为为2.27-3ubuntu1.3的更新,本次更新进行了对于Tcache分配机制的更新,这将导致一批对于Tcache的利用失效,本文从源码的角度分析了变动的源码与保护。

 

0x02 获取源码

由于此软件包目前还处于测试状态(proposed),我们无法使用apt-get update && apt-get upgrade直接获取到此版本。

运行以下命令启用获取测试状态软件包源码:

sudo echo 'deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
' >> /etc/apt/sources.list
sudo echo 'deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
' >> /etc/apt/sources.list

接下来运行sudo apt-get update更新软件源

?:更新完软件源后,严禁直接使用sudo apt-get upgrade更新软件,部分测试版核心软件(例如glibc)一旦被更新到测试版将无法进行降级。

接下来运行sudo apt-get source libc6/bionic-proposed以获取目标源码

⚠️:接下来请编辑/etc/apt/sources.list,注释掉刚刚添加的源,并运行sudo apt-get update更新软件源

 

0x03 代码变更

1.Tcache结构体定义变更

原始定义:

/* We overlay this structure on the user-data portion of a chunk when
   the chunk is stored in the per-thread cache.  */
typedef struct tcache_entry
{
  struct tcache_entry *next;
} tcache_entry;

新定义:

/* We overlay this structure on the user-data portion of a chunk when
   the chunk is stored in the per-thread cache.  */
typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;

意义:

加入了额外指针key,可以用来检查链表完整性。

2.加入Tcache数量限制

原始定义:

新定义:

#define MAX_TCACHE_COUNT 127    /* Maximum value of counts[] entries.  */

意义:

限制Tcache的数量

3.tcache_put加入了新步骤

原始定义:

/* Caller must ensure that we know tc_idx is valid and there's room
   for more chunks.  */
static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

新定义:

/* Caller must ensure that we know tc_idx is valid and there's room
   for more chunks.  */
static __always_inline void 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]);
}

意义:

现在,将chunk置入Tcache中时,将会新增一个指针指向Tcache,用于检查Double Free

4.tcache_get加入了新步骤

原始定义:

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  return (void *) e;
}

新定义:

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  e->key = NULL;
  return (void *) e;
}

意义:

现在,将chunkTcache中移除时,将会清空新增的指针。

5.int_free中加入了新检查

原始定义:

#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);

    if (tcache
    && tc_idx < mp_.tcache_bins
    && tcache->counts[tc_idx] < mp_.tcache_count)
      {
    tcache_put (p, tc_idx);
    return;
      }
  }
#endif

新定义:

#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);
    if (tcache != NULL && tc_idx < mp_.tcache_bins)
      {
        /* 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_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->counts[tc_idx] < mp_.tcache_count)
          {
            tcache_put (p, tc_idx);
            return;
          }
      }
  }
#endif

意义:

现在,调用int_free时,将会检查整个Tcache链表,如果发现将要释放的chunk已存在于链表中将会报错free(): double free detected in tcache 2

6._int_realloc中更改了定义

原始定义:

void*
_int_realloc(mstate av, mchunkptr oldp, INTERNAL_SIZE_T oldsize,
         INTERNAL_SIZE_T nb)
{
  .........

  unsigned long    copysize;        /* bytes to copy */
  unsigned int     ncopies;         /* INTERNAL_SIZE_T words to copy */
  INTERNAL_SIZE_T* s;               /* copy source */
  INTERNAL_SIZE_T* d;               /* copy destination */

  .........
            {
              /*
                 Unroll copy of <= 36 bytes (72 if 8byte sizes)
                 We know that contents have an odd number of
                 INTERNAL_SIZE_T-sized words; minimally 3.
               */

              copysize = oldsize - SIZE_SZ;
              s = (INTERNAL_SIZE_T *) (chunk2mem (oldp));
              d = (INTERNAL_SIZE_T *) (newmem);
              ncopies = copysize / sizeof (INTERNAL_SIZE_T);
              assert (ncopies >= 3);

              if (ncopies > 9)
                memcpy (d, s, copysize);

              else
                {
                  *(d + 0) = *(s + 0);
                  *(d + 1) = *(s + 1);
                  *(d + 2) = *(s + 2);
                  if (ncopies > 4)
                    {
                      *(d + 3) = *(s + 3);
                      *(d + 4) = *(s + 4);
                      if (ncopies > 6)
                        {
                          *(d + 5) = *(s + 5);
                          *(d + 6) = *(s + 6);
                          if (ncopies > 8)
                            {
                              *(d + 7) = *(s + 7);
                              *(d + 8) = *(s + 8);
                            }
                        }
                    }
                }

              _int_free (av, oldp, 1);
              check_inuse_chunk (av, newp);
              return chunk2mem (newp);
            }
        }
    }

.........
}

新定义:

void* _int_realloc(mstate av, mchunkptr oldp, INTERNAL_SIZE_T oldsize,
         INTERNAL_SIZE_T nb)
{
.........
          /*
             Avoid copy if newp is next chunk after oldp.
           */
          if (newp == next)
            {
              newsize += oldsize;
              newp = oldp;
            }
          else
            {
                memcpy (newmem, chunk2mem (oldp), oldsize - SIZE_SZ);
              _int_free (av, oldp, 1);
              check_inuse_chunk (av, newp);
              return chunk2mem (newp);
            }
        }
    }

.........
}

意义:

此处由原来的逐位复制直接变更为使用memcpy,实现上更加简洁。

7.do_set_tcache_count发生了变更

原始定义:

static inline int __always_inline do_set_tcache_count (size_t value)
{
  LIBC_PROBE (memory_tunable_tcache_count, 2, value, mp_.tcache_count);
  mp_.tcache_count = value;
  return 1;
}

新定义:

static inline int __always_inline do_set_tcache_count (size_t value)
{
  if (value <= MAX_TCACHE_COUNT)
    {
      LIBC_PROBE (memory_tunable_tcache_count, 2, value, mp_.tcache_count);
      mp_.tcache_count = value;
    }
  return 1;
}

意义:

限制tcache_count的数目必须小于MAX_TCACHE_COUNT(即127),防止发生溢出。

 

0x04 变化总结

以上几大变化基本上都是针对的Tcache进行的更新,且更新后的代码基本100%照搬Glibc 2.31的代码,因此我们可以直接利用2.31的思路,直接篡改key指针就可以完成利用了。

(完)