SEH 进阶(1)

SHE进阶

了解了上一篇的文章之后,我们写一个简单的例子来验证我们的想法,并学习新的知识。

不同的编译器提供的增强版本SHE 可能不同,但是它们都是基于windows 底层SHE 的。我们使用Win10 1703 + VS2010 生成X86 Rlease 程序来验证已经学过的知识,后面使用XP x86 7600 来学习编译器版本的SHE。

编译如下程序

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>

DWORD  scratch;

EXCEPTION_DISPOSITION
__cdecl
except_handler(
    struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,
    struct _CONTEXT *ContextRecord,
    void * DispatcherContext )
{
    unsigned i;

    // Indicate that we made it to our exception handler
    printf( "Hello from an exception handler\n" );

    // Change EAX in the context record so that it points to someplace
    // where we can successfully write
    ContextRecord->Eax = (DWORD)&scratch;

    // Tell the OS to restart the faulting instruction
    return ExceptionContinueExecution;
}

int main()
{
    DWORD handler = (DWORD)except_handler;

    __asm
    {                           // Build EXCEPTION_REGISTRATION record:
        push    handler         // Address of handler function
        push    FS:[0]          // Address of previous handler
        mov     FS:[0],ESP      // Install new EXECEPTION_REGISTRATION
    }

    __asm
    {
        mov     eax,0           // Zero out EAX
        mov     [eax], 1        // Write to EAX to deliberately cause a fault
    }

    printf( "After writing!\n" );

    __asm
    {                           // Remove our EXECEPTION_REGISTRATION record
        mov     eax,[ESP]       // Get pointer to previous record
        mov     FS:[0], EAX     // Install previous record
        add     esp, 8          // Clean our EXECEPTION_REGISTRATION off stack
    }

    return 0;
}

 

当我们使用VS 并添加我们自定义的异常处理程序,将有以下警告,且程序运行崩溃

warning C4733: 内联 asm 分配到“FS:0”: 处理程序未注册为安全处理程序

 

解决方案:

在 Visual Studio 开发环境中设置此链接器选项

1.      打开项目的“属性页”对话框。有关详细信息,请参见设置 Visual C++ 项目属性。

2.      选择 Linker 文件夹。

3.      选择“命令行”属性页。

4.      将该选项/SAFESEH:NO键入“附加选项”框中。



之后我们的程序正确执行并输出如下结果:


#include <windows.h>
#include <stdio.h>

EXCEPTION_DISPOSITION
	__cdecl
	_except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
	void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
	void * DispatcherContext )
{
	printf( "Home Grown handler: Exception Code: %08X Exception Flags %X",
		ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags );

	if ( ExceptionRecord->ExceptionFlags & 1 )
		printf( " EH_NONCONTINUABLE" );
	if ( ExceptionRecord->ExceptionFlags & 2 )
		printf( " EH_UNWINDING" );
	if ( ExceptionRecord->ExceptionFlags & 4 )
		printf( " EH_EXIT_UNWIND" );
	if ( ExceptionRecord->ExceptionFlags & 8 )
		printf( " EH_STACK_INVALID" );
	if ( ExceptionRecord->ExceptionFlags & 0x10 )
		printf( " EH_NESTED_CALL" );

	printf( "\n" );

	// Punt... We don't want to handle this... Let somebody else handle it
	return ExceptionContinueSearch;
}

void HomeGrownFrame( void )
{
	DWORD handler = (DWORD)_except_handler;

	__asm
	{                           // Build EXCEPTION_REGISTRATION record:
		push    handler         // Address of handler function
			push    FS:[0]          // Address of previous handler
		mov     FS:[0],ESP      // Install new EXECEPTION_REGISTRATION
	}

	*(PDWORD)0 = 0;             // Write to address 0 to cause a fault

	printf( "I should never get here!\n" );

	__asm
	{                           // Remove our EXECEPTION_REGISTRATION record
		mov     eax,[ESP]       // Get pointer to previous record
		mov     FS:[0], EAX     // Install previous record
			add     esp, 8          // Clean our EXECEPTION_REGISTRATION off stack
	}
}

int main()
{
	_try {
		HomeGrownFrame(); 
	}
	_except( EXCEPTION_EXECUTE_HANDLER ) {
		printf( "Caught the exception in main()\n" );
	}

	return 0;
}

运行后出现如下结果:



我们看到,except_handler函数被调用了两次,且传入的参数不同。

