☰
Linux 内核态与用户态隔离机制全解析:默认权限边界、SMEP/SMAP 与 KPTI 的攻防实战指南(ctf-wiki)
2026/9/28 3:46:42 网站建设 项目流程
  • 文档
  • 网络安全
  • 教程

【免费下载链接】ctf-wiki

Come and join us, we need you!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载

用户态与内核态之间的隔离,是 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

文档 用户代码不可执行 给出两种典型思路:

  1. 修改 CR4 寄存器:把 CR4 寄存器的第 20 位置为 0 后,就可以重新执行用户态代码。一般而言使用0x6f0来设置 CR4,这样 SMAP 和 SMEP 都会被同时关闭。内核中修改 CR4 的代码最终会调用到native_write_cr4,当我们能够劫持控制流后,可以执行内核中的 gadget 来修改 CR4。从另一个维度看,内核中本身存在固定的修改 CR4 的代码,例如refresh_pce函数、set_tsc_mode等函数内部都有相关操作,可以作为目标跳转点。

  2. 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

文档 用户数据不可访问 给出两种方式:

  1. 设置 CR4 寄存器:把 CR4 的第 21 位置为 0 后即可访问用户态数据。同样推荐使用0x6f0设置 CR4,一次性关闭 SMAP 与 SMEP。方式与攻 SMEP 相同:劫持控制流后执行修改 CR4 的 gadget,或跳转到内核中固定的写 CR4 代码段(如refresh_pce、set_tsc_mode)。

  2. 调用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_RETURN

sysret 路径:使用 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 sysretq
signal handler

也可以在用户态注册 signal handler 来执行位于用户态的代码。这种方式的优势在于无需切换页表——当用户态进程收到信号并从内核返回时,内核会经由正常的信号处理流程,在内核态完成必要的页表切换后跳转到用户态的 signal handler。关于 KPTI 绕过更完整的利用链,可进一步阅读仓库的 kpti-bypass 与 ret2ptregs 章节(后者借助 pt_regs 中保存的寄存器在返回用户态时恢复现场)。

从防御到绕过:四道防线的完整对抗图谱

综合 基础知识 与本文前述内容,可以画出一张攻防对应表:

防御机制防护目标绕过思路仓库对应章节
默认特权隔离用户态不可访问/执行内核内容通过内核漏洞获取执行流后再反向利用基础利用
SMEP内核态不可执行用户态代码修改 CR4 第 20 位;ret2dirbypass-smep、ret2dir
SMAP内核态不可访问用户态数据修改 CR4 第 21 位;调用 copy_from/to_userbypass-smep
KPTI用户态不可见内核页表;模拟 SMEP修改页表权限;复用 SWITCH_TO_USER_CR3_STACK;signal handlerkpti-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!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载
上一篇:QQ截图独立版:不装QQ也能用的离线截图与OCR工具
下一篇:douyin-downloader 完整指南:抖音视频批量下载与去水印,3 步归档一整个博主主页

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询