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 函数,来解决文章开头出现的问题。