Windows try-except异常 try-Finally学习

在Windows中使用SEH异常

Windows SEH异常处理,可以用结构体

typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;  下一个异常处理结构体的地址
PEXCEPTION_ROUTINE Handler;  当前异常处理函数的首地址
} EXCEPTION_REGISTRATION_RECORD;

来表示,当我们使用汇编编写代码时,x86下,不论是在用户态还是在内核态,都使用fs:[0]来指向当前的异常处理结构体地址。  x64下应该使用gs段寄存器,此处只讨论x86即使用fs段寄存器。

fs寄存器指向了 NT_TIB

在windbg中使用 dt _NT_TIB

0:000> dt _NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB

偏移0x18指向 NT_TIB首地址,可以看到NT_TIB首地址是EXCEPTION_REGISTRATION_RECORD数据。

在x86 windbg中查看

0:000> dd fs:[0]
0053:00000000 0039f8b8 003a0000 0039c000 00000000
0053:00000010 00001e00 00000000 00418000 00000000
0053:00000020 00000b18 00003364 00000000 006377b8
0053:00000030 00415000 00000000 00000000 00000000
0053:00000040 00000000 00000000 00000000 00000000
0053:00000050 00000000 00000000 00000000 00000000
0053:00000060 00000000 00000000 00000000 00000000
0053:00000070 00000000 00000000 00000000 00000000
0:000> dt _NT_TIB
RaiseInt3!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB
0:000> dd 00418000
00418000 0039f8b8 003a0000 0039c000 00000000
00418010 00001e00 00000000 00418000 00000000
00418020 00000b18 00003364 00000000 006377b8
00418030 00415000 00000000 00000000 00000000
00418040 00000000 00000000 00000000 00000000
00418050 00000000 00000000 00000000 00000000
00418060 00000000 00000000 00000000 00000000
00418070 00000000 00000000 00000000 00000000

可以看到NT_TIB地址为00418000

在汇编中使用SEH时 需要通过如下形式:

push offset  sehHandle   注释:sehHandle为异常处理函数

push dword ptr fs:[0]     注释:next

mov fs:[0],esp     注释:让fs:[0]指向当前栈顶,当前栈顶是_EXCEPTION_REGISTRATION_RECORD结构体。

通过这几条语句可以在栈上建立一个新的SEH结构体,并让FS:[0]指向这个新的SEH结构体。

在栈上去掉这个SEH框架时,通过如下语句:

pop eax   注释:next

mov fs:[0], eax

此处没有考虑平衡堆栈,通过这几条语句从SEH链表中去掉栈上的当前SEH框架。

 

这是在汇编中可以使用此种方式,但是在VC++中如果每一个要插入异常处理的地方都需要程序员手动插入SEH框架,那太过繁琐且容易出错。

VC++编译器为了在编程中支持使用SEH,方便程序员编写代码,使用try except finally关键字来支持程序员编写代码。

__try
{
  //此处编写可能会抛出异常的代码
}
__except (EXCEPTION_CONTINUE_EXECUTION)  
{
  //此处为异常处理函数 

}

上面代码中__except 括号中可以为常量或者函数,或者多个函数,含义为异常过滤函数

__except 括号中的数据可以如下:

#define EXCEPTION_EXECUTE_HANDLER 1   表明执行except下面中括号中的代码 ,即执行异常处理函数,含义为 异常过滤函数返回EXCEPTION_EXECUTE_HANDLER ,那么就执行我下面的异常处理函数
#define EXCEPTION_CONTINUE_SEARCH 0   表明不执行except下面括号中的代码,此次发生的异常当前的异常处理函数处理不了,需要继续往后面搜寻,即使用EXCEPTION_REGISTRATION_RECORD中的next变量继续往后面搜寻。
#define EXCEPTION_CONTINUE_EXECUTION -1  表明不执行except下面括号中的代码,此次发生的异常当前的异常过滤函数已经处理了即修复了异常,可以从异常触发的地方继续往后面执行。

一般情况下异常过滤函数都是一个函数,动态的根据异常类型来选择返回上面三个变量中的某一个。

需要说明的是只有发生了异常才可能执行异常过滤函数,只有异常过滤函数返回EXCEPTION_EXECUTE_HANDLER ,才会执行下面中括号中的异常处理函数。

类似于try except还有一种形式为 try finally,如下

PVOID vv = NULL;
__try
{
  vv = malloc(10);

  __leave;

  printf("Hello");
}
__finally
{
  if (NULL != vv)
  {
    free(vv);
    vv = NULL;
  }
}