为什么同一个异常处理函数在这里被调用了两次?

首先我们观察两次调用的调用堆栈。




我们看到其调用堆栈并不相同。

从我们已经知道的知识来讲。

HomeGrownFrame 函数内部安装了一个自己的异常,在main函数中调用该函数之前已经进入一个异常函数中。因此我们可以得到类似于下面的堆栈:

0xFFFFFFFEH

__except_handler4

_try _catch 对应的异常处理函数

HomeGrownFrame 函数内部安装的异常块,except_handler

当该异常出现的时候,except_handler返回ExceptionContinueSearch,然后函数依然向下搜索,直到有异常处理函数声明了可以处理它。但是,为什么这里显示调用了两次except_handler函数。

 

为了搞清楚这个问题,我们要学习编译器提供的SHE 的机制,这里我们用Ring0 学习,其实机制是相同的。

Ring0层的实验

最简单的单个try 块的情况

#include "SEHDemo.h"
void SEHTest() {
	int i = 0;
	__try
	{
		i++;
	}
	__except(EXCEPTION_EXECUTE_HANDLER)
	{
		DbgPrint("Hello\r\n");
	}
}
NTSTATUS
	DriverEntry(IN PDRIVER_OBJECT pDriverObj, IN PUNICODE_STRING pRegistryString)
{
	SEHTest();
	return STATUS_SUCCESS;
}

代码逆向如下

.text:00011010 ; _DWORD __stdcall SEHTest()
.text:00011010 _SEHTest@0      proc near               ; CODE XREF: DriverEntry(x,x)+5 p
.text:00011010
.text:00011010 i               = dword ptr -1Ch
.text:00011010 ms_exc          = CPPEH_RECORD ptr -18h

对应结构体如下

struct EH3_EXCEPTION_REGISTRATION
{
	DWORD	Next;
	DWORD	ExceptionHandler;
	DWORD	ScopeTable;
	DWORD	TryLevel;
};
struct CPPEH_RECORD
{
	DWORD	OleEsp;
	DWORD	ExcPtr;
	EH3_EXCEPTION_REGISTRATION registration;
};

stru_12108 对应的结构体
.rdata:00012108 stru_12108      dd 0FFFFFFFEh           ; GSCookieOffset
.rdata:00012108                                         ; DATA XREF: SEHTest()+7 o
.rdata:00012108                 dd 0                    ; GSCookieXOROffset ; SEH scope table for function 11010
.rdata:00012108                 dd 0FFFFFFD4h           ; EHCookieOffset
.rdata:00012108                 dd 0                    ; EHCookieXOROffset
.rdata:00012108                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN5          ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN6          ; ScopeRecord.HandlerFunc

.text:00011010
.text:00011010                 mov     edi, edi
.text:00011012                 push    ebp
.text:00011013                 mov     ebp, esp
.text:00011015                 push    0FFFFFFFEh
.text:00011017                 push    offset stru_12108
.text:0001101C                 push    offset __except_handler4
.text:00011021                 mov     eax, large fs:0
.text:00011027                 push    eax
.text:00011028                 add     esp, -0Ch

.text:0001102B                 push    ebx
.text:0001102C                 push    esi
.text:0001102D                 push    edi

对应堆栈如下

 

 

.text:0001102E                 mov     eax, ___security_cookie
.text:00011033                 xor     [ebp+ms_exc.registration.ScopeTable], eax
.text:00011036                 xor     eax, ebp
.text:00011038                 push    eax
.text:00011039                 lea     eax, [ebp+ms_exc.registration]
.text:0001103C                 mov     large fs:0, eax
.text:00011042                 mov     [ebp+ms_exc.old_esp], esp
.text:00011045                 mov     [ebp+i], 0
.text:0001104C                 mov     [ebp+ms_exc.registration.TryLevel], 0

对应堆栈如下


 

;这三条指令是内部执行的指令 i++

.text:00011053                 mov     eax, [ebp+i]
.text:00011056                add     eax, 1  
.text:00011059                 mov     [ebp+i], eax


;下面是异常的卸载

 

