您的位置:首页 > 编程语言 > C语言/C++

Reversing Microsoft Visual C++ Part I: Exception Handling

2007-12-01 14:19 555 查看
译自OpenRCE: http://www.openrce.org/articles/full_view/21

概要

Microsoft Visual C++是win32下使用最广的编译器, 所以熟悉它的内部工作原理对win32逆向非常重要. 能认出编译器自动生成的"粘合代码"(辅助代码)可以帮助你快速集中于那些由程序员编写的实际代码. 同样也有助于把握程序的高层结构.

在两部分文章中的第一部分, 我会集中于栈布局(stack layout), 异常处理(exception handling) 和MSVC编译的程序里的相关结构. 假设读者已了解汇编程序,寄存器调用约定等的一些知识.

术语:

Stack frame: 栈帧, 被某个函数使用的一段堆栈段. 通常包含函数参数, 返回地址, 保存的寄存器现场, 局部变量和其他一些特定于这个函数的数据. 在x86(和大多数的其他架构)上, 调用者与被调用者的栈帧是相邻的.

Frame pointer: 栈帧指针, 一个指向栈帧里固定地址的寄存器或其他变量. 通常栈帧里的所有数据都是用它来作相对寻址. 在x86上它通常是ebp而且通常是指向返回地址的下面.

Object: 对象, 一个(C++)类的实例.

Unwindable Object: 可展开对象, 一个被指定为auto存储级别(auto storage-class)的局部对象, 它被分配在栈里, 而且在离开作用范围之后被销毁.

Stack Unwinding: 栈展开, 自动销毁上面那些对象, 在因异常使程序流离开它们的作用范围时发生.

在C或C++程序里可以使用两种异常:

SEH(Structured Exception Handling) 异常, 结构化异常处理异常. 就是平常说的Win32或系统异常. 在著名的Matt Pietrek的文章[1]里有详尽的描述. 他们是C程序里唯一有效的异常. 编译器级支持包括关键字__try, __except, __finally 还有其他一些.

C++异常(有时称为"EH"). 在SEH之上实现. C++异常允许抛出和捕获任意类型. 它的一个十分重要的特性就是在异常处理期间自动展开堆栈, 而且MSVC用了一个非常复杂的底层框架来保证它在任何情况下都能工作.

在下面的图示中, 内存地址是从上往下增长的, 于是堆栈是"越长越高"的. 这也是IDA中表示堆栈的方法, 和大多数其他的文章相反.

基本栈帧布局:

大多数的基本栈帧看起来像下面这样:
...

局部变量

保存的其他寄存器值

保存的ebp值

返回地址

函数参数

...

注意: 如果允许省略栈帧指针, 那么可能会没有保存ebp.

SEH

在使用了编译器级SEH的情况下, 堆栈布局可能会有一点复杂.

SEH3 Stack Layout 当在一个函数里没有__except块的时候, Saved ESP是没有用的. Scope table是一个描述了每个__try块和它们之间关系的结构的数组:

struct _SCOPETABLE_ENTRY {
DWORD EnclosingLevel;
void* FilterFunc;
void* HandlerFunc;
}

更多的关于SEH实现的细节参考[1].了解try块就看try level变量是怎么被更新的.每个try块都有分配到一个唯一数字,嵌套关系则对应于两个ScopeTable间的关系.比如,一个ScopeTable i 的EnclosingLevel=j, 那么try块j包含try块i. 函数体的try level看成是-1. 例子看附录1.

缓冲区溢出保护

Whidbey(MSVC 2005)编译器在SEH帧里添加了一些缓冲区溢出的保护措施. 它的完整栈帧布局看起来像下面这样:

SEH4 Stack Layout GS Cookie只在函数用/GS开关编译的时候出现. EH Cookie则总是出现. SEH4的ScopeTable和SEH3的基本一样, 只是增加了头部:

struct _EH4_SCOPETABLE {
DWORD GSCookieOffset;
DWORD GSCookieXOROffset;
DWORD EHCookieOffset;
DWORD EHCookieXOROffset;
_EH4_SCOPETABLE_RECORD ScopeRecord[1];
};

struct _EH4_SCOPETABLE_RECORD {
DWORD EnclosingLevel;
long (*FilterFunc)();
union {
void (*HandlerAddress)();
void (*FinallyFunc)();
};
};

GSCookieOffset=-2表示GS Cookie没有使用. EH Cookie总是出现. 偏移都相对于ebp. 检查的时候计算:(ebp+CookieXOROffset) ^ [ebp+CookieOffset] == 和_security_cookie异或之后的一个栈里的ScopeTable的指针. 在SEH4里, 最外层的块等级(Scope level)是-2, 而不是像SEH3一样的-1.