finally被称为异常终止处理函数,try except和try fianlly是可以相互嵌套的。

finally的含义是finally中括号中的代码必定会被执行,不论是否发生异常,所以finally更适用于对于那些需要释放资源,释放锁等需求,可以编写更优雅的代码,不需要在每个return前面检查已经申请的资源是否需要释放。

在try中还可以使用__leave可以更优雅的退出try代码块,进入finally前置的一个代码块,在前置代码块中会对某个局部变量进行赋值,用来后续判断是异常进入了finally代码块还是自己主动退出try块进入异常代码块。

在finally中可以使用AbnormalTermination()来判断是由于异常进入了终止处理代码中还是使用了__leave进入了终止处理代码,AbnormalTermination就是使用局部变量来判断的。

 

try的用法介绍完毕,下面介绍为了支持try这些关键字,编译器做了哪些事情?

VC++编译器为了支持try except finally这些关键字,扩展了EXCEPTION_REGISTRATION_RECORD这个结构体的内容。当使用try块嵌套时,一个函数中使用的所有嵌套try只会在栈上保存一个扩展的SEH结构。

包含如下内容:

EXCEPTION_REGISTRATION_RECORD

TryArray  这是自己定义的名字,实际不是这样的,windbg中断调试程序时,一种方式是远程线程DbgUiRemoteBreakin,函数会注册seh链表,当DbgUiRemoteBreakin调用DbgBreakPoint触发int 3时,如果没有调试器,那么注册的seh链表捕获异常并处理,在测试中看到这个值不单纯是TryArray数组地址,使用__SEH_prolog4注册异常链表时,会将这个值与ntdll模块的___security_cookie进行异或再进行保存。(测试与2023/03/01,环境为windows 10 21H2 x64系统中的32位notepad程序 32位ntdll)。

tryLevel   初始为0FFFFFFFEh,表明不在try块中

ebp

在栈上的框架中,sehhandle指向_except_handler4(或者其他的函数,根据当前编译器版本),在vs中是个类似的固定函数,TryArray指向一个数据结构,前面16字节的数据(这是在vs2013中看到的)

下面是数组,数组中包含多项内容,每一项如下:

pre   默认为0xFFFFFFE

filterFunc  异常过滤函数

executeFunction

可以使用VS编写代码来查看更加详细的内容,此处介绍笔者通过学习其他的书籍、视频等理解的内容。

在汇编代码执行时,每一次进入不同的try时,会给tryLevel这个变量赋予一个新的值,作为索引,每一次离开一个try时,又会重新修改这个值。FFFFFFFF

在刚进入此函数前,会将代码中所有的异常过滤、异常处理、try的嵌套内容都保存在一个数组中

在触发异常时,会进入内核态,内核态再进入用户态中的nt!KiUserExceptionDispatcher函数,在调用异常处理函数_except_handler4时,会根据tryLevel来判断当前是哪个try块中的代码出了问题。

根据索引在上面提到的数组中找到对应的项,

如果filterFunc为NULL,那么代表这是try finally块,会继续往上寻找。如果pre为0xFFFFFFE,那么代表不需要再往上寻找了,在本函数中,这已经是最外层的try了,这时继续往上寻找异常过滤函数的话,已经不在此函数范围内了。如果pre不是0xFFFFFFE(如果有一层嵌套,那么pre为0,如果两层嵌套,那么最里面的嵌套pre为1,这是通过VS2013调试出来的),那么代表在当前函数中还有其它的try包含了当前这个try,那么可以让索引-1,找到前面那个索引,然后查看异常过滤函数、异常处理函数等,进行处理。

如果filterFunc为不NULL,那么表明有异常过滤函数,那么执行异常过滤函数,根据返回值判断是否执行异常处理函数,以及是否继续往下执行、继续往上搜索等。

需要注意有如下几点:

1、except括号中哪怕是一个常量,如EXCEPTION_EXECUTE_HANDLER,也会编译成函数形式,如xor eax,eax ret   因为在异常处理函数中会以函数调用的方式调用异常过滤函数

2、finally代码块执行前有个前置代码模块,会设置局部变量tryLevel,表明已经离开了当前try块,如果修改为0FFFFFFFEh表明不在任何try块中,其余为0 1 2等都表明在try块中。

