首页
社区
课程
招聘
[原创]浅析64位Windows的SEH机制
发表于: 2小时前 76

[原创]浅析64位Windows的SEH机制

2小时前
76

浅析64位Windows的SEH机制

笔者花费了一些时间,学习了64位WindowsSEH机制的底层原理。现写为博客,希望对大家有帮助。

注意:本文是从逆向工程的角度分析64位Windows的SEH机制,阅读本文的前提是能够从程序员(开发)的角度理解SEH机制。

如果读者从未了解过SEH机制,请先阅读以下文章:

Windows SEH机制(一)

Windows SEH之全局展开

Windows SEH机制(二)

要使SEH发挥作用,必须得到编译器、硬件和操作系统的配合支持。SEH 在特定平台上的具体实现方式可能因架构而异。

下图总结了Windows异常处理机制的基本框架(摘自《加密与解密》):

图片描述

示例程序

这里先给出一个基本的示例程序,便于我们讨论:

// 示例程序 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节区中。该结构体定义如下:

  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:

图片描述

.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结构体定义如下:

// 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字段的影响:

#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的函数如下:

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的指针),该结构体定义如下(务必仔细阅读):

// 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块。

整体展示

VOID FrobThePointer(PUCHAR UserAddress)
{
        __try
        {
            *UserAddress = 0;
            *UserAddress = 1;
         }
         __except (EXCEPTION_EXECUTE_HANDLER)
         {
            DbgPrint("Bad Address\n");
         }
 }

以上面的代码为例,生成的汇编代码大致如下:

<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. 触发异常

; 在用户态执行
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:用户态栈指针。
    • RFLAGSCSSS 等段寄存器。
    • 通用寄存器 RAXRCXRDX 等。
  • CPU 根据 IDT(中断描述符表) 的条目 0,跳转到预设的中断处理程序入口。在 Windows 中,这是 KiDivideErrorFault(或类似的陷阱处理程序)。

3. 内核陷阱处理 (KiDivideErrorFault)

// 伪代码,位于 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. 构建 CONTEXTEXCEPTION_POINTERS

// 在 KiDispatchException 内部
CONTEXT ContextRecord = {0};
// 从 KTRAP_FRAME 填充 ContextRecord 的所有寄存器
RtlpCaptureContext(&ContextRecord, pTrapFrame);

EXCEPTION_POINTERS ExceptionPointers = {0};
ExceptionPointers.ExceptionRecord = &ExceptionRecord;
ExceptionPointers.ContextRecord = &ContextRecord;

2. 首次尝试:用户模式异常分发内核检查异常地址,发现 RIP 在用户空间。于是调用 RtlDispatchException 函数(这是用户态异常分发的内核入口)。

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 的查找过程这个函数是第一阶段查找的具体实现。

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 仍在内核模式,但该函数是用户态代码,内核会临时切换到用户态执行它。

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,知道找到了处理程序。

// 在 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 数组。

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. 用户态恢复执行

; 这是用户态,但 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 恢复用户态上下文

调用链总结如下:

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

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 在异常处理流程中的具体工作

让我们结合一个具体例子,跟踪流程。假设有以下函数:

// 对应的汇编序言
MyFunc:
    push    rbx        ; 保存非易失寄存器
    push    rsi
    sub     rsp, 0x20  ; 分配局部变量空间
    ...                ; 函数体

编译器会为它生成以下逻辑的 UNWIND_CODE 数组(伪代码表示):

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 和寄存器 RBXRSI 被恢复到进入函数时的状态。

场景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. 从当前 RSPCodeOffset 计算原始值在栈上的位置。
  2. 将该值读入 CONTEXT 结构中的对应寄存器字段。
  3. 更新 CONTEXT 中的 RSP 值,模拟栈指针移动。

栈内存本身的内容不会被修改,只是 CONTEXT 被更新,为后续恢复执行做准备。



传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 3
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回