译自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的情况下, 堆栈布局可能会有一点复杂.DWORD EnclosingLevel;
void * FilterFunc;
void * HandlerFunc;
}
缓冲区溢出保护
Whidbey(MSVC 2005)编译器在SEH帧里添加了一些缓冲区溢出的保护措施. 它的完整栈帧布局看起来像下面这样: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)();
};
};
C++异常模型实现
当C++异常处理(try/catch)或者可展开对象出现在函数里的时候,事情开始变得复杂.mov eax OFFSET __ehfuncinfo
jmp ___CxxFrameHandler
__ehfuncinfo是一个FuncInfo类型的结构, 他完整描述了函数中所有try/catch块和可展开对象.
// 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, 只是没有过滤函数:
int toState; // target state
void ( * action)(); // action to perform (unwind funclet address)
};
Try块描述符. 描述了一个try{}块, 包括相关的catch块.
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()
// 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开启):
// 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的类型.
// 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结构(被异常处理程序用来匹配抛出异常的类型).// 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 ];
};
描述一个可以捕获这次异常的类型:
// 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)函数. 这里有几种用于不同函数类型的变量:Name | Type | EH Cookie | GS Cookie | Catch 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_GS | C++ 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程序
考虑以下反汇编:_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的里面. 综上, 我们可以写出以下代码:
{
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++异常的示例
未完待续...