0x00

记录一下不同版本glibc下的堆管理保护机制的变化(开个坑,以后慢慢记录补充

先贴上几篇参考博客

不同版本glibc的堆管理和新增保护机制 - Luexp
glibc各版本的堆保护 | jkilopu’s blog
Glibc中堆管理的变化 | 白里个白
Glibc高版本堆利用方法总结 - LynneHuan - 博客园
然后ubuntu和glibc不同版本的对应关系

1
2
3
4
5
6
7
8
9
10
11
12
13
ubuntu-libc version

2.23=“16.04”
2.24=“17.04”
2.26=“17.10”
2.27=“18.04”
2.28=“18.10”
2.29=“19.04”
2.30=“19.10”
2.31=“20.04”
2.32=“20.10”
2.33=“21.04”
2.34=“22.04”

0x01 关于tcachebins

在glibc2.26+,引入了tcachebins
有两个比较关键的函数tcache_get()tcache_put()(以下为glibc2.28中的源码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static 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]);
}

static 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;
}

这两个函数会在函数 __int_free__libc_malloc的开头被调用,其中 tcache_put 当所请求的分配大小不大于0x408并且当给定大小的 tcache bin 未满时调用。一个 tcache bin 中的最大块数mp_.tcache_count是7
当程序进行 malloc 操作时,会优先检查 tcache 是否有可用的 chunk,如果有,就直接返回。同样,当进行 free 操作时,如果 chunk 的大小符合要求,并且对应的 tcache bin 还未满(默认每个 bin 可以存放 7 个 chunk),就会把 chunk 放入 tcache。否则,会按照原来的流程,放入 unsorted bin 或者其他的 bin 中
tcache_get中,仅仅检查了 tc_idx,此外,我们可以将 tcache 当作一个类似于 fastbin 的单独链表,只是它的 check,并没有 fastbin 那么复杂,仅仅检查 tcache->entries[tc_idx] = e->next.
tcachebins并不是由main_arena(位于libc数据段)管理的,而是由tcache_perthread_struct(位于堆段)管理的。具体管理机制见另一篇博客

0x02 tcache_key与safe-linking

参考博客
Safe-Linking 机制的绕过 | ZIKH26’s Blog
tcache bin key加密机制
在glibc2.29+,tcache bin引入了tcache的key加密机制,tcache_entry中新加了一个指针key

1
2
3
4
struct tcache_entry {
struct tcache_entry *next;
void key;
};

我们看引入了key之后的tcache_put()的部分源码(以下为glibc2.32+)

1
2
3
4
5
6
7
8
9
10
11
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache_key;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

chunk2mem函数是返回我们free的chunk的实际数据区,也就是去除推头0x10的部分。将其key字段(即fd后的0x8字节)设置为tcache_key。在free一个chunk时候,会检查其key字段的值,如果等于对应tcachebins的tcache_key,说明已经释放过,则会报错中断,如下

1
2
3
4
[DEBUG] Received 0x29 bytes:
b'free(): double free detected in tcache 2\n'
[DEBUG] Received 0x2b bytes:
b'timeout: the monitored command dumped core\n'

要绕过这一机制进行double free,需要能够破坏free的chunk的key值即可。
至于safe-linking,在glibc2.32+引入,在上面源码就可以看到

1
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);

这一段就是对tcache_entry中的next指针加密。这个宏定义如下

1
2
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))

也就是被释放的chunk在tcache->entry中的next指针值等于被释放的chunk本身的mem地址右移12位再异或上其要指向的chunk(原本tcachebins中的第一个chunk)的mem地址,这一加密结果也会同步到tcachebins中的chunk的fd指针中。我们自己简单验证一下。在glibc2.35版本中我们可以看到如下

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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x290 (with flag bits: 0x291)

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x555555559290
Size: 0x90 (with flag bits: 0x91)
fd: 0x555555559

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x555555559320
Size: 0x90 (with flag bits: 0x91)
fd: 0x55500000c7f9

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x5555555593b0
Size: 0x90 (with flag bits: 0x91)
fd: 0x55500000c669

Top chunk | PREV_INUSE
Addr: 0x555555559440
Size: 0x20bc0 (with flag bits: 0x20bc1)

pwndbg> bins
tcachebins
0x90 [ 3]: 0x5555555593c0 —▸ 0x555555559330 —▸ 0x5555555592a0 ◂— 0
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
empty
pwndbg> print/x (0x5555555593c0 >> 12) ^ 0x555555559330
$1 = 0x55500000c669
pwndbg> print/x (0x555555559330 >> 12) ^ 0x5555555592a0
$2 = 0x55500000c7f9
pwndbg> print/x (0x5555555592a0 >> 12) ^ 0
$3 = 0x555555559

这是笔者自己写的先申请三个chunk再先后free的程序的调试结果。可以看到第三个chunk的fd为0x55500000c669,并不是其应该指向的0x555555559330。我们经过safe-linking的加密,(0x5555555593c0 >> 12) ^ 0x555555559330=0x55500000c669。
特别的,对于第一个释放的chunk1,其fd值为0x555555559,因为其在释放时对应的tcachebins为空,其本来应该指向的是0,那么加密之后其实就是其本身fd地址右移12位。在这种情况下,其加密其实并没有达到预期效果,只需左移12位便可以恢复其地址,实现绕过
我发现:

1
2
3
4
5
6
pwndbg>  print/x (0x55500000c7f9 ^ 0x5555555592a0) << 12
$4 = 0x555555559000
pwndbg> print/x (0x55500000c669 ^ 0x555555559330) << 12
$5 = 0x555555559000
pwndbg> print/x (0x555555559 ^ 0) << 12
$6 = 0x555555559000

在同一页内存的chunk,其fd值异或上其指向的真实值再左移12位便得到了heap_base。(堆内存按页分配,heap_base都是0x1000的整数倍)
例如在一个存在uaf漏洞的程序中,我们申请一个chunk,然后放入tcachebin中,再访问其fd值,得到的fd值左移12位便是heap_base,其实是heap_base =(leak_add ^ 0)<< 12,因为此时对应的tcachebins中只有这一个chunk,其fd指向的真实地址就是0
然后就是关于entries指针数组和对应tcachebins的关系,其实二者指向的是同一块内存,源码中在操作时候对e->key,e->next的赋值其实也就是对tcachebins中相应chunk的key字段和fd域的赋值。
在safe-linking引入后,tcache_perthread_struct的entries数组中存放的依然是对应tcachebins的第一个chunk的真实的mem地址(也就是fd地址)(索引方式见我的另一篇博客),所以我们劫持tcache_perthread_struct后仍然可以直接覆盖entries数组的元素为我们的目标地址。
然而在safe-linking保护下,我们使用tcache poisoning(类似fastbin attack)修改tcachebins中的chunk的fd域的时候,就需要将目标地址进行加密操作再覆盖。
例如我们已经泄露了heap_base,要修改tcachebins中的第一个chunk的fd,便要计算val =((heap_base+0x2a0) >> 12) ^ target_add,用这个val来投毒。target_add便是我们想利用tcache poisoning申请的地址,至于heap_base+0x2a0便是我们想要修改fd域的chunk的mem地址(0x290是在glibc2.32+下的tcache_perthread_struct的大小,那么heap+0x290+0x10便是我们申请的第一个chunk的mem地址,也就是此例子中我们要进行投毒的chunk。具体的地址要具体调试分析,此处只是为了方便以申请的第一个chunk为例。)
然而在实际上,右移12位后,16进制下地址的后三位都被移出,不会产生影响。也就是说,对和heap_base同一页(单位0x1000)下的chunk进行tcache poisoning时候,只需要直接使用(heap_base>>12)^target_add即可。

0x03 关于hooks

各种hooks在glibc2.34+都被移除了。在glibc2.34之前的版本中对hooks的劫持时是控制程序执行流的重要方法
在malloc.c中可以看到几个全局钩子

1
2
3
4
5
6
7
8
/* 钩子指针声明 */
void *weak_variable (*__malloc_hook)(size_t size, const void *caller) = 0;
void *weak_variable (*__realloc_hook)(void *ptr, size_t size, const void *caller) = 0;
void *weak_variable (*__memalign_hook)(size_t alignment, size_t size, const void *caller) = 0;
void weak_variable (*__free_hook)(void *ptr, const void *caller) = 0;

/* 初始化钩子 */
void (*weak_variable __malloc_initialize_hook)(void) = 0;

这几个钩子也正是pwn中劫持的重点关照对象。这几个hook大多都能在libc中直接查到偏移位置。(例如libc.sym['__free_hook']
关于钩子的触发,以__malloc_hook为例,源码如下

1
2
3
4
5
6
7
8
9
10
11
12
void *__libc_malloc(size_t bytes) {
// [1] 钩子检查
void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
if (__builtin_expect(hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS(0)); // 触发钩子调用

// [2] 常规分配流程
void *result = _int_malloc(&main_arena, bytes);

// [3] 分配失败处理
return malloc_check(result, bytes);
}

可以看到在执行__libc_malloc函数时候会先检查__malloc_hook,如果钩子非空,则直接执行指向的函数(或被篡改的可执行的地址)。其他几个全局钩子也是类似的,调用对应的函数时候,都会先检查对应的hook,不空则直接执行。
比如打__free_hook,改为system函数然后在chunk中写入'/bin/sh\x00',直接free对应chunk即可。我们看源码

1
2
3
4
5
6
7
8
9
10
void __libc_free(void *mem) {
// 钩子检查
void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);
if (__builtin_expect(hook != NULL, 0)) {
(*hook)(mem, RETURN_ADDRESS(0));
return; // 跳过后续释放逻辑
}
// 正常释放流程
_int_free(&main_arena, mem, 0);
}

因为__free_hook函数会以传入__libc_free的对应chunk的mem地址为参数。而打__malloc_hook时候则更多需要one_gadget的辅助了
glibc2.34+各类hooks被移除,因此也需要掌握其他方法.