此系列文章旨在探讨在VS2019环境下,代码还原中遇到x86的异常和x64的异常时如何准确还原try-catch的嵌套关系,如何定位核心的catch代码块,准确还原try的包含范围,以及获取thorw参数的值及其类型。如果表述或者内容有误恳请各位前辈斧正。
环境
编译器: Microsoft Visual Studio Community 2019 16.11.34
系统:Microsoft Windows 11 专业工作站版 10.0.22631 版本 22631
分析工具: IDA 7.5
大纲
此系列文章我们将从如下
5
个方面入手讨论有关异常还原的奥妙。
1.异常还原中的核心问题
2.x86中的三代异常还原
3.x64中三代异常还原
4.x64中四代异常还原
5.编译器优化后的异常代码结构还原技巧
我们分为3个章节,本文为第一章,章节目录如下:
1.x86中的三代异常还原(本文)
2.x64中三代异常与四代还原
3.编译器优化后的异常代码结构还原技巧
异常还原中的核心问题
首先对于异常还原我们需要解决的问题有如下几点
1.try范围确定
2.try嵌套关系确定
3.throw类型及其值确定
4.catch代码块的定位
当我们能够准确做到以上四点之后就能够准确还原源程序中的try-catch结构,在微软的异常体系架构中并未使用C++标准的异常体系,反而发展出了自己的异常体系微软的异常架构按照还原难度从易到难如下排列:
1.x86中的三代异常还原
2.x64中三代异常还原
3.x64中四代异常还原
本篇文章将会介绍x86中的三代异常还原相关的还原技巧,首先我们从32位程序的3代异常说起。
x86中的三代异常还原
x86中的三代异常识别
首先要识别x86的3代异常是比较简单的,在拥有异常处理的函数当中其开头一般都会有如下两点行为,请谨记这两点识别技巧:
1.进入-1的try_level
2.放入SEH回调函数
形如下图:
在我们点击进入到SEH回调函数指针之后会见到
__CxxFrameHandler3
这个调用,一般来讲IDA都会将其进行标记,此标志就代表着3代异常处理框架。如下图所示。
此调用
主要依托于RTTI机制进行异常的派发
。
throw类型及其值确定
接下来我们先回到
throw
的识别,对于throw来讲我们一般是通过
_CxxThrowException
来抛出异常的,一般我们在IDA中看到有形如
_CxxThrowException
时就可以判定为抛出点。形如下图所示:
当我们找到此调用的时候还原throw参数的值及其类型就比较简单了,此调用传入两个参数:
1.pExceptionObject 抛出参数值的指针
2.pThrowInfo _ThrowInfo结构体指针
CxxThrowException(pExceptionObj, __ThrowInfo)
想要
确定值
就查看跟踪
pExceptionObject
的赋值流程。想要确定抛出参数类型就分析
_ThrowInfo
结构体。接下来我们对
_ThrowInfo
结构体进行解析,在微软的头文件
eh.h
里面有包含异常管理的结构体定义。值得注意的是VC6和VS2019的结构体是具有差异的。此
_ThrowInfo
结构体的定义如下:
typedef const struct _s_ThrowInfo {
unsigned int
attributes;
(Bit field)
PMFN
pmfnUnwind;
when exception has been handled or aborted
int (__cdecl * pForwardCompat)(...);
frame handler
CatchableTypeArray* pCatchableTypeArray;
pointers to types
#endif
} ThrowInfo;
字段解释
1.
attributes throw
的信息
2.
pmfnUnwind
异常的展开
3.
pForwardCompat
为了兼容的框架
4.
pCatchableTypeArray Catch
类型数组指向一个数组
CatchableTypeArray
定义:
typedef const struct _s_CatchableTypeArray {
int nCatchableTypes;
CatchableType* arrayOfCatchableTypes[];
} CatchableTypeArray;
字段解析
1.
nCatchableTypes
类型
2.
arrayOfCatchableTypes
CatchableType*
结构体
CatchableType*
结构体定义如下:
typedef const struct _s_CatchableType {
unsigned int properties;
#if _EH_RELATIVE_TYPEINFO
int pType;
#else
TypeDescriptor * pType;
#endif
PMD thisDisplacement;
int sizeOrOffset;
PMFN copyFunction;
} CatchableType;
我们给出
TypeDescriptor
定义
typedef struct TypeDescriptor
{
#if defined(_WIN64) || defined(_RTTI) || defined(BUILDING_C1XX_FORCEINCLUDE)
const void * pVFTable;
#else
unsigned long hash;
#endif
void * spare;
char name[];
} TypeDescriptor;
至此我们可以知道抛出异常的类型了,我们再用图像梳理一遍参数的指向流程。
这就是throw的识别方式,根据传参分析到thorw的类型然后跟踪第一个参数
pExceptionObject
拿到参数的值。接下来我们回到catch的还原方法。
catch块的定位与还原
对于catch的还原我们需要对传入的SEH回调函数指针进行分析,此函数的调用会传入一个参数,此参数IDA在DEBUG版是不会解析的,我们需要搞清楚这个表的结构,此结构体叫
FuncInfo
此结构体定义如下:
typedef const struct _s_FuncInfo
{
unsigned int magicNumber:29;
unsigned int bbtFlags:3;
__ehstate_t maxState;
UnwindMapEntry* pUnwindMap;
unsigned int nTryBlocks;
TryBlockMapEntry* pTryBlockMap;
unsigned int nIPMapEntries;
void* pIPtoStateMap;
ESTypeList* pESTypeList;
int EHFlags;
} FuncInfo;
重要字段解析
1.
nTryBlocks
这个函数写了几个try
2.
pTryBlockMap
每个try的信息由此结构体指明
3.
maxState
最大状态
其中固定第一个
magicNumber
为
19930522
是固定值据传是发明这个结构体的日期。
TryBlockMapEntry
结构体记录了Try的各种信息定义如下。
typedef const struct _s_TryBlockMapEntry {
__ehstate_t tryLow;
__ehstate_t tryHigh;
__ehstate_t catchHigh;
int nCatches;
HandlerType* pHandlerArray;
} TryBlockMapEntry;
重要参数解析
1.
tryLow
2.
tryHigh
如果前两个字段相同则证明无嵌套
3.
catchHigh
这三个字段用于匹配第几个try
4.
nCatches
此try有几个catch
5.
pHandlerArray
Catch的类型
HandlerType
结构体解析,此结构体主要用来描述Catch的信息。
typedef const struct _s_HandlerType {
unsigned int adjectives;
TypeDescriptor* pType;
ptrdiff_t dispCatchObj;
void * addressOfHandler;
} HandlerType;
重要参数解析
1.
adjectives
try_level
2.
pType
RTTI 指针指向接收异常类型
3.
addressOfHandler
catch代码块指针
我们至此可以通过分析FuncInfo来获取到catch代码块的位置,我贴出图来辅助大家理解此结构。
try范围还原技巧
接下来我们需要确定try的范围,在Debug版中可以通过确定try等级下标来确定进入的是第几个try,但是在Release版中等级下标的语句是有可能被编译器优化掉的,我们先给出Debug版的图例稍后在
编译器优化后的异常代码结构还原技巧
此章节讨论Release版本的判断技巧。
try嵌套识别技巧
接下来我们需要看的就是,try的嵌套范围。Try的嵌套不能简单通过
stat_tryLevel
看出,在Release版中因为没有退出赋值
-1
的情况所以就无法看出,所以通过
tryLow
tryHigh
catchHigh
这三个字段来描述是否有嵌套。
以如上的
TryBlockMap
举例很明显能看出包含关系,其中下面的参数为
0,2,3
也就是说从0到2都会进入,然后上面那个只是
1,1,3
也就是说下面的
try
包含上面的
try
,这样就可以明确的还原出嵌套关系。
我们给出复杂点的示例用于判断练习
解析
:从上到下我们将其标号为0~5,其嵌套关系如下表示
如果在Catch中再写try只是单独在Catch块中具有
try-catch
结构不会嵌入当前层次的分析。
结语
对于异常处理的还原,在还原代码结构的时候是十分重要的。我们需要准确地还原出异常结构,并且识别出编译器对其的优化处理,这样才能更好地还原软件的行为,做到二进制上的相同,同时使得还原后的代码更具有健壮性,维护性。
看雪ID:TeddyBe4r
https://bbs.kanxue.com/user-home-983513.htm
*本文为看雪论坛精华文章,由 TeddyBe4r 原创,转载请注明来自看雪社区