浅析64位Windows的SEH机制

admin 2026-04-10 02:44:32 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文从逆向工程角度深入分析64位Windows的SEH异常处理机制,重点解析RUNTIMEFUNCTION和UNWINDINFO等关键结构体在.pdata节区的存储方式。文章通过具体代码示例详细说明异常定位流程:通过二分查找定位异常函数,再遍历SCOPE_TABLE确定具体try块,并阐述了ExceptionHandler的触发条件与处理逻辑。 综合评分: 85 文章分类: 逆向分析,二进制安全,Windows安全,漏洞分析


cover_image

浅析64位Windows的SEH机制

mb_nnqgphwf mb_nnqgphwf

看雪学苑

2026年4月7日 18:00 上海

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

#

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

Windows SEH机制(一)

https://blog.csdn.net/Oorchi/article/details/157365722

Windows SEH之全局展开

https://blog.csdn.net/Oorchi/article/details/157480340

Windows SEH机制(二)

https://blog.csdn.net/Oorchi/article/details/157479380

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

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

示例程序

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

// 示例程序 test.c
#include&nbsp;<windows.h>
#include&nbsp;<stdio.h>

intmain()&nbsp;{
&nbsp; &nbsp; __try {
printf("__try block\n");
&nbsp; &nbsp; }
&nbsp; &nbsp; __except (EXCEPTION_EXECUTE_HANDLER) {
printf("__except block\n");
&nbsp; &nbsp; }
return&nbsp;0;
}

#

RUNTIME_FUNCTION结构体

64位中的SEH机制不再把异常处理下相关信息放到栈上,而是由编译器生成相关信息,并在放到PE文件中一个特定的节区中:.pdata。在64位SEH机制中需要学习的第一个结构就是RUNTIME_FUNCTION。若干RUNTIME_FUNCTION结构体组成一个数组,放在.pdata节区中

该结构体定义如下:

typedef&nbsp;struct&nbsp;_RUNTIME_FUNCTION&nbsp;{
&nbsp; &nbsp; DWORD BeginAddress; &nbsp; &nbsp;// Start RVA of SEH code chunk
&nbsp; &nbsp; DWORD EndAddress; &nbsp; &nbsp; &nbsp;// End RVA of SEH code chunk
&nbsp; &nbsp; DWORD UnwindData; &nbsp; &nbsp; &nbsp;// Rva of an UNWIND_INFO structure that describes this code frame
&nbsp; } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

该结构体包含三个字段:

  • BeginAddress:函数的起始地址
  • EndAddress:函数的结束地址
  • 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:000000014000400Cddrvamain&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;;&nbsp;FunctionStart
.pdata:0000000140004010&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ddrvabyte_140001096&nbsp; &nbsp;;&nbsp;FunctionEnd
.pdata:0000000140004014&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ddrvastru_140002804&nbsp; &nbsp;;&nbsp;UnwindInfo

可以将前两个字段的值放到放到反汇编窗口中查看,就是main函数的起始和结束地址。

接下来说说UnwindData指向的UNWIND_INFO结构体。

UNWIND_INFO结构体

这个结构体十分关键,其中的内容是SEH的核心。UNWIND_INFO结构体定义如下:

// Unwind info flags
#define&nbsp;UNW_FLAG_NHANDLER 0x0
#define&nbsp;UNW_FLAG_EHANDLER 0x01
#define&nbsp;UNW_FLAG_UHANDLER 0x02
#define&nbsp;UNW_FLAG_CHAININFO 0x04

