跳转至

简介

本节我们主要介绍在针对 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_KERNELGFP_KERNEL_ACCOUNT 是内核中最为常见与通用的分配 flag,由多个常用 bit 组合而成,这两种 flag 的区别主要在于 GFP_KERNEL_ACCOUNTGFP_KERNEL 多启用了一个 bit ___GFP_ACCOUNT_BIT ——表示该对象使用 MEMCG 机制进行数据记录,该机制为 Memory CGroups 的一部分,主要用于统计追踪内核对象的分配,在内核启用了 CONFIG_MEMCG_KMEM=y 时生效。若未开启 CONFIG_MEMCG_KMEM ,则 GFP_KERNELGFP_KERNEL_ACCOUNT 等价。

我们主要关注当这种机制启用时内核对象分配所发生的变化:

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

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

此外,当使用了 __GFP_ZERO 这一 flag 时,分配的内核对象会在返回给使用者之前进行数据清零的工作,即等价于 kzalloc()

在对内核镜像或模块进行二进制逆向分析时,我们通常仅能看到 flag 为一个常量值,因此我们需要手动分析启动了那些 bit。以下是常见的分配 flag 对应的值:

  • GFP_KERNEL:0xCC0
  • GFP_KERNEL | __GFP_ZERO: 0xDC0
  • GFP_KERNEL_ACCOUNT:0x400CC0
  • GFP_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.4mm/slab_common.cinclude/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 一般会有 14kmem_cache ,根据这个规则我们便能计算 kmalloc_caches[N] 对应的大小,以下是两个例子:

  • kmalloc_caches[12]: 大小范围位于 KMALLOC_NORMAL 对应的 index 0~13 ,取 kmalloc_info[12 - 0] ,得到大小 4k,对应 kmem_cachekmalloc-4k,对应 flag 为 GFP_KERNEL
  • kmalloc_caches[54]: 大小范围位于 KMALLOC_CGROUP 对应的 index 42~55 ,取 kmalloc_info[54 - 42] ,得到大小 4k,对应 kmem_cachekmalloc-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_jarkmalloc-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

https://lwn.net/Articles/976460/