自动化与脚本
利用 Linux 内核漏洞提权时如何搭建环境…
在 CTF 或真实场景中利用 Linux 内核漏洞(UAF、OOB、竞态、类型混淆)实现提权时,按流程搭建 QEMU 调试环境、构造利用原语、绕过 KASLR/SMEP/SMAP/KPTI 并干净返回用户态。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
利用 Linux 内核漏洞提权时如何搭建通用
解决思路
- 按五步利用模型推进:1) 发现漏洞(UAF、OOB、竞态、整数溢出、类型混淆);2) 构造原语(任意读、任意写、受控 RIP);3) 绕过缓解机制(KASLR、SMEP、SMAP、KPTI);4) 提权(commit_creds、modprobe_path、命名空间逃逸);5) 干净返回用户态(KPTI trampoline、iretq/sysretq、swapgs)。
- 搭建 QEMU + 自定义内核环境:下载并编译内核(如 linux-6.1),执行 make defconfig,用 scripts/config --disable RANDOMIZE_BASE 关闭 KASLR、--disable RANDOMIZE_LAYOUT 关闭 FG-KASLR、--enable DEBUG_INFO 开启调试信息,然后 make -j$(nproc)。
- 用 QEMU 启动内核:qemu-system-x86_64 -kernel bzImage -initrd rootfs.cpio.gz -append "console=ttyS0 nokaslr quiet" -nographic -s -S -monitor /dev/null -m 256M -cpu kvm64,+smep,+smap(-s -S 在 :1234 开 GDB server 并暂停)。
- 用 GDB 调试内核:gdb vmlinux,target remote :1234,add-symbol-file vmlinux 0xffffffff81000000 加载符号,用 b commit_creds 或 b *0xffffffff81234567 下断点,pwndbg/GEF 可用于内核调试。
- 修改 initramfs:mkdir rootfs && cd rootfs,cpio -idmv < ../rootfs.cpio.gz 解包,编辑 init 脚本并放入 exploit 二进制,再用 find . | cpio -o --format=newc | gzip > ../rootfs.cpio.gz 重新打包。
- 识别常见漏洞类型:UAF(对象已释放但指针仍可访问,如 CVE-2022-0847 DirtyPipe)、OOB 读写(数组索引或大小检查缺失,如 CVE-2021-22555 Netfilter)、竞态条件(检查与使用之间的 TOCTOU,如 CVE-2016-5195 DirtyCow)、整数溢出(大小计算回绕,常见于 ioctl handler)、类型混淆(对象被错误转换类型,如 CVE-2023-0179 Netfilter)、双重释放(SLUB 分配器利用)、栈溢出(内核栈很小,仅 8KB–16KB,较罕见)。
- 提权方法一:调用 commit_creds(prepare_kernel_cred(0)) 将当前进程凭据设为 root(uid=0、gid=0);内核 ROP 链等价形式为 pop rdi; ret → 0 → prepare_kernel_cred 地址 → mov rdi, rax; ...; ret(或 pop rdi + 已知位置)→ commit_creds 地址 → kpti_trampoline 或 swapgs+iretq 返回用户态。
- 提权方法二:覆写 modprobe_path(内核 .data 中的 "/sbin/modprobe")为 "/tmp/x",再触发未知二进制格式让内核以 root 执行 /tmp/x;具体做法是写 /tmp/x 脚本(如 cp /flag /tmp/flag && chmod 777 /tmp/flag)并 chmod +x,再用 echo -ne '\xff\xff\xff\xff' > /tmp/dummy、chmod +x /tmp/dummy、执行 /tmp/dummy 触发。
- 提权方法三:若拥有任意写并能找到当前 task 的 cred 指针,直接将该 cred 结构中的 uid/gid 字段清零。
- 提权方法四(容器场景):覆写 init_nsproxy 或操纵命名空间指针以逃逸容器隔离。
- 获取受控 RIP 的来源:被破坏的函数指针(UAF 对象含类 vtable 分发,覆写指针)、被破坏的返回地址(内核栈溢出,罕见)、被破坏的 ops 结构(file_operations、seq_operations 等模块操作结构)。
- 使用 seq_operations 劫持(常见 CTF 模式):该结构含 start/stop/next/show 四个函数指针,大小 0x20(可放入 kmalloc-32);打开 /proc/self/stat 会分配 seq_operations,通过 UAF 覆写 start,在调用 read() 时获得受控 RIP。
- 内核栈迁移 gadget:xchg eax, esp; ret(迁移到 RAX 低 32 位指向的地址,如已知地址的 mmap 缓冲区)、mov rsp, [rdi+X]; ...(RDI 指向受控数据时)、push rdi; pop rsp; ...(迁移到 RDI,即被劫持函数的第一个参数);注意 SMEP 开启后不能执行用户态代码,ROP 链必须只用内核 gadget。
- ret2usr(SMEP 之前):从内核上下文直接调用用户态函数(如覆写内核函数指针指向用户内存中的 escalate(),其中调用 commit_creds(prepare_kernel_cred(0)));SMEP 会阻止内核执行用户映射页。
- 通过 iretq 返回用户态(传统方式):ROP 链结尾用 swapgs; iretq,栈上需依次放置 [user_rip][user_cs][user_rflags][user_rsp][user_ss];利用前需保存用户态状态:user_cs=0x33、user_ss=0x2b、user_rflags(用 pushfq 保存)、user_rsp、user_rip(如 get_shell 函数地址)。
- 通过 KPTI trampoline 返回用户态(KPTI 开启时):KPTI 分离内核/用户页表,直接 swapgs; iretq 会崩溃;应跳转到内核自带的 swapgs_restore_regs_and_return_to_usermode trampoline(mov rdi, rsp; ...; swapgs; iretq),并在栈上放置 [RIP, CS, RFLAGS, RSP, SS]。
- 通过信号处理器返回:利用前先设置信号处理器,commit_creds 后触发该信号,经信号处理器返回用户态,避免手动 swapgs/iretq。
- QEMU 调试技巧:-s -S 在 :1234 开 GDB server 并暂停;-monitor /dev/null 禁用 QEMU monitor 获得更干净输出;-append "nokaslr" 关闭 KASLR 便于调试;-cpu kvm64,+smep,+smap 启用特定 CPU 特性;GDB 中用 info registers 查看所有寄存器;maintenance packet Qqemu.PhyMemMode:1 在 GDB 中读物理内存;cat /proc/kallsyms 查看内核符号地址(若可读);cat /sys/kernel/notes 查看内核 build ID。
- 按决策树推进:先判断漏洞类型(UAF → 识别被释放对象并喷射替换;OOB → 确定读写范围并瞄准相邻对象;竞态 → 用 userfaultfd、FUSE 可靠触发;整数溢出 → 分析如何转化为 OOB 或分配混淆;类型混淆 → 分析混淆类型能访问什么)。
- 构造原语后按能力选择路径:有受控 RIP → 内核 ROP 或 ret2usr(无 SMEP 时);有任意读 → 先泄漏 KASLR 基址再求受控 RIP;有任意写 → 覆写 modprobe_path(最简单)或直接覆写 cred 结构;仅有受限写 → 瞄准已知对象中的函数指针。
- 针对缓解机制选择绕过:KASLR → 需先信息泄漏(可读的 /proc/kallsyms、时序或 OOB 读);SMEP → 只能用内核 ROP(不能执行用户代码);SMAP → 内核不能读用户数据(改用 copy_from_user gadget);KPTI → 用 KPTI trampoline 干净返回;FG-KASLR → 函数偏移随机化(改用 modprobe_path 等数据段目标)。
- 按能力选择提权方式:有受控 RIP + KASLR 绕过 → ROP 链 prepare_kernel_cred(0) → commit_creds;仅有任意写 → 覆写 modprobe_path;有任意写 + KASLR 绕过 → 将 cred 的 uid/gid 覆写为 0;有受控函数调用 → 调用 commit_creds(prepare_kernel_cred(0))。
- 返回用户态按 KPTI 状态选择:KPTI 关闭 → ROP 结尾 swapgs; iretq;KPTI 开启 → 跳转 KPTI trampoline;替代方案 → 信号处理器 + process_one_work 返回路径。
适用边界 · 注意事项
- 需要先具备 QEMU、可编译内核的构建环境与 GDB 调试能力,纯用户态或非 Linux 内核目标不适用。
- 涉及 SMEP/SMAP/KPTI/FG-KASLR 等缓解机制时,需先完成对应绕过(如信息泄漏),否则直接套用 ROP 或 ret2usr 会失败。
- 内核栈很小(8KB–16KB),栈溢出类漏洞罕见,不能假设存在栈溢出作为入口。