第13章 异常处理
C++标准中规定了异常处理的语法,各编译器厂商必须遵守这些语法。但由于C++标准中并没有规定异常处理的实现过程,因此导致经不同厂商的编译器编译后产生的异常处理代码各不相同。本书一直使用微软C++编译器系列中的Microsoft Visual C++6.0,因此本章依然针对VC++6.0来讲解。VC++的异常处理与Windows的SEH机制密切相关,大家在学习本章之前,应熟练掌握SEH机制。国内已经有不少书对这些知识作出了精辟的讲解,例如《加密与解密(第三版)》(段钢编著)和《琢石成器—Windows环境下32位汇编语言程序设计》(罗云彬著)等,值得大家认真阅读,为了避免重复讲解,本书不涉及SEH的相关知识。
13.1 异常处理的相关知识
C++中的异常处理机制由try、throw、catch语句组成。
try语句块负责监视异常。
throw用于异常信息的发送,也称之为抛出异常。
catch用于异常的捕获,并作出相应的处理。
异常处理的基本C++语法如下:
try{//异常检测
//执行代码
throw异常类型;//抛出异常
}
catch(捕获异常类型){//异常捕获
//处理代码
}
catch(捕获异常类型){//异常捕获
//处理代码
}
……
从C++处理异常的语法中看到,异常的处理流程为:检测异常→产生异常→抛出异常→捕获异常。但对于用户而言,编译器隐藏了异常捕获的流程。在运行过程中,异常产生时会自动地匹配到对应的处理代码中,而这个过程的代码由编译器产生,也较为复杂。异常处理通常是由编译器和操作系统共同完成的,所以不同操作系统环境下的编译器对异常捕获和异常处理的分派过程各有不同。在用户量最多的Windows操作系统环境下,各个编译器也是基于操作系统的异常接口来分别实现C++中的异常处理,所以即使在Windows环境下,不同的编译器处理异常的实现方式也不同。
本章以VC++为例,从逆向分析的角度去讲解VC++处理异常的技术细节,大家若是对其原理兴趣不大,可以跳过13.1~13.3节,直接阅读13.4节来识别try、throw、catch语句的方法。如果以后对VC++实现异常处理的过程又有了兴趣,可以再回头来阅读13.1~13.3节的内容。
VC++在处理异常时会在具有异常处理功能的函数的入口处注册一个异常回调函数,当该函数内有异常抛出时,便会执行这个已注册的异常回调函数。所有的异常信息都会被记录在相关表格中,异常回调函数根据这些表格中的信息进行异常的匹配处理工作。想要了解异常的处理流程,就需要从这些记录相关信息的表格入手。
那么,如何找到这些记录异常信息的表格呢?可以从异常回调函数入手,如图13-1所示。
图 13-1 异常回调函数
图13-1中显示,在调用函数__CxxFrameHandler前,向eax传入了一个全局地址,这是一个以寄存器方式传参的函数,eax便是这个函数的参数。地址标号stru_426658就是要找的第一张表—FuncInfo函数信息表。FuncInfo表的大小为0x14字节,有5个数据成员,记录了try块的信息以及每个try块中所对应的catch块的信息等。有了FunInfo表便可以顺藤摸瓜找到记录catch块信息的表格。查看地址标号stru_426658中的数据,如图13-2所示。
图 13-2 地址标号stru_426658的相关数据
图13-2中的数据就是FuncInfo函数信息表的相关数据,其结构如下:
FuncInfo struc;(sizeof=0x14)
magicNumber dd?;编译器生成标记固定数字0x19930520
maxState dd?;最大栈展开数的下标值
pUnwindMap dd?;指向栈展开函数表的指针,指向UnwindMapEntry表结构
dwTryCount dd?;try块数量
pTryBlockMap dd?;try块列表,指向TryBlockMapEntry表结构
FuncInfo ends
FuncInfo表结构中提供了两个表格信息,分别为UnwindMapEntry和TryBlockMapEntry。UnwindMapEntry表结构配合maxState项使用,maxState中记录了异常需要展开的次数,展开时需要执行的函数由UnwindMapEntry表结构记录,其结构信息如下:
UnwindMapEntry struc;(sizeof=0x08)
toState dd?;栈展开数下标值
lpFunAction dd?;展开执行函数
UnwindMapEntry ends
由于展开过程中可能存在多个对象,因此以数组形式记录每个对象的析构信息。toState项用于判断结构是否处于数组中,lpFunAction项则用于记录析构函数所在的地址。
结合图13-2找到用来记录try块信息表TryBlockMapEntry的地址标号stru_426688,查看此地址标号中的数据,如图13-3所示。
图 13-3 地址标号stru_426688的相关数据
表TryBlockMapEntry中有5个数据成员,其结构如下:
TryBlockMapEntry struc;(sizeof=0x14)
tryLow dd?;try块的最小状态索引,用于范围检查
tryHigh dd?;try块的最大状态索引,用于范围检查
catchHigh dd?;catch块的最高状态索引,用于范围检查
dwCatchCount dd?;catch块个数
pCatchHandlerArray dd?;catch块描述,指向_msRttiDscr表结构
TryBlockMapEntry ends
TryBlockMapEntry表结构用于判断异常产生在哪个try块中。tryLow项与tryHigh项用于检查产生的异常是否来源于try块中,而catchHigh块则是用于匹配catch块时的检查项。每个catch块都会对应一个_msRttiDscr表结构,由表结构中的pCatchHandlerArray项记录。结合图13-2,找到_msRttiDscr表的相关信息,如图13-4所示。
图 13-4 地址标号stru_4266A0的相关数据
图13-4中的数据所对应的便是_msRttiDscr表结构,该结构用于描述try块中的某一个catch块的信息,由4个数据成员组成,如下所示:
_msRttiDscr struc;(sizeof=0x10)
nFlag dd?;用于catch块的匹配检查
pType dd?;catch块要捕捉的类型,指向TypeDescriptor表结构
dispCatchObjOffset dd?;用于定位异常对象在当前EBP中的偏移位置
CatchProc dd?;catch块的首地址
_msRttiDscr ends
nFlag标记用于检查catch块类型的匹配,标记值所代表的含义如下:
标记值1:常量
标记值2:变量
标记值4:未知
标记值8:引用
_msRttiDscr表结构中的pType项与CatchProc项最为关键。在抛出异常对象时,需要复制抛出的异常对象信息,dispCatchObjOffset项用于定位异常对象在当前EBP中的偏移位置。CatchProc项中保存了异常处理catch块的首地址,这样在匹配异常后便可正确地执行catch语句块。异常的匹配信息记录在pType所指向的结构中。Type所指向的结构的描述如下所示:
TypeDescriptor struc
hash dd?;类型名称的Hash数值
spare dd?;保留,可能用于RTTI名称记录
name db?;类型名称
TypeDescriptor ends
TypeDescriptor为异常类型结构,其中name项用于记录抛出异常的类型名称,是一个字符型数组,图13-4中的地址标号??_R0M@8保存了TypeDescriptor表结构的首地址,跟踪到此地址处,如图13-5所示。
图 13-5 TypeDescriptor表结构的信息
根据图13-5中的数据显示,name项为'.M',表示异常捕获为int类型。当抛出异常类型为对象时,由成员spare来保存包含类型名称的字符串。如以下代码所示:
class CMyException{
public:
char szShow[32];
};
void main(){
try{
CMyException MyException;
strcpy(MyException.szShow,"err……");
throw&MyException;
}
catch(CMyException*e){
printf("%s\r\n",e->szShow);
}
}
按照以上结构对应关系,查找到TypeDescriptor表结构的信息,如图13-6所示。
图 13-6 自定义异常类型的TypeDescriptor表结构
根据图13-6中显示的信息,此时spare项中保存了类的名称。有了这些信息后,就可以通过与抛出异常时的信息进行对比,得到对应的表结构,通过_msRttiDscr表结构中的CatchProc项得到catch块的首地址。根据图13-4中显示的信息,可以得知处理int类型异常的catch语句块的首地址在地址标号sub_40107D处,跟踪到此地址处,相关信息如图13-7所示。
图 13-7 catch块处理代码
到此,在处理异常过程中所接触到的表结构已经被找到,接下来还需要找到抛出异常时产生的表格信息。抛出异常的工作由throw语句完成,找到调用throw时的代码信息,如图13-8所示。
图 13-8 抛出异常产生的反汇编代码
观察图13-8,在调用抛出异常函数时传递了一个全局参数__TI1H。这个标号便是抛出异常时所需要的表结构信息—ThrowInfo,其结构说明如下:
ThrowInfo struc;(sizeof=0x10)
nFlag dd?;抛出异常类型标记
pDestructor dd?;异常对象的析构函数地址
pForwardCompat dd?;未知
pCatchTableTypeArray dd?;catch块类型表,指向CatchTableTypeArray表结构
ThrowInfo ends
ThrowInfo表结构中携带了类型信息,用于匹配抛出的异常类型。当nFlag为1时,表示抛出常量类型的异常;当nFlag为2时,则表示抛出变量类型的异常。由于在try块中产生的异常被处理后不会再返回try块中,pDestructor的作用就是记录try块中的异常对象的析构函数地址,当异常处理完成后调用异常对象的析构函数。
抛出的异常所对应的catch块的类型信息就被记录在pCatchTableTypeArray所指向的结构中。借助图13-8显示的ThrowInfo表结构的地址和pCatchTableTypeArray项所保存的地址,可以找到表结构CatchTableTypeArray,如图13-9所示。
图 13-9 CatchTableTypeArray表结构的信息
图13-9中显示了CatchTableTypeArray表结构中的数据,结构说明如下:
CatchTableTypeArray struc;(sizeof=0x8)
dwCount dd?;CatchTableType数组包含的元素个数
ppCatchTableType dd?;catch块的类型信息,类型为CatchTableType**
CatchTableTypeArray ends
ppCatchTableType指向一个指针数组,dwCount用于描述数组中的元素个数。图13-9中显示只有一个元素,该元素数据为__CT??_R0H@84,这个地址标号指向了CatchTableType表结构。CatchTableType中含有处理异常时所需的相关信息,如图13-10所示。
图 13-10 CatchTableType表结构的信息
图13-10中的第二项数据是不是很眼熟呢?回看图13-5,地址标号??_ROH@8指向一个TypeDescriptor表结构,于是在处理异常时可以根据这项进行对比,找到正确的catch块并进行处理。CatchTableType表结构中还包含了其他信息,如下所示:
CatchTableType struc;(sizeof=0x1C)
flag dd?;异常对象类型标志
pTypeInfo dd?;指向异常类型结构,TypeDescriptor表结构
thisDisplacement PMD?;基类信息
sizeOrOffset dd?;类的大小
pCopyFunction dd?;复制构造函数的指针
CatchTableType ends
flag标记用于判断异常对象属于哪种类型,如指针、引用、对象等。标记值所代表的含义如下:
标记值0x1:简单类型复制;
标记值0x2:已被捕获;
标记值0x4:有虚表基类复制;
标记值0x8:指针和引用类型复制。
当异常类型为对象时,由于对象存在基类等相关信息,因此需要将它们也记录下来,thisDisplacement保存了记录基类信息结构的首地址。
PMD struc;(sizeof=0xC)
dwOffsetToThis dd?;基类偏移
dwOffsetToVBase dd?;虚基类偏移
dwOffsetToVbTable dd?;基类虚表偏移
PMD ends
图13-11是异常回调与异常抛出的结构关系图。
图 13-11 异常回调与异常抛出的结构关系图