3、如果在finally代码块中调用AbnormalTermination()函数,那么finally前置代码块还会修改一个局部变量,这个局部变量用来表明是正常运行到finally(包括try正常执行完毕和使用__leave关键字),还是通过异常(展开)调用的finally函数。

4、finally代码块中代码为函数形式,是有ret指令的,except下面的中括号代码是不包含ret指令的,如果except后面中括号代码被执行,那么会执行到return语句处。finally前置代码块以call的方式调用finally代码块,TryArray中保存的是finally代码块的地址,不是前置块的地址,_except_handler4函数也是以call的方式调用finally块代码的方式。

5、finally的执行时机有两种:第一种为try调用完成或者try中使用了__leave关键字,如果是try代码调用完成,那么顺序执行finally前置代码执行块,在这个块中call调用finally代码块。如果是__leave关键字,那么会使用jmp等跳转指令跳转到finally前置代码块起始处。

第二种为触发了异常,因为当前是finally代码块,所以只能往上搜寻是否有过滤函数可以处理此异常,假如有过滤函数返回了可以处理,那么在调用异常处理函数前,会有一个叫做unwind展开的过程,分为局部展开和全局展开,不懂,暂时理解为展开,这个展开过程会再次搜寻之前的异常SEH框架,对于那些没有异常过滤函数即finally代码块,会去调用finally代码块来做一些释放资源等程序员希望做的事情。而调用finally时会去访问finally块所在的函数栈帧中的局部变量,如果在执行finally代码时查看寄存器的值,会发现此时esp会出于比较高的地址,即虚拟地址比较小,而ebp与原来执行try块代码时一致,访问局部变量时是使用ebp来进行访问,而操作系统提供的异常处理框架会在unwind以及调用过滤处理函数时还原ebp的值。

 

问题:

1、在一级级往上寻找可以处理异常的代码时,当调用到某一层异常过滤函数,发现找到了可以处理此异常的SEH框架,那么还会再次从触发异常的地方开始遍历,需要进行资源的释放,例如摘掉SEH链表,调用try块对应的finally块。而调用finally代码时需要访问每层函数的局部变量,就需要还原EBP的值。

2、默认的异常过滤函数返回,即继续搜寻,可以调用到finally函数,应该是因为顶层异常了,所以unwind。使用自定义函数返回EXCEPTION_CONTINUE_SEARCH或者EXCEPTION_EXECUTE_HANDLER都可以unwind。使用EXCEPTION_CONTINUE_EXECUTION无法unwind这也正常,从异常处继续执行当然不需要继续unwind了。

 

#define EXCEPTION_NONCONTINUABLE 0x1 // Noncontinuable exception
#define EXCEPTION_UNWINDING 0x2 // Unwind is in progress
#define EXCEPTION_EXIT_UNWIND 0x4 // Exit unwind is in progress
#define EXCEPTION_STACK_INVALID 0x8 // Stack out of limits or unaligned
#define EXCEPTION_NESTED_CALL 0x10 // Nested exception handler call
#define EXCEPTION_TARGET_UNWIND 0x20 // Target unwind in progress
#define EXCEPTION_COLLIDED_UNWIND 0x40 // Collided exception handler call

使用asm插入SEH链表时,在调用SEH handle时有四个参数如:

EXCEPTION_DISPOSITION __cdecl HANDLER1(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT* ContextRecord,
void* DispatcherContext)
{
  printf("flags:%d\n", ExceptionRecord->ExceptionFlags);
  MessageBox(NULL, L"进入异常处理函数\r\n", NULL, NULL);
  return ExceptionContinueSearch;
}

其中ExceptionRecord->ExceptionFlags表明是因为什么进入了这个异常,当unwind时ExceptionRecord->ExceptionFlags为2。

当触发异常时,这个函数会进入两次,第一次ExceptionFlags为0,第二次异常ExceptionFlags为2。

第一次进入异常处理函数的调用栈如下(注意:这是手动插入了SEH框架,没有使用vc++的try eexcept框架):

06 00aff240 0053143f USER32!MessageBoxW+0x45
07 00aff324 774e8b32 TestAsmException!HANDLER1+0x4f [f:\vscode\testasmexception\testasmexception\testasmexception\testasmexception.cpp @ 25]
08 00aff348 774e8b04 ntdll!ExecuteHandler2+0x26
09 00aff414 774d4fa6 ntdll!ExecuteHandler+0x24
0a 00aff414 00531508 ntdll!KiUserExceptionDispatcher+0x26
0b 00affa34 00531496 TestAsmException!fun2+0x28 [f:\vscode\testasmexception\testasmexception\testasmexception\testasmexception.cpp @ 52]
0c 00affb10 00531543 TestAsmException!fun1+0x36 [f:\vscode\testasmexception\testasmexception\testasmexception\testasmexception.cpp @ 44]
0d 00affbe4 00531ae9 TestAsmException!wmain+0x23 [f:\vscode\testasmexception\testasmexception\testasmexception\testasmexception.cpp @ 65]

