Reversing Microsoft Visual C++ Part I: Exception Handling

译自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 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 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 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++异常的示例

 

未完待续...

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值