.text:0001105C                 mov    [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:00011063                 jmp     short loc_11082

.text:00011065
.text:00011065 $LN5:                                   ; DATA XREF:.rdata:stru_12108o
.text:00011065                 mov     eax, 1          ; Exception filter 0 for function11010
.text:0001106A
.text:0001106A $LN7:
.text:0001106A                 retn
.text:0001106B ;---------------------------------------------------------------------------
.text:0001106B
.text:0001106B $LN6:                                   ; DATA XREF: .rdata:stru_12108o
.text:0001106B                 mov     esp, [ebp+ms_exc.old_esp] ; Exceptionhandler 0 for function 11010
.text:0001106E                 push    offset Format   ; "Hello\r\n"
.text:00011073                 call    _DbgPrint
.text:00011078                 add     esp, 4
.text:0001107B                 mov     [ebp+ms_exc.registration.TryLevel],0FFFFFFFEh


对应的堆栈


.text:00011082 loc_11082:                              ; CODE XREF: SEHTest()+53 j
.text:00011082                 mov     ecx, [ebp+ms_exc.registration.Next]
.text:00011085                 mov     large fs:0, ecx


;恢复fs:0 即原来的异常处理函数

;堆栈平衡


.text:0001108C                 pop     ecx
.text:0001108D                 pop     edi
.text:0001108E                 pop     esi
.text:0001108F                 pop     ebx
.text:00011090                 mov     esp, ebp
.text:00011092                 pop     ebp
.text:00011093                 retn
.text:00011093 _SEHTest@0      endp

 

上面是仅仅一个try 语句时候的情况,那么,当有多个try 语句,是怎样的情况呢?

void SEHTest(){
	int i = 0;
	__try {
		i++;
	}
	__except(EXCEPTION_EXECUTE_HANDLER) {
		DbgPrint("Hello\r\n");
	}
	__try{
		i--;
	}
	__except(EXCEPTION_EXECUTE_HANDLER){
		DbgPrint("Hello\r\n");
	}
}

前面简历堆栈的操作是相同的,而且其第一个try 块的操作也是相同的,不同的如下所示。在将结构体的TryLevel 设置为 0FFFFFFFEh 之后,原来的函数已经开始了结束函数的操作,而这里是跳转到了下一个目标地址。


.text:0001105C                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:00011063                 jmp     short loc_11082

.text:00011065 $LN6:                                   ; DATA XREF: .rdata:stru_12108 o
.text:00011065                 mov     eax, 1          ; Exception filter 0 for function 11010
.text:0001106A
.text:0001106A $LN8:
.text:0001106A                 retn
.text:0001106B ; ---------------------------------------------------------------------------
.text:0001106B
.text:0001106B $LN7:                                   ; DATA XREF: .rdata:stru_12108 o
.text:0001106B                 mov     esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 11010
.text:0001106E                 push    offset Format   ; "Hello\r\n"
.text:00011073                 call    _DbgPrint
.text:00011078                 add     esp, 4
.text:0001107B                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:00011082

该目标地址处的操作如下所示,将TryLevel 设置为1,而上面为0



.text:00011082 loc_11082:                              ; CODE XREF: SEHTest()+53 j
.text:00011082                 mov     [ebp+ms_exc.registration.TryLevel], 1
.text:00011089                 mov     ecx, [ebp+i]
.text:0001108C                 sub     ecx, 1
.text:0001108F                 mov     [ebp+i], ecx
进行i—操作

.text:00011092                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh


同样是将TryLevel 设置为 0FFFFFFFEh:


.text:00011099                 jmp     short loc_110B8
.text:0001109B ; ---------------------------------------------------------------------------
.text:0001109B
.text:0001109B $LN10:                                  ; DATA XREF: .rdata:stru_12108 o
.text:0001109B                 mov     eax, 1          ; Exception filter 1 for function 11010
.text:000110A0
.text:000110A0 $LN12:
.text:000110A0                 retn
.text:000110A1 ; ---------------------------------------------------------------------------
.text:000110A1
.text:000110A1 $LN11:                                  ; DATA XREF: .rdata:stru_12108 o
.text:000110A1                 mov     esp, [ebp+ms_exc.old_esp] ; Exception handler 1 for function 11010
.text:000110A4                 push    offset Format   ; "Hello\r\n"
.text:000110A9                 call    _DbgPrint
.text:000110AE                 add     esp, 4
.text:000110B1                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:000110B8
.text:000110B8 loc_110B8:                              ; CODE XREF: SEHTest()+89 j

到这里向下函数的执行是一样的,即恢复原来的fs:0,然后恢复堆栈。  

另外,上下两个程序中引用的结构体,也发生了变化。上面的结构体的长度和下面的结构体长度不同。很明显,这是一个不定长的结构


.rdata:00012108 stru_12108      dd 0FFFFFFFEh           ; GSCookieOffset
.rdata:00012108                                         ; DATA XREF: SEHTest()+7 o
.rdata:00012108                 dd 0                    ; GSCookieXOROffset ; SEH scope table for function 11010
.rdata:00012108                 dd 0FFFFFFD4h           ; EHCookieOffset
.rdata:00012108                 dd 0                    ; EHCookieXOROffset
第一个异常块
.rdata:00012108                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN6          ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN7          ; ScopeRecord.HandlerFunc
第二个异常块
.rdata:00012108                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN10         ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN11         ; ScopeRecord.HandlerFunc

我们再次增加函数内try结构的个数如下

void SEHTest(){
    int i = 0;
    __try{
        i++;
    }
    __except(EXCEPTION_EXECUTE_HANDLER){
        DbgPrint("Hello\r\n");
    }
    __try{
        i--;
    }
    __except(EXCEPTION_EXECUTE_HANDLER){
        DbgPrint("Hello\r\n");
    }
    __try{
        i += 2;
    }
    __except(EXCEPTION_EXECUTE_HANDLER){
        DbgPrint("Hello\r\n");
    }
}

我们发现其添加了如下结构


loc_110B8:
mov     [ebp+ms_exc.registration.TryLevel], 2
mov     edx, [ebp+i]
add     edx, 2
mov     [ebp+i], edx
mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
jmp     short loc_110EE

上述变长结构体变为这样:

.rdata:00012108 stru_12108      dd 0FFFFFFFEh           ; GSCookieOffset
.rdata:00012108                                         ; DATA XREF: SEHTest()+7 o
.rdata:00012108                 dd 0                    ; GSCookieXOROffset ; SEH scope table for function 11010
.rdata:00012108                 dd 0FFFFFFD4h           ; EHCookieOffset
.rdata:00012108                 dd 0                    ; EHCookieXOROffset

.rdata:00012108                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN7          ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN8          ; ScopeRecord.HandlerFunc
.rdata:00012108                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN11         ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN12         ; ScopeRecord.HandlerFunc
.rdata:00012108                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN15         ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN16         ; ScopeRecord.HandlerFunc


似乎发现了什么,每添加一个try 块,添加一个代码块,且其TryLevel递增,上面的结构体增加一个单位的子结构。

然后我们再添加嵌套形式的SHE,使其格式如下:

void SEHTest() {
	int i = 0;
	__try {
		i++;
	}
	__except(EXCEPTION_EXECUTE_HANDLER)	{
		DbgPrint("Hello\r\n");
	}
	__try {
		i--;
		__try {
			i += 2;
		}
		__except(EXCEPTION_EXECUTE_HANDLER) {
			DbgPrint("Hello\r\n");
		}
	}
	__except(EXCEPTION_EXECUTE_HANDLER)	{
		DbgPrint("Hello\r\n");
	}
}

上面第一个代码块的执行依然不变,但是,当执行下面的结构的时候,我们来看。

.text:00011082 loc_11082:                              ; CODE XREF: SEHTest()+53 j
.text:00011082                 mov     [ebp+ms_exc.registration.TryLevel], 1
.text:00011089                 mov     ecx, [ebp+i]
.text:0001108C                 sub     ecx, 1
.text:0001108F                 mov     [ebp+i], ecx


i-- 对应的代码块,TryLevel 为 1,但是其运行结束后没有将TryLevel 恢复到0FFFFFFFEh。而是直接执行了下面的嵌套的try 块


.text:00011092                 mov     [ebp+ms_exc.registration.TryLevel], 2
.text:00011099                 mov     edx, [ebp+i]
.text:0001109C                 add     edx, 2
.text:0001109F                 mov     [ebp+i], edx
.text:000110A2                 mov     [ebp+ms_exc.registration.TryLevel], 1

i += 2 对应的代码块,TryLevel为 2,然后其执行结束后,自动将TryLevel 设置为1,也不是0FFFFFFFEh。


.text:000110A9                 jmp     short loc_110C8

最后函数跳转到如下地址,然后将其TryLevel 设置为 0FFFFFFFEh ,然后恢复fs:0 恢复堆栈等操作。


.text:000110C8 loc_110C8:                              ; CODE XREF: SEHTest()+99 j
.text:000110C8                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:000110CF                 jmp     short loc_110EE

我们可以看到,try 块与其包含的代码块的对应是通过TryLevel 来对应的,在进入一个新块之前会设置这个块的等级,然后在执行完这个块之后会恢复到之前TryLevel。而这里的TryLevel可以理解为对应的变长结构体的子结构体的下标。

 

下面我们来看这个子结构有什么变化,因为与上个程序相比,尽管块的个数没有改变,但是块的结构改变了------增加了嵌套的try 块这样的结构。

.rdata:00012108 stru_12108      dd 0FFFFFFFEh           ; GSCookieOffset
.rdata:00012108                                         ; DATA XREF: SEHTest()+7 o
.rdata:00012108                 dd 0                    ; GSCookieXOROffset ; SEH scope table for function 11010
.rdata:00012108                 dd 0FFFFFFD4h           ; EHCookieOffset
.rdata:00012108                 dd 0                    ; EHCookieXOROffset

.rdata:00012108                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN7          ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN8          ; ScopeRecord.HandlerFunc
.rdata:00012108                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN11         ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN12         ; ScopeRecord.HandlerFunc
.rdata:00012108                 dd 1                    ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN15         ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN16         ; ScopeRecord.HandlerFunc

左边是之前的,右边是现在的。很容易发现,第三个结构的EnclosingLevel成员不同,综合代码我们发现,其值跟代码中“进入3号try 块之前的TryLevel 状态以及,出3号try块之后恢复的TryLevel 状态相同”如果在对比前面两个try 块对应的子结构发现,这个EnclosingLevel 就是try 块对应的外层的try 块的下标。而0FFFFFFFEh 代表的是外层没有try 块(这个编译器生成的try 块)。


掌握了单个函数内部的try 嵌套之后我们来看try 块内部调用函数的情况。

void SubFunc() {
	int j;
	__try {
		j++;
	}
	__except(EXCEPTION_EXECUTE_HANDLER)	{
		DbgPrint("World\r\n");
	}
}
void SEHTest() {
	int i = 0;
	__try {
		SubFunc();
	}
	__except(EXCEPTION_EXECUTE_HANDLER) {
		DbgPrint("Hello\r\n");
	}
}

函数SEHTest 内部是最简单的形式了。如下


.text:00011017                 push    offset stru_12108
.text:0001101C                 push    offset __except_handler4

另外,看那个变长结构:

.rdata:00012104                 dd rva _unwind_handler4
.rdata:00012108 stru_12108      dd 0FFFFFFFEh           ; GSCookieOffset
.rdata:00012108                                         ; DATA XREF: SubFunc()+7 o
.rdata:00012108                 dd 0                    ; GSCookieXOROffset ; SEH scope table for function 11010
.rdata:00012108                 dd 0FFFFFFD4h           ; EHCookieOffset
.rdata:00012108                 dd 0                    ; EHCookieXOROffset
.rdata:00012108                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012108                 dd offset $LN5          ; ScopeRecord.FilterFunc
.rdata:00012108                 dd offset $LN6          ; ScopeRecord.HandlerFunc


.rdata:00012128 stru_12128      dd 0FFFFFFFEh           ; GSCookieOffset
.rdata:00012128                                         ; DATA XREF: SEHTest()+7 o
.rdata:00012128                 dd 0                    ; GSCookieXOROffset ; SEH scope table for function 110A0
.rdata:00012128                 dd 0FFFFFFD4h           ; EHCookieOffset
.rdata:00012128                 dd 0                    ; EHCookieXOROffset
.rdata:00012128                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:00012128                 dd offset $LN5_0        ; ScopeRecord.FilterFunc
.rdata:00012128                 dd offset $LN6_0        ; ScopeRecord.HandlerFunc


两个函数分别有两个该结构。

到这里我们就了解了编译器的SHE 的结构了。一个函数如果有SEH 结构,将只进行一次注册,即使用编译器提供的__except_handler4函数,并用一个变长结构来表示单个函数内部try 块的嵌套结构。如果try 块中有函数调用,且该函数内部也有SHE 结构,其嵌套是通过系统SHE 机制来确保的。即,当子函数中的SHE 不能满足需要,将遍历其父函数的SHE 函数(对于同一编译器来说是同一个异常处理函数)。这样可以确保不同编译器编译的模块之间的调用不会因为SHE 的实现不同而导致错误。下一节我们来看看这个__except_handler4 函数,来解决文章开头出现的问题。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值