异常处理原理及栈展开相关操作
windows系统的异常处理方式
windows的异常处理方式在逆向工程核心原理那本书里已经了解过了,这里再写一遍c代码如何实现异常处理,加深对之前汇编代码实现异常处理的理解。简而言之,windows的异常处理就是调用windows的API来实现的
windows处理异常的相关结构体:
_EXCEPTION_RECORD结构体:
操作系统用于记录异常信息的结构体
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; // 异常编号,记录是哪种异常
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord; // 嵌套的异常
PVOID ExceptionAddress; // 导致异常的指令地址
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 异常的详细信息
} EXCEPTION_RECORD;
NT_TIB结构体:
FS寄存器里面放的就是这个结构体的地址
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; //fs:[0]
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
#if defined(_MSC_EXTENSIONS)
union {
PVOID FiberData;
DWORD Version;
};
#else
PVOID FiberData;
#endif
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;
typedef NT_TIB *PNT_TIB;
_EXCEPTION_REGISTRATION_RECORD结构体:
位于SHE链上的结构体
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next; //ext成员指向下一个_EXCEPTION_REGISTRATION_RECORD结构体指针
PEXCEPTION_ROUTINE Handler; //handler成员是异常处理函数
} EXCEPTION_REGISTRATION_RECORD;
//若Next成员的值为FFFFFFFF,则表示它是链表最后一个结点
异常处理函数的结构
c语言在编写异常处理时的函数声明
EXCEPTION_DISPOSITION __cdecl _except_handler (
EXCEPTION_RECORD *pRecord, // 上面有写到,操作系统记录异常信息的结构体
EXCEPTION_REGISTRATION_RECORD *pFrame, // 上面同样也有,SHE链上的结构体
CONTEXT *pContext, // 线程上下文,用来备份cpu寄存器的值
PVOID pValue
);
windows系统API异常处理简单的代码实现:
#include <iostream>
#include <windows.h>
using namespace std;
int const ConstantZero = 0;
EXCEPTION_DISPOSITION MyExceptionHandler(
EXCEPTION_RECORD* ExceptionRecord,
EXCEPTION_REGISTRATION_RECORD* EstablisherFrame,
CONTEXT* pContext,
PVOID pValue
)
{
printf(
"An exception occurred at address 0x%p, with ExceptionCode = 0x%08x!\n",
ExceptionRecord->ExceptionAddress,
ExceptionRecord->ExceptionCode
);
return ExceptionContinueSearch; // 继续处理异常
}
int main() {
NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();
_EXCEPTION_REGISTRATION_RECORD Registration;
Registration.Handler = (PEXCEPTION_ROUTINE)&MyExceptionHandler;
Registration.Next = &Registration;
printf("my word:%d\n",ConstantZero);
const_cast<int&>(ConstantZero) = 1;
printf("new word:%d\n",ConstantZero);
TIB->ExceptionList = TIB->ExceptionList->Next;
return 0;
}
VS提供的代码异常处理
**try/**except 和 __try/finally 语句
注意:不是标准c++的try语句
异常处理常量:
- EXCEPTION_CONTINUE_EXECUTION 允许异常处的指令再次执行
- EXCEPTION_CONTINUE_SEARCH 继续寻找异常处理该异常的处理器
- EXCEPTION_EXECUTE_HANDLER 返回这个值堆栈将展开到这(如果发生异常,会跳转到__except下面的大括号里执行**)**
以下是代码示例
void MyWonderfulProgram()
{
__try
{
}
__except( MyExceptionFilter() )
{
}
}
MyExceptionFilter() 异常评估器(异常过滤器),在这个函数里面也可以处理异常,返回一个异常代码如ExceptionContinueSearch,当然如果返回的是ExceptionContinueSearch着表示__except下面的代码块中会无条件的处理异常,下面的块就没有任何用处。
只能在__except内部能够使用的函数
异常信息记录结构体
typedef struct _EXCEPTION_POINTERS
{ PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS, *LPEXCEPTION_POINTERS;
返回该结构体的函数GetExceptionInformation
EXCEPTION_POINTERS* GetExceptionInformation();
返回异常代码的函数GetExceptionCode
DWORD GetExceptionCode()
VS提供的代码异常处理对上面代码的实现:
#include <iostream>
#include <Windows.h>
#include <excpt.h>
using namespace std;
int const ConstantZero = 0;
int MyExceptionFilter(_EXCEPTION_POINTERS* Pointers) {
if(Pointers->ExceptionRecord->ExceptionCode != STATUS_ACCESS_VIOLATION||
Pointers->ExceptionRecord->ExceptionInformation[0] != EXCEPTION_WRITE_FAULT)
{
return EXCEPTION_CONTINUE_SEARCH;
}
void* WriteAddress = (void*)Pointers->ExceptionRecord->ExceptionInformation[1];
if (!VirtualProtect(WriteAddress, sizeof(int), PAGE_READWRITE)) {
return EXCEPTION_CONTINUE_SEARCH;
}
return EXCEPTION_CONTINUE_EXECUTION;
}
int main() {
printf("my word:%d\n", ConstantZero);
__try
{
const_cast<int&>(ConstantZero) = 1;
}
__except ( MyExceptionFilter(GetExceptionInformation()) )
{
}
printf("new word:%d\n", ConstantZero);
return 0;
}
****try/fially块:
在运行完**try的块后,**finally都将执行。
通过这个块可以自定义在栈展开期间发生的操作。(栈展开将在下面介绍)
如下:
保证了资源在即使出现了异常的情况下也能正常解锁。
返回EXCEPTION_EXECUTE_HANDLER进行栈展开
也就是在运行程序会跳转到__except下面的代码块中执行。
当多级函数嵌套的时候,如果在很深层的函数内部发生异常,则其工作会很复杂,下面就对其工作原理进行了解,这也是本篇博客要重点写的部分。
实现原理:
当有多个__try块异常代码嵌套的时候,如图
编译器会将每个块都分一个状态等级,越往里越大,越往下也越大。
ScopeTable数组:
构建一个ScopeTable数组来表示这些状态,比如这里这个数组中的数据有三个,-1不用
当发生异常的时候,只需要直接冲表中查找对应的处理方式就行
struct SCOPETABLE_ENTRY
{
int32_t EnclosingLevel; // 状态等级
FILTER_CALLBACK* Filter; // 指向异常过滤器的地址,如果是finally没有位nullptr
HANDLER_CALLBACK* Handler; // 指向except/finally的块地址
}
C_EXCEPTION_REGISTARATION_RECORD结构体:
编译器用来记录异常的注册情况(比我们注册异常时用的SHE链的结构体更详细)
struct C_EXCEPTION_REGISTARATION_RECORD
{
void* StackPointer;// 记录栈帧,便于在发生异常时定位栈位置
EXCEPTION_POINTERS* Exception;// 包含线程上下文以及操作系统记录异常信息的结构体
EXCEPTION_REGISTRATION_RECORD HandlerRegistration;//异常处理结构体SHE链上的那个
SCOPETABLE_ENTRY* ScopeTable;//上面创建的指针
int TryLevel; // 当前状态等级
}
**try/**except 和 __try/finall的实现
EXCEPTION_DISPOSITION _except_handler3(
EXCEPTION_RECORD* ExceptionRecord,
EXCEPTION_REGISTRATION_RECORD* EstablisherFrame,
CONTEXT* ContextRecord,
void* DispatcherContext
)
{
C_EXCEPTION_REGISTARATION_RECORD* RN = RNFromEstablisherFrame(EstablisherFrame);
EXCEPTION_POINTERS tmp{ ExceptionRecord,ContextRecord };
RN->ExceptionPointers = &tmp;
if ((ExceptionRecord->ExceptionFlags & EXCEPTION_UNWINDING) == 0) {
for (int I = RN->TryLevel; I != 1; I = RN->ScopeTable[I].EnclosingLevel) {
if (RN->ScopeTable[I].Filter == nullptr) { continue; }
int FilterResult = RN->ScopeTable[I].Filter();
switch (FilterResult)
{
case EXCEPTION_CONTINUE_SEARCH: continue;
case EXCEPTION_CONTINUE_EXECUTION: return ExceptionContinueExecution;
case EXCEPTION_EXECUTE_HANDLER:
RtlUnwind(EstablisherFrame, ExceptionRecord);// 这一步是从内向外找过滤器来处理异常
_local_unwind(RN, RN->TryLevel);// 将内部全部finally运行结束
RN->ScopeTable[I].Handler();// 运行目标__except /__finall的块
assert(false);
}
}
}
else {
_local_unwind(RN, -1);
}
}
栈展开:
- 全局展开(遍历过滤器) :
采用windowsAPI的RtlUnwind函数,其内部实现代码如下
void RtlUnwind(
EXCEPTION_REGISTRATION_RECORD* TargetFrame,
void* TargetTp,
EXCEPTION_RECORD* ExceptionRecord,
void* ReturnValue
)
{
ExceptionRecord->ExceptionFlags |= EXCEPTION_UNWINDING; // 告诉系统这个异常处理正在被展开
NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();
while (TIB->ExceptionList != TargetFrame)
{
TIB->ExceptionList->Handler(ExceptionRecord, TIB->ExceptionList);
TIB->ExceptionList = CurrentRecord->next;// 遍历到目标
}
}
- 局部展开(处理块里的内容):
void _local_unwind(
C_EXCEPTION_REGISTARATION_RECORD* RN,
int stop
)
{
while (RN->TryLevel != stop)
{
SCOPETABLE_ENTRY* CurrentEntry = &RN->ScopeTable[RN->TryLevel];
if (CurrentEntry->Filter == nullptr)
{
CurrentEntry->Handler();// 调用finally的块
}
RN->TryLevel = CurrentEntry->EnclosingLevel;
}
}
对上面的栈展开进行一遍解释
首先RtlUnwind(EstablisherFrame, ExceptionRecord)会从异常触发点开始向外层遍历依次进入过滤器,寻找处理异常的位置,然后调用_local_unwind(RN, RN->TryLevel)运行异常点到处理异常位置之间的所有finally。最后,在调用RN->ScopeTable[I].Handler()类的代码运行之前,会把堆栈更新指针更新,然后释放深层的堆栈内存(改栈指针) ,开始执行except块中的内容。
总结
c++与vs的处理方式极其类似,这里就不赘述了。本来没打算写这文章的,刚开始在研究vm混淆的,结果vm要模拟堆栈,对堆栈有个栈展开操作,所以就先来把这个学了。
参考视频:https://www.bilibili.com/video/BV1UU4y1K7et?spm_id_from=333.337.search-card.all.click