简介¶
本节我们主要介绍在针对 SLUB allocator 进行利用时的一些前置补充知识与注意事项。
内核堆利用与绑核¶
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 进程绑定至指定核心的模板:
#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);
printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}
通用 kmalloc flag¶
在内核对象的分配当中,最常用的函数是 kmalloc()
,该函数原型如下(来自内核版本 6.14.4
):
static __always_inline __alloc_size(1) void *kmalloc_noprof(size_t size, gfp_t flags)
//...
#define kmalloc(...) alloc_hooks(kmalloc_noprof(__VA_ARGS__))
其中第一个参数 size 为分配的内核对象的大小,第二个参数 flag 为 Get-Free-Page flag
,表示内核在分配过程中所采取的策略,不同的 bit 代表着不同的策略,例如当 ___GFP_KSWAPD_RECLAIM_BIT
启用时意味着在内存不足时唤醒 kswapd
进行内存回收。
GFP_KERNEL
与 GFP_KERNEL_ACCOUNT
是内核中最为常见与通用的分配 flag,由多个常用 bit 组合而成,这两种 flag 的区别主要在于 GFP_KERNEL_ACCOUNT
比 GFP_KERNEL
多启用了一个 bit ___GFP_ACCOUNT_BIT
——表示该对象使用 MEMCG 机制进行数据记录,该机制为 Memory CGroups 的一部分,主要用于统计追踪内核对象的分配,在内核启用了 CONFIG_MEMCG_KMEM=y
时生效。若未开启 CONFIG_MEMCG_KMEM
,则 GFP_KERNEL
与 GFP_KERNEL_ACCOUNT
等价。
我们主要关注当这种机制启用时内核对象分配所发生的变化:
- 常规情况下他们的分配都来自同一个
kmem_cache
——即通用的kmalloc-xx
。 - 对于开启了
CONFIG_MEMCG_KMEM
编译选项的 kernel 而言(通常都是默认开启),其会为使用GFP_KERNEL_ACCOUNT
进行分配的通用对象创建一组独立的kmem_cache
——名为kmalloc-cg-*
,从而导致使用这两种 flag 的 object 之间的隔离。
在5.9 版本之前
GFP_KERNEL
与GFP_KERNEL_ACCOUNT
存在隔离机制,在 这个 commit 中取消了隔离机制,自内核版本 5.14 起,在 这个 commit 当中又重新引入。
此外,当使用了 __GFP_ZERO
这一 flag 时,分配的内核对象会在返回给使用者之前进行数据清零的工作,即等价于 kzalloc()
。
在对内核镜像或模块进行二进制逆向分析时,我们通常仅能看到 flag 为一个常量值,因此我们需要手动分析启动了那些 bit。以下是常见的分配 flag 对应的值:
GFP_KERNEL
:0xCC0GFP_KERNEL | __GFP_ZERO
: 0xDC0GFP_KERNEL_ACCOUNT
:0x400CC0GFP_KERNEL_ACCOUNT | __GFP_ZERO
: 0x400DC0
kmalloc 编译优化¶
对于分配的内核对象的大小为固定值的情况下,在编译期我们便能知道将会从哪一个 kmem_cache
进行分配,因此内核会将对 kmalloc()
的调用优化为对 kmem_cache_alloc_noprof()
的调用,该函数原型如下(来自内核版本 6.14.4
):
void *kmem_cache_alloc_noprof(struct kmem_cache *cachep,
gfp_t flags) __assume_slab_alignment __malloc;
#define kmem_cache_alloc(...) alloc_hooks(kmem_cache_alloc_noprof(__VA_ARGS__))
实际上,内核原生的 kmem_cache
(即 kmalloc-xx
等)的属性大致是编译期确定的,会按固定顺序存放在一个全局 kmem_cache
数组 kmalloc_caches
当中(定义于 mm/slab_common.c
),因此在发生此类编译优化时你可能会在反编译器中看到形如 kmem_cache_alloc_noprof(kmalloc_caches[3], 0xCC0)
的形式,其原始定义如下(来自内核版本 6.14.4
):
typedef struct kmem_cache * kmem_buckets[KMALLOC_SHIFT_HIGH + 1];
kmem_buckets kmalloc_caches[NR_KMALLOC_TYPES] __ro_after_init =
{ /* initialization for https://llvm.org/pr42570 */ };
EXPORT_SYMBOL(kmalloc_caches);
该变量实际上在内核内存初始化函数之一的 create_kmalloc_caches()
中通过 new_kmalloc_cache()
初始化,具体分析过程留给读者课后练习,这里我们直接给出 index 计算所需参考的代码位置(来自内核版本 6.14.4
,mm/slab_common.c
,include/linux/slab.h
):
#define INIT_KMALLOC_INFO(__size, __short_size) \
{ \
.name[KMALLOC_NORMAL] = "kmalloc-" #__short_size, \
KMALLOC_RCL_NAME(__short_size) \
KMALLOC_CGROUP_NAME(__short_size) \
KMALLOC_DMA_NAME(__short_size) \
KMALLOC_RANDOM_NAME(RANDOM_KMALLOC_CACHES_NR, __short_size) \
.size = __size, \
}
/*
* kmalloc_info[] is to make slab_debug=,kmalloc-xx option work at boot time.
* kmalloc_index() supports up to 2^21=2MB, so the final entry of the table is
* kmalloc-2M.
*/
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
INIT_KMALLOC_INFO(0, 0),
INIT_KMALLOC_INFO(96, 96),
INIT_KMALLOC_INFO(192, 192),
INIT_KMALLOC_INFO(8, 8),
INIT_KMALLOC_INFO(16, 16),
INIT_KMALLOC_INFO(32, 32),
INIT_KMALLOC_INFO(64, 64),
INIT_KMALLOC_INFO(128, 128),
INIT_KMALLOC_INFO(256, 256),
INIT_KMALLOC_INFO(512, 512),
INIT_KMALLOC_INFO(1024, 1k),
INIT_KMALLOC_INFO(2048, 2k),
INIT_KMALLOC_INFO(4096, 4k),
INIT_KMALLOC_INFO(8192, 8k),
INIT_KMALLOC_INFO(16384, 16k),
INIT_KMALLOC_INFO(32768, 32k),
INIT_KMALLOC_INFO(65536, 64k),
INIT_KMALLOC_INFO(131072, 128k),
INIT_KMALLOC_INFO(262144, 256k),
INIT_KMALLOC_INFO(524288, 512k),
INIT_KMALLOC_INFO(1048576, 1M),
INIT_KMALLOC_INFO(2097152, 2M)
};
enum kmalloc_cache_type {
KMALLOC_NORMAL = 0,
#ifndef CONFIG_ZONE_DMA
KMALLOC_DMA = KMALLOC_NORMAL,
#endif
#ifndef CONFIG_MEMCG
KMALLOC_CGROUP = KMALLOC_NORMAL,
#endif
KMALLOC_RANDOM_START = KMALLOC_NORMAL,
KMALLOC_RANDOM_END = KMALLOC_RANDOM_START + RANDOM_KMALLOC_CACHES_NR,
#ifdef CONFIG_SLUB_TINY
KMALLOC_RECLAIM = KMALLOC_NORMAL,
#else
KMALLOC_RECLAIM,
#endif
#ifdef CONFIG_ZONE_DMA
KMALLOC_DMA,
#endif
#ifdef CONFIG_MEMCG
KMALLOC_CGROUP,
#endif
NR_KMALLOC_TYPES
};
在内核源码中对 kmalloc_caches
的使用形式为 kmalloc_caches[type][index]
, 但实际上的使用是一维指针数组 ,每个 type 占据一段连续的 index, 且大于 8k 的大小通常不启用 ,因此每个 type 一般会有 14
个 kmem_cache
,根据这个规则我们便能计算 kmalloc_caches[N]
对应的大小,以下是两个例子:
kmalloc_caches[12]
: 大小范围位于KMALLOC_NORMAL
对应的 index0~13
,取kmalloc_info[12 - 0]
,得到大小 4k,对应kmem_cache
为kmalloc-4k
,对应 flag 为GFP_KERNEL
。kmalloc_caches[54]
: 大小范围位于KMALLOC_CGROUP
对应的 index42~55
,取kmalloc_info[54 - 42]
,得到大小 4k,对应kmem_cache
为kmalloc-cg-4k
,对应 flag 为GFP_KERNEL_ACCOUNT
。
该计算启用的
kmalloc_cache_type
为[KMALLOC_NORMAL,KMALLOC_RECLAIM,KMALLOC_DMA,KMALLOC_CGROUP]
,来自 Gentoo Linux 的默认配置。
需要注意的是, 这种计算方式仅在一般情况下正确 ,对于一些复杂的特殊情况, 超出 KMALLOC_NORMAL 范围的 index 计算未必可靠 (即 GFP_KERNEL
以外的计算都未必完全准确,因为我们有可能拿不准启用的 type 数量以及每个 type 的最大 cache 大小),因此比较准确的方式是进行动态调试查看对应的 kmem_cache::name
。
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
,彼此之间互不干扰。
Reference¶
https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/
https://arttnba3.cn/2023/02/24/OS-0X04-LINUX-KERNEL-MEMORY-6.2-PART-III/
https://kernel.org/doc/html/v5.4/admin-guide/cgroup-v1/memory.html
https://lwn.net/Articles/821664/
https://github.com/torvalds/linux/commit/10befea91b61c4e2c2d1df06a2e978d182fcf792
https://github.com/torvalds/linux/commit/494c1dfe855ec1f70f89552fce5eadf4a1717552