// UNWIND_CODE 3 bytes structure
typedef&nbsp;union&nbsp;_UNWIND_CODE&nbsp;{
struct&nbsp;{
&nbsp; &nbsp; UBYTE CodeOffset;
&nbsp; &nbsp; UBYTE UnwindOp :&nbsp;4;
&nbsp; &nbsp; UBYTE OpInfo :&nbsp;4;
&nbsp; };
&nbsp; USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

typedef&nbsp;struct&nbsp;_UNWIND_INFO&nbsp;{
&nbsp; UBYTE Version :&nbsp;3; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// + 0x00 - Unwind info structure version
&nbsp; UBYTE Flags :&nbsp;5; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// + 0x00 - Flags (see above)
&nbsp; UBYTE SizeOfProlog; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// + 0x01
&nbsp; UBYTE CountOfCodes; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// + 0x02 - Count of unwind codes
&nbsp; UBYTE FrameRegister :&nbsp;4; &nbsp; &nbsp;// + 0x03
&nbsp; UBYTE FrameOffset :&nbsp;4; &nbsp; &nbsp; &nbsp;// + 0x03
&nbsp; UNWIND_CODE UnwindCode[1]; &nbsp;// + 0x04 - Unwind code array
&nbsp; UNWIND_CODE MoreUnwindCode[((CountOfCodes +&nbsp;1) & ~1) -&nbsp;1];
union&nbsp;{
&nbsp; &nbsp; OPTIONAL ULONG ExceptionHandler; &nbsp; &nbsp;// Exception handler routine
&nbsp; &nbsp; OPTIONAL ULONG FunctionEntry;
&nbsp; };
&nbsp; OPTIONAL ULONG ExceptionData[]; &nbsp; &nbsp; &nbsp;&nbsp;// C++ Scope table structure
} UNWIND_INFO, *PUNWIND_INFO;

UNWIND_INFO结构体定义较为复杂,我们只关注其中的关键部分。

Flags字段

根据Flags的不同,UNWIND_INFO结构体的匿名联合体字段有不同的解释,ExceptionData字段是否有效也于Flags字段有关。

下面的UNWIND_INFO结构体的定义更好的展示了Flags字段的影响:

#define&nbsp;UNW_FLAG_NHANDLER 0x0
#define&nbsp;UNW_FLAG_EHANDLER 0x1
#define&nbsp;UNW_FLAG_UHANDLER 0x2
#define&nbsp;UNW_FLAG_CHAININFO 0x4

typedef&nbsp;struct&nbsp;_UNWIND_INFO&nbsp;{
&nbsp; &nbsp; UBYTE Version &nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;3;
&nbsp; &nbsp; UBYTE Flags &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;5;
&nbsp; &nbsp; UBYTE SizeOfProlog;
&nbsp; &nbsp; UBYTE CountOfCodes;
&nbsp; &nbsp; UBYTE FrameRegister &nbsp;:&nbsp;4;
&nbsp; &nbsp; UBYTE FrameOffset &nbsp; &nbsp;:&nbsp;4;
&nbsp; &nbsp; UNWIND_CODE UnwindCode[1];
union&nbsp;{
//
// If (Flags & UNW_FLAG_EHANDLER)
//
&nbsp; &nbsp; &nbsp; &nbsp; OPTIONAL ULONG ExceptionHandler;
//
// Else if (Flags & UNW_FLAG_CHAININFO)
//
&nbsp; &nbsp; &nbsp; &nbsp; OPTIONAL ULONG FunctionEntry;
&nbsp; &nbsp; };
//
// If (Flags & UNW_FLAG_EHANDLER)
//
&nbsp; &nbsp; OPTIONAL ULONG ExceptionData[];
} UNWIND_INFO, *PUNWIND_INFO;

为了快速讲清楚重点,这里我们只讨论Flags设置为UNW_FLAG_EHANDLER的情况。

一个典型的Flags被设置为UNW_FLAG_EHANDLER的函数如下:

void&nbsp;ExceptFunc() { __try {}&nbsp;__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&nbsp;struct&nbsp;_C_SCOPE_TABLE_ENTRY&nbsp;{
&nbsp; ULONG Begin; &nbsp; &nbsp; &nbsp; &nbsp;// +0x00 - Begin of guarded code block,__try块第一条指令的偏移量
&nbsp; ULONG End; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// +0x04 - End of target code block,__try块内最后一条指令之后指令的偏移量
&nbsp; ULONG Handler; &nbsp; &nbsp; &nbsp;// +0x08 - Exception filter function (or “__finally” handler)
&nbsp; ULONG Target; &nbsp; &nbsp; &nbsp;&nbsp;// +0x0C - Exception handler pointer (the code inside __except block)
} C_SCOPE_TABLE_ENTRY, *PC_SCOPE_TABLE_ENTRY;

// C Scope table
typedef&nbsp;struct&nbsp;_C_SCOPE_TABLE&nbsp;{
&nbsp; ULONG NumEntries; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// +0x00 - Number of entries
&nbsp; C_SCOPE_TABLE_ENTRY Table[1]; &nbsp;&nbsp;// +0x04 - Scope table array
} C_SCOPE_TABLE, *PC_SCOPE_TABLE;

RUNTIME_FUNCTION描述了包含SEH的函数的整个范围,而SCOPE_TABLE描述了函数内每个单独的__try/__except块。

整体展示

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

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

<00> mov &nbsp; &nbsp;&nbsp;[rsp+0x8],rcx
<05> sub &nbsp; &nbsp; rsp,0x28
<09> mov &nbsp; &nbsp; rax,[rsp+0x30]// Move UserAddress into RAX
<0e> mov &nbsp; &nbsp; byte ptr&nbsp;[rax],0x0 &nbsp; &nbsp;// *UserAddress = 0;
<11> mov &nbsp; &nbsp; rax,[rsp+0x30]// Move UserAddress into RAX
<16> mov &nbsp; &nbsp; byte ptr&nbsp;[rax],0x1 &nbsp; &nbsp;// *UserAddress = 1;
<19> jmp &nbsp; &nbsp; FrobThePointer+0x28 &nbsp;&nbsp;// Success!
<1b> lea &nbsp; &nbsp; rcx,"Bad&nbsp;Address\n" &nbsp;&nbsp;// Begin of code in except block...
// &nbsp;prepare to DbgPrint
<22> call &nbsp; &nbsp;DbgPrint
<27> nop
<28> add &nbsp; &nbsp; 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] &nbsp;; 假设 [rcx]=0,触发&nbsp;#DE

2. CPU 硬件操作

  • CPU 检测到除零,中断号 0 (#DE)被触发。

  • CPU 自动切换到内核模式(通过预设的 TSS 或 MSR 加载GS基址)。

  • CPU 将关键的用户态上下文压入当前线程的内核栈,形成一个KTRAP_FRAME结构。这包括:

  • RIP:指向idiv的下一条指令。

  • RSP:用户态栈指针。

  • RFLAGS、CS、SS等段寄存器。

  • 通用寄存器RAXRCXRDX等。

  • CPU 根据IDT(中断描述符表)的条目 0,跳转到预设的中断处理程序入口。在 Windows 中,这是KiDivideErrorFault(或类似的陷阱处理程序)。

3. 内核陷阱处理 (KiDivideErrorFault)

// 伪代码,位于 ntoskrnl.exe
VOID&nbsp;KiDivideErrorFault()&nbsp;{
// 1. 建立更完整的陷阱帧
&nbsp; &nbsp; KTRAP_FRAME* pTrapFrame = GetCurrentTrapFrame();

// 2. 将异常信息打包为 EXCEPTION_RECORD
&nbsp; &nbsp; EXCEPTION_RECORD ExceptionRecord = {0};
&nbsp; &nbsp; ExceptionRecord.ExceptionCode = STATUS_INTEGER_DIVIDE_BY_ZERO;&nbsp;// 0xC0000094
&nbsp; &nbsp; ExceptionRecord.ExceptionAddress = pTrapFrame->Rip;
&nbsp; &nbsp; ExceptionRecord.NumberParameters =&nbsp;0;

// 3. 调用公共的异常分发例程
&nbsp; &nbsp; KiExceptionDispatch(&ExceptionRecord, pTrapFrame);
}

第二阶段:内核异常分发 (KiExceptionDispatch/KiDispatchException)

这是最核心的调度器。其内部逻辑复杂,但关键步骤如下:

1. 构建CONTEXTEXCEPTION_POINTERS

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

EXCEPTION_POINTERSExceptionPointers&nbsp;=&nbsp;{0};
ExceptionPointers.ExceptionRecord = &ExceptionRecord;
ExceptionPointers.ContextRecord = &ContextRecord;

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

BOOLEAN RtlDispatchException(PEXCEPTION_RECORD pExceptionRecord, PCONTEXT pContextRecord) {
// 【关键点1】检查 VEH(向量化异常处理程序)
if&nbsp;(RtlCallVectoredExceptionHandlers(pExceptionRecord, pContextRecord)) {
return&nbsp;TRUE;&nbsp;// VEH 处理了异常
&nbsp; &nbsp; }

// 【关键点2】定位 RUNTIME_FUNCTION
&nbsp; &nbsp; PRUNTIME_FUNCTION pRuntimeFunction = RtlLookupFunctionEntry(
&nbsp; &nbsp; &nbsp; &nbsp; pContextRecord->Rip, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 控制点
&nbsp; &nbsp; &nbsp; &nbsp; &ImageBase, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 输出:模块基址
NULL&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 历史表
&nbsp; &nbsp; );

if&nbsp;(pRuntimeFunction ==&nbsp;NULL) {
// 没有函数表,通常是 JIT 代码,无法展开
return&nbsp;FALSE;
&nbsp; &nbsp; }

// 【关键点3】获取 UNWIND_INFO
&nbsp; &nbsp; PUNWIND_INFO pUnwindInfo = (PUNWIND_INFO)(ImageBase + pRuntimeFunction->UnwindInfoAddress);

// 【关键点4】调用语言特定的异常处理程序(例如 __C_specific_handler)
if&nbsp;(pUnwindInfo->Flags & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER)) {
// 计算 ExceptionData 地址
&nbsp; &nbsp; &nbsp; &nbsp; PVOID pHandlerData = RtlpGetHandlerData(pUnwindInfo);

// 这个函数是 MSVC 运行时提供的,负责解析 SCOPE_RECORD
&nbsp; &nbsp; &nbsp; &nbsp; EXCEPTION_DISPOSITION disposition = __C_specific_handler(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pExceptionRecord,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (PVOID)pContextRecord->Rip,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pContextRecord,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pHandlerData
&nbsp; &nbsp; &nbsp; &nbsp; );

if&nbsp;(disposition == ExceptionExecuteHandler) {
// 找到了处理器,准备展开
return&nbsp;TRUE;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

// 【关键点5】如果没有处理器,尝试展开一帧(用于 C++ 异常传播)
&nbsp; &nbsp; RtlUnwindEx(...);
return&nbsp;FALSE;
}

3.RtlLookupFunctionEntry的查找过程

这个函数是第一阶段查找的具体实现。

PRUNTIME_FUNCTION&nbsp;RtlLookupFunctionEntry(
&nbsp; &nbsp; IN ULONG64 ControlPc,
&nbsp; &nbsp; OUT PULONG64 ImageBase,
&nbsp; &nbsp; IN OUT PT_RUNTIME_FUNCTION* HistoryTable
)&nbsp;{
// 1. 通过 ControlPc 找到所属的 PE 模块 (DLL/EXE)
PLDR_DATA_TABLE_ENTRYpModule&nbsp;=&nbsp;LdrFindEntryForAddress(ControlPc);
&nbsp; &nbsp; *ImageBase = pModule->DllBase;

// 2. 从 PE 头定位 .pdata 节
PIMAGE_NT_HEADERSpNtHeaders&nbsp;=&nbsp;(PIMAGE_NT_HEADERS)(*ImageBase + ((PIMAGE_DOS_HEADER)*ImageBase)->e_lfanew);
PIMAGE_SECTION_HEADERpSection&nbsp;=&nbsp;IMAGE_FIRST_SECTION(pNtHeaders);
for&nbsp;(WORDi&nbsp;=0; i < pNtHeaders->FileHeader.NumberOfSections; i++, pSection++) {
if&nbsp;(memcmp(pSection->Name,&nbsp;".pdata",&nbsp;6) ==&nbsp;0) {
break;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

// 3. 计算 RUNTIME_FUNCTION 数组的起始和结束
PRUNTIME_FUNCTIONpFunctionTable&nbsp;=&nbsp;(PRUNTIME_FUNCTION)(*ImageBase + pSection->VirtualAddress);
ULONGNumberOfFunctions&nbsp;=&nbsp;pSection->Misc.VirtualSize / sizeof(RUNTIME_FUNCTION);

// 4. 【核心】二分查找
return&nbsp;RtlpBinarySearch(pFunctionTable, ControlPc - *ImageBase, NumberOfFunctions);
}

4.__C_specific_handler的工作(用户态回调!)

这是 MSVC 运行时库 (vcruntimexxx.dll) 提供的函数。注意:此时 CPU 仍在内核模式,但该函数是用户态代码,内核会临时切换到用户态执行它。

EXCEPTION_DISPOSITION&nbsp;__C_specific_handler(
&nbsp; &nbsp; PEXCEPTION_RECORD pExceptionRecord,
&nbsp; &nbsp; PVOID EstablisherFrame, &nbsp;// 实际上是发生异常的函数栈帧指针
&nbsp; &nbsp; PCONTEXT pContextRecord,
&nbsp; &nbsp; PEXCEPTION_REGISTRATION_RECORD pDispatcherContext
)&nbsp;{
// 1. 从 DispatcherContext 中提取 SCOPE_RECORD 数组
PSCOPE_RECORDpScopeRecords&nbsp;=&nbsp;(PSCOPE_RECORD)pDispatcherContext;
DWORDscopeCount&nbsp;=&nbsp;pScopeRecords->Count;
&nbsp; &nbsp; pScopeRecords++;&nbsp;// 指向第一个 SCOPE_RECORD

// 2. 计算异常在函数内的偏移
DWORD_PTRImageBase&nbsp;=&nbsp;...;
PRUNTIME_FUNCTIONpRF&nbsp;=&nbsp;...;
DWORDoffsetInFunction&nbsp;=&nbsp;(DWORD)(pContextRecord->Rip - ImageBase - pRF->BeginAddress);

// 3. 线性遍历 SCOPE_RECORD
for&nbsp;(DWORDi&nbsp;=0; i < scopeCount; i++) {
if&nbsp;(offsetInFunction >= pScopeRecords[i].BeginAddress &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; offsetInFunction < pScopeRecords[i].EndAddress) {
// 4. 找到匹配的 try 块!调用过滤器
if&nbsp;(pScopeRecords[i].HandlerAddress) {
intfilterResult&nbsp;=&nbsp;((FILTER_FUNC)pScopeRecords[i].HandlerAddress)();
if&nbsp;(filterResult == EXCEPTION_EXECUTE_HANDLER) {
// 5. 设置要跳转的目标地址
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pDispatcherContext->TargetIp = ImageBase + pRF->BeginAddress + pScopeRecords[i].JumpTarget;
return&nbsp;ExceptionExecuteHandler;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
return&nbsp;ExceptionContinueSearch;
}

第三阶段:栈展开与最终派发

1. 内核收到ExceptionExecuteHandler后的操作

内核的KiDispatchException收到RtlDispatchException返回TRUE,知道找到了处理程序。

// 在 KiDispatchException 中
if&nbsp;(FirstPassSuccess) {&nbsp;// 即 RtlDispatchException 返回 TRUE
// 1. 获取目标地址(由 __C_specific_handler 设置)
ULONG64TargetIp&nbsp;=&nbsp;GetTargetIpFromDispatcherContext();

// 2. 执行栈展开
&nbsp; &nbsp; RtlUnwindEx(
&nbsp; &nbsp; &nbsp; &nbsp; TargetFrame, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 展开到哪个帧
&nbsp; &nbsp; &nbsp; &nbsp; TargetIp, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 要跳转的目标地址
&nbsp; &nbsp; &nbsp; &nbsp; pExceptionRecord,
&nbsp; &nbsp; &nbsp; &nbsp; ReturnValue, &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 返回值
&nbsp; &nbsp; &nbsp; &nbsp; pContextRecord,
&nbsp; &nbsp; &nbsp; &nbsp; HistoryTable
&nbsp; &nbsp; );

// 3. 修改陷阱帧中的 RIP
&nbsp; &nbsp; pTrapFrame->Rip = TargetIp;

// 4. 【关键】恢复执行
&nbsp; &nbsp; KiContinuePreviousMode(pContextRecord, pTrapFrame, PreviousMode);
}

2.RtlUnwindEx的展开过程

这是实际的栈帧展开器,它解释执行UNWIND_CODE数组。

VOID&nbsp;RtlUnwindEx(...) {
// 循环展开每一帧,直到到达目标帧
while&nbsp;(CurrentFrame < TargetFrame) {
// 对当前帧,查找其 RUNTIME_FUNCTION
&nbsp; &nbsp; &nbsp; &nbsp; PRUNTIME_FUNCTION pRF =&nbsp;RtlLookupFunctionEntry(CurrentRip, &ImageBase, NULL);
&nbsp; &nbsp; &nbsp; &nbsp; PUNWIND_INFO pUnwindInfo = (PUNWIND_INFO)(ImageBase + pRF->UnwindInfoAddress);

// 解释执行 UNWIND_CODE
for&nbsp;(int i =&nbsp;0; i < pUnwindInfo->CountOfCodes; i++) {
switch&nbsp;(pUnwindInfo->UnwindCode[i].UnwindOp) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case UWOP_PUSH_NONVOL:
// 模拟 pop 操作,恢复寄存器
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Context->Rsp +=&nbsp;8;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Context->Rbx = *(PULONG64)(Context->Rsp -&nbsp;8);
break;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case UWOP_ALLOC_SMALL:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Context->Rsp += (pUnwindInfo->UnwindCode[i].OpInfo *&nbsp;8) +&nbsp;8;
break;
// ... 其他展开操作码
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }

// 如果有终止处理器(__finally),现在调用它
if&nbsp;(pUnwindInfo->Flags & UNW_FLAG_UHANDLER) {
CallTerminationHandler(pUnwindInfo, Context);
&nbsp; &nbsp; &nbsp; &nbsp; }

// 移动到调用者帧
&nbsp; &nbsp; &nbsp; &nbsp; CurrentRip = *(PULONG64)Context->Rsp;
&nbsp; &nbsp; &nbsp; &nbsp; Context->Rsp +=&nbsp;8;
&nbsp; &nbsp; }
}

第四阶段:返回用户态,执行处理块

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&nbsp;// Windows Kernel Internal (KI) API
->&nbsp;RtlDispatchException&nbsp;// main logic for exception handling
->&nbsp;RtlpCallVectoredHandlers&nbsp;// call any VEH
->&nbsp;RtlLookupFunctionEntry&nbsp;// look for valid PRUNTIME_FUNCTION entry in ExceptionDirectory
->&nbsp;RtlpLookupDynamicFunctionEntry&nbsp;// if no valid PRUNTIME_FUNCTION, run any dynamic callbacks
->&nbsp;RtlVirtualUnwind / RtlpxVirtualUnwind&nbsp;// perform stack frame unwinding
->&nbsp;RtlpExecuteHandlerForException&nbsp;// 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&nbsp;union&nbsp;_UNWIND_CODE&nbsp;{
struct&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; UBYTE CodeOffset; &nbsp;// 距离序言开始的偏移量(以字节为单位)
&nbsp; &nbsp; &nbsp; &nbsp; UBYTE UnwindOp:4; &nbsp;// 展开操作码
&nbsp; &nbsp; &nbsp; &nbsp; UBYTE OpInfo:4; &nbsp; &nbsp;// 操作码附加信息
&nbsp; &nbsp; };
&nbsp; &nbsp; USHORT FrameOffset; &nbsp; &nbsp;// 当 UnwindOp 为某些值时,整个USHORT表示帧偏移
} UNWIND_CODE, *PUNWIND_CODE;

字段含义

  • CodeOffset:这是最关键、最精妙的设计。它表示本展开操作应该在函数序言的哪个“进度点”之后执行。系统通过比较异常地址(RIP)与函数起始地址的偏移量,来决定执行UNWIND_CODE数组中的哪些指令。这确保了无论异常发生在序言后、函数体中还是尾声前,栈都能被正确恢复。
  • UnwindOp:展开操作码。定义了具体的恢复操作(如pop寄存器、增加RSP等)。
  • OpInfo:操作信息,通常是寄存器编号或大小参数。

UNWIND_CODE在异常处理流程中的具体工作

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

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

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

UNWIND_CODE UnwindCode[] = {
&nbsp; &nbsp; {CodeOffset:&nbsp;0x0, UnwindOp: UWOP_PUSH_NONVOL, OpInfo: RBX},&nbsp;//&nbsp;对应&nbsp;push&nbsp;rbx
&nbsp; &nbsp; {CodeOffset:&nbsp;0x1, UnwindOp: UWOP_PUSH_NONVOL, OpInfo: RSI},&nbsp;//&nbsp;对应&nbsp;push&nbsp;rsi
&nbsp; &nbsp; {CodeOffset:&nbsp;0x2, UnwindOp: UWOP_ALLOC_SMALL, OpInfo:&nbsp;4} &nbsp; &nbsp;// 对应&nbsp;sub&nbsp;rsp,0x20&nbsp;(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”操作是逻辑上的。实际过程是:

  • 从当前RSPCodeOffset计算原始值在栈上的位置。
  • 将该值读入CONTEXT结构中的对应寄存器字段。
  • 更新CONTEXT中的RSP值,模拟栈指针移动。

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

#

#

看雪ID:mb_nnqgphwf

https://bbs.kanxue.com/user-home-931393.htm

*本文为看雪论坛优秀文章,由 mb_nnqgphwf 原创,转载请注明来自看雪社区

往期推荐

安卓逆向基础知识之frida Hook

2025 强网杯和强网拟态部分题解

在逆向分析方面-unidbg真的适合 MCP 吗?

AI静态分析,内核模块隐藏 Frida 特征,绕过linker私有结构遍历崩溃链

某安全so库深度解析

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 mbnnqgphwf mbnnqgphwf《浅析64位Windows的SEH机制》

评论:0   参与:  0