free 堆
glibc 2.43
数据字段被替换的第一个位置
若有以下定义:
int *a = malloc(sizeof(int));
int *b = malloc(sizeof(int));
然后把它们 free 掉:
free(a);
free(b);
再看栈上空间时:
x/4gx a
x/4gx b
得到:
0x555555559010: 0x0000000555555559 0x8e4b6aee6b5319b4
0x555555559020: 0x0000000000000000 0x0000000000000021
0x555555559030: 0x000055500000c549 0x8e4b6aee6b5319b4
0x555555559040: 0x0000000000000000 0x0000000000000411
其中 a 空间记录的 0x0000000555555559 是自身的地址右移 12 位后的值,由于它是第一个堆 chunk,所以其结构体 next 字段为 NULL,故按照计算公式:
\text{encoded data} = \text{next} \oplus (\text{address} \gg 12)
其应当计算作:
0 \oplus (0\text{x}555555559010 \gg 12) = 0\text{x}555555559
同理,b 空间记录的 0x000055500000c549 是自身的地址右移 12 位后的值,由于它是第二个堆 chunk,所以其结构体 next 字段为 0x555555559010,即 a 的 chunk 地址,故按照计算公式:
0\text{x}555555559010 \oplus (0\text{x}555555559030 \gg 12) = 0\text{x}55555550000c549
数据字段被替换的第二个位置
参照上面的程序,a 和 b 的第二段都变成了 0x8e4b6aee6b5319b4。
malloc.c 中生成 tcache key 的方法如下:
// L2895 key 就是 tcache_entry 的第二个字段 → 位于 user data + 8
typedef struct tcache_entry {
struct tcache_entry *next;
uintptr_t key; // ← 用于检测 double free
} tcache_entry;
static uintptr_t tcache_key; // L2963 进程级(注意:不是 __thread)
static void tcache_key_initialize (void) // L2974
{
if (__getrandom_nocancel_nostatus_direct (&tcache_key, sizeof(tcache_key),
GRND_NONBLOCK) != sizeof (tcache_key))
tcache_key = 0; // getrandom 失败 → 0
int minimum_bits = __WORDSIZE / 4; // 16
int maximum_bits = __WORDSIZE - minimum_bits; // 48
while (tcache_key <= 0x1000000
|| tcache_key >= ((uintptr_t) ULONG_MAX) - 0x1000000
|| stdc_count_ones (tcache_key) < minimum_bits
|| stdc_count_ones (tcache_key) > maximum_bits)
{ tcache_key = random_bits(); // 时钟派生的兜底
tcache_key = (tcache_key << 32) | random_bits(); }
}
static __always_inline void tcache_put_n (...) // L3016
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
e->key = tcache_key; // ← 明文写入,不参与 safe-linking
e->next = PROTECT_PTR (&e->next, *ep); // ← 只有 next 被 mangle
...
}
static __always_inline void *tcache_get_n (...) // L3057
{
...
e->key = 0; // ← 取出即清零
}
// __libc_free L3365
if (__glibc_unlikely (e->key == tcache_key))
return tcache_double_free_verify (e);
源码里只有长度/位密度约束(非 0、>0x1000000、popcount 在 16..48 之间),没有常量。故有结论:同进程内恒定、跨进程随机。
当 popcount = 34 时,正好得到 0x8e4b6aee6b5319b4,所以 a 和 b 的第二段都变成了这个值。
相较于 glibc 2.35,glibc 2.43 的栈有些变化
TCACHE_FILL_COUNT是一个有关填充 tcache 的常量宏,其在 glibc 2.43 中的值为16,而在 glibc 2.35 中的值为7。其表示每个 tcache bin 最多能装多少个空闲块的容量上限。它决定一块刚free()的 chunk 究竟进 tcache 并被写入 key,还是被送进_int_free_chunk()走 unsorted bin(并可能与相邻空闲块合并)。如果有了 16 个tcache bin 存在(即被free()了 16 次),第 17 个 chunk 就会被送进 unsorted bin。-
glibc 2.43 中
tcache_max最大支持 4 MiB。 -
glibc 2.43 中不存在 fastbin,2.42 及以前的 fastbin 操作不再奏效。
所以 double free 之前要塞满 tcache 然后 fastbin dup 或覆写 key,现在只能用 tcache dup 或者依照 unsorted bin 的方式去做。
