Linux x86 CET 影子栈Shadow Stack从内核配置到 arch_prctl 实战详解【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxCETControl-flow Enforcement Technology是 x86 处理器面向控制流劫持攻击如 ROP的硬件防护机制其中影子栈是用户态防护的核心。本文以 Linux 内核文档 shstk.rst 为主线完整梳理影子栈的原理、启用条件、arch_prctl()接口、信号/克隆/执行等生命周期行为并结合 arch/x86/kernel/shstk.c 等源码揭示内核在幕后实际做了什么帮助你在 64 位 x86 系统上正确启用并验证影子栈保护。CET 背景影子栈与 IBTCET 涵盖一组相关的 x86 处理器特性用于保护应用和内核免受控制流劫持攻击。它引入两项关键机制影子栈Shadow Stack一个由处理器从内存中分配的第二栈区应用无法直接修改。执行CALL指令时处理器把返回地址同时压入普通栈和影子栈函数返回时处理器弹出影子栈中的副本与普通栈副本比对两者不一致则触发控制保护故障control-protection fault。间接分支跟踪IBT验证间接CALL/JMP的目标确实是编译器用ENDBR操作数标记过的合法目标。需要注意两点前提与 shstk.rst 一致并非所有 CPU 都同时支持影子栈和 IBT当前 64 位内核中仅支持用户态影子栈和内核 IBT即内核本身使用影子栈、用户态使用 IBT 的组合不在当前支持范围内。Kconfig 中的说明也印证了这一点arch/x86/Kconfig 中X86_USER_SHADOW_STACK的 help 文本指出影子栈保护是检测函数返回地址被破坏的硬件特性有助于缓解 ROP 攻击应用必须显式启用才能获得保护旧的用户态不会免费获得防护且支持影子栈的 CPU 最早于 2020 年发布。启用影子栈的前提条件使用用户态影子栈需要三者齐备硬件支持CPU 支持 CET 影子栈内核配置编译时打开 Kconfig 选项X86_USER_SHADOW_STACK。从源码结构看该选项还depends on AS_WRUSS、depends on X86_64、depends on PER_VMA_LOCK并select X86_CET见 arch/x86/Kconfig用户态库支持用户态库如 glibc编译时启用 CET 相关特性。运行时可以通过内核启动参数nousershstk整体禁用影子栈。其实现位于 arch/x86/kernel/cpu/common.cif (cmdline_find_option_bool(boot_command_line, nousershstk)) setup_clear_cpu_cap(X86_FEATURE_USER_SHSTK);即启动参数会清掉USER_SHSTK这个 CPU 特性位之后arch_prctl启用路径将返回-ENOTSUPP。工具链方面要构建支持影子栈的内核需要Binutils v2.29或LLVM v6及以上版本WRUSS汇编指令需要较新的汇编器支持。运行时验证/proc/cpuinfo若处理器支持 CET/proc/cpuinfo会显示对应特性其中user_shstk表示当前内核与硬件组合下支持用户态影子栈。这个特性位的定义在 arch/x86/include/asm/cpufeatures.h#define X86_FEATURE_USER_SHSTK (11*3223) /* user_shstk Shadow stack support for user mode applications */特性位只有在硬件SHSTK特性存在且CONFIG_X86_USER_SHADOW_STACK打开时才会被设置见 setup_cet()user_shstk cpu_feature_enabled(X86_FEATURE_SHSTK) IS_ENABLED(CONFIG_X86_USER_SHADOW_STACK); ... if (user_shstk) set_cpu_cap(c, X86_FEATURE_USER_SHSTK);通过 hardening 配置快速启用如果希望开箱即用地开启包括影子栈在内的一批安全加固选项可以直接启用 arch/x86/configs/hardening.config其中包含CONFIG_X86_USER_SHADOW_STACKy应用如何标记 CET 能力ELF note编译时启用了 CET 特性的应用其 ELF 文件会带有相应的 note可用readelf或llvm-readelf验证readelf -n application | grep -a SHSTK properties: x86 feature: SHSTK关键事实内核并不直接解析这些 ELF 标记。应用或加载器必须通过下一节的arch_prctl()接口显式启用 CET 特性在 glibc 等动态链接器中这一步通常发生在动态加载器或静态运行时对象内部。arch_prctl() 启用接口详解ELF 特性应由加载器通过如下arch_prctl()启用。这些操作仅在 64 位用户应用中受支持按线程粒度生效启用状态在clone时继承因此只要第一个线程启用了该特性它就会传播到应用的所有线程。接口定义在 arch/x86/include/uapi/asm/prctl.h#define ARCH_SHSTK_ENABLE 0x5001 #define ARCH_SHSTK_DISABLE 0x5002 #define ARCH_SHSTK_LOCK 0x5003 #define ARCH_SHSTK_UNLOCK 0x5004 #define ARCH_SHSTK_STATUS 0x5005 #define ARCH_SHSTK_SHSTK (1ULL 0) #define ARCH_SHSTK_WRSS (1ULL 1)各接口语义如下接口参数含义行为arch_prctl(ARCH_SHSTK_ENABLE, feature)feature为单个特性启用该特性一次只能操作一个特性arch_prctl(ARCH_SHSTK_DISABLE, feature)feature为单个特性禁用该特性一次只能操作一个特性arch_prctl(ARCH_SHSTK_LOCK, features)features为特性掩码把特性锁定在当前启用/禁用状态。掩码中所有置位都会被处理未置位被忽略该掩码与既有值做 OR。锁定后对应的特性位不能再被启用或禁用arch_prctl(ARCH_SHSTK_UNLOCK, features)features为特性掩码解锁特性。仅能通过 ptrace 对其它线程操作用于CONFIG_CHECKPOINT_RESTORE场景arch_prctl(ARCH_SHSTK_STATUS, addr)addr为存放结果的地址把当前已启用的特性位复制到addr指向的用户空间地址返回值为 0 表示成功失败时 errno 为-EPERM请求修改的特性已被锁定-ENOTSUPP特性不被硬件或内核支持-EINVAL参数非法如不存在的特性位、一次传入多个特性等-EFAULT无法把信息复制回用户空间。当前该接口支持的两个特性位是ARCH_SHSTK_SHSTK影子栈和ARCH_SHSTK_WRSSWRSS即允许用户态回写影子栈特性。WRSS 只能在影子栈启用时才可启用且影子栈被禁用时 WRSS 会自动禁用——这与源码中wrss_control()的行为一致若ARCH_SHSTK_SHSTK未启用则返回-EPERM见 arch/x86/kernel/shstk.cshstk_disable()在禁用影子栈时会把MSR_IA32_U_CET清零同时清掉 WRSS见 shstk.c。内核侧 prctl 分发的源码视角上述语义的最终实现集中在 shstk_prctl()long shstk_prctl(struct task_struct *task, int option, unsigned long arg2) { unsigned long features arg2; if (option ARCH_SHSTK_STATUS) { return put_user(task-thread.features, (unsigned long __user *)arg2); } if (option ARCH_SHSTK_LOCK) { task-thread.features_locked | features; return 0; } /* Only allow via ptrace */ if (task ! current) { if (option ARCH_SHSTK_UNLOCK IS_ENABLED(CONFIG_CHECKPOINT_RESTORE)) { task-thread.features_locked ~features; return 0; } return -EINVAL; } /* Do not allow to change locked features */ if (features task-thread.features_locked) return -EPERM; /* Only support enabling/disabling one feature at a time. */ if (hweight_long(features) 1) return -EINVAL; ... }几个值得注意的实现细节锁定检查先于启停操作features task-thread.features_locked时直接返回-EPERM这正是文档中-EPERM错误来源一次只允许一个特性hweight_long(features) 1时返回-EINVAL对应一次只能操作一个特性的约束UNLOCK 仅限 ptrace对非当前线程即被 ptrace 附加的线程只放行ARCH_SHSTK_UNLOCK且要求编译时打开CONFIG_CHECKPOINT_RESTORE特性状态实际保存在task_struct-thread.features中结构定义见 arch/x86/include/asm/shstk.hstruct thread_shstk记录每个线程影子栈的base和size。通过 /proc/$PID/status 验证运行状态要确认某应用是否真的在影子栈保护下运行可以读/proc/$PID/status其中会根据实际启用情况报告wrss或shstkx86_Thread_features: shstk wrss x86_Thread_features_locked: shstk wrss输出逻辑在 arch/x86/kernel/cpu/proc.c直接遍历thread.features和thread.features_locked两个位域并拼出字符串。影子栈的内核实现影子栈大小分配一个任务的影子栈从内存分配为固定大小MIN(RLIMIT_STACK, 4 GB)即按普通栈的最大尺寸分配但上限 4 GB。使用clone3系统调用时由于传入了显式的栈大小影子栈会以该值替代 rlimit。对应源码是 adjust_shstk_size()static unsigned long adjust_shstk_size(unsigned long size) { if (size) return PAGE_ALIGN(size); return PAGE_ALIGN(min_t(unsigned long long, rlimit(RLIMIT_STACK), SZ_4G)); }此外有两个从源码结构中可以确认的实现约束影子栈总是分配在4 GB 以上地址空间MAP_ABOVE4G从而把影子栈挡在 32 位地址空间之外这是后文 32 位 ABI 信号不支持的基础VM_SHADOW_STACK的 vma 带有守护页guard page。注释解释了原因INCSSP指令最多能把 SSP 移动 2040 字节且移动前后会读取被弹出的元素因此一个页的间隙就足以防止单次操作把 SSP 挪到相邻栈上见 alloc_shstk() 上方注释。启用影子栈的完整动作在 shstk_setup() 中分配 vma 后把MSR_IA32_PL3_SSP写为影子栈顶地址addr size影子栈向下增长并在MSR_IA32_U_CET中置CET_SHSTK_EN位这两个 MSR 操作在 FPU 寄存器锁内完成保证不会在上下文切换窗口中被打断。信号路径主程序与信号处理函数共用同一个影子栈。由于影子栈只存储返回地址一个足够大的影子栈可以覆盖程序栈与信号替代栈都耗尽的情形。信号发生时的处理信号前的旧状态被压入普通栈影子栈相关的状态被压入影子栈。目前这项状态只有旧的 SSP以一个特殊格式压入——最高位bit 63置 1sigreturn时内核校验并恢复这个旧 SSP 令牌内核还会把普通 restorer 地址压入影子栈帮助用户态避免经过 restorer 的 sigreturn 路径上触发影子栈违例。影子栈信号帧格式与文档一致|1...old SSP| - 指向信号前旧 ssp 的指针sigframe 令牌格式 bit 63 置 1 | ...| - 未来可能添加其它状态源码印证put_shstk_data()在写入时强制data | SHSTK_DATA_BITbit 63 置位防止信号帧令牌被当成返回地址处理shstk.c信号投递时setup_signal_shadow_stack()先压入旧 SSP 令牌、再压入 restorer 地址最后更新MSR_IA32_PL3_SSPshstk.csigreturn时restore_signal_shadow_stack()弹出令牌并校验其校验包括令牌所在地址必须落在VM_SHADOW_STACK的 VMA 内、恢复后的 SSP 必须 8 字节对齐且小于TASK_SIZE_MAXshstk_pop_sigframe()。32 位 ABI 信号在影子栈进程中不受支持。Linux 的做法是把影子栈分配在 32 位地址空间之外从而阻止 32 位执行一旦通过远调用far call或返回用户态进入 32 位模式硬件会触发#GP以段错误segfault投递给进程且寄存器状态表现为被返回的用户态 IP 引发了该段错误。Fork 与线程影子栈的 vma 带有VM_SHADOW_STACK标志其 PTE 要求为只读且脏read-only and dirty。当某个影子栈 PTE 不再是 ROdirty 时影子栈访问会触发一个页面错误码中带有影子栈访问位的缺页异常。fork()任务 fork 子进程时影子栈的 PTE 被复制且父、子双方的影子栈 PTE 都清除脏位下次影子栈访问时产生的影子栈缺页按缺页复制/复用的常规路径处理copy-on-write 语义pthread带 CLONE_VM 的 clone内核为新线程分配一个全新的影子栈新建影子栈在 ASLR 行为上类似mmap()线程退出时其影子栈被禁用并释放。源码层面shstk_alloc_thread_stack() 体现了三种分支影子栈未启用则直接跳过CLONE_VFORK子进程与父进程共享同一影子栈shstk-base清零以免释放逻辑误操作不带CLONE_VM的子进程沿用父进程的影子栈副本靠 COW 保护只有共享地址空间的新线程才会走alloc_shstk()分配独立影子栈。释放逻辑在 shstk_free()并特别处理了 fork(CLONE_VM) 失败后子进程退出时释放影子栈、以及防止二次释放WARN_ON(!shstk-size)的场景。Execexec时内核会禁用影子栈特性此后用户态可以自行选择重新启用或锁定。对应实现是 reset_thread_features()void reset_thread_features(void) { memset(current-thread.shstk, 0, sizeof(struct thread_shstk)); current-thread.features 0; current-thread.features_locked 0; }清空了thread_shstk及特性位/锁定位因此新映像的加载器如 glibc需要重新走一遍arch_prctl(ARCH_SHSTK_ENABLE, ...)流程。小结启用影子栈的完整检查清单内核编译开启CONFIG_X86_USER_SHADOW_STACK可用 arch/x86/configs/hardening.config 一并启用工具链满足 Binutils ≥ 2.29 或 LLVM ≥ 6确认cat /proc/cpuinfo中出现user_shstk若启动参数带了nousershstk则该位不会出现确认应用 ELF 带有 CET notereadelf -n app | grep -a SHSTK由动态加载器如 glibc调用arch_prctl(ARCH_SHSTK_ENABLE, ARCH_SHSTK_SHSTK)及按需的ARCH_SHSTK_WRSS并可配合ARCH_SHSTK_LOCK锁定运行时通过/proc/$PID/status中的x86_Thread_features行确认shstk/wrss实际生效。核心实现全部集中在 arch/x86/kernel/shstk.c约 630 行接口声明在 arch/x86/include/asm/shstk.hUAPI 常量在 arch/x86/include/uapi/asm/prctl.h进一步阅读建议结合 shstk.rst 原文与上述源码交叉对照。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考