C++异常模型实现

当C++异常处理(try/catch)或者可展开对象出现在函数里的时候,事情开始变得复杂.

C++ EH Stack Layout EH处理程序对每个函数都不同(不像SEH), 通常是下面这样:

;(VC7+)
mov eax OFFSET __ehfuncinfo
jmp ___CxxFrameHandler

__ehfuncinfo是一个FuncInfo类型的结构, 他完整描述了函数中所有try/catch块和可展开对象.

struct FuncInfo {
// compiler version.
// 0x19930520: up to VC6, 0x19930521: VC7.x(2002-2003), 0x19930522: VC8 (2005)
DWORD magicNumber;

// number of entries in unwind table
int maxState;

// table of unwind destructors
UnwindMapEntry* pUnwindMap;

// number of try blocks in the function
DWORD nTryBlocks;

// mapping of catch blocks to try blocks
TryBlockMapEntry* pTryBlockMap;

// not used on x86
DWORD nIPMapEntries;

// not used on x86
void* pIPtoStateMap;

// VC7+ only, expected exceptions list (function "throw" specifier)
ESTypeList* pESTypeList;

// VC8+ only, bit 0 set if function was compiled with /EHs
int EHFlags;
};

Unwind map类似于SEH的ScopeTable, 只是没有过滤函数:

struct UnwindMapEntry {
int toState; // target state
void (*action)(); // action to perform (unwind funclet address)
};

Try块描述符. 描述了一个try{}块, 包括相关的catch块.

struct TryBlockMapEntry {
int tryLow;
int tryHigh; // this try {} covers states ranging from tryLow to tryHigh
int catchHigh; // highest state inside catch handlers of this try
int nCatches; // number of catch handlers
HandlerType* pHandlerArray; //catch handlers table
};

catch块描述符. 描述了try块中的其中一个catch()

struct HandlerType {
// 0x01: const, 0x02: volatile, 0x08: reference
DWORD adjectives;

// RTTI descriptor of the exception type. 0=any (ellipsis)
TypeDescriptor* pType;

// ebp-based offset of the exception object in the function stack.
// 0 = no object (catch by type)
int dispCatchObj;

// address of the catch handler code.
// returns address where to continues execution (i.e. code after the try block)
void* addressOfHandler;
};

期望捕获异常的列表(在MSVC中实现了但是默认被禁止, 用/d1ESrt开启):

struct ESTypeList {
// number of entries in the list
int nCount;

// list of exceptions; it seems only pType field in HandlerType is used
HandlerType* pTypeArray;
};

RTTI类型描述符. 描述了一个C++类型. 在这里用来匹配抛出的异常类型和catch的类型.

struct TypeDescriptor {
// vtable of type_info class
const void * pVFTable;

// used to keep the demangled name returned by type_info::name()
void* spare;

// mangled type name, e.g. ".H" = "int", ".?AUA@@" = "struct A", ".?AVA@@" = "class A"
char name[0];
};

不像SEH, 每个try块不会有一个关联状态值. 编译器不仅会在进入/离开try块的时候改变状态值, 在创建/销毁对象时也会. 这样就有可能知道哪些对象在异常发生时需要展开. 你仍然可以通过检查关联状态范围和catch处理器返回的地址来得知try块的边界.(见附录2)

抛出C++异常

throw语句被转化为调用_CxxThrowException(), 它实际上抛出一个win32(SEH)异常, 代码是0xE06D7363 ('msc'|0xE0000000). 自定义参数包括异常对象指针和它的ThrowInfo结构(被异常处理程序用来匹配抛出异常的类型).

struct ThrowInfo {
// 0x01: const, 0x02: volatile
DWORD attributes;

// exception destructor
void (*pmfnUnwind)();

// forward compatibility handler
int (*pForwardCompat)();

// list of types that can catch this exception.
// i.e. the actual type and all its ancestors.
CatchableTypeArray* pCatchableTypeArray;
};

struct CatchableTypeArray {
// number of entries in the following array
int nCatchableTypes;
CatchableType* arrayOfCatchableTypes[0];
};

描述一个可以捕获这次异常的类型:

struct CatchableType {
// 0x01: simple type (can be copied by memmove), 0x02: can be caught by reference only, 0x04: has virtual bases
DWORD properties;

// see above
TypeDescriptor* pType;

// how to cast the thrown object to this type
PMD thisDisplacement;

// object size
int sizeOrOffset;

// copy constructor address
void (*copyFunction)();
};

