SEH X64(2)
上一篇文章,我们介绍X64 SEH 操作所需要的操作,与X86相比,它是静态的并且这些静态信息足够用于展开操作,下面我们来分析展开操作相关的函数。
首先来看异常的分发函数。这里的调用流程其实跟X86 SEH 类似,KiDispatchException--->RtlDispatchException---->RtlpExecuteHadnlerForException。KiDispatchException逻辑类似,我们来看RtlDispatchException中的操作。
该函数整体上就是一直沿着调用栈向上搜索,直到找到一个能够处理该异常的异常处理函数,或者达到栈的边界,或者到了调用栈的最上层。如果其中的某个异常处理函数不处理该异常的话,该函数的prolog 就会被进行虚拟展开的操作(因为PE中记录了足够的信息以满足展开操作的需要)。
其中有几个函数需要了解。
PRUNTIME_FUNCTION
RtlLookupFunctionEntry (
IN ULONG64 ControlPc,
OUT PULONG64 ImageBase,
IN OUT PUNWIND_HISTORY_TABLE HistoryTable OPTIONAL
)
了解该函数需要首先知道结构体UNWIND_HISTORY_TABLE_ENTRY 的含义
#define UNWIND_HISTORY_TABLE_SIZE 12
typedef struct _UNWIND_HISTORY_TABLE_ENTRY {
ULONG64 ImageBase;
PRUNTIME_FUNCTION FunctionEntry;
} UNWIND_HISTORY_TABLE_ENTRY, *PUNWIND_HISTORY_TABLE_ENTRY;
#define UNWIND_HISTORY_TABLE_NONE 0
#define UNWIND_HISTORY_TABLE_GLOBAL 1
#define UNWIND_HISTORY_TABLE_LOCAL 2
typedef struct _UNWIND_HISTORY_TABLE {
ULONG Count;
UCHAR Search;
ULONG64 LowAddress;
ULONG64 HighAddress;
UNWIND_HISTORY_TABLE_ENTRY Entry[UNWIND_HISTORY_TABLE_SIZE];
} UNWIND_HISTORY_TABLE, *PUNWIND_HISTORY_TABLE;
这里RtlDispatchException 函数在调用RtlLookupFunctionEntry的时候传入了HistoryTable但是其成员Search=UNWIND_HISTORY_TABLE_NONE。然后该函数直接调用RtlLookupFunctionTable 得到ControlPc 对应的模块的ImageBase 和 FunctionTable(原理是遍历KLDR_DATA_TABLE_ENTRY结构,其中有该模块对应的ExceptionTable 和 ExceptionTableSize)。然后通过二分查找找到ControlPc 对应的函数的FunctionEntry,如果HistoryTable 结构没有满,然后尝试将此次的查找结果放到里面记录起来(HistoryTable 就是为了避免重复查找而服务的)。
另外,RtlLookupFunctionEntry 的通常流程为当提供HisotryTable
如果HistoryTable 指定的搜索方式为UNWIND_HISTORY_TABLE_GLOBAL,现在全局表RtlpUnwindHistoryTable 开始搜索,然后再在HistoryTable 中搜索。
如果HistoryTable 指定的搜索方式为UNWIND_HISTORY_TABLE_LOCAL,在HistoryTable 中搜索。
否则执行最上面的操作。
在得到了FunctionEntry 之后,RtlDispatchException 函数调用RtlVirtualUnwind 函数如下:
ExceptionRoutine = RtlVirtualUnwind(UNW_FLAG_EHANDLER,
ImageBase,
ControlPc,
FunctionEntry,
& ContextRecord1,
&HandlerData,
&EstablisherFrame,
NULL);
函数原型如下
PEXCEPTION_ROUTINE
RtlVirtualUnwind (
IN ULONG HandlerType,
IN ULONG64 ImageBase,
IN ULONG64 ControlPc,
IN PRUNTIME_FUNCTION FunctionEntry,
IN OUT PCONTEXT ContextRecord,
OUT PVOID *HandlerData,
OUT PULONG64 EstablisherFrame,
IN OUT PKNONVOLATILE_CONTEXT_POINTERS ContextPointers OPTIONAL
)
该函数的功能是,执行虚拟展开操作,然后返回一些信息,参数中表明OUT 的:ContextRecord,HandlerData,EstablisherFrame。
HandlerType
可能的取值:
就是上一篇Flags 的可能取值。
CONTEXT 的结构如下
typedef struct DECLSPEC_ALIGN(16) _CONTEXT {
//
// Register parameter home addresses.
//
// N.B. These fields are for convience - they could be used to extend the
// context record in the future.
//
DWORD64 P1Home;
DWORD64 P2Home;
DWORD64 P3Home;
DWORD64 P4Home;
DWORD64 P5Home;
DWORD64 P6Home;
//
// Control flags.
//
DWORD ContextFlags;
DWORD MxCsr;
//
// Segment Registers and processor flags.
//
WORD SegCs;
WORD SegDs;
WORD SegEs;
WORD SegFs;
WORD SegGs;
WORD SegSs;
DWORD EFlags;
//
// Debug registers
//
DWORD64 Dr0;
DWORD64 Dr1;
DWORD64 Dr2;
DWORD64 Dr3;
DWORD64 Dr6;
DWORD64 Dr7;
//
// Integer registers.
//
DWORD64 Rax;
DWORD64 Rcx;
DWORD64 Rdx;
DWORD64 Rbx;
DWORD64 Rsp;
DWORD64 Rbp;
DWORD64 Rsi;
DWORD64 Rdi;
DWORD64 R8;
DWORD64 R9;
DWORD64 R10;
DWORD64 R11;
DWORD64 R12;
DWORD64 R13;
DWORD64 R14;
DWORD64 R15;
//
// Program counter.
//
DWORD64 Rip;
//
// Floating point state.
//
union {
XMM_SAVE_AREA32 FltSave;
struct {
M128A Header[2];
M128A Legacy[8];
M128A Xmm0;
M128A Xmm1;
M128A Xmm2;
M128A Xmm3;
M128A Xmm4;
M128A Xmm5;
M128A Xmm6;
M128A Xmm7;
M128A Xmm8;
M128A Xmm9;
M128A Xmm10;
M128A Xmm11;
M128A Xmm12;
M128A Xmm13;
M128A Xmm14;
M128A Xmm15;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
//
// Vector registers.
//
M128A VectorRegister[26];
DWORD64 VectorControl;
//
// Special debug control registers.
//
DWORD64 DebugControl;
DWORD64 LastBranchToRip;
DWORD64 LastBranchFromRip;
DWORD64 LastExceptionToRip;
DWORD64 LastExceptionFromRip;
} CONTEXT, *PCONTEXT;
如果该函数没有使用栈帧的话,EstablisherFrame 返回RSP。但是如果controlpc 从prolog 中离开函数,就不是这样了。这种情况下可能不需要建立帧,因为control 还没有真正的进入函数,prolog 在简历之前不会引用栈帧。即,如果尚未建立栈帧,在展开操作中不应该出现保存展开代码。
函数使用了帧指针,且ControlPc 不在Prolog 内,或者unwind_info 包含一个链表,创建者帧就是帧指针的内容。(&ContextRecord->Rax)[UnwindInfo->FrameRegister] – UnwindInfo->FrameOffset*16;
如果函数使用了帧指针,ControlPc 在prolog 内离开函数,必须在展开代码中查找设置帧指针的展开代码,以确定栈指针的内容或者帧指针的内容是否应该用于建立者的帧。这可能并不是真正的建立者帧。在这种情况下,建立者帧可能不需要创建,因为control 并没有真正进入函数,prolog 在建立之前还不使用建立者的帧,即,如果尚未建立该结构,在展开操作期间就不应该出现保存展开代码。
这些假设的正确性基于展开代码的顺序。
如果controlpc 在epilog,模拟执行epilog 剩下的指令,并返回没有异常处理函数。
检查下面的指令
// add rsp, imm8
// or
// add rsp, imm32
// or
// lea rsp, -disp8[fp]
// or
// lea rsp, -disp32[fp]
之后检查对于 整数非易失性寄存器的pop 操作。
检查完之后,如果下面的指令是return 或者 适当的跳转指令的话,当前controlpc 是在epilog 中,模拟执行其剩下的代码即可。否则,对prolog 进行展开操作。
适当的跳转目标地址:
无条件跳转到当前函数的开始或者外面,就相当于一个函数调用。另外,如果跳转到自己的开始部分,被认为是递归调用。
如果确认是epilog 的话,模拟执行其中的add rsp 或者 lea rsp 的操作以及pop 非易失性寄存器的操作,并返回null。
Control 如果从epilog 外面离开该函数的话,展开这个函数还有其它的链接起来的展开信息,这个展开操作是通过调用RtlpUnwindPrologue 函数实现的,该函数处理的就是展开代码并进行prologue 的逆操作。如果是链式的展开信息,该函数还会进行递归操作。如果指定了ContextPointers 参数的话,该函数会将所做的操作记录在该结构体内。该函数内部的实现就是根据UnwindOp进行逆操作,我们来看一个,其中UWOP_SET_FPREG 的操作如下
case UWOP_SET_FPREG:
ContextRecord->Rsp = IntegerRegister[UnwindInfo->FrameRegister];
ContextRecord->Rsp -= UnwindInfo->FrameOffset * 16;
break;
这样我们应该对这个FrameRegister 和 FrameOffset 的理解更清晰一点。另外再看UWOP_PUSH_MACHFRAME
// Push a machine frame on the stack.
//
// The operation information determines whether the machine
// frame contains an error code or not.
//
case UWOP_PUSH_MACHFRAME:
MachineFrame = TRUE;
ReturnAddress = (PULONG64)(ContextRecord->Rsp);
StackAddress = (PULONG64)(ContextRecord->Rsp + (3 * 8));
if (OpInfo != 0) {
ReturnAddress += 1;
StackAddress += 1;
}
ContextRecord->Rip = *ReturnAddress;
ContextRecord->Rsp = *StackAddress;
break;
看代码果然是清晰明了。
如果control 在prolog 之外离开特定的函数,且该函数有与指定类型匹配的处理程序,那么将返回特定于语言的异常处理程序的地址,否则返回NULL。
最后,RtlVirtualUnwind 函数执行后,ContextRecord 中的Rip 就是call 指令之后的Rip,而RSP 也就是call 指令之前的RSP。ExceptionData 赋值给HandlerData,对于微软的编译器,UNWIND_INFO::ExceptionRoutine 一般指向的nt!__C_specific_handler。ExceptionData 就是ControlPc 所在函数的SCOPE_TABLE。
由于ContextRecord->Rip 此时为父函数的地址,然后可以以此找到父函数对应的RUNTIME_FUNCTION,推动整个遍历过程。
找到异常处理函数后,我们来看其执行。通过调用RtlpExecuteHandlerForException 执行。其内部注册了一个异常处理函数RtlpExceptionHandler,用于捕获执行异常处理函数中产生的异常,其返回ExceptionNestedException 或 ExceptionContinueSearch。
至于这个函数的异常处理函数是怎么安装的,它就是pe文件中直接安装的而已,是静态的。当然可以手动修改。
可以看到其flags 为3,即该函数即处理异常也负责展开。
然后我们来看__C_specific_handler函数的处理流程
要理解这个函数,首先应该理解X64SCOPE_TABLE 中SCOPTE_ENTRY的分布。
void SubFunc() {
unsigned int j = 0x12345678;
__try {
j++;
}
__except(EXCEPTION_EXECUTE_HANDLER) {
DbgPrint("TRY_1\r\n");
}
__try {
__try{
*(int *)0 = 0;
}
__except(EXCEPTION_EXECUTE_HANDLER) {
DbgPrint("TRY_3\r\n");
}
}
__except(EXCEPTION_EXECUTE_HANDLER) {
DbgPrint("TRY_2\r\n");
}
}
我们看到SCOPE_TABLE在PE中的存放顺序是首先按照大的块的顺序存放,即首先是第一个try 块。然后是嵌套try 块中内部的块。然后才是外层的try块。SCOPE_TABLE 中的块的存放顺序就是这样的,首先按照__try块的先后位置排序。然后按照先最底层try ,然后向外依次存放的规则。
下面我们来分析MS 提供的异常处理函数的代码。
代码中注释已经很清晰,不再赘述。
__int64 __fastcall _C_specific_handler(PEXCEPTION_RECORD ExceptionRecord, PVOID TargetFrame, PCONTEXT ContextRecord, DISPATCHER_CONTEXT *Dispatcher_Context) { ULONG64 ImageBase; // [email protected] SCOPE_TABLE *local_ScopeTable; // [email protected] unsigned __int64 ControlPcRVA; // [email protected] unsigned __int64 TargetIp_SUB_ImageBase; // [email protected] signed __int64 v16; // [email protected] PEXCEPTION_RECORD v20; // [sp+30h] [bp-38h]@2 _except_validate_context_record(ContextRecord); ImageBase = Dispatcher_Context->ImageBase; local_ScopeTable = (SCOPE_TABLE *)Dispatcher_Context->HandlerData; ControlPcRVA = Dispatcher_Context->ControlPc - ImageBase; if ( ExceptionRecord->ExceptionFlags & 0x66 ) { int Local_ScopeIndex = Dispatcher_Context->ScopeIndex; TargetIp_SUB_ImageBase = Dispatcher_Context->TargetIp - ImageBase; while ( 1 ) { if ( Local_ScopeIndex >= local_ScopeTable->Count ) break; v16 = 2i64 * Local_ScopeIndex; if ( ControlPcRVA >= local_ScopeTable->ScopeRecord[Local_ScopeIndex].BeginAddress && ControlPcRVA < local_ScopeTable->ScopeRecord[Local_ScopeIndex].EndAddress ) { // 只有包含目标位置的try 块对我们来说才是有意义的。 // 另外,根据scope_entry 的分布规律,第一个找到是包含异常代码的最内层的try块 if ( ExceptionRecord->ExceptionFlags & 0x20 ) // 如果当前函数就是展开的目标函数,即最后一个需要展开的函数。 { ULONG ScopeIndexForEnum = 0; if ( local_ScopeTable->Count ) { do { if ( TargetIp_SUB_ImageBase >= local_ScopeTable->ScopeRecord[ScopeIndexForEnum].BeginAddress && TargetIp_SUB_ImageBase < local_ScopeTable->ScopeRecord[ScopeIndexForEnum].EndAddress // 目标地址应该是一个ExceptionHandler 的起始地址,包含此地址的应该是一个更外层的try块(如果有的话) // 否则继续查找,找到遍历完所有的scope_entry,因此此条件一直成立 // 由于Local_ScopeIndex 为ExceptionHandler 所在的try 块的时候,函数将识别出来并返回,因此不会进入这个多重的判断语句并break。这么做应该是为了保证程序的正确性。 && local_ScopeTable->ScopeRecord[ScopeIndexForEnum].JumpTarget == local_ScopeTable->ScopeRecord[Local_ScopeIndex].JumpTarget && local_ScopeTable->ScopeRecord[ScopeIndexForEnum].HandlerAddress == local_ScopeTable->ScopeRecord[Local_ScopeIndex].HandlerAddress ) { break; } ++ScopeIndexForEnum; } while ( ScopeIndexForEnum < local_ScopeTable->Count ); } if ( ScopeIndexForEnum != local_ScopeTable->Count ) return ExceptionContinueSearch; } if ( local_ScopeTable->ScopeRecord[Local_ScopeIndex].JumpTarget ) // 表示当前为 __try/__except 块 { if ( TargetIp_SUB_ImageBase == local_ScopeTable->ScopeRecord[Local_ScopeIndex].JumpTarget ) { // 当前的ExceptHandler块的地址就是将要执行的ExceptHandler的地址,直接返回找到了目标块即可 // 这是本次展开操作的终点 return ExceptionContinueSearch; } } else { // 当前为__try/__finally 块 // 将Dispatcher_Context->ScopeIndex ++ ,越过这个__try/__finally // 在执行此finally 的时候产生异常并被捕获后,展开将继续执行,并越过这个finally块。 Dispatcher_Context->ScopeIndex = Local_ScopeIndex + 1; LOBYTE(v16) = 1; ((void (__fastcall *)(signed __int64, PVOID))(ImageBase + local_ScopeTable->ScopeRecord[Local_ScopeIndex].HandlerAddress))( v16, TargetFrame); } } ++Local_ScopeIndex; } } else { /** * 异常操作相对比较简单,遍历SCOPE_TABLE 中的SCOPE_ENTRY 数组,依次寻找包含目标代码的__try/__except 块,并执行其filter 函数 * 根据filter 函数的返回值 * EXCEPTION_CONTINUE_EXECUTION 直接返回 ExceptionContinueExecution * EXCEPTION_EXECUTE_HANDLER 调用RtlUnwindEx 执行handler 函数 * EXCEPTION_CONTINUE_SEARCH 继续遍历 */ ULONG local_ScopetableIndex = Dispatcher_Context->ScopeIndex; v20 = ExceptionRecord; while ( local_ScopetableIndex < local_ScopeTable->Count ) { if ( ControlPcRVA >= local_ScopeTable->ScopeRecord[local_ScopetableIndex].BeginAddress && ControlPcRVA < local_ScopeTable->ScopeRecord[local_ScopetableIndex].EndAddress && local_ScopeTable->ScopeRecord[local_ScopetableIndex].JumpTarget ) { if ( local_ScopeTable->ScopeRecord[local_ScopetableIndex].HandlerAddress == 1 ) goto LABEL_33; int ReturnValueOfExceptionHandler = ((int (__fastcall *)(PEXCEPTION_RECORD *, PVOID))(ImageBase + local_ScopeTable->ScopeRecord[local_ScopetableIndex].HandlerAddress))( &v20, TargetFrame); if ( ReturnValueOfExceptionHandler < 0 ) // EXCEPTION_CONTINUE_EXECUTION = -1 return ExceptionContinueExecution; if ( ReturnValueOfExceptionHandler > 0 ) // EXCEPTION_EXECUTE_HANDLER = 1 { LABEL_33: NLG_Notify( ImageBase + local_ScopeTable->ScopeRecord[local_ScopetableIndex].JumpTarget, TargetFrame, 1i64); RtlUnwindEx( TargetFrame, (PVOID)(ImageBase + local_ScopeTable->ScopeRecord[local_ScopetableIndex].JumpTarget), ExceptionRecord, (PVOID)ExceptionRecord->ExceptionCode, Dispatcher_Context->ContextRecord, Dispatcher_Context->HistoryTable); //_NLG_Return2(); } // 否则EXCEPTION_CONTINUE_SEARCH = 0 } ++local_ScopetableIndex; } } return ExceptionContinueSearch; }