首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
逆向工程
发新帖
15
7
[原创]浅析64位Windows的SEH机制
发表于: 2026-3-29 22:01
13502
[原创]浅析64位Windows的SEH机制
mb_nnqgphwf
2026-3-29 22:01
13502
# 浅析64位Windows的SEH机制 笔者花费了一些时间,学习了64位WindowsSEH机制的底层原理。现写为博客,希望对大家有帮助。 **注意:本文是从逆向工程的角度分析64位Windows的SEH机制,阅读本文的前提是能够从程序员(开发)的角度理解SEH机制。** 如果读者从未了解过SEH机制,请先阅读以下文章: <a href="elink@f7eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8V1!0G2M7X3y4Z5K9g2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6V1k6i4c8S2K9h3I4K6i4K6u0r3x3e0f1%4x3K6j5#2y4K6t1J5">Windows SEH机制(一)</a> <a href="elink@676K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8V1!0G2M7X3y4Z5K9g2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6V1k6i4c8S2K9h3I4K6i4K6u0r3x3e0f1%4y4o6R3H3x3K6b7H3">Windows SEH之全局展开</a> <a href="elink@764K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8V1!0G2M7X3y4Z5K9g2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6V1k6i4c8S2K9h3I4K6i4K6u0r3x3e0f1%4y4o6M7&6x3K6R3H3">Windows SEH机制(二)</a> > 要使SEH发挥作用,必须得到编译器、硬件和操作系统的配合支持。SEH 在特定平台上的具体实现方式可能因架构而异。 下图总结了Windows异常处理机制的基本框架(摘自《加密与解密》):  ## 示例程序 这里先给出一个基本的示例程序,便于我们讨论: ```cpp // 示例程序 test.c #include <windows.h> #include <stdio.h> int main() { __try { printf("__try block\n"); } __except (EXCEPTION_EXECUTE_HANDLER) { printf("__except block\n"); } return 0; } ``` ## RUNTIME_FUNCTION结构体 **64位中的SEH机制不再把异常处理下相关信息放到栈上,而是由编译器生成相关信息,并在放到PE文件中一个特定的节区中:.pdata。在64位SEH机制中需要学习的第一个结构就是RUNTIME_FUNCTION。若干RUNTIME_FUNCTION结构体组成一个数组,放在.pdata节区中**。该结构体定义如下: ```cpp typedef struct _RUNTIME_FUNCTION { DWORD BeginAddress; // Start RVA of SEH code chunk DWORD EndAddress; // End RVA of SEH code chunk DWORD UnwindData; // Rva of an UNWIND_INFO structure that describes this code frame } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION; ``` 该结构体包含三个字段: 1. BeginAddress:函数的起始地址 2. EndAddress:函数的结束地址 3. UnwindData:指向UNWIND_INFO结构体的指针 通过PE工具,我们可以查看到该结构体数组。例如,对于上面的程序,使用cff程序查看异常目录:  **这个结构体的数量和__try块的数量无关,而与包含__try块的函数有关(不管函数中有多少个__try块)。并且可以看到,RUNTIME_FUNCTION在.pdata中的排序方式是以BeginAddress为准的升序排序。** > 为什么这样排列?这是为了支持 O(log n) 时间复杂度的二分查找算法。当异常发生时,内核异常分发器(RtlLookupFunctionEntry)需要通过异常地址 RIP快速定位所属函数。二分查找是唯一能在对数时间内完成此任务的高效算法,而二分查找的前提条件就是数据集有序 注意,上面的三个地址都是相对于ImageBase的偏移量。所以尽管是在64位系统上,这三个地址也只有32位。 我们使用IDA来查看main函数对应的RUNTIME_FUNCTION条目。通过快捷键g就可以快速定位到main函数的RUNTIME_FUNCTION:  ```txt .pdata:000000014000400C dd rva main ; FunctionStart .pdata:0000000140004010 dd rva byte_140001096 ; FunctionEnd .pdata:0000000140004014 dd rva stru_140002804 ; UnwindInfo ``` 可以将前两个字段的值放到放到反汇编窗口中查看,就是main函数的起始和结束地址。 接下来说说UnwindData指向的UNWIND_INFO结构体。 --- ## UNWIND_INFO结构体 这个结构体十分关键,其中的内容是SEH的核心。UNWIND_INFO结构体定义如下: ```cpp // Unwind info flags #define UNW_FLAG_NHANDLER 0x0 #define UNW_FLAG_EHANDLER 0x01 #define UNW_FLAG_UHANDLER 0x02 #define UNW_FLAG_CHAININFO 0x04 // UNWIND_CODE 3 bytes structure typedef union _UNWIND_CODE { struct { UBYTE CodeOffset; UBYTE UnwindOp : 4; UBYTE OpInfo : 4; }; USHORT FrameOffset; } UNWIND_CODE, *PUNWIND_CODE; typedef struct _UNWIND_INFO { UBYTE Version : 3; // + 0x00 - Unwind info structure version UBYTE Flags : 5; // + 0x00 - Flags (see above) UBYTE SizeOfProlog; // + 0x01 UBYTE CountOfCodes; // + 0x02 - Count of unwind codes UBYTE FrameRegister : 4; // + 0x03 UBYTE FrameOffset : 4; // + 0x03 UNWIND_CODE UnwindCode[1]; // + 0x04 - Unwind code array UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1]; union { OPTIONAL ULONG ExceptionHandler; // Exception handler routine OPTIONAL ULONG FunctionEntry; }; OPTIONAL ULONG ExceptionData[]; // C++ Scope table structure } UNWIND_INFO, *PUNWIND_INFO; ``` UNWIND_INFO结构体定义较为复杂,我们只关注其中的关键部分。 ### Flags字段 **根据Flags的不同,UNWIND_INFO结构体的匿名联合体字段有不同的解释,ExceptionData字段是否有效也于Flags字段有关。** 下面的UNWIND_INFO结构体的定义更好的展示了Flags字段的影响: ```cpp #define UNW_FLAG_NHANDLER 0x0 #define UNW_FLAG_EHANDLER 0x1 #define UNW_FLAG_UHANDLER 0x2 #define UNW_FLAG_CHAININFO 0x4 typedef struct _UNWIND_INFO { UBYTE Version : 3; UBYTE Flags : 5; UBYTE SizeOfProlog; UBYTE CountOfCodes; UBYTE FrameRegister : 4; UBYTE FrameOffset : 4; UNWIND_CODE UnwindCode[1]; union { // // If (Flags & UNW_FLAG_EHANDLER) // OPTIONAL ULONG ExceptionHandler; // // Else if (Flags & UNW_FLAG_CHAININFO) // OPTIONAL ULONG FunctionEntry; }; // // If (Flags & UNW_FLAG_EHANDLER) // OPTIONAL ULONG ExceptionData[]; } UNWIND_INFO, *PUNWIND_INFO; ``` **为了快速讲清楚重点,这里我们只讨论Flags设置为UNW_FLAG_EHANDLER的情况。**一个典型的Flags被设置为UNW_FLAG_EHANDLER的函数如下: ```cpp void ExceptFunc() { __try {} __except(1) {} } ``` 当Flags设置为UNW_FLAG_EHANDLER时,此时union中的ExceptionHandler(编程语言相关)字段有效,由编译器负责填写。**如果发生异常且指令指针为>= BeginAddress和< EndAddress,就调用该ExceptionHandler**。 **ExceptionHandler会解析ExceptionData字段来确定如何处理发生的异常。** ### C_SCOPE_TABLE结构体 > SCOPE译为范围,能力 **ExceptionData是指向C_SCOPE_TABLE结构体的指针的偏移量(也就是指向C_SCOPE_TABLE的指针)**,该结构体定义如下(务必仔细阅读): ```cpp // C Scope table entry typedef struct _C_SCOPE_TABLE_ENTRY { ULONG Begin; // +0x00 - Begin of guarded code block,__try块第一条指令的偏移量 ULONG End; // +0x04 - End of target code block,__try块内最后一条指令之后指令的偏移量 ULONG Handler; // +0x08 - Exception filter function (or “__finally” handler) ULONG Target; // +0x0C - Exception handler pointer (the code inside __except block) } C_SCOPE_TABLE_ENTRY, *PC_SCOPE_TABLE_ENTRY; // C Scope table typedef struct _C_SCOPE_TABLE { ULONG NumEntries; // +0x00 - Number of entries C_SCOPE_TABLE_ENTRY Table[1]; // +0x04 - Scope table array } C_SCOPE_TABLE, *PC_SCOPE_TABLE; ``` **RUNTIME_FUNCTION描述了包含SEH的函数的整个范围,而SCOPE_TABLE描述了函数内每个单独的__try/__except块。** ## 整体展示 ```cpp VOID FrobThePointer(PUCHAR UserAddress) { __try { *UserAddress = 0; *UserAddress = 1; } __except (EXCEPTION_EXECUTE_HANDLER) { DbgPrint("Bad Address\n"); } } ``` 以上面的代码为例,生成的汇编代码大致如下: ```asm <00> mov [rsp+0x8],rcx <05> sub rsp,0x28 <09> mov rax,[rsp+0x30] // Move UserAddress into RAX <0e> mov byte ptr [rax],0x0 // *UserAddress = 0; <11> mov rax,[rsp+0x30] // Move UserAddress into RAX <16> mov byte ptr [rax],0x1 // *UserAddress = 1; <19> jmp FrobThePointer+0x28 // Success! <1b> lea rcx,"Bad Address\n" // Begin of code in except block... // prepare to DbgPrint <22> call DbgPrint <27> nop <28> add rsp,0x28 <2c> ret ``` 套到RUMTIME_FUNCTION和UNWIND_INFO结构体如下:  ## 定位到异常位置的流程(以除0异常为例) 到现在为止,我们可以总结一下x64的SEH机制如何定位到发生异常的地方:**假设发生异常的地址为EA,先通过二分查找在RUNTIME_FUNCTION(比较规则:BeginAddress < EA < EndAddress)定位到发生异常的函数,然后遍历ExceptionData指向的C_SCOPE_TABLE_ENTRY数组,定位到发生异常的try块(比较规则:Begin < EA < End)。** 这是查找的基本流程,但是我们可以再深入一些。 > 每当发生异常时,就会调用内部 Windows 函数 RtlDispatchException。该函数在用户模式异常的 NTDLL 模块中实现,在内核模式异常的 NTOSKRNL 模块中实现,方式略有不同。该函数通过执行一些初始检查来开始执行:如果存在用户模式VEH,则将调用该异常处理程序;否则将进行标准 SEH 处理。 > 标准SEH处理:复制异常时的线程上下文,并利用RtlLookupFunctionEntry函数来执行一项重要任务:获取PE文件的ImageBase和RUNTIME_FUNCTION结构。 --- 现在,我们来追踪一个 64 位 Windows 程序发生除零异常 (`#DE`) 后,从 CPU 陷阱到最终执行 `__except` 块的**完整、详细的系统级调用链和数据结构操作**。这将深入到内核和运行时库的内部。 --- ### 第一阶段:硬件陷阱与内核接管 **1. 触发异常** ```asm ; 在用户态执行 mov eax, 1 cdq idiv dword ptr [rcx] ; 假设 [rcx]=0,触发 #DE ``` **2. CPU 硬件操作** - CPU 检测到除零,**中断号 0 (`#DE`)** 被触发。 - CPU 自动切换到**内核模式**(通过预设的 TSS 或 MSR 加载 `GS` 基址)。 - CPU 将关键的**用户态上下文**压入**当前线程的内核栈**,形成一个 `KTRAP_FRAME` 结构。这包括: - `RIP`:指向 `idiv` 的下一条指令。 - `RSP`:用户态栈指针。 - `RFLAGS`、`CS`、`SS` 等段寄存器。 - 通用寄存器 `RAX`、`RCX`、`RDX` 等。 - CPU 根据 **IDT(中断描述符表)** 的条目 0,跳转到预设的中断处理程序入口。在 Windows 中,这是 **`KiDivideErrorFault`**(或类似的陷阱处理程序)。 **3. 内核陷阱处理 (`KiDivideErrorFault`)** ```c // 伪代码,位于 ntoskrnl.exe VOID KiDivideErrorFault() { // 1. 建立更完整的陷阱帧 KTRAP_FRAME* pTrapFrame = GetCurrentTrapFrame(); // 2. 将异常信息打包为 EXCEPTION_RECORD EXCEPTION_RECORD ExceptionRecord = {0}; ExceptionRecord.ExceptionCode = STATUS_INTEGER_DIVIDE_BY_ZERO; // 0xC0000094 ExceptionRecord.ExceptionAddress = pTrapFrame->Rip; ExceptionRecord.NumberParameters = 0; // 3. 调用公共的异常分发例程 KiExceptionDispatch(&ExceptionRecord, pTrapFrame); } ``` --- ### 第二阶段:内核异常分发 (`KiExceptionDispatch` / `KiDispatchException`) 这是最核心的调度器。其内部逻辑复杂,但关键步骤如下: **1. 构建 `CONTEXT` 和 `EXCEPTION_POINTERS`** ```c // 在 KiDispatchException 内部 CONTEXT ContextRecord = {0}; // 从 KTRAP_FRAME 填充 ContextRecord 的所有寄存器 RtlpCaptureContext(&ContextRecord, pTrapFrame); EXCEPTION_POINTERS ExceptionPointers = {0}; ExceptionPointers.ExceptionRecord = &ExceptionRecord; ExceptionPointers.ContextRecord = &ContextRecord; ``` **2. 首次尝试:用户模式异常分发** 内核检查异常地址,发现 `RIP` 在用户空间。于是调用 **`RtlDispatchException`** 函数(这是用户态异常分发的内核入口)。 ```c BOOLEAN RtlDispatchException(PEXCEPTION_RECORD pExceptionRecord, PCONTEXT pContextRecord) { // 【关键点1】检查 VEH(向量化异常处理程序) if (RtlCallVectoredExceptionHandlers(pExceptionRecord, pContextRecord)) { return TRUE; // VEH 处理了异常 } // 【关键点2】定位 RUNTIME_FUNCTION PRUNTIME_FUNCTION pRuntimeFunction = RtlLookupFunctionEntry( pContextRecord->Rip, // 控制点 &ImageBase, // 输出:模块基址 NULL // 历史表 ); if (pRuntimeFunction == NULL) { // 没有函数表,通常是 JIT 代码,无法展开 return FALSE; } // 【关键点3】获取 UNWIND_INFO PUNWIND_INFO pUnwindInfo = (PUNWIND_INFO)(ImageBase + pRuntimeFunction->UnwindInfoAddress); // 【关键点4】调用语言特定的异常处理程序(例如 __C_specific_handler) if (pUnwindInfo->Flags & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER)) { // 计算 ExceptionData 地址 PVOID pHandlerData = RtlpGetHandlerData(pUnwindInfo); // 这个函数是 MSVC 运行时提供的,负责解析 SCOPE_RECORD EXCEPTION_DISPOSITION disposition = __C_specific_handler( pExceptionRecord, (PVOID)pContextRecord->Rip, pContextRecord, pHandlerData ); if (disposition == ExceptionExecuteHandler) { // 找到了处理器,准备展开 return TRUE; } } // 【关键点5】如果没有处理器,尝试展开一帧(用于 C++ 异常传播) RtlUnwindEx(...); return FALSE; } ``` **3. `RtlLookupFunctionEntry` 的查找过程** 这个函数是第一阶段查找的具体实现。 ```c PRUNTIME_FUNCTION RtlLookupFunctionEntry( IN ULONG64 ControlPc, OUT PULONG64 ImageBase, IN OUT PT_RUNTIME_FUNCTION* HistoryTable ) { // 1. 通过 ControlPc 找到所属的 PE 模块 (DLL/EXE) PLDR_DATA_TABLE_ENTRY pModule = LdrFindEntryForAddress(ControlPc); *ImageBase = pModule->DllBase; // 2. 从 PE 头定位 .pdata 节 PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(*ImageBase + ((PIMAGE_DOS_HEADER)*ImageBase)->e_lfanew); PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders); for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++, pSection++) { if (memcmp(pSection->Name, ".pdata", 6) == 0) { break; } } // 3. 计算 RUNTIME_FUNCTION 数组的起始和结束 PRUNTIME_FUNCTION pFunctionTable = (PRUNTIME_FUNCTION)(*ImageBase + pSection->VirtualAddress); ULONG NumberOfFunctions = pSection->Misc.VirtualSize / sizeof(RUNTIME_FUNCTION); // 4. 【核心】二分查找 return RtlpBinarySearch(pFunctionTable, ControlPc - *ImageBase, NumberOfFunctions); } ``` **4. `__C_specific_handler` 的工作(用户态回调!)** 这是 MSVC 运行时库 (`vcruntimexxx.dll`) 提供的函数。**注意:此时 CPU 仍在内核模式,但该函数是用户态代码,内核会临时切换到用户态执行它。** ```c EXCEPTION_DISPOSITION __C_specific_handler( PEXCEPTION_RECORD pExceptionRecord, PVOID EstablisherFrame, // 实际上是发生异常的函数栈帧指针 PCONTEXT pContextRecord, PEXCEPTION_REGISTRATION_RECORD pDispatcherContext ) { // 1. 从 DispatcherContext 中提取 SCOPE_RECORD 数组 PSCOPE_RECORD pScopeRecords = (PSCOPE_RECORD)pDispatcherContext; DWORD scopeCount = pScopeRecords->Count; pScopeRecords++; // 指向第一个 SCOPE_RECORD // 2. 计算异常在函数内的偏移 DWORD_PTR ImageBase = ...; PRUNTIME_FUNCTION pRF = ...; DWORD offsetInFunction = (DWORD)(pContextRecord->Rip - ImageBase - pRF->BeginAddress); // 3. 线性遍历 SCOPE_RECORD for (DWORD i = 0; i < scopeCount; i++) { if (offsetInFunction >= pScopeRecords[i].BeginAddress && offsetInFunction < pScopeRecords[i].EndAddress) { // 4. 找到匹配的 try 块!调用过滤器 if (pScopeRecords[i].HandlerAddress) { int filterResult = ((FILTER_FUNC)pScopeRecords[i].HandlerAddress)(); if (filterResult == EXCEPTION_EXECUTE_HANDLER) { // 5. 设置要跳转的目标地址 pDispatcherContext->TargetIp = ImageBase + pRF->BeginAddress + pScopeRecords[i].JumpTarget; return ExceptionExecuteHandler; } } } } return ExceptionContinueSearch; } ``` --- ### 第三阶段:栈展开与最终派发 **1. 内核收到 `ExceptionExecuteHandler` 后的操作** 内核的 `KiDispatchException` 收到 `RtlDispatchException` 返回 `TRUE`,知道找到了处理程序。 ```c // 在 KiDispatchException 中 if (FirstPassSuccess) { // 即 RtlDispatchException 返回 TRUE // 1. 获取目标地址(由 __C_specific_handler 设置) ULONG64 TargetIp = GetTargetIpFromDispatcherContext(); // 2. 执行栈展开 RtlUnwindEx( TargetFrame, // 展开到哪个帧 TargetIp, // 要跳转的目标地址 pExceptionRecord, ReturnValue, // 返回值 pContextRecord, HistoryTable ); // 3. 修改陷阱帧中的 RIP pTrapFrame->Rip = TargetIp; // 4. 【关键】恢复执行 KiContinuePreviousMode(pContextRecord, pTrapFrame, PreviousMode); } ``` **2. `RtlUnwindEx` 的展开过程** 这是实际的栈帧展开器,它解释执行 `UNWIND_CODE` 数组。 ```c VOID RtlUnwindEx(...) { // 循环展开每一帧,直到到达目标帧 while (CurrentFrame < TargetFrame) { // 对当前帧,查找其 RUNTIME_FUNCTION PRUNTIME_FUNCTION pRF = RtlLookupFunctionEntry(CurrentRip, &ImageBase, NULL); PUNWIND_INFO pUnwindInfo = (PUNWIND_INFO)(ImageBase + pRF->UnwindInfoAddress); // 解释执行 UNWIND_CODE for (int i = 0; i < pUnwindInfo->CountOfCodes; i++) { switch (pUnwindInfo->UnwindCode[i].UnwindOp) { case UWOP_PUSH_NONVOL: // 模拟 pop 操作,恢复寄存器 Context->Rsp += 8; Context->Rbx = *(PULONG64)(Context->Rsp - 8); break; case UWOP_ALLOC_SMALL: Context->Rsp += (pUnwindInfo->UnwindCode[i].OpInfo * 8) + 8; break; // ... 其他展开操作码 } } // 如果有终止处理器(__finally),现在调用它 if (pUnwindInfo->Flags & UNW_FLAG_UHANDLER) { CallTerminationHandler(pUnwindInfo, Context); } // 移动到调用者帧 CurrentRip = *(PULONG64)Context->Rsp; Context->Rsp += 8; } } ``` --- ### 第四阶段:返回用户态,执行处理块 **1. 最终返回 (`KiContinuePreviousMode`)** 内核将修改后的 `CONTEXT` 写回陷阱帧,然后执行 `iretq`(或 `sysret`)指令,**返回到用户态**。但返回的 `RIP` 不是原来的 `idiv` 之后,而是 `__except` 块的地址。 **2. 用户态恢复执行** ```asm ; 这是用户态,但 RIP 已指向 __except 块 ; 假设 __except 块地址是 0x140001050 0x140001050: mov rcx, str_inside_except 0x140001057: call printf ; ... 异常处理代码 ``` --- 关键数据结构和调用链总结: | 阶段 | 关键函数 | 关键数据结构 | 操作 | | :--- | :--- | :--- | :--- | | **硬件陷阱** | `KiDivideErrorFault` | `KTRAP_FRAME` | 保存用户上下文 | | **内核分发** | `KiDispatchException` | `EXCEPTION_RECORD`, `CONTEXT` | 构建异常信息 | | **用户分发** | `RtlDispatchException` | - | 协调分发流程 | | **函数查找** | `RtlLookupFunctionEntry` | `RUNTIME_FUNCTION` 数组 | 二分查找函数 | | **展开信息** | - | `UNWIND_INFO`, `UNWIND_CODE` | 描述栈帧布局 | | **作用域查找** | `__C_specific_handler` | `SCOPE_RECORD` 数组 | 线性查找 try 块 | | **栈展开** | `RtlUnwindEx` | `UNWIND_CODE` | 解释执行展开指令 | | **返回** | `KiContinuePreviousMode` | `KTRAP_FRAME` | 恢复用户态上下文 | --- 调用链总结如下: ```txt KiUserExceptionDispatcher // Windows Kernel Internal (KI) API -> RtlDispatchException // main logic for exception handling -> RtlpCallVectoredHandlers // call any VEH -> RtlLookupFunctionEntry // look for valid PRUNTIME_FUNCTION entry in ExceptionDirectory -> RtlpLookupDynamicFunctionEntry // if no valid PRUNTIME_FUNCTION, run any dynamic callbacks -> RtlVirtualUnwind / RtlpxVirtualUnwind // perform stack frame unwinding -> RtlpExecuteHandlerForException // execute exception handler! ``` **上面的代码涉及到了较多的结构体,大家可以参考钱松林老师的《C++反汇编与逆向分析技术揭秘》一书。** ## UNWIND_CODE结构体 在UNWIND_INFO结构体中有一个UnwindCode字段,指向UNWIND_CODE结构体。 `UNWIND_CODE` 结构体是 **64 位 SEH 机制中的“展开脚本”或“逆操作指令集”**。它不直接参与异常处理的**决策**(这是 `SCOPE_RECORD` 的工作),而是负责异常处理流程中的**执行**环节——即如何安全、正确地将栈帧恢复到函数调用前的状态。这是实现栈展开(Stack Unwind)的基石。 --- ### `UNWIND_CODE` 的核心角色:栈帧的“构建蓝图”与“拆卸手册” 你可以将函数调用过程想象成搭积木: - **函数序言 (Prolog)**:是“搭积木”的过程。它在栈上分配空间、保存寄存器。 - **函数尾声 (Epilog)**:是“拆积木”的过程。它恢复寄存器、释放栈空间,然后返回。 - **异常发生时**:程序突然中断,积木搭到一半。`UNWIND_CODE` 就是一份**紧急拆卸手册**,告诉系统如何从这个“半成品”状态,安全地拆回到调用前的样子,而不管积木搭到了哪一步。 **关键点**:异常可能发生在函数序言之后、尾声之前的**任何位置**。系统必须能够从**任意点**将栈恢复到函数入口之前的状态。`UNWIND_CODE` 提供了完成此操作所需的**精确、逐步的逆操作指令**。 --- ### `UNWIND_CODE` 结构体详解 定义位于 `winnt.h`: ```c typedef union _UNWIND_CODE { struct { UBYTE CodeOffset; // 距离序言开始的偏移量(以字节为单位) UBYTE UnwindOp:4; // 展开操作码 UBYTE OpInfo:4; // 操作码附加信息 }; USHORT FrameOffset; // 当 UnwindOp 为某些值时,整个USHORT表示帧偏移 } UNWIND_CODE, *PUNWIND_CODE; ``` **字段含义**: - **`CodeOffset`**:**这是最关键、最精妙的设计。** 它表示**本展开操作应该在函数序言的哪个“进度点”之后执行**。系统通过比较异常地址(`RIP`)与函数起始地址的偏移量,来决定执行 `UNWIND_CODE` 数组中的哪些指令。这确保了无论异常发生在序言后、函数体中还是尾声前,栈都能被正确恢复。 - **`UnwindOp`**:展开操作码。定义了具体的恢复操作(如 `pop` 寄存器、增加 `RSP` 等)。 - **`OpInfo`**:操作信息,通常是寄存器编号或大小参数。 --- ### `UNWIND_CODE` 在异常处理流程中的具体工作 让我们结合一个具体例子,跟踪流程。假设有以下函数: ```c // 对应的汇编序言 MyFunc: push rbx ; 保存非易失寄存器 push rsi sub rsp, 0x20 ; 分配局部变量空间 ... ; 函数体 ``` 编译器会为它生成以下逻辑的 `UNWIND_CODE` 数组(伪代码表示): ```c UNWIND_CODE UnwindCode[] = { {CodeOffset: 0x0, UnwindOp: UWOP_PUSH_NONVOL, OpInfo: RBX}, // 对应 push rbx {CodeOffset: 0x1, UnwindOp: UWOP_PUSH_NONVOL, OpInfo: RSI}, // 对应 push rsi {CodeOffset: 0x2, UnwindOp: UWOP_ALLOC_SMALL, OpInfo: 4} // 对应 sub rsp,0x20 (4*8=0x20) }; ``` **注意**:`CodeOffset` 是累加的。第一条对应序言第0字节(`push rbx`),第二条对应第1字节(`push rsi`),第三条对应第2字节(`sub rsp,0x20`)。 #### 场景分析:异常发生在不同位置 **场景1:异常发生在序言之后,函数体内部**(例如在 `sub rsp, 0x20` 之后) 1. 系统计算 `RIP` 偏移,假设为 `0x05`。 2. 遍历 `UNWIND_CODE` 数组,执行所有 `CodeOffset <= 0x05` 的指令(即全部三条): - 执行逆操作:`add rsp, 0x20` (恢复栈分配) - 执行逆操作:模拟 `pop rsi` (从栈上恢复 `rsi` 的值到 `CONTEXT` 结构) - 执行逆操作:模拟 `pop rbx` (恢复 `rbx`) 3. 栈指针 `RSP` 和寄存器 `RBX`、`RSI` 被恢复到进入函数时的状态。 **场景2:异常发生在序言执行过程中**(例如在 `push rsi` 之后,`sub rsp,0x20` 之前) 1. `RIP` 偏移假设为 `0x01`。 2. 只执行 `CodeOffset <= 0x01` 的指令(前两条): - 执行逆操作:模拟 `pop rsi` - 执行逆操作:模拟 `pop rbx` 3. **注意**:`UWOP_ALLOC_SMALL` 对应的操作(`add rsp,0x20`)**不会被执行**,因为对应的 `sub rsp,0x20` 指令还没执行!这就是 `CodeOffset` 机制的精髓:**只撤销那些已经执行了的序言操作**。 **场景3:用于 `__finally` 的展开** 当为 `__finally` 展开时,过程相同。`UNWIND_CODE` 确保栈被正确恢复,然后系统会跳转到 `__finally` 块执行清理代码,最后继续展开。 --- ### 主要的 `UnwindOp` 操作码及其逆操作 | 操作码 | 含义 | 对应的序言指令示例 | 展开时的逆操作(由 `RtlUnwindEx` 执行) | | :--- | :--- | :--- | :--- | | **`UWOP_PUSH_NONVOL`** | 压入非易失寄存器 | `push rbx` | 从栈上弹出值,并**恢复 `CONTEXT` 中对应寄存器的值**。`RSP += 8`。 | | **`UWOP_ALLOC_SMALL`** | 分配小栈空间 | `sub rsp, 0x20` | `RSP += 分配大小`。 | | **`UWOP_ALLOC_LARGE`** | 分配大栈空间 | `sub rsp, 0x1000` | `RSP += 分配大小`。 | | **`UWOP_SAVE_NONVOL`** | 保存非易失寄存器到栈上 | `mov [rsp+0x10], rbx` | 从栈上指定偏移处读取值,**恢复 `CONTEXT` 中对应寄存器的值**。 | | **`UWOP_SAVE_XMM128`** | 保存 XMM 寄存器 | `movaps [rsp+0x20], xmm6` | 恢复 `CONTEXT` 中的 XMM 寄存器。 | | **`UWOP_SET_FPREG`** | 建立帧指针 | `mov rbp, rsp` | 将 `RBP` 恢复为 `CONTEXT` 中保存的调用者值。 | | **`UWOP_PUSH_MACHFRAME`** | 压入机器帧(用于硬件中断) | 由硬件中断自动完成 | 特殊中断恢复。 | **重要提示**:展开时的“pop”操作是**逻辑上的**。实际过程是: 1. 从当前 `RSP` 和 `CodeOffset` 计算原始值在栈上的位置。 2. 将该值读入 `CONTEXT` 结构中的对应寄存器字段。 3. 更新 `CONTEXT` 中的 `RSP` 值,模拟栈指针移动。 **栈内存本身的内容不会被修改**,只是 `CONTEXT` 被更新,为后续恢复执行做准备。 ---
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
#调试逆向
#系统底层
收藏
・
15
点赞
・
7
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
期待更多优质内容的分享,论坛有你更精彩!
2026-7-16 05:41
yuanyouran
+1
非常支持你的观点!
2026-4-7 09:27
Dy1an
感谢你分享这么好的资源!
2026-4-1 14:14
马来
你的帖子非常有用,感谢分享!
2026-4-1 10:10
我的小拇指啊
谢谢你的细致分析,受益匪浅!
2026-3-30 00:14
小柯南
为你点赞!
2026-3-29 22:33
东方玻璃
感谢你的贡献,论坛因你而更加精彩!
2026-3-29 22:27
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
4
)
xyzliao
雪 币:
650
活跃值:
(5605)
能力值:
( LV3,RANK:20 )
在线值:
发帖
4
回帖
127
粉丝
4
关注
私信
xyzliao
2
楼
太硬核了
2026-4-3 10:47
0
yuanyouran
雪 币:
305
活跃值:
(3075)
能力值:
( LV2,RANK:10 )
在线值:
发帖
5
回帖
184
粉丝
0
关注
私信
yuanyouran
3
楼
感谢分享~~
2026-4-7 09:27
0
MsScotch
雪 币:
4184
活跃值:
(4926)
能力值:
( LV2,RANK:10 )
在线值:
发帖
3
回帖
394
粉丝
5
关注
私信
MsScotch
4
楼
tql
2026-4-8 10:27
0
yuanyouran
雪 币:
305
活跃值:
(3075)
能力值:
( LV2,RANK:10 )
在线值:
发帖
5
回帖
184
粉丝
0
关注
私信
yuanyouran
5
楼
tql
2026-4-9 22:37
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mb_nnqgphwf
7
发帖
7
回帖
30
RANK
关注
私信
他的文章
[原创]详细分析MSVC中std::string对象内存布局
213
[原创]分析Windows Section Object机制
999
[原创]I Pack You加密壳:实现页粒度的动态解密和惰性加密
6206
[原创]I Pack You:实现基本的软件壳框架
2682
[原创]调试Windows11启动过程
12241
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部