Windows异常世界历险记(四)——VC6中结构化异常处理机制的反汇编分析(中)

继续填坑,本系列的上一篇文章《Windows异常世界历险记(三)——VC6中结构化异常处理机制的反汇编分析(上)》是差不多1年前写的,由于时间间隔相对较长,因此在正式开始前,先简要回顾一下WinMain和RaiseExcept函数中的主要流程。

WinMain中的主要流程

接着上一篇的那个SEH测试程序说,还是从最开始的一段入手分析:

45:       __try
00401255   mov         dword ptr [ebp-4],0
46:       {
47:           __try
0040125C   mov         dword ptr [ebp-4],1
48:           {
49:               RaiseExcept();
00401263   call        @ILT+5(_RaiseExcept) (0040100a)
00401268   mov         dword ptr [ebp-4],0
0040126F   call        $L74596 (00401276)
00401274   jmp         $L74599 (00401294)

由于一来就是两个try块,因此ebp-4这个局部变量处存放的是tryLevel,进入外层的try后该标记被置为0,进入内层try块后该标记被设置为1。接着执行RaiseExcept函数,由于执行完RaiseExcept函数后,内层try块的作用域就结束了,因此紧接着将tryLevel又设置为0了(表明内层try执行结束)。

下面call到外层finally函数中执行(在之前的分析中,我们已经了解了正常情况下finally块的调用是以call和ret形式完成的,因此完全可将其称为finally函数)。finally函数执行完ret后,流程返回到00401274处,jmp到00401294,此时即将出外层try块,因此将tryLevel设置为-1,然后跳到外层try块外即WinMain尾部。

RaiseExcept中的主要流程

理解了WinMain中的代码再看这一段会发现其实很相似,因此此处关于tryLevel的设置不再赘述。直接看函数入口处:

24:   void RaiseExcept()
25:   {
00401100   push        ebp
00401101   mov         ebp,esp
00401103   push        0FFh
00401105   push        offset string "In Inner::finally"+18h (004220c8)
0040110A   push        offset __except_handler3 (00401460)
0040110F   mov         eax,fs:[00000000]
00401115   push        eax
00401116   mov         dword ptr fs:[0],esp
0040111D   add         esp,0FFFFFFB4h
00401120   push        ebx
00401121   push        esi
00401122   push        edi
00401123   mov         dword ptr [ebp-18h],esp

据此可以画出栈上的情况:

地址 内容 备注
ebp-18 当前esp 指向保存有“易挥发”寄存器组值的栈顶
ebp-14 异常信息结构指针 分析except_handler3可知
ebp-10 原fs:[00000000] fs:[0]
ebp-C __except_handler3 和上面一起构成标准的SEH链结构
ebp-8 004220c8 scopetable指针
ebp-4 0xFF tryLevel
ebp 原ebp ebp也被异常处理巧妙地使用了
ebp+4 返回地址

接下来由于两次进入try块分别设置了tryLevel为0和1,然后就到了我们故意制造Access Violation的现场了。按照之前文章《Windows异常世界历险记(一)——Windows系统用户级结构化异常处理机制(SEH)的基础知识和Unwind展开操作》的分析,异常触发后,当用户层的SEH机制开始处理该异常时,首先找到的异常处理函数就应该是__except_handler3。下面的重点就来分析这个函数的行为。

_except_handler3

这部分原计划是把调试和逆向分析过程逐步展现出来,但考虑到这样做以后文章篇幅会大幅度增加,并且整个分析过程其实也没什么特别之处,反倒有些枯燥,因此这里直接给出我逆向后得到的C语言伪码。

需要说明的是,下面大部分的结构和变量名都是我在分析中自己取的,个人认为except_handler3函数本就是用汇编写的。这里给出C伪码仅仅是为了方便理解其工作原理,对这部分感兴趣的朋友建议还是应该自己逆下_except_handler3

#include 

typedef struct VC6_EXCEPTION_INFORMATION
{
	PEXCEPTION_RECORD pExceptRecord;
	PCONTEXT pContext;
}VC6_EXCEPTION_INFORMATION, *PVC6_EXCEPTION_INFORMATION;

typedef struct VC6_SCOPETABLE
{
	DWORD dwOuterLevel;
	PVOID pfnFilter;
	PVOID pfnHandler;
}VC6_SCOPETABLE, *PVC6_SCOPETABLE;

typedef struct VC6_EXCEPTION_REGISTRATION_RECORD
{
	DWORD	dwCurrentESP;						// 保存的ESP值
	PVC6_EXCEPTION_INFORMATION pExceptInfo;		// 异常信息结构指针
	EXCEPTION_REGISTRATION_RECORD pSEHNode;		// 经典的异常链节点
	PVC6_SCOPETABLE	pScopeTable;					// VC的惯用伎俩——表结构
	DWORD	dwTryLevel;							// 当前try的层级
	DWORD	dwEBP;								// 这个其实不是本结构的成员,只是本结构在栈上挨着函数入口处压入的EBP而已
}VC6_EXCEPTION_REGISTRATION_RECORD, *PVC6_EXCEPTION_REGISTRATION_RECORD;

void global_unwind2(const PEXCEPTION_REGISTRATION_RECORD pSEHNode)
{
	RtlUnwind(pSEHNode, NULL, NULL, NULL);
}

void NLG_Notify(int i)
{
	// 这个函数看起来就是记录了下传入·的参数、eax和ebp的值到一个指定位置,暂时不清楚用途
}

// 局部展开(注意与原生SEH的局部展开概念加以区分),以函数为单位,当前的tryLevel为起点,
// 传入的nFinishTryLevel为终点(或达到本函数内最外层的try之外),由内向外执行所有的try-finally中的finally函数
PEXCEPTION_REGISTRATION_RECORD __cdecl local_unwind2(PEXCEPTION_REGISTRATION_RECORD pSEHNode, int nFinishTryLevel)
{
	PVC6_EXCEPTION_REGISTRATION_RECORD pVC6SEHNode = CONTAINING_RECORD(pSEHNode, VC6_EXCEPTION_REGISTRATION_RECORD, pSEHNode);
	PVC6_SCOPETABLE pScopeTable;
	int tryLevel;

	while (TRUE)
	{
		pScopeTable = pVC6SEHNode->pScopeTable;
		tryLevel = pVC6SEHNode->dwTryLevel;
		if (tryLevel == -1 || tryLevel == nFinishTryLevel)
		{
			break;
		}
		pVC6SEHNode->dwTryLevel = pScopeTable[tryLevel].dwOuterLevel;
		if (!pScopeTable[tryLevel].pfnFilter)	// finally型pfnFilter为NULL
		{
			NLG_Notify(257);
			pScopeTable[tryLevel].pfnHandler();	// 执行finally函数
		}
	}
	return pSEHNode;
}

EXCEPTION_DISPOSITION __cdecl _except_handler3(
	PEXCEPTION_RECORD pExceptionRecord, 
	PEXCEPTION_REGISTRATION_RECORD pSEHNode,
	PCONTEXT pContext, 
	void *pDispatchContext)
{
	PVC6_EXCEPTION_REGISTRATION_RECORD pVC6SEHNode;
	int tryLevel;
	PVC6_SCOPETABLE pScopeTable;
	EXCEPTION_DISPOSITION result;
	VC6_EXCEPTION_INFORMATION stVC6ExceptInfo;

	// #define CONTAINING_RECORD(address, type, field) ((type *)((PCHAR)(address) - (ULONG_PTR)(&((type *)0)->field)))
	pVC6SEHNode = CONTAINING_RECORD(pSEHNode, VC6_EXCEPTION_REGISTRATION_RECORD, pSEHNode);

	// 若为退出展开,则调用local_unwind2执行局部展开
	if (pExceptionRecord->ExceptionFlags & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND))
	{
		local_unwind2(pSEHNode, -1);
		result = ExceptionContinueSearch;
	}
	else
	{
		// 构建异常信息结构,并写入栈上对应字段中
		stVC6ExceptInfo.pExceptRecord = pExceptionRecord;
		stVC6ExceptInfo.pContext = pContext;
		pVC6SEHNode->pExceptInfo = &stVC6ExceptInfo;

		tryLevel = pVC6SEHNode->dwTryLevel;
		pScopeTable = pVC6SEHNode->pScopeTable;

		while (tryLevel != -1)
		{
			// 若为try-except型
			if (pScopeTable[tryLevel].pfnFilter)
			{
				// 执行过滤函数,并根据其返回值进行判定
				int nRet = pScopeTable[tryLevel].pfnFilter();
				if (nRet != 0)
				{
					//#define EXCEPTION_EXECUTE_HANDLER       1
					//#define EXCEPTION_CONTINUE_SEARCH       0
					//#define EXCEPTION_CONTINUE_EXECUTION    -1
					if (nRet < 0)
					{
						return ExceptionContinueExecution;
					}
					// 执行全局展开
					global_unwind2(pSEHNode);
					DWORD dwOrgEBP = pVC6SEHNode->dwEBP;
					// 栈基址指针切换
					_asm
					{
						mov ebp, dwOrgEBP
					}
					// 执行局部展开
					local_unwind2(pSEHNode, tryLevel);
					NLG_Notify(1);
					pVC6SEHNode->dwTryLevel = pScopeTable[tryLevel].dwOuterLevel;
					// 调用异常处理函数
					pScopeTable[tryLevel].pfnHandler();
				}
			}
			tryLevel = pScopeTable[tryLevel].dwOuterLevel;
		}
		result = ExceptionContinueSearch;
	}
	return result;
}

为了方便分析和阅读,已在关键之处加入注释。在下一节中,将结合例子的执行流程对该函数执行流程进行分析。

你可能感兴趣的:(VC,异常处理,SEH,except_handler3,反汇编,调试与反汇编)