slub allocator之kernel UAF
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 | #include <sched.h> |
通用 kmalloc flag
GFP_KERNEL
与 GFP_KERNEL_ACCOUNT
是内核中最为常见与通用的分配 flag,常规情况下他们的分配都来自同一个 kmem_cache
——即通用的 kmalloc-xx
。
这两种 flag 的区别主要在于 GFP_KERNEL_ACCOUNT
比 GFP_KERNEL
多了一个属性——表示该对象与来自用户空间的数据相关联,因此我们可以看到诸如 msg_msg
、pipe_buffer
、sk_buff的数据包
的分配使用的都是 GFP_KERNEL_ACCOUNT
,而 ldt_struct
、packet_socket
等与用户空间数据没有直接关联的结构体则使用 GFP_KERNEL
。
在 5.9 版本之前GFP_KERNEL
与 GFP_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_jar
与 kmalloc-192
便是两个独立的 kmem_cache
,彼此之间互不干扰。