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文件中直接安装的而已,是静态的。当然可以手动修改。

image

可以看到其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");
    }
}

image

image

我们看到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; // r15@1
	SCOPE_TABLE *local_ScopeTable; // rbx@1
	unsigned __int64 ControlPcRVA; // rbp@1

	unsigned __int64 TargetIp_SUB_ImageBase; // rsi@13
	signed __int64 v16; // rcx@14
	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;
}
  
     下一篇介绍剩下的内容
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当您想在VS Code中使用mingw-w64作为C/C++编译器时,您需要按照以下步骤进行安装和配置: 步骤1:安装mingw-w64 1. 访问mingw-w64官方网站:https://mingw-w64.org/doku.php/download,并选择适合您操作系统的安装程序。 2. 下载适用于您的操作系统的安装程序,并运行它。 3. 在安装向导中选择合适的选项,例如选择目标架构(x86或x64)和线程模型(posix或win32)等。 4. 选择安装目录并开始安装。 5. 完成安装后,确保您知道mingw-w64的安装路径。 步骤2:配置VS Code 1. 打开VS Code,并安装C/C++扩展。可以通过扩展视图(Ctrl+Shift+X)搜索"C/C++"并进行安装。 2. 安装完成后,打开设置(File -> Preferences -> Settings)。 3. 在设置中搜索"C++"以找到相关的C/C++配置选项。 4. 找到"C++: Intelli Sense Engine"选项,并将其设置为"Tag Parser"。 5. 找到"C++: Default"配置选项,并单击"Edit in settings.json"链接。 6. 在"settings.json"文件中,找到"includePath"属性,并添加mingw-w64的安装路径。示例:"C:\\mingw-w64\\x86_64-8.1.0-posix-seh-rt_v6-rev0\\mingw64\\lib\\gcc\\x86_64-w64-mingw32\\8.1.0\\include"。 7. 保存"settings.json"文件并关闭。 步骤3:验证配置 1. 创建一个新的C/C++项目或打开一个现有的项目。 2. 在代码文件中编写一些C/C++代码。 3. 按Ctrl+Shift+B(或选择"Terminal -> Run Build Task")来构建代码。 4. 如果一切顺利,您应该能够成功编译和运行C/C++代码。 这就是在VS Code中安装和配置mingw-w64的简单教程。希望对您有帮助!如果您有任何其他问题,请随时问我。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值