Post

malloc.c glibc 2.31

malloc.c diff from glibc 2.27, 2.31

malloc.c glibc 2.31

glibc 2.31과 2.27버전에서 malloc.c의 파일의 git diff를 이용해서 변경 점을 분석해보자

MAX_TCACHE_COUNT

1
2
3
4
5
6
7
8
9
@@ -321,6 +321,10 @@

 /* This is another arbitrary limit, which tunables can change.  Each
    tcache bin will hold at most this number of chunks.  */
 # define TCACHE_FILL_COUNT 7
+
+/* Maximum chunks in tcache bins for tunables.  This value must fit the range
+   of tcache->counts[] entries, else they may overflow.  */
+# define MAX_TCACHE_COUNT UINT16_MAX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
-/* Take a chunk off a bin list */
-#define unlink(AV, P, BK, FD) {                                            \
-    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
-      malloc_printerr ("corrupted size vs. prev_size");			      \
-    FD = P->fd;								      \
-    BK = P->bk;								      \
-    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \
-      malloc_printerr ("corrupted double-linked list");			      \
-    else {								      \
-        FD->bk = BK;							      \
-        BK->fd = FD;							      \
-        if (!in_smallbin_range (chunksize_nomask (P))			      \
-            && __builtin_expect (P->fd_nextsize != NULL, 0)) {		      \
-	    if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)	      \
-		|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
-	      malloc_printerr ("corrupted double-linked list (not small)");   \
-            if (FD->fd_nextsize == NULL) {				      \
-                if (P->fd_nextsize == P)				      \
-                  FD->fd_nextsize = FD->bk_nextsize = FD;		      \
-                else {							      \
-                    FD->fd_nextsize = P->fd_nextsize;			      \
-                    FD->bk_nextsize = P->bk_nextsize;			      \
-                    P->fd_nextsize->bk_nextsize = FD;			      \
-                    P->bk_nextsize->fd_nextsize = FD;			      \
-                  }							      \
-              } else {							      \
-                P->fd_nextsize->bk_nextsize = P->bk_nextsize;		      \
-                P->bk_nextsize->fd_nextsize = P->fd_nextsize;		      \
-              }								      \
-          }								      \
-      }									      \
-}
-
+static void
+unlink_chunk (mstate av, mchunkptr p)
+{
+  if (chunksize (p) != prev_size (next_chunk (p)))
+    malloc_printerr ("corrupted size vs. prev_size");
+
+  mchunkptr fd = p->fd;
+  mchunkptr bk = p->bk;
+
+  if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
+    malloc_printerr ("corrupted double-linked list");
+
+  fd->bk = bk;
+  bk->fd = fd;
+  if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
+    {
+      if (p->fd_nextsize->bk_nextsize != p
+	  || p->bk_nextsize->fd_nextsize != p)
+	malloc_printerr ("corrupted double-linked list (not small)");
+
+      if (fd->fd_nextsize == NULL)
+	{
+	  if (p->fd_nextsize == p)
+	    fd->fd_nextsize = fd->bk_nextsize = fd;
+	  else
+	    {
+	      fd->fd_nextsize = p->fd_nextsize;
+	      fd->bk_nextsize = p->bk_nextsize;
+	      p->fd_nextsize->bk_nextsize = fd;
+	      p->bk_nextsize->fd_nextsize = fd;
+	    }
+	}
+      else
+	{
+	  p->fd_nextsize->bk_nextsize = p->bk_nextsize;
+	  p->bk_nextsize->fd_nextsize = p->fd_nextsize;
+	}
+    }
+}
  • 달라진 건 함수의 형태가 inline 으로 바뀌고 인자의 개수만 바뀌고 내용적으로 바뀐건 없다.

global_max_fast

1
2
3
4
5
6
@@ -1640,7 +1621,7 @@ static INTERNAL_SIZE_T global_max_fast;

 #define set_max_fast(s) \
   global_max_fast = (((s) == 0)						      \
-                     ? SMALLBIN_WIDTH : ((s + SIZE_SZ) & ~MALLOC_ALIGN_MASK))
+                     ? MIN_CHUNK_SIZE / 2 : ((s + SIZE_SZ) & ~MALLOC_ALIGN_MASK))
  • 값이 바뀐건 아니지만 계산하는 방법이 달라졌다.

