- 文档
- 网络安全
- 教程
【免费下载链接】ctf-wiki
Come and join us, we need you!
用户态与内核态之间的隔离,是 Linux 内核 pwn 攻防博弈的第一道门槛:攻击者抢占了内核执行流之后,究竟能不能跳回用户态执行自己准备的 Shellcode、能不能在内核态读取用户态内存,完全由 SMEP、SMAP、KPTI 这几项硬件与软件机制决定。本指南以 ctf-wiki 仓库中 内核态与用户态隔离 一节为核心骨架,系统梳理默认隔离模型、SMEP/SMAP/KPTI 的原理、开启关闭与状态查看方法,并结合仓库中的绕过章节给出 ret2usr、改 CR4、修改页表等可落地的利用思路,帮助读者建立完整的"防御—绕过"闭环知识体系。
隔离的分类:为什么内核要把"自己"与"用户态"分开
在 ctf-wiki 的 防御机制总览 中,内核防御机制被划分为隔离、访问控制、异常检测、随机化四大类;而 隔离机制导读 进一步指出,根据隔离主体的不同,隔离又分为两类:
- 内核态与用户态的隔离:即本文讨论的主题,隔离的是两个不同特权级运行环境之间的数据与代码访问边界;
- 内核自身内部不同对象间的隔离:例如堆对象之间的隔离,对应 inner-kernel 一节的堆块隔离内容。
本文聚焦第一类。在 基础知识 中已经交代过背景:Intel CPU 将权限分为 Ring0~Ring3 四级,现代操作系统只使用 Ring0(内核态)与 Ring3(用户态)。内核态拥有完全的硬件访问能力,而用户态只有部分能力。隔离机制要解决的核心问题是:这条特权级边界,在代码和数据两个维度上分别由什么来守护,以及被攻破之后如何防御。
目标文档 readme.md 用四句话精炼地给出了完整的隔离防线清单:
| 防线 | 防护内容 |
|---|---|
| 默认机制 | 用户态不可直接访问内核态的数据、执行内核态的代码 |
| SMEP | 内核态不可执行用户态的代码 |
| SMAP | 内核态不可访问用户态的数据 |
| KPTI | 用户态不可看到内核态的页表;内核态不可执行用户态的代码(模拟) |
下面逐项展开。
默认隔离:用户态不可直接触碰内核态
"默认机制"是内核与用户态隔离的第一层,它由 CPU 特权级本身保证:用户态运行在 Ring3,不具备执行特权指令(如修改 CR4、加载页表基址等)的能力,也不具备访问内核虚拟地址空间的权限。从 基础知识 的虚拟内存布局可知,Linux 将高地址虚拟内存空间分配给内核、低地址空间分配给用户进程,用户进程的页表中本就不映射内核空间,因此"用户态不可直接访问内核态的数据、执行内核态的代码"是硬件页表与特权级共同作用下的默认状态。
需要强调的是,这个默认边界保护的是从用户态向内核态的越权访问。而攻击者真正关心的往往是另一个方向:在内核态拿到控制流之后,能否反过来利用用户态资源。后者的防护正是 SMEP、SMAP、KPTI 存在的意义。
SMEP:内核态不可执行用户态代码
威胁模型
文档 用户代码不可执行 指出:起初,CPU 在内核态执行代码时可以直接跳转到用户态地址执行。一旦攻击者控制了内核中的执行流(例如劫持某个函数指针),就可以把指令指针指到用户态地址空间——而用户态内存中的 Shellcode 是攻击者完全可控的,这使攻击变得极其容易实施。这种把内核控制流重定向到用户态代码的攻击手法,就是经典的ret2usr(return-to-user)。
为了防范这类攻击,研究者提出:当 CPU 位于内核态时,不允许执行用户态的代码。在 Linux 内核中,这个防御措施的实现是与指令集架构相关的:x86 下是 SMEP,ARM 下是 PXN。
原理与寄存器位
x86 下对应的保护机制名为SMEP(Supervisor Mode Execution Protection),由CR4 控制寄存器中的第 20 位标记是否开启(1 为开启,0 为关闭)。
从 cr4.png 的位域结构可以看到,CR4 中除了第 20 位 SMEP 与第 21 位 SMAP 之外,还包含 PCIDE(第 16 位)、PAE(第 5 位)、PSE(第 4 位)等众多控制位。这意味着攻击者修改 CR4 时往往不能简单地只清一位,而是需要重新写入一个保留其他关键位状态的新值——这也解释了为何绕过时普遍采用 0x6f0 这样的整体赋值(见下文"攻击 SMEP")。
开启与关闭
开启:默认情况下 SMEP 保护是开启的。如果使用 qemu 启动内核,可以在-append选项中添加+smep来显式开启,这也是 CTF 启动脚本中常见的写法——例如 基础知识 中 CISCN2017 babydriver 的boot.sh就使用了-cpu kvm64,+smep来开启 SMEP。
关闭:在/etc/default/grub的如下两行中添加nosmep:
GRUB_CMDLINE_LINUX_DEFAULT="quiet" GRUB_CMDLINE_LINUX="initrd=/install/initrd.gz"然后运行update-grub并重启系统即可关闭 SMEP。如果使用 qemu 启动内核,则在-append选项中添加nosmep来关闭。
状态查看
通过如下命令检查 SMEP 是否开启,如果输出了smep字符串则说明开启,否则未开启:
grep smep /proc/cpuinfo攻击 SMEP
文档 用户代码不可执行 给出两种典型思路:
修改 CR4 寄存器:把 CR4 寄存器的第 20 位置为 0 后,就可以重新执行用户态代码。一般而言使用
0x6f0来设置 CR4,这样 SMAP 和 SMEP 都会被同时关闭。内核中修改 CR4 的代码最终会调用到native_write_cr4,当我们能够劫持控制流后,可以执行内核中的 gadget 来修改 CR4。从另一个维度看,内核中本身存在固定的修改 CR4 的代码,例如refresh_pce函数、set_tsc_mode等函数内部都有相关操作,可以作为目标跳转点。ret2dir(返回内核直接映射区):这一思路在 基础知识 与仓库的 ret2dir 章节中均有交代——利用内核线性映射区对物理地址空间的完整映射,找到用户空间对应页框的内核空间地址,用这个内核地址完成对用户数据的访问/执行,从而绕开"不能执行用户态地址"的限制。
更完整的 SMEP 绕过实战可以继续阅读仓库的 bypass-smep 与 ret2usr 章节。
SMAP:内核态不可访问用户态数据
威胁模型
如果只防"执行"而不防"访问",仍然存在严重问题:文档 用户数据不可访问 指出,在劫持控制流后,攻击者可以通过栈迁移(stack pivot)将栈迁移到用户态地址空间,然后在用户态内存中精心布置 ROP 链,进而实现提权。因为用户态内存完全可控,这相当于给了攻击者一个"无限大的可控栈"。为防范此类攻击,需要禁止内核态访问用户态的数据。
原理与寄存器位
x86 下对应的保护机制名为SMAP(Supervisor Mode Access Protection),由CR4 寄存器中的第 21 位标记是否开启。注意 user-data-access.md 原文此处存在笔误(写作"第 21 位用来标记是否开启 SMEP"),结合 cr4.png 的位域标注可以确认:第 20 位是 SMEP、第 21 位是 SMAP。
与 SMEP 类似,该防御在 ARM 下的对应物是PAN(Privileged Access Never)。
开启与关闭
开启:默认情况下 SMAP 保护是开启的。使用 qemu 启动内核时,在-append选项中添加+smap开启。
关闭:在/etc/default/grub的两行配置中添加nosmap,然后运行update-grub并重启系统即可关闭。使用 qemu 启动内核时,在-append选项中添加nosmap关闭。
GRUB_CMDLINE_LINUX_DEFAULT="quiet" GRUB_CMDLINE_LINUX="initrd=/install/initrd.gz"状态查看
grep smap /proc/cpuinfo同样地,如果输出包含smap字符串则说明保护已开启。
攻击 SMAP
文档 用户数据不可访问 给出两种方式:
设置 CR4 寄存器:把 CR4 的第 21 位置为 0 后即可访问用户态数据。同样推荐使用
0x6f0设置 CR4,一次性关闭 SMAP 与 SMEP。方式与攻 SMEP 相同:劫持控制流后执行修改 CR4 的 gadget,或跳转到内核中固定的写 CR4 代码段(如refresh_pce、set_tsc_mode)。调用
copy_from_user/copy_to_user:在劫持控制流后,攻击者可以调用copy_from_user和copy_to_user来访问用户态内存。这两个函数是内核提供的、经过安全检查的用户空间数据拷贝接口,其内部会临时清空禁止访问用户态内存的标志(AC 标志),因此在内核态利用这两个函数读写用户态数据是合法路径。这与 基础知识 中"内核中memcpy对应copy_from_user()/copy_to_user()"的映射关系一致——它们是内核态访问用户数据的主要 API。
KPTI:内核页表隔离
介绍与动机
KPTI(Kernel Page Table Isolation,内核页表隔离)最初的主要目的是缓解 KASLR 的绕过以及 CPU 侧信道攻击(尤其是 Meltdown 漏洞:利用 CPU 乱序执行与预测执行的硬件缺陷,从用户态侧信道读取内核数据)。在 KPTI 机制中,内核态空间与用户态空间的内存隔离进一步得到增强,其页表布局发生根本性变化:
- 内核态的页表包括用户空间内存的页表和内核空间内存的页表;
- 用户态的页表只包括用户空间内存的页表,以及必要的内核空间内存的页表(如用于处理系统调用、中断等信息所需的内存)。
Kernel page table isolation 原理图,展示内核态与用户态使用不同页表集
Linux 4.15 中引入了 KPTI 机制,并被反向移植到 Linux 4.14.11、4.9.75、4.4.110。
与 SMEP/SMAP 的关系:软件模拟
在 x86_64 的 PTI 机制中,内核态的用户空间内存映射部分被全部标记为不可执行(NX)。也就是说,之前不具有 SMEP 特性的硬件,如果开启了 KPTI 保护,也就具备了类似 SMEP 的特性。此外,SMAP 模拟理论上也可以以类似方式引入,只是目前尚未实现。因此在开启了 KPTI 保护的内核中:
- 如果没有开启 SMAP 保护,内核仍然可以访问用户态空间的内存;
- 但不能跳转到用户态空间执行 Shellcode(相当于模拟了 SMEP)。
这也是目标文档 readme.md 中"KPTI:用户态不可看到内核态的页表;内核态不可执行用户态的代码(模拟)"的含义——前半句描述页表隔离,后半句描述对 SMEP 的软件模拟效果。正如 基础知识 所总结的:"对于开启了 KPTI 的内核而言,内核页表的用户地址空间无执行权限,这使得 ret2usr 彻底成为过去式"。
开启与关闭
- 使用 qemu 启动内核时,在
-append选项中添加kpti=1开启 KPTI; - 使用 qemu 启动内核时,在
-append选项中添加nopti关闭 KPTI。
状态查看
文档 kpti.md 给出两种查看方式:
/home/pwn # dmesg | grep 'page table' [ 0.000000] Kernel/User page tables isolation: enabled /home/pwn # cat /proc/cpuinfo | grep pti fpu_exception : yes flags : ... pti smep smap第一种通过内核启动日志中的Kernel/User page tables isolation: enabled确认;第二种通过/proc/cpuinfo的 flags 中是否出现pti确认(该方式实际反映的是硬件/启动参数的配置情况,两种方法可结合使用)。
攻击 KPTI
KPTI 机制与 SMAP、SMEP 不太一样:由于与源码紧密结合,似乎没有办法在运行时刻直接关闭。仓库的 kpti.md 给出了三种绕过思路:
修改页表
开启 KPTI 后,用户态空间的所有数据都被标记了 NX 权限,但我们可以考虑修改对应页表项的权限使其重新获得可执行权限。当内核没有开启 SMEP 时,修改页表权限后就可以返回到用户态执行用户态代码。
SWITCH_TO_USER_CR3_STACK:复用内核的返回用户态代码
开启 KPTI 后,用户态进入内核态时会进行页表切换;从内核态恢复用户态时也会进行页表切换。如果能够控制内核执行"返回用户态时所执行的页表切换代码",也就可以正常返回用户态。通过分析内核态到用户态切换的代码可知,页表切换主要依靠SWITCH_TO_USER_CR3_STACK汇编宏:
.macro SWITCH_TO_USER_CR3_STACK scratch_reg:req pushq %rax SWITCH_TO_USER_CR3_NOSTACK scratch_reg=\scratch_reg scratch_reg2=%rax popq %rax .endm .macro SWITCH_TO_USER_CR3_NOSTACK scratch_reg:req scratch_reg2:req ALTERNATIVE "jmp .Lend_\@", "", X86_FEATURE_PTI mov %cr3, \scratch_reg ALTERNATIVE "jmp .Lwrcr3_\@", "", X86_FEATURE_PCID /* * Test if the ASID needs a flush. */ movq \scratch_reg, \scratch_reg2 andq $(0x7FF), \scratch_reg /* mask ASID */ bt \scratch_reg, THIS_CPU_user_pcid_flush_mask jnc .Lnoflush_\@ /* Flush needed, clear the bit */ btr \scratch_reg, THIS_CPU_user_pcid_flush_mask movq \scratch_reg2, \scratch_reg jmp .Lwrcr3_pcid_\@ .Lnoflush_\@: movq \scratch_reg2, \scratch_reg SET_NOFLUSH_BIT \scratch_reg .Lwrcr3_pcid_\@: /* Flip the ASID to the user version */ orq $(PTI_USER_PCID_MASK), \scratch_reg .Lwrcr3_\@: /* Flip the PGD to the user version */ orq $(PTI_USER_PGTABLE_MASK), \scratch_reg mov \scratch_reg, %cr3 .Lend_\@: .endm关键点在于:除了切换页表,我们还需要返回到用户态,因此需要复用内核中返回用户态的代码。内核返回到用户态主要有两种方式:iret和sysret。
iret 路径:通过伪造如下栈结构,跳转到swapgs_restore_regs_and_return_to_usermode中的movq %rsp, %rdi处,就可以同时完成页表切换和返回用户态:
fake rax fake rdi RIP CS EFLAGS RSP SS对应的内核汇编片段如下(省略了 POP_REGS 之后的 trampoline 栈拷贝细节):
movq %rsp, %rdi movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp ... SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi popq %rdi SWAPGS INTERRUPT_RETURNsysret 路径:使用 sysret 时,首先需要保证rcx保存返回用户态后要执行的代码地址(RIP),r11保存 eflags:
rcx, save the rip of the code to be executed when returning to userspace r11, save eflags然后构造如下栈:
fake rdi rsp, the stack of the userspace最后跳转至entry_SYSCALL_64中如下代码,即可返回到用户态:
SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi popq %rdi popq %rsp swapgs sysretqsignal handler
也可以在用户态注册 signal handler 来执行位于用户态的代码。这种方式的优势在于无需切换页表——当用户态进程收到信号并从内核返回时,内核会经由正常的信号处理流程,在内核态完成必要的页表切换后跳转到用户态的 signal handler。关于 KPTI 绕过更完整的利用链,可进一步阅读仓库的 kpti-bypass 与 ret2ptregs 章节(后者借助 pt_regs 中保存的寄存器在返回用户态时恢复现场)。
从防御到绕过:四道防线的完整对抗图谱
综合 基础知识 与本文前述内容,可以画出一张攻防对应表:
| 防御机制 | 防护目标 | 绕过思路 | 仓库对应章节 |
|---|---|---|---|
| 默认特权隔离 | 用户态不可访问/执行内核内容 | 通过内核漏洞获取执行流后再反向利用 | 基础利用 |
| SMEP | 内核态不可执行用户态代码 | 修改 CR4 第 20 位;ret2dir | bypass-smep、ret2dir |
| SMAP | 内核态不可访问用户态数据 | 修改 CR4 第 21 位;调用 copy_from/to_user | bypass-smep |
| KPTI | 用户态不可见内核页表;模拟 SMEP | 修改页表权限;复用 SWITCH_TO_USER_CR3_STACK;signal handler | kpti-bypass、ret2ptregs |
需要注意一个叠加效应:KPTI 开启后即使关闭 SMEP,ret2usr 依然不可行,因为用户态页表中根本没有内核代码的映射、且内核页表中的用户地址空间部分无执行权限。因此在现代内核上,ret2usr 已彻底成为过去式,主流打法转向在内核态完成全链路利用(如通过 ROP 调用commit_creds(prepare_kernel_cred(&init_task))提权,再借助 KPTI 绕过技巧优雅返回用户态)。
CTF 场景中的实践要点
结合 基础知识 中 CISCN2017 babydriver 的boot.sh示例,CTF kernel pwn 中防御机制完全由启动参数决定,分析题目时应重点观察:
qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep-cpu kvm64,+smep:在 CPU 模型中显式加入 smep(同样可用+smap);-append内核命令行参数:可包含nosmep、nosmap、nopti、kpti=1等,决定 SMAP/SMEP/KPTI 的开关;- 进入题目环境后,用
grep smep /proc/cpuinfo、grep smap /proc/cpuinfo确认硬件保护位,用dmesg | grep 'page table'确认 KPTI 是否启用。
判断清楚题目开启了哪些隔离机制,再决定采用改 CR4、ret2dir、还是 KPTI 绕过路线,是 kernel pwn 解题的第一步,也是本文所梳理的四道防线知识的直接落点。
小结
用户态与内核态的隔离由四条防线共同构成:默认特权级与页表隔离(用户态不可越权访问内核)、SMEP(内核态禁执行用户态代码)、SMAP(内核态禁访问用户态数据)、KPTI(页表级隔离并软件模拟 SMEP)。它们分别由 CPU 硬件特性(CR4 第 20/21 位)与内核软件机制(页表隔离与入口/出口汇编宏)实现,并各自存在成熟的绕过技术。理解这四道防线的原理、开启关闭方式与对抗思路,是进行 Linux 内核 pwn 分析的基本功;更深入的利用链细节,可以继续阅读仓库 exploitation/rop 目录下的系列章节。
- 文档
- 网络安全
- 教程
【免费下载链接】ctf-wiki
Come and join us, we need you!
相关推荐
CTF Wiki:Linux 内核防御机制之隔离(KPTI / SMEP / SMAP / 堆块隔离)全解析
CTF Wiki:Linux 内核防御机制之隔离(KPTI / SMEP / SMAP / 堆块隔离)全解析 在内核 Pwn 攻防中,"隔离"是四类核心防御机制
文档网络安全教程CTF Linux 内核 Pwn:SMEP/SMAP 与用户代码不可执行防护的攻防全解析
CTF Linux 内核 Pwn:SMEP/SMAP 与用户代码不可执行防护的攻防全解析 本篇文章聚焦 CTF Linux 内核 Pwn 中最基础也最关键的防御
文档网络安全教程如何在ComfyUI中快速部署DynamiCrafter模型?完整安装指南与环境配置
如何在ComfyUI中快速部署DynamiCrafter模型?完整安装指南与环境配置 ComfyUI DynamiCrafterWrapper是一个专为Comf
文档网络安全教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考