释放AMD显卡潜能:Radeon Software Slimmer让你的驱动更精简高效
【免费下载链接】ctf-wiki
Come and join us, we need you!
本文是 CTF-Wiki 内核利用(kernel-mode exploitation)系列的基础篇章。它基于 内核堆概述 一文展开,系统讲解 Linux 内核从物理内存到对象分配的全链路:
页 → 区 → 节点三级结构、以页为粒度的 buddy system、以及面向内核对象分配的 slub allocator。读完本文,你将理解内核堆与用户态 glibc 堆的本质差异,掌握kmem_cache_cpu/kmem_cache_node/ freelist / partial / full 等核心概念,为后续阅读 freelist 劫持、堆喷、UAF 利用 等实战章节打下理论基础。
从用户态堆到内核堆
类似于用户态进程中的堆(heap),内核也拥有自己的一套动态内存管理机制。为了方便讨论,我们同样将内核中动态分配的内存称为“堆”。但与用户态 glibc 的 ptmalloc2 相比,内核堆是一个跨越物理内存与虚拟内存两层视角、由多个分配器协同工作的复杂系统。
Linux kernel 将内存分为 页 → 区 → 节点 三级结构,并主要由两个内存管理器负责动态分配:
- buddy system:以内存页(page)为粒度,管理所有可用的物理内存;
- slab allocator:向 buddy system 请求内存页后,将其划分为多个较小的对象(object),实现细粒度的对象级内存管理。
这两层分配器构成了内核堆利用的全部战场:前者决定了“物理页如何分配、相邻与否”,后者决定了“对象从哪个缓存池、以何种顺序分配”。理解二者的配合关系,是理解内核堆漏洞(溢出、UAF、任意分配)的前提。
页 → 区 → 节点三级结构
上图是一张十分经典的内核内存三级结构 Overview,自顶向下依次是:
- 节点(node):对应结构体
pgdata_list; - 区(zone):对应结构体
struct zone,图中展示了多种类型的 zone; - 页(page):对应结构体
struct page。
页(page)
Linux kernel 使用 page 结构体来表示一个物理页框(physical page frame),每个物理页框都对应一个独立的 page 结构体。page 结构体是整个内核内存管理的最小单位载体,buddy system 与 slab allocator 都围绕它展开:buddy system 用它管理页的空闲/占用状态,slub allocator 则在每一张页的 page 结构体上记录 freelist 等信息。
区(zone)
在 Linux 中,一个节点内不同用途的内存区域被划分为不同的区(zone),对应结构体 struct zone。不同类型的内存有各自适用的分配场景(例如 DMA 区、普通区等),zone 级别的划分让内核可以按用途差异化管理物理内存。
节点(node)
zone 再向上一层便是节点(node)。Linux 将**内存控制器(memory controller,MC)**作为节点划分的依据:
- 对于 UMA 架构而言,系统中只有一个节点;
- 对于 NUMA 架构而言,通常有多个节点;
- 对于同一个内存控制器下的 CPU 而言,其对应的节点被称为本地内存(local memory),不同处理器之间通过总线进行进一步的连接。
如下图所示,一个 MC 对应一个节点:
对内核堆利用而言,NUMA/UMA 的区分意味着“对象到底从哪个内存节点分配”并不总是确定的,这也是后文“绑核”技巧要解决的一类问题。
buddy system:以页为粒度的物理内存管理
buddy system 是 Linux kernel 中一个较为底层的内存管理系统,以内存页为粒度管理着所有的物理内存。它存在于**区(zone)**这一级别,对当前 zone 所拥有的所有物理页框进行管理。
free_area 与 order 分阶管理
在每个 zone 结构体中,都有一个 free_area 结构体数组,用于存储 buddy system 按照 order 管理的空闲页面:
struct zone {
//...
struct free_area free_area[MAX_ORDER];
//...
};
其中的 MAX_ORDER 是一个常量,值为 11。
buddy system 按照空闲页面的连续大小进行分阶管理。这里的 order 的实际含义是连续空闲页面的大小,不过单位不是页面数,而是“阶”,即对于每个下标 order,其中所存储的连续页面大小为:
$$ 2^{order} $$
也就是说:free_area[0] 管理 1 页,free_area[1] 管理连续 2 页,free_area[2] 管理连续 4 页……直到 free_area[10] 管理连续 1024 页(即 MAX_ORDER - 1 阶)。free_area 中存放的页面通过自身的相应字段连接成双向链表结构,相同 order 的空闲页集合构成一条链表。
分配流程:向上借页,一分为二
当内核需要分配连续内存页时,buddy system 遵循如下过程:
- 首先将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标(order)取出连续内存页;
- 若对应下标的链表为空,则从下一个更高的 order 中取出内存页,一分为二:一半装载到当前下标对应的链表中(挂回当前 order),另一半返还给上层调用;
- 若下一个 order 也为空,则继续向更高的 order 重复该借页、拆分的过程,直至成功或到达
MAX_ORDER上限。
这个“拆 buddy”的过程保证了分配粒度始终是 2 的幂,且拆分出的两半物理地址连续——这一点正是后文 cross-cache 页级堆风水利用的物理基础。
释放流程:向上归还,两两合并
当内存页被释放时:
- 将对应的连续内存页释放到对应 order 的链表上;
- 检索是否存在可以合并的“兄弟页”(buddy),若有,则进行合成,放入更高 order 的链表中;
- 重复合并过程,直到无法找到相邻的兄弟页为止。
这种“按阶管理、按需拆分、成对合并”的设计,既保证了物理内存的高利用率,又避免了外部碎片无法回收的问题。值得注意的关键特性是 buddy system 遵循 LIFO(后进先出)原则——后释放的页会被更先分配出去,这正是页级堆风水利用的基石之一(详见后文 cross-cache 章节)。
slab allocator:细粒度的对象分配器
slab allocator 是更为细粒度的内存管理器:它通过向 buddy system 请求单张或多张连续内存页,再将页面分割成**同等大小的对象(object)**返还给上层调用者,从而实现对内核对象(结构体、缓冲区等)的高效复用分配。
slab allocator 一共有三种版本:
- slab:最初的版本,机制比较复杂,效率不高;
- slob:用于嵌入式等场景的极为简化版本;
- slub:优化后的版本,也是现在的通用版本。
slub 基本结构
slub 版本的 allocator 为现在绝大多数 Linux kernel 所装配的版本,因此 CTF-Wiki 的内核堆利用章节主要叙述的也是 slub allocator,其基本结构如下图所示:
我们将 slub allocator 每次向 buddy system 请求得来的单张/多张内存页称之为一个 slub。一个 slub 被分割为多个同等大小的对象(object),每个 object 作为一个被分配实体。在 slub 的第一张内存页对应的 page 结构体上,freelist 成员指向该张内存页上的第一个空闲对象;一个 slub 上的所有空闲对象组成一个以 NULL 结尾的单向链表。
一个 object 可以类比为用户态 glibc 中的 chunk。不过 object 并不像 chunk 那样需要一个 header 来记录元数据,因为
page结构体与物理内存之间存在线性对应关系,我们可以直接通过 object 地址找到其对应的page结构体,从而反查其所属 slub、cache 等管理信息。这一“无头对象”的特性,也直接影响了内核堆溢出的利用模型(详见下文 cross-cache 章节的讨论)。
kmem_cache:对象分配的缓存组件
kmem_cache 是一个基本的 allocator 组件,用于分配**某个特定大小(某种特定用途)**的对象。所有的 kmem_cache 构成一个双向链表,并存在两个对应的结构体数组 kmalloc_caches 与 kmalloc_dma_caches(在开启 MEMCG 的内核上还有 kmalloc-cg-* 系列,详见后文)。
一个 kmem_cache 主要由两个模块组成:
kmem_cache_cpu:这是一个 percpu 变量(即每个 CPU 核心上都独立保留有一个副本,其原理是以gs寄存器作为 percpu 段的基址进行寻址),用于表示当前核心正在使用的 slub。正因为每个核心操作的是自己私有的 slub,当前 CPU 从kmem_cache_cpu上取 object 时不需要加锁,从而极大地提高了性能;kmem_cache_node:可以理解为当前kmem_cache的 slub 集散中心,其中存放着两个 slub 链表:- partial:该 slub 上存在一定数量的空闲 object,但并非全部空闲;
- full:该 slub 上的所有 object 都已被分配出去。
分配/释放过程
基于上述结构,slub allocator 的分配与释放行为如下。
分配路径(自快到慢的三级查找):
- 首先从
kmem_cache_cpu上取对象,若有空闲对象则直接返回; - 若
kmem_cache_cpu上的 slub 已经无空闲对象,则将该 slub 从kmem_cache_cpu上取下,并尝试从 partial 链表上取一个 slub 挂载到kmem_cache_cpu上,然后再取出空闲对象返回; - 若
kmem_cache_node的 partial 链表也空了,那就向 buddy system 请求分配新的内存页,将其划分为多个 object 之后再交给kmem_cache_cpu,取出空闲对象返回上层调用。
释放路径(根据所属 slub 分三种情况):
- 若被释放 object 属于
kmem_cache_cpu的 slub:直接使用头插法插入当前 CPU slub 的 freelist; - 若被释放 object 属于
kmem_cache_node的 partial 链表上的 slub:直接使用头插法插入对应 slub 的 freelist; - 若被释放 object 属于 full slub:该 object 会成为对应 slub 的 freelist 头节点,且该 slub 会被放置到 partial 链表。
以上便是 slub allocator 的基本原理。可以看到:分配遵循“CPU 本地 → partial → 向 buddy 取新页”的延迟内存扩展策略,释放则遵循“就近归位 + 头插法”的 LIFO 语义。这套行为决定了内核堆的分配顺序可预测性,也决定了漏洞利用中“堆风水”的基本盘。
从内存管理到漏洞利用:基于仓库的实战延伸
理解了 buddy system 与 slub allocator 之后,我们再结合 CTF-Wiki 仓库中同目录下的利用章节,提炼几条直接由上述机制推导出的实战要点。
绑核:将多核 slub 模型简化为单 CPU 模型
slub allocator 会优先从当前核心的 kmem_cache_cpu 中进行内存分配。在多核架构下存在多个 kmem_cache_cpu,由于进程调度算法会保持核心间的负载均衡,exp 进程可能在不同核心上运行,导致内核对象的分配来自不同的 kmem_cache_cpu,利用模型随之变得复杂,也降低了漏洞利用的成功率(例如在 core 0 上构造好 double free,下一步却被调度到 core 1,利用链瞬间失效)。
因此,为了保证漏洞利用的稳定,需要将进程绑定到特定的某个 CPU 核心上。绑定后,slub allocator 的模型便简化成了单个 kmem_cache_node + 单个 kmem_cache_cpu,后续利用也更容易复现。仓库中文版 slub/introduction.md 给出了如下绑核模板:
#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);
}
分配 flag 与 kmem_cache 的合并/隔离
内核对象的分配最常用 kmalloc() 函数,其第二个参数为 Get-Free-Page flag(gfp_t),不同 bit 代表不同的分配策略(如内存不足时是否唤醒 kswapd 回收等)。以下是二进制逆向时最常见的几种 flag 常量值(出自 slub/introduction.md,基于内核 6.14.4 分析):
GFP_KERNEL:0xCC0GFP_KERNEL | __GFP_ZERO:0xDC0(等价于kzalloc())GFP_KERNEL_ACCOUNT:0x400CC0GFP_KERNEL_ACCOUNT | __GFP_ZERO:0x400DC0
其中 GFP_KERNEL_ACCOUNT 比 GFP_KERNEL 多启用了一个 ___GFP_ACCOUNT_BIT:表示该对象使用 MEMCG 机制进行统计追踪(内核开启 CONFIG_MEMCG_KMEM=y 时生效)。这一 bit 直接决定了对象的隔离情况:在开启 CONFIG_MEMCG_KMEM 的内核上,使用 GFP_KERNEL_ACCOUNT 分配的对象会进入独立的 kmalloc-cg-* 缓存,与使用 GFP_KERNEL 的对象隔离,互不干扰。
此外,slab alias(slub 合并)机制会使得:当一个 kmem_cache 创建时,若已存在能分配相等/近似大小对象的 kmem_cache,则不会创建新的 kmem_cache,而是为原有 kmem_cache 起一个 alias 作为“新”的 kmem_cache 返回。例如 cred_jar 在旧版内核中是 kmalloc-192 的 alias;而若创建时设置了 SLAB_ACCOUNT flag,则会新建独立的 kmem_cache(新版内核中 cred_jar 与 kmalloc-192 便是两个互不干扰的独立缓存)。判断目标 object 是否“混用”缓存,是内核堆利用选型的第一步。
从 slub 到 buddy:cross-cache overflow 与页级堆风水
以上机制合起来,催生了内核堆利用中最具威力的一类手法。仓库中的 Cross-Cache Overflow 与页级堆风水 一文指出:
- 由于 slub allocator 底层会向 buddy system 请求页面,不同
kmem_cache使用的页面在物理内存上有可能相邻; - 若漏洞对象在页面 A、溢出目标在相邻页面 B,则可能实现跨越不同 kmem_cache 的堆溢出,打破
kmalloc-xx/kmalloc-cg-*等缓存之间的隔离,对近乎任意的内核结构体进行覆写; - 而由于 buddy system 遵循 LIFO,且从高阶 order 拆分出的两份低阶连续内存页物理连续,我们可以通过“耗尽 low-order 页 → 间隔释放 → 在目标 kmem_cache 上堆喷占位”的方式,手工构造一个已知的页级内存排布,这便是页级堆风水(page-level heap fengshui)。
(在 corCTF2022 cache-of-castaways 一题中,官方方案正是通过 PF_PACKET socket 的 setsockopt(PACKET_TX_RING) 触发 alloc_pg_vec() → __get_free_pages() 调用链完成页喷射与页排布,再利用 6 字节越界写篡改相邻页面上 cred->uid 完成提权。完整分析见 cross-cache.md。)
可见,本文所讲的 buddy system 分阶拆分与 slub 的页请求机制,正是高级内核堆利用(跨缓存溢出、页级风水)的理论根基。
相关文档与下一步阅读
本仓库 docs/zh-tw/docs/pwn/linux/kernel-mode/exploitation/heap/ 目录围绕本文主题形成了一条完整的知识链,推荐按以下顺序继续阅读:
- freelist 劫持:覆写 freelist next 指针实现内核任意地址分配;
- 堆喷(spray):在目标 kmem_cache 上批量分配对象以达成堆风水;
- UAF 利用:利用释放后未置空指针等缺陷完成利用;
- Cross-Cache Overflow 与页级堆风水:针对 buddy system 的跨缓存溢出利用;
- Page UAF:页级 UAF 利用手法;
- 若需补充 kmalloc flag、
kmalloc_caches索引计算、slub 合并与隔离等进阶前置知识,可参考中文版 slub/introduction.md。
原文档末尾的 REFERENCE 指向多篇外部内核内存管理系列文章(arttnba3 的 Linux Kernel Memory 系列及部分中文博客),本文从可验证性出发未附外链;读者可结合本文与仓库内实战章节交叉印证。另需注意:文中涉及的
MAX_ORDER = 11、flag 常量值与kmalloc_caches布局均以文档所述内核版本为基准,不同内核版本(尤其 5.x / 6.x)间存在差异,实战分析时应以目标内核的实际配置为准。
【免费下载链接】ctf-wiki
Come and join us, we need you!
更多推荐






所有评论(0)