从ntdll!KiUserExceptionDispatcher调用ntdll!ExecuteHandler,再调用ntdll!ExecuteHandler2,再调用我们注册的异常处理函数。

第二次进入异常处理函数的调用栈如下(注意:这是手动插入了SEH框架,没有使用vc++的try except框架):

06 00afedd0 0053143f USER32!MessageBoxW+0x45
07 00afeeb4 774e8b32 TestAsmException!HANDLER1+0x4f [f:\vscode\testasmexception\testasmexception\testasmexception\testasmexception.cpp @ 25]
08 00afeed8 774e8b04 ntdll!ExecuteHandler2+0x26
09 00aff2b4 774d70d9 ntdll!ExecuteHandler+0x24
0a 00aff2d8 774d6a7d ntdll!_EH4_GlobalUnwind+0x15
0b 00aff304 774dae20 ntdll!_except_handler4_common+0xdd
0c 00aff324 774e8b32 ntdll!_except_handler4+0x20
0d 00aff348 774e8b04 ntdll!ExecuteHandler2+0x26
0e 00aff414 774d4fa6 ntdll!ExecuteHandler+0x24
0f 00aff414 00531508 ntdll!KiUserExceptionDispatcher+0x26
10 00affa34 00531496 TestAsmException!fun2+0x28 [f:\vscode\testasmexception\testasmexception\testasmexception\testasmexception.cpp @ 52]
11 00affb10 00531543 TestAsmException!fun1+0x36 [f:\vscode\testasmexception\testasmexception\testasmexception\testasmexception.cpp @ 44]
12 00affbe4 00531ae9 TestAsmException!wmain+0x23 [f:\vscode\testasmexception\testasmexception\testasmexception\testasmexception.cpp @ 65]

可以看到栈帧0a是 ntdll!_EH4_GlobalUnwind,是ntdll库提供的全局展开的函数,seh框架是在fun1函数中插入的,fun1函数中调用了fun2函数,fun2函数中出现异常。

要查看参数可以使用命令 .frame /c 07 切换到07栈帧上下文环境,使用.cxr可以再切换回来。

当ExceptionRecord中ExceptionFlags为0时,此时ContextRecord变量中eip为异常指令的地址,ebp为触发异常时函数的EBP值,ContextRecord为触发异常时的寄存器环境。

在SEH中异常处理函数会被调用两次,一次是异常处理,一次是unwind。

异常包含的内容相当丰富,还是需要继续挖掘。

 

在windbg中使用命令!exchain 可以遍历FS:[0]链条。

 

 

 

 

在14.30.30705\crt\src\i386\chandler4.c代码中找到如下数据结构体,下次再继续学习。

typedef struct _EH4_EXCEPTION_REGISTRATION_RECORD
{
PVOID SavedESP;
PEXCEPTION_POINTERS ExceptionPointers;
EXCEPTION_REGISTRATION_RECORD SubRecord;
UINT_PTR EncodedScopeTable;
ULONG TryLevel;
} EH4_EXCEPTION_REGISTRATION_RECORD, *PEH4_EXCEPTION_REGISTRATION_RECORD;
typedef LONG(__cdecl *PEXCEPTION_FILTER_X86)(void);   异常过滤函数类型
typedef void(__cdecl *PEXCEPTION_HANDLER_X86)(void);   异常处理函数类型
typedef void(__fastcall *PTERMINATION_HANDLER_X86)(BOOL);   这个BOOL是代表什么含义,第一次第二次还是 局部全局  终止处理函数类型
typedef struct _EH4_SCOPETABLE_RECORD
{
ULONG EnclosingLevel;
PEXCEPTION_FILTER_X86 FilterFunc;
union
{
PEXCEPTION_HANDLER_X86 HandlerAddress;
PTERMINATION_HANDLER_X86 FinallyFunc;
} u;
} EH4_SCOPETABLE_RECORD, *PEH4_SCOPETABLE_RECORD;

 

posted @ 2022-09-30 19:02  psj00  阅读(234)  评论(0)    收藏  举报