tcache->key

1
2
3
4
5
6
7
8
@@ -2904,4 +2894,6 @@ typedef struct tcache_entry
    time), this is for performance reasons.  */
typedef struct tcache_entry
{
  struct tcache_entry *next;
+  /* This field exists to detect double frees.  */
+  struct tcache_perthread_struct *key;
} tcache_entry;
  • DFB 방지를 위한 key값이 entry에 추가되었다.

tcache_count

1
2
3
4
5
6
7
8
@@ -2915,12 +2905,10 @@ typedef struct tcache_entry
    time), this is for performance reasons.  */
 typedef struct tcache_perthread_struct
 {
-  char counts[TCACHE_MAX_BINS];
+  uint16_t counts[TCACHE_MAX_BINS];
   tcache_entry *entries[TCACHE_MAX_BINS];
 } tcache_perthread_struct;
  • counts 자료형이 char에서 uint16_t 2byte로 바뀌었다.

tcache put

1
2
3
4
5
6
7
8
9
10
11
12
@@ -2930,7 +2918,6 @@ 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]);
  • tc_idx 가 64보다 작은지 검사하는 루틴이 빠졌다.
  • DFB 방지를 위해 free된 tcache_chunk에 key변수에 tcache 구조체의 주소가 저장된다.

tcache get

1
2
3
4
5
6
7
8
9
10
11
@@ -2947,15 +2934,14 @@ 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;
 }
  • tc_idx와 tcache->entries를 검사하는 로직이 사라졌다.
  • 그리고 e->key에는 Null을 넣어주고 반환한다.

__libc_malloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@@ -3039,16 +3034,19 @@ __libc_malloc (size_t bytes)
 #if USE_TCACHE
   /* int_free also calls request2size, be careful to not pad twice.  */
   size_t tbytes;
-  checked_request2size (bytes, tbytes);
+  if (!checked_request2size (bytes, &tbytes))
+    {
+      __set_errno (ENOMEM);
+      return NULL;
+    }
   size_t tc_idx = csize2tidx (tbytes);

   MAYBE_INIT_TCACHE ();

   DIAG_PUSH_NEEDS_COMMENT;
   if (tc_idx < mp_.tcache_bins
-      /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
       && tcache
-      && tcache->entries[tc_idx] != NULL)
+      && tcache->counts[tc_idx] > 0)
     {
       return tcache_get (tc_idx);
     }
  • checked_request2size함수에서 실패 하면 에러를 표출하고 종료된다.
    • 함수에서 req size가 9223372036854775807L보다 크면 false이기 때문에 그럴일은 없을 거 같다.
  • 앞에서 tcache_get 검증이 사라진 이유가 tcache->counts[tc_idx] > 0 검증이 추가 되었기 때문이다.

_int_malloc

unsorted chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@@ -3741,11 +3728,22 @@ _int_malloc (mstate av, size_t bytes)
  for (;; )
    {
      int iters = 0;
       while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
         {
           bck = victim->bk;
-          if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
-              || __builtin_expect (chunksize_nomask (victim)
-				   > av->system_mem, 0))
-            malloc_printerr ("malloc(): memory corruption");
           size = chunksize (victim);
+          mchunkptr next = chunk_at_offset (victim, size);
+
+          if (__glibc_unlikely (size <= 2 * SIZE_SZ)
+              || __glibc_unlikely (size > av->system_mem))
+            malloc_printerr ("malloc(): invalid size (unsorted)");
+          if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
+              || __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
+            malloc_printerr ("malloc(): invalid next size (unsorted)");
+          if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
+            malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
+          if (__glibc_unlikely (bck->fd != victim)
+              || __glibc_unlikely (victim->fd != unsorted_chunks (av)))
+            malloc_printerr ("malloc(): unsorted double linked list corrupted");
+          if (__glibc_unlikely (prev_inuse (next)))
+            malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

  • unsorted chunk의 사이즈만 체크를 했다면
  • next_chunk의 prev_size를 체크해서 현재 unsorted chunk사이즈와 일치한지 확인한다.
  • unsorted chunk의 forward를 체크하여 unsorted bin 인지 체크한다.
  • next_chunk의 prev_bit를 보아서 unsorted chunk가 free된 상태인지 체크를 한다.

결과적으로 unsorted bin에 있는 청크가 사이즈가 변조 되었는지 prev_inuse bit가 변조되었는지 fd가 변조되었는지 확인한다.

1
2
3
4
5
6
7
8
@@ -3784,6 +3782,8 @@ _int_malloc (mstate av, size_t bytes)
             }

           /* remove from unsorted list */