// Pointer-to-member descriptor.
struct PMD {
// member offset
int mdisp;

// offset of the vbtable (-1 if not a virtual base)
int pdisp;

// offset to the displacement value inside the vbtable
int vdisp;
};

我们会在下一篇文章更深入研究这个.

序言和结语(Prologs and Epilogs)

相比把建立栈帧的代码放在函数体, 编译器可能会调用指定的"序言和结语"(prolog and epilog)函数. 这里有几种用于不同函数类型的变量:
NameTypeEH CookieGS CookieCatch Handlers
_SEH_prolog/_SEH_epilog SEH3 --
_SEH_prolog4/_SEH_epilog4 S EH4 +-
_SEH_prolog4_GS/_SEH_epilog4_GS SEH4 ++
_EH_prolog C++ EH--+/-
_EH_prolog3/_EH_epilog3 C++ EH+--
_EH_prolog3_catch/_EH_epilog3 C++ EH+-+
_EH_prolog3_GS/_EH_epilog3_GS C++ EH++-
_EH_prolog3_catch_GS/_EH_epilog3_catch_GSC++ EH+++

SEH2

显然被用在MSVC 1.xx(由crtdll.dll导出). 在某些旧的NT程序下会见到.
...

Saved edi

Saved esi

Saved ebx

Next SEH frame

Current SEH handler (__except_handler2)

Pointer to the scopetable

Try level

Saved ebp (of this function)

Exception pointers

Local variables

Saved ESP

Local variables

Callee EBP

Return address

Function arguments

...


附录1: 示例SEH程序

考虑以下反汇编:
func1 proc near

_excCode = dword ptr -28h
buf = byte ptr -24h
_saved_esp = dword ptr -18h
_exception_info = dword ptr -14h
_next = dword ptr -10h
_handler = dword ptr -0Ch
_scopetable = dword ptr -8
_trylevel = dword ptr -4
str = dword ptr 8

push ebp
mov ebp, esp
push -1
push offset _func1_scopetable
push offset _except_handler3
mov eax, large fs:0
push eax
mov large fs:0, esp
add esp, -18h
push ebx
push esi
push edi

; --- end of prolog ---

mov [ebp+_trylevel], 0 ;trylevel -1 -> 0: beginning of try block 0
mov [ebp+_trylevel], 1 ;trylevel 0 -> 1: beginning of try block 1
mov large dword ptr ds:123, 456
mov [ebp+_trylevel], 0 ;trylevel 1 -> 0: end of try block 1
jmp short _endoftry1

_func1_filter1: ; __except() filter of try block 1
mov ecx, [ebp+_exception_info]
mov edx, [ecx+EXCEPTION_POINTERS.ExceptionRecord]
mov eax, [edx+EXCEPTION_RECORD.ExceptionCode]
mov [ebp+_excCode], eax
mov ecx, [ebp+_excCode]
xor eax, eax
cmp ecx, EXCEPTION_ACCESS_VIOLATION
setz al
retn

_func1_handler1: ; beginning of handler for try block 1
mov esp, [ebp+_saved_esp]
push offset aAccessViolatio ; "Access violation"
call _printf
add esp, 4
mov [ebp+_trylevel], 0 ;trylevel 1 -> 0: end of try block 1

_endoftry1:
mov edx, [ebp+str]
push edx
lea eax, [ebp+buf]
push eax
call _strcpy
add esp, 8
mov [ebp+_trylevel], -1 ; trylevel 0 -> -1: end of try block 0
call _func1_handler0 ; execute __finally of try block 0
jmp short _endoftry0

_func1_handler0: ; __finally handler of try block 0
push offset aInFinally ; "in finally"
call _puts
add esp, 4
retn

_endoftry0:
; --- epilog ---
mov ecx, [ebp+_next]
mov large fs:0, ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
retn
func1 endp

_func1_scopetable
;try block 0
dd -1 ;EnclosingLevel
dd 0 ;FilterFunc
dd offset _func1_handler0 ;HandlerFunc

;try block 1
dd 0 ;EnclosingLevel
dd offset _func1_filter1 ;FilterFunc
dd offset _func1_handler1 ;HandlerFunc

try块0没有过滤器, 所以它是一个__finally{}块. try块1的EnclosingLevel是0, 所以它在try块0的里面. 综上, 我们可以写出以下代码:

void func1 (char* str)
{
char buf[12];
__try // try block 0
{
__try // try block 1
{
*(int*)123=456;
}
__except(GetExceptCode() == EXCEPTION_ACCESS_VIOLATION)
{
printf("Access violation");
}
strcpy(buf,str);
}
__finally
{
puts("in finally");
}
}

附录2: C++异常的示例

未完待续...
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: