slub allocator之kernel UAF

UAF 即 Use After Free,通常指的是对于释放后未重置的垂悬指针的利用,此前在用户态下的 heap 阶段对于 ptmalloc 的利用很多都是基于 UAF 漏洞进行进一步的利用。

在 CTF 当中,内核的 “堆内存” 主要指的是直接映射区(direct mapping area),常用的分配函数 kmalloc 从此处分配内存,常用的分配器为 slub,若是在 kernel 中存在着垂悬指针,我们同样可以以此完成对 slab/slub 内存分配器的利用,通过 Kernel UAF 完成提权。

内核堆利用与绑核

slub allocator 会优先从当前核心的 kmem_cache_cpu 中进行内存分配,在多核架构下存在多个 kmem_cache_cpu ,由于进程调度算法会保持核心间的负载均衡,因此我们的 exp 进程可能会被在不同的核心上运行,这也就导致了利用过程中 kernel object 的分配有可能会来自不同的 kmem_cache_cpu ,这使得利用模型变得复杂,也降低了漏洞利用的成功率。

比如说你在 core 0 上整了个 double free,准备下一步利用时 exp 跑到 core 1 去了,那就很容易让人摸不着头脑 :(

因此为了保证漏洞利用的稳定,我们需要将我们的进程绑定到特定的某个 CPU 核心上,这样 slub allocator 的模型对我们而言便简化成了 kmem_cache_node + kmem_cache_cpu ,我们也能更加方便地进行漏洞利用。

现笔者给出如下将 exp 进程绑定至指定核心的模板:

1
2
3
4
5
6
7
8
9
10
11
#include <sched.h>

/* to run the exp on the specific core only */
void bind_cpu(int core)
{
cpu_set_t cpu_set;

CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}

通用 kmalloc flag

GFP_KERNELGFP_KERNEL_ACCOUNT 是内核中最为常见与通用的分配 flag,常规情况下他们的分配都来自同一个 kmem_cache ——即通用的 kmalloc-xx

这两种 flag 的区别主要在于 GFP_KERNEL_ACCOUNTGFP_KERNEL 多了一个属性——表示该对象与来自用户空间的数据相关联,因此我们可以看到诸如 msg_msgpipe_buffersk_buff的数据包 的分配使用的都是 GFP_KERNEL_ACCOUNT ,而 ldt_structpacket_socket 等与用户空间数据没有直接关联的结构体则使用 GFP_KERNEL

在 5.9 版本之前GFP_KERNELGFP_KERNEL_ACCOUNT 存在隔离机制,在 这个 commit 中取消了隔离机制,自内核版本 5.14 起,在 这个 commit 当中又重新引入:

  • 对于开启了 CONFIG_MEMCG_KMEM 编译选项的 kernel 而言(通常都是默认开启),其会为使用 GFP_KERNEL_ACCOUNT 进行分配的通用对象创建一组独立的 kmem_cache ——名为 kmalloc-cg-\* ,从而导致使用这两种 flag 的 object 之间的隔离。

slub 合并 & 隔离

slab alias 机制是一种对同等 / 相近大小 object 的 kmem_cache 进行复用的一种机制:

  • 当一个 kmem_cache 在创建时,若已经存在能分配相等 / 近似大小的 object 的 kmem_cache ,则不会创建新的 kmem_cache,而是为原有的 kmem_cache 起一个 alias,作为 “新的” kmem_cache 返回

举个🌰,cred_jar 是专门用以分配 cred 结构体的 kmem_cache,在 Linux 4.4 之前的版本中,其为 kmalloc-192 的 alias,即 cred 结构体与其他的 192 大小的 object 都会从同一个 kmem_cache——kmalloc-192 中分配。

对于初始化时设置了 SLAB_ACCOUNT 这一 flag 的 kmem_cache 而言,则会新建一个新的 kmem_cache 而非为原有的建立 alias,🌰如在新版的内核当中 cred_jarkmalloc-192 便是两个独立的 kmem_cache彼此之间互不干扰