+          if (__glibc_unlikely (bck->fd != victim))
+            malloc_printerr ("malloc(): corrupted unsorted chunks 3");
           unsorted_chunks (av)->bk = bck;
           bck->fd = unsorted_chunks (av);
  • unsorted list에서 chunk를 제거할 때 victim->bk->fd == victim인지 확인 절차가 추가된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@@ -3864,10 +3864,14 @@ _int_malloc (mstate av, size_t bytes)
	 {
	   victim->fd_nextsize = fwd;
	   victim->bk_nextsize = fwd->bk_nextsize;
+      if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
+           malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
	   fwd->bk_nextsize = victim;
	   victim->bk_nextsize->fd_nextsize = victim;
	 }
   bck = fwd->bk;
+      if (bck->fd != fwd)
+      malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
 }
}
else

unsorted list에서 largebin 으로 이동 할 때도 마찬가지로 largbin list에 bk_nextsize, bk에 fd_nextsize, fd가 제대로 연결 되어있는지 확인한다.

use top

```c{open true} @@ -4099,6 +4103,9 @@ _int_malloc (mstate av, size_t bytes) victim = av->top; size = chunksize (victim);

  • if (__glibc_unlikely (size > av->system_mem))
  • malloc_printerr (“malloc(): corrupted top size”); + if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) { remainder_size = size - nb; ```

  • top chunk의 사이즈보다 요청한 사이즈가 클 때 에러를 출력한다.

_int_free

tcache dfb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@@ -4174,13 +4181,33 @@ _int_free (mstate av, mchunkptr p, int have_lock)
 #if USE_TCACHE
   {
     size_t tc_idx = csize2tidx (size);
-
-    if (tcache
-	&& tc_idx < mp_.tcache_bins
-	&& tcache->counts[tc_idx] < mp_.tcache_count)
+    if (tcache != NULL && tc_idx < mp_.tcache_bins)
       {
-	tcache_put (p, tc_idx);
-	return;
+	/* 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
  • tc_idx를 구해 tcache에 속하는지 확인 후 DFB확인을 한다.
  • e->key == tcache 인지 확인한다. 여기서 종료하는 것이 아니라 tcache를 돌면서 e 가 진짜로 존재하는지 찾는다.

regular bin consolidate

1
2
3
4
5
6
7
8
9
@@ -4301,7 +4328,9 @@ _int_free (mstate av, mchunkptr p, int have_lock)
       prevsize = prev_size (p);
       size += prevsize;
       p = chunk_at_offset(p, -((long) prevsize));
-      unlink(av, p, bck, fwd);
+      if (__glibc_unlikely (chunksize(p) != prevsize))
+        malloc_printerr ("corrupted size vs. prev_size while consolidating");
+      unlink_chunk (av, p);
     }
  • mmap bit 가 없는 청크 중 regular bin 이 free 되었을 때 prev_chunk와 consolidate과정이 일어난다. 현재 청크의 prev_size 값과 prev_chunk의 size가 일치하는지 체크한다.

  • malloc_consolidate 함수에서도 마찬가지로 prev_chunk와 unlink를 진행 할 때 prev_size 체크를 진행한다.

정리

exploit 기법에 영향을 줄 만큼 크게 바뀐것은 tcache->key, tcache_put, get 실행될 때 counts의 개수를 체크한다.

consolidate가 진행될 때 prev_chunk와 통합되면 prev_size와 prev_chunk size를 체크한다.!

This post is licensed under CC BY 4.0 by the author.