ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

深入解析 .NET Runtime 的栈缓冲区溢出防护:Guard Stack(GS)机制在 RyuJIT 中的实现

深入解析 .NET Runtime 的栈缓冲区溢出防护:Guard Stack(GS)机制在 RyuJIT 中的实现 深入解析 .NET Runtime 的栈缓冲区溢出防护Guard StackGS机制在 RyuJIT 中的实现【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读本文基于 docs/design/coreclr/jit/Stack Buffer Overflow Protection.md 展开系统讲解 .NET 代码生成器RyuJIT为抵御栈缓冲区溢出攻击而内建的 Guard StackGS防护机制。文章涵盖 GS 的威胁模型不安全缓冲区与易受攻击数据、栈帧布局重排、栈 CookieStack Canary的生成与校验、脆弱参数影子拷贝等核心设计并结合 src/coreclr/jit/gschecks.cpp、src/coreclr/jit/codegenxarch.cpp 等源码给出实现级佐证。读完本文你将理解 .NET 托管代码在引入stackalloc、C# 固定大小缓冲区等不安全构造后运行时与 JIT 如何在方法入口、出口以及 EH/GC 栈遍历等关键点捍卫栈帧完整性以及如何结合 CET 等硬件机制做纵深防御。背景托管安全之外的不安全面.NET 本质上是类型安全与内存安全的托管平台但它同时提供了一组底层设施用于与原生代码互操作或表达一些无法被静态证明安全的构造。这些潜在不安全构造一旦被滥用就可能威胁 .NET 运行时栈的完整性——攻击者通过改写栈帧上的关键信息例如代码地址和数据地址即可劫持控制流。为此.NET 代码生成器内建了栈缓冲区溢出保护Stack Buffer Overflow Protection又称 Guard Stack缩写GS在程序执行的关键节点——包括栈遍历EH/GC与方法返回——校验栈的完整性。GS 是 .NET 运行时更广泛的安全缓解体系的一部分与该仓库中的 CETControl-flow Enforcement Technology特性设计 属于同一防御纵深的不同层次。威胁模型什么需要被防护GS 的目标是检测来自不安全缓冲区的越界写入buffer overrun这类越界写入可能破坏栈上易受攻击的数据。仓库文档明确给出了两类不安全缓冲区动态分配在栈上的内存区域C# 中的stackalloc即 IL 层面的localloc。被语言编译器标记为不安全的值的类型通过System.Runtime.CompilerServices.UnsafeValueTypeAttribute标记典型例子是 C# 的固定大小缓冲区fixed-size buffers。作为对照易受攻击的数据通常指栈帧中的代码与数据地址——这些地址一旦被改写攻击者就能将控制流导向任意位置。JIT 编译器在导入 IL 阶段就会识别这些不安全缓冲区并打上标记从源码看有两个明确入口处理localloc时将对应局部变量标记为不安全缓冲区见 src/coreclr/jit/importer.cpplvaTable[stackallocAsLocal].lvIsUnsafeBuffer true处理值类型局部变量时通过getClassAttribs查询CORINFO_FLG_UNSAFE_VALUECLASS特性若命中则调用setNeedsGSSecurityCookie()并置位lvIsUnsafeBuffer见 src/coreclr/jit/lclvars.cpp。也就是说GS 是否启用是由方法体里是否存在localloc或UnsafeValueTypeAttribute值类型驱动的编译器据此决定该方法是否需要安全 Cookie 与栈布局重排。GS 的两道防线GS 通过两种互补方式保护易受攻击数据栈布局重排relocation尽可能把易受攻击的数据下移到栈帧更低的位置即放到不安全缓冲区之下。由于栈向下增长缓冲区越界通常向高地址栈顶方向蔓延因此把指针类数据放在缓冲区下方可以天然隔离。栈 CookieStack Canary对于无法搬移的数据典型如返回地址在不安全缓冲区与不可搬移的易受攻击数据之间分配一个栈 Cookie。Cookie 的值每次运行都不同并且在方法退出前、以及运行时为 EH/GC 触发栈遍历时都会被校验。任何试图越过缓冲区去污染返回地址/保存寄存器区域的越界写几乎必然先破坏 Cookie从而在真正造成危害前被拦截。带不安全缓冲区方法的栈帧布局仓库文档给出了启用 GS 后典型方法的栈帧布局注意栈向下增长因此调用者帧在上方、被调用者帧在下方Stack Frame栈帧memory arguments内存参数return address返回地址saved frame pointer保存的帧指针callee save area被调用者保存区stack cookie栈 Cookiefixed-sized unsafe buffers without pointers不含指针的固定大小不安全缓冲区fixed-sized unsafe buffers with pointers含指针的固定大小不安全缓冲区shadow copies of vulnerable memory arguments脆弱内存参数的影子拷贝local variables局部变量dynamically allocated buffers / localloc动态分配缓冲区outgoing arguments传出参数(stack pointer points here)栈指针指向此处布局细节值得展开脆弱的内存参数会被搬迁到不安全固定缓冲区之下的影子拷贝区域方法体内对参数的引用被改写为指向影子拷贝在固定大小缓冲区区域内含指针的缓冲区排在不含指针缓冲区的更低地址。原因很直观缓冲区越界写是向高地址方向进行的把含指针即可能被用于构造任意地址的缓冲区放在更低处能最大程度压缩攻击者利用的空间Cookie 紧贴在不可搬移的易受攻击数据返回地址、保存的帧指针、callee save 区之下成为缓冲区越界攻击必须翻越的最后一道屏障localloc动态缓冲区位于帧的最低处位于所有静态布局数据之下。Cookie 的运行时随机性来源Cookie 值“每次运行不同”由运行时VM层保证。JIT 在gsGSChecksInitCookie见 src/coreclr/jit/gschecks.cpp中通过info.compCompHnd-getGSCookie(gsGlobalSecurityCookieVal, gsGlobalSecurityCookieAddr)向 VM 查询全局安全 Cookie 的值或地址若 VM 提供的是固定随机值gsGlobalSecurityCookieVal非零代码生成器直接将该立即数写入栈上的 Cookie 槽若提供的是全局地址gsGlobalSecurityCookieAddr则每次方法执行时从该地址加载当前值再写入栈槽从而天然支持跨运行/跨进程的随机刷新。该 Cookie 局部变量lvaGSSecurityCookie被刻意标记为地址暴露AddressExposed防止优化器把 Cookie 的初始化与校验优化掉见 src/coreclr/jit/gschecks.cpp。GS 在 JIT 流水线中的位置GS 相关变换在 RyuJIT 中是一个独立的编译阶段——GS Cookie 阶段PHASE_GS_COOKIE在 src/coreclr/jit/compiler.cpp 中通过DoPhase(this, PHASE_GS_COOKIE, Compiler::gsPhase)驱动。gsPhase见 src/coreclr/jit/gschecks.cpp的逻辑如下若getNeedsGSSecurityCookie()为真方法含不安全缓冲区调用gsGSChecksInitCookie()创建 Cookie 局部变量并获取 Cookie 值/地址若compGSReorderStackLayout为真允许重排栈布局调用gsCopyShadowParams()对脆弱参数做影子拷贝。否则在 Debug 构建下通过compGSSecurityCheckBlocker输出诊断信息如GS security check requested, but not provided便于排查为什么某方法未获得防护。其中compGSReorderStackLayout见 src/coreclr/jit/compiler.h控制是否执行栈布局重排与参数影子化即便不允许重排Cookie 的初始化与校验依然会进行。脆弱参数的影子拷贝Shadow Copy这是 GS 最具 RyuJIT 特色的一部分不仅把缓冲区与数据在布局上隔离还会把可能被用作任意读写媒介的指针参数复制到安全区域源码实现位于 src/coreclr/jit/gschecks.cpp 的gsCopyShadowParams。1. 发现脆弱参数gsFindVulnerableParamsgsFindVulnerableParams见 src/coreclr/jit/gschecks.cpp扫描方法全部基本块LIR 表示的树节点收集三类证据间接访问遇到GT_IND、GT_BLK、GT_STOREIND、GT_STORE_BLK、数组长度/下界访问等解引用操作时把被解引用地址所依赖的局部变量标记为指针lvIsPtr。这里体现了指针在间接访问下是脆弱的这一判定原则——恶意输入可以通过解引用任意读写内存赋值等价类assign group遇到GT_STORE_LCL_VAR/GT_STORE_LCL_FLD时把目标与源的所有依赖局部并入同一个赋值等价类gsUnionAssignGroups并在等价类内部传播指针标记。也就是说只要等价类中有一个变量被判定为指针整个等价类的参数都会被识别为脆弱调用点this参数以及间接调用CT_INDIRECT的函数指针会被视为写穿透指针write-through pointer因为函数指针直接控制将要执行的代码间接也能造成内存写入。此外lvIsUnsafeBuffer本身就视为脆弱。整个分析还会避开GT_SELECT的条件条件不影响值的传播并在 Debug 构建下用JITDUMP输出等价类信息。2. 创建并改写影子拷贝gsParamsToShadows 与后续找到脆弱参数后gsParamsToShadows见 src/coreclr/jit/gschecks.cpp执行gsCreateShadowingLocals为每个脆弱参数创建一个新的临时局部shadow copy拷贝类型信息、地址暴露标记、寄存器分配倾向、结构体布局等属性并把lvIsUnsafeBuffer/lvIsPtr一并继承见 src/coreclr/jit/gschecks.cppgsRewriteTreeForShadowParam遍历全部 IR把所有引用原参数的局部节点改写为引用影子拷贝见 src/coreclr/jit/gschecks.cppgsCopyIntoShadow在函数第一个基本块fgFirstBB的开头插入参数 - 影子拷贝的拷贝指令见 src/coreclr/jit/gschecks.cpp。几个值得注意的边界情况varargs 方法直接跳过影子拷贝if (info.compIsVarArgs) return;因为可变参数栈布局无法安全重排结构体参数会完整继承lvIsMultiRegArg/lvIsMultiRegRet等属性保持 ABI 语义一致在 x86 IJWItanium JIT/Windows 互操作场景下对需要特殊拷贝的结构体参数会调用getSpecialCopyHelper生成的辅助函数完成拷贝并正确处理 reverse P/Invoke 转换的插入位置Jmp 尾调用如果方法使用Jmp指令做尾调用由于被调用者期望从原始参数槽取值必须在GT_JMP之前把影子拷贝复制回原始参数见 src/coreclr/jit/gschecks.cpp。代码生成Cookie 的写入与校验序言Prolog中写入 Cookie在方法序言中genSetGSSecurityCookiex86/x64 实现见 src/coreclr/jit/codegenxarch.cpp把 Cookie 值写入栈槽若只有固定值且值在 32 位范围内mov dword ptr [frame.GSSecurityCookie], #Value若为 64 位大值AMD64先用寄存器加载立即数再mov [frame.GSSecurityCookie], reg若为全局地址mov eax, dword ptr [GlobalSecurityCookieAddr]后写入栈槽x86/x64 固定使用 EAX/RAX 以保证可编码性。该函数在序言寄存器初始化路径被调用见 src/coreclr/jit/codegencommon.cpp。ARM64、LoongArch64、RISC-V 平台均有对应实现codegenarmarch.cpp、codegenloongarch64.cpp、codegenriscv64.cpp会按各自 ISA 选择EA_PTR_DSP_RELOC等寻址/重定位方式加载全局地址。OSROn-Stack Replacement特例若当前为 OSR 方法且原始帧已经初始化过安全 CookieHasSecurityCookie()则不再重复初始化——Cookie 属于原始帧见 src/coreclr/jit/codegenxarch.cpp。出口Epilogue校验与 FailFast在方法返回路径上genEmitGSCookieCheck见 src/coreclr/jit/codegenxarch.cpp生成如下序列以 x64 为例加载/比较若为固定 32 位值直接cmp mem64, imm32若为 64 位值或全局地址则先加载到寄存器再cmp mem64, reg64相等则跳转到正常返回标签不相等则调用CORINFO_HELP_FAIL_FAST辅助函数。也就是说Cookie 校验失败会触发立即的、不可捕获的进程退出FailFast因为此时进程完整性已经存疑继续执行任何托管代码都不可信。校验调用点位于 src/coreclr/jit/codegencommon.cpp并会把BBF_HAS_JMP标志尾调用传入以便在校验失败路径上正确处理尾调用场景。运行时栈遍历中的 Cookie 校验GS 校验不仅发生在方法出口。运行时在进行EH异常处理与 GC 栈遍历时同样需要校验 Cookie因此 JIT 必须把 Cookie 的位置暴露给运行时。在 GC 信息GCInfo生成阶段JIT 会写入gsCookieOffset字段见 src/coreclr/jit/gcencode.cppheader-gsCookieOffset INVALID_GS_COOKIE_OFFSET; if (m_compiler-getNeedsGSSecurityCookie()) { assert(m_compiler-lvaGSSecurityCookie ! BAD_VAR_NUM); int stkOffs m_compiler-lvaTable[m_compiler-lvaGSSecurityCookie].GetStackOffset(); header-gsCookieOffset m_compiler-isFramePointerUsed() ? -stkOffs : stkOffs; ... }注意这里根据是否使用帧指针对偏移做符号修正帧指针相对 vs 栈指针相对保证运行时在任意栈遍历点都能正确定位 Cookie。类似地src/coreclr/jit/gcencode.cpp 中还会通过lvaGetCallerSPRelativeOffset计算相对于 Caller SP 的 Cookie 偏移供 EH 展开等场景使用。这使得EH/GC 栈遍历时校验 Cookie成为运行时可验证、可执行的机制而不仅是 JIT 出口处的本地检查。与硬件缓解机制的配合CETGS 通过软件方式保护栈上数据而返回地址还可以叠加硬件机制保护。当宿主机支持时.NET 可以利用 Intel/AMD 的Control-flow Enforcement TechnologyCET——影子栈shadow stack技术使得返回地址被硬件独立保存与校验即使攻击者改写了内存中的返回地址也无法与影子栈中的真实值匹配。详见仓库中的 CET 特性设计文档。GSCookie 布局重排与 CET硬件影子栈互为补充GS 防御缓冲区越界对栈帧数据的污染CET 则对返回地址提供独立于内存的硬件级完整性保证。如何观察 GS 的实际效果RyuJIT 提供 JIT dump 机制可用于观察 GS 变换的过程使用JITDUMPDebug/Checked 构建可在gsPhase执行时输出是否请求了安全 Cookie等价赋值类影子拷贝创建等诊断信息见 src/coreclr/jit/gschecks.cpp、src/coreclr/jit/gschecks.cpp在反汇编中可以看到mov ... [frame.GSSecurityCookie]序言写入、cmp mem64, imm32/reg64jnecall CORINFO_HELP_FAIL_FAST出口校验的特征指令序列栈帧布局方面可结合 arm64-jit-frame-layout.md 等帧布局文档理解各架构下 Cookie 槽与缓冲区的相对位置若想深入理解整个优化流水线中 GS 阶段所处的位置可参阅 ryujit-overview.md。小结GS 的核心设计要点设计要点机制源码依据不安全缓冲区识别localloc/UnsafeValueTypeAttribute固定大小缓冲区src/coreclr/jit/importer.cpp、src/coreclr/jit/lclvars.cpp栈布局重排易受攻击数据下移、含指针缓冲区排在更低地址src/coreclr/jit/gschecks.cpp栈 Cookie运行间随机值出口与栈遍历双路径校验src/coreclr/jit/codegenxarch.cpp脆弱参数影子拷贝指针/解引用分析 赋值等价类 IR 改写src/coreclr/jit/gschecks.cppCookie 位置暴露GCInfo 中编码gsCookieOffset供 EH/GC 栈遍历校验src/coreclr/jit/gcencode.cpp校验失败处理不可捕获的 FailFastCORINFO_HELP_FAIL_FASTsrc/coreclr/jit/codegenxarch.cpp硬件叠加CET 影子栈保护返回地址docs/design/features/cet-feature.mdGS 的价值在于把托管运行时内部引入不安全构造的成本显式化任何使用stackalloc、固定大小缓冲区等能力的代码路径都会被 JIT 自动包裹上布局重排 随机 Cookie 关键点校验的防护网从而在代码与数据地址被真正劫持之前用一次不可捕获的快速失败终止攻击进程——这是 .NET 运行时安全缓解体系中一道朴实而关键的防线。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表