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
|
unlink
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);
_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를 체크한다.!