调试实战 | 从转储文件找出抛出的异常




理论篇


缘起


最近在分析转储文件时,遇到了一个由throw抛出的异常。尽管在windbg中使用!analyze -v迅速知道了异常码是0xe06d7363(对应的ASCII码是.msc),但是根据异常码并不能确定具体抛出来的是哪种异常。针对这种情况,确定具体的异常类型才有意义。


本篇文章会简单介绍与抛出异常相关的内容,包括关键的函数及结构体。下一篇文章会通过实例介绍几种典型情况(有调试符号 / 没有调试符号 /32位程序 /64位程序)下的定位方法。


说明:对源码不感兴趣的小伙伴而可以直接跳到【解析方法小结】查看结论。


突破口


throw关键字编译后对应的函数是_CxxThrowException(),该函数内部会通过RaiseException()触发异常。_CxxThrowException()是有源码可查的,我们可以从这个函数入手,先来熟悉下这个函数以及相关的结构体。


_CxxThrowException


该函数定义在vs自带的throw.cpp中,一般在crtsrcvcruntime目录下。直接用everything搜索throw.cpp,然后打开即可。vs2019中的实现代码如下,有删减:


extern "C" __declspec(noreturn) void __stdcall
_CxxThrowException(
void* pExceptionObject, // The object thrown
_ThrowInfo* pThrowInfo // Everything we need to know about it
) {
EHTRACE_ENTER_FMT1("Throwing object @ 0x%p", pExceptionObject);

static const EHExceptionRecord ExceptionTemplate = { // A generic exception record
EH_EXCEPTION_NUMBER, // Exception number
EXCEPTION_NONCONTINUABLE, // Exception flags (we don't do resume)
nullptr, // Additional record (none)
nullptr, // Address of exception (OS fills in)
EH_EXCEPTION_PARAMETERS, // Number of parameters
{ EH_MAGIC_NUMBER1, // Our version control magic number
nullptr, // pExceptionObject
nullptr,
#if EH_EXCEPTION_PARAMETERS == 4
nullptr // Image base of thrown object
#endif
} // pThrowInfo
};
EHExceptionRecord ThisException = ExceptionTemplate; // This exception

ThrowInfo* pTI = (ThrowInfo*)pThrowInfo;
// deleted ...

ThisException.params.pExceptionObject = pExceptionObject;
ThisException.params.pThrowInfo = pTI;
#if _EH_RELATIVE_TYPEINFO
PVOID ThrowImageBase = RtlPcToFileHeader((PVOID)pTI, &ThrowImageBase);
ThisException.params.pThrowImageBase = ThrowImageBase;
#endif
// deleted ...

EHTRACE_EXIT;
RaiseException( ThisException.ExceptionCode,
ThisException.ExceptionFlags,
ThisException.NumberParameters,
(PULONG_PTR)&ThisException.params );
}


根据源码可知,_CxxThrowException()内部会调用RaiseException()RaiseException()的原型如下:


VOID WINAPI RaiseException(
_In_ DWORD dwExceptionCode,
_In_ DWORD dwExceptionFlags,
_In_ DWORD nNumberOfArguments,
_In_reads_opt_(nNumberOfArguments) CONST ULONG_PTR* lpArguments
);


_CxxThrowException调用RaiseException()时传递的各个参数值如下:


dwExceptionCode的值是EH_EXCEPTION_NUMBER,对应的十六进制值是0xe06d7363,也就是.msc


dwExceptionFlags的值是EXCEPTION_NONCONTINUABLE,对应的十六进制值是0x1


nNumberOfArguments的值是EH_EXCEPTION_PARAMETERS,在32位程序中是3,在64位程序中是4。定义如下:


#if (defined(_M_AMD64) || defined(_M_ARM) || defined(_M_ARM64)) && !defined(_CHPE_X86_ARM64_EH_)
#define EH_EXCEPTION_PARAMETERS 4 // Number of parameters in exception record
#else
#define EH_EXCEPTION_PARAMETERS 3 // Number of parameters in exception record
#endif


lpArguments指向具体的参数,来自ThisException.paramsThisException的类型是EHExceptionRecord,其定义如下:


EHExceptionRecord


typedef struct EHExceptionRecord {
unsigned long ExceptionCode; // The code of this exception. (= EH_EXCEPTION_NUMBER)
unsigned long ExceptionFlags; // Flags determined by NT
struct _EXCEPTION_RECORD* ExceptionRecord; // An extra exception record (not used)
void* ExceptionAddress; // Address at which exception occurred
unsigned long NumberParameters; // Number of extended parameters. (= EH_EXCEPTION_PARAMETERS)

struct EHParameters {
unsigned long magicNumber; // = EH_MAGIC_NUMBER1
void * pExceptionObject; // Pointer to the actual object thrown
ThrowInfo* pThrowInfo; // Description of thrown object
#if _EH_RELATIVE_TYPEINFO
void * pThrowImageBase; // Image base of thrown object
#endif
} params; // <-----

} EHExceptionRecord;


根据定义可知,ThisException.params的类型是EHExceptionRecord::EHParameters,如果_EH_RELATIVE_TYPEINFO0,则包含3个成员,否则就会包含第 4 个成员pThrowImageBase

_EH_RELATIVE_TYPEINFO32位程序中是0,在64位程序中是1,定义如下:


#if defined(_M_CEE_PURE) || defined(BUILDING_C1XX_FORCEINCLUDE)
#define _EH_RELATIVE_TYPEINFO 0 // <-----
#define _EH_RELATIVE_FUNCINFO 0
#define _RTTI_RELATIVE_TYPEINFO 0
#elif defined(_CHPE_X86_ARM64_EH_)
#define _EH_RELATIVE_TYPEINFO 0 // <-----
#define _EH_RELATIVE_FUNCINFO 1
#define _RTTI_RELATIVE_TYPEINFO 0
#elif defined(_M_ARM)
#define _EH_RELATIVE_TYPEINFO 1 // <-----
#define _EH_RELATIVE_FUNCINFO 1
#define _RTTI_RELATIVE_TYPEINFO 0
#elif defined(_M_AMD64) || defined(_M_ARM64)
#define _EH_RELATIVE_TYPEINFO 1 // <-----
#define _EH_RELATIVE_FUNCINFO 1
#define _RTTI_RELATIVE_TYPEINFO 1
#else
#define _EH_RELATIVE_TYPEINFO 0 // <-----
#define _EH_RELATIVE_FUNCINFO 0
#define _RTTI_RELATIVE_TYPEINFO 0
#endif


EHExceptionRecord::EHParameters结构体的成员数量与调用RaiseException()时的nNumberOfArguments参数值是对应的。


32位程序中,nNumberOfArguments的值是3EHExceptionRecord::EHParameters刚好有3个成员,在64位程序中nNumberOfArguments的值是4EHExceptionRecord::EHParameters刚好有4个成员。


EHExceptionRecord::EHParameters中的pExceptionObjectpThrowInfo是查找异常类型的关键。


其中,pExceptionObject是异常对象的地址,pThrowInfo的类型是ThrowInfo,用来描述异常对象的类型信息。一起来看看ThrowInfo的定义。


ThrowInfo


typedef const struct _s_ThrowInfo {
unsigned int attributes; // Throw Info attributes (Bit field)
PMFN pmfnUnwind; // Destructor to call when exception has been handled or aborted
#if _EH_RELATIVE_TYPEINFO && !defined(BUILDING_C1XX_FORCEINCLUDE)
int pForwardCompat; // Image relative offset of Forward compatibility frame handler
int pCatchableTypeArray; // Image relative offset of CatchableTypeArray
#else
int (__cdecl * pForwardCompat)(...); // Forward compatibility frame handler
CatchableTypeArray* pCatchableTypeArray; // Pointer to list of pointers to types
#endif
} ThrowInfo;


pmfnUnwind是处理异常时会调用的回卷函数,一般是析构函数,可以根据此值判断异常对象的类型!


pForwardCompat一般情况下都是0,不用太关心。


pCatchableTypeArray非常重要,记录了类型信息。


_EH_RELATIVE_TYPEINFO在上面已经贴出来了,在32位程序中被定义为0,在64位程序中被定义为1


所以,pForwardCompatpCatchableTypeArray32位程序中是地址,在64位程序中是偏移。


还记得EHExceptionRecord::EHParameters64位程序中有4个成员吗?第4个成员就是抛出异常对应的模块基址,用这个基址加上这里的偏移就得到了对应成员在内存中的位置。一定要记住这个结论,在分析64位程序的异常对象类型时会用到!


接下来看看关键的CatchableTypeArray类型的定义,摘录如下:


CatchableTypeArray


typedef const struct _s_CatchableTypeArray {
int nCatchableTypes;
#if _EH_RELATIVE_TYPEINFO
int arrayOfCatchableTypes[]; // Image relative offset of Catchable Types
#else
CatchableType* arrayOfCatchableTypes[];
#endif
} CatchableTypeArray;


nCatchableTypes记录了数组arrayOfCatchableTypes的数量。


arrayOfCatchableTypes记录了异常类型信息。同样的,在32位程序中是地址,在64位程序中是偏移。

说明:这里为什么使用数组呢?因为抛出的异常可能继承自某个基类。arrayOfCatchableTypes会把继承链上的所有类型信息按照从子类到基类的顺序记录下来。拿std::bad_alloc举例,它继承自std::exception。所以,nCatchableTypes的值为2arrayOfCatchableTypes[0]记录了std::bad_alloc的类型信息,arrayOfCatchableTypes[1]记录了std::exception的类型信息。


再来看看结构体CatchableType的定义,摘录如下:


CatchableType


typedef const struct _s_CatchableType {
unsigned int properties; // Catchable Type properties (Bit field)
#if _EH_RELATIVE_TYPEINFO
int pType; // Image relative offset of TypeDescriptor
#else
TypeDescriptor* pType; // Pointer to the type descriptor for this type
#endif
PMD thisDisplacement; // Pointer to instance of catch type within thrown object.
int sizeOrOffset; // Size of simple-type object or offset into
// buffer of 'this' pointer for catch object
PMFN copyFunction; // Copy constructor or CC-closure
} CatchableType;


我们只需要关注pType成员即可。同样的,在32位程序中是地址,在64位程序中是偏移。pType对应的类型是TypeDescriptor,接下来看看TypeDescriptor的定义。


TypeDescriptor


typedef struct TypeDescriptor
{
#if defined(_WIN64) || defined(_RTTI) || defined(BUILDING_C1XX_FORCEINCLUDE)
const void* pVFTable; // Field overloaded by RTTI
#else
unsigned long hash; // Hash value computed from type's decorated name
#endif
void* spare; // reserved, possible for RTTI
char name[]; // The decorated name of the type; 0 terminated.
} TypeDescriptor;


其中,name成员是经过名字改编后的异常类型,它是一个以结尾的字符串,可以在windbg中通过da查看。


源码有点乱,还是在windbg中看的直观舒服,还可以看到偏移。以下是32位和64位程序中对应的结构体定义:


关键结构


32 位关键结构


0:000> dt EHExceptionRecord
TestThrowException!EHExceptionRecord
+0x000 ExceptionCode : Uint4B
+0x004 ExceptionFlags : Uint4B
+0x008 ExceptionRecord : Ptr32 _EXCEPTION_RECORD
+0x00c ExceptionAddress : Ptr32 Void
+0x010 NumberParameters : Uint4B
+0x014 params : EHExceptionRecord::EHParameters //<----

0:000> dt EHExceptionRecord::EHParameters
TestThrowException!EHExceptionRecord::EHParameters
+0x000 magicNumber : Uint4B
+0x004 pExceptionObject : Ptr32 Void
+0x008 pThrowInfo : Ptr32 _s_ThrowInfo //<----

0:000> dt _s_ThrowInfo
TestThrowException!_s_ThrowInfo
+0x000 attributes : Uint4B
+0x004 pmfnUnwind : Ptr32 void
+0x008 pForwardCompat : Ptr32 int
+0x00c pCatchableTypeArray : Ptr32 _s_CatchableTypeArray //<----

0:000> dt _s_CatchableTypeArray
TestThrowException!_s_CatchableTypeArray
+0x000 nCatchableTypes : Int4B
+0x004 arrayOfCatchableTypes : [0] Ptr32 _s_CatchableType //<----

0:000> dt _s_CatchableType
TestThrowException!_s_CatchableType
+0x000 properties : Uint4B
+0x004 pType : Ptr32 TypeDescriptor //<----
+0x008 thisDisplacement : PMD
+0x014 sizeOrOffset : Int4B
+0x018 copyFunction : Ptr32 void

0:000> dt TypeDescriptor
TestThrowException!TypeDescriptor
+0x000 hash : Uint4B
+0x004 spare : Ptr32 Void
+0x008 name : [0] Char //<====


64 位关键结构


0:000> dt EHExceptionRecord
VCRUNTIME140!EHExceptionRecord
+0x000 ExceptionCode : Uint4B
+0x004 ExceptionFlags : Uint4B
+0x008 ExceptionRecord : Ptr64 _EXCEPTION_RECORD
+0x010 ExceptionAddress : Ptr64 Void
+0x018 NumberParameters : Uint4B
+0x020 params : EHExceptionRecord::EHParameters //<----

0:000> dt EHExceptionRecord::EHParameters
VCRUNTIME140!EHExceptionRecord::EHParameters
+0x000 magicNumber : Uint4B
+0x008 pExceptionObject : Ptr64 Void
+0x010 pThrowInfo : Ptr64 _s_ThrowInfo //<----
+0x018 pThrowImageBase : Ptr64 Void

0:000> dt _s_ThrowInfo
VCRUNTIME140!_s_ThrowInfo
+0x000 attributes : Uint4B
+0x004 pmfnUnwind : Int4B
+0x008 pForwardCompat : Int4B
+0x00c pCatchableTypeArray : Int4B //<----

0:000> dt CatchableTypeArray
VCRUNTIME140!CatchableTypeArray
+0x000 nCatchableTypes : Int4B
+0x004 arrayOfCatchableTypes : [0] Int4B //<----

0:000> dt CatchableType
VCRUNTIME140!CatchableType
+0x000 properties : Uint4B
+0x004 pType : Int4B //<----
+0x008 thisDisplacement : PMD
+0x014 sizeOrOffset : Int4B
+0x018 copyFunction : Int4B

0:000> dt TypeDescriptor
VCRUNTIME140!TypeDescriptor
+0x000 pVFTable : Ptr64 Void
+0x008 spare : Ptr64 Void
+0x010 name : [0] Char //<====

划重点:务必记住以上结构体的定义,尤其是关键字段的偏移。这是解析的依据!


解析方法小结


1.先找到EHParameters类型的对象(可省略此步)


可以通过 RaiseException() 的第四个参数查找。

在 32 位程序中,定位方法非常简单,可以直接查看 RaiseException() 的第 4 个参数,ebp+0x14。

在 x64 位中可以通过 _CxxThrowException() 的 rsp + 0x28 定位。因为在调用 RaiseException() 的时候, _CxxThrowException() 会把此参数存在自己栈帧中 rsp + 0x28 的位置。

2.再找到ThrowInfo类型的对象

解析 EHParameters 中的第 3 个成员 pThrowInfo,在 32 位程序中偏移是 0x8,在 64 位程序中偏移是 0x10。
说明: 还有两种查看方法: 
1、对于 32 位程序可以通过 _CxxThrowException() 对应栈帧的第 2 个参数(ebp+c)直接查看。 
2、如果有 vcruntimexxx.dll 的调试符号,可以直接切到 _CxxThrowException() 对应的栈帧,windbg 会自动帮忙列出对应的值。

3.再找到CatchableTypeArray类型的对象

解析 ThrowInfo 的第 4 个成员 pCatchableTypeArray,其偏移是 0xc(32位 64 位通用)。

需要注意的是,此成员在 32 位程序中是地址;在 64 位程序中是偏移,需要加上镜像基址得到最终的地址。

4.再找到CatchableType类型的对象

解析 CatchableTypeArray 的第 2 个成员 arrayOfCatchableTypes ,偏移是 0x4(32位 64 位通用)。

该成员记录了 CatchableType 数组的首地址或者偏移。

需要注意的是,此成员在 32 位程序中是地址;在 64 位程序中是偏移,需要加上镜像基址得到最终的地址。

说明: 第 1 个成员 nCatchableTypes 记录了 CatchableType 数组的个数。


5.再找到TypeDescriptor类型的对象

解析 CatchableType 数组中的每个对象(其实,只需要解析第一个即可)。重点关注第 2 个成员 pType,偏移是 0x4(32位 64 位通用)。

需要注意的是,此成员在 32 位程序中是地址;在 64 位程序中是偏移,需要加上镜像基址得到最终的地址。

6.最后找到异常类型名

解析 TypeDescriptor 对象,只需要关注第 3 个成员 name 成员即可,在 32 位程序中偏移是 0x8,在 64 位程序中偏移是 0x10。

它是一个以 结尾的字符串,可以在 windbg 用 da 显示其内容。

总结


throw对应的实现函数是_CxxThrowException()函数,该函数定义在throw.cpp中,可以查看源码。


_CxxThrowException()内部会调用RaiseException(),调用时传递的错误码是0xe06d7363(对应的字符是.msc)。


◆【关键结构】中的结构体是解析时的依据,务必要熟悉。


◆【解析方法小结】中总结的方法是通用方法,适用于任何情况。在实际解析过程中还可以利用虚表等其它相关信息进行解析。


◆在解析过程中,需要注意的是在64位程序中,很多成员变量都是相对于发生异常模块的偏移,而不是直接可用的地址,需要先把偏移转换成虚拟地址后再使用。




实战篇


上边介绍了定位抛出异常的理论知识,接下来会通过几个实例介绍各种情况下的定位方法。有调试符号如何定位?没有调试符号如何定位?32位程序如何定位?64位程序又该如何定位?


其实,32位程序和64位程序定位过程大同小异,只不过在解析过程中需要注意,很多关键字段在64位程序中是偏移,需要加上模块基址得到虚拟地址后才能使用,而在32位程序中对应的字段就是虚拟地址,可以直接使用。


没有调试符号的时候定位异常类型会比较困难,需要根据上一篇文章中总结的步骤一步步的找到异常类型。有调试符号的情况会比较容易,有很多简便的查看方法。


一起来实战吧!


在开始实战之前,把相关结构体再贴一下,方便参考。


32 位关键结构


0:000> dt EHExceptionRecord
TestThrowException!EHExceptionRecord
+0x000 ExceptionCode : Uint4B
+0x004 ExceptionFlags : Uint4B
+0x008 ExceptionRecord : Ptr32 _EXCEPTION_RECORD
+0x00c ExceptionAddress : Ptr32 Void
+0x010 NumberParameters : Uint4B
+0x014 params : EHExceptionRecord::EHParameters //<----

0:000> dt EHExceptionRecord::EHParameters
TestThrowException!EHExceptionRecord::EHParameters
+0x000 magicNumber : Uint4B
+0x004 pExceptionObject : Ptr32 Void
+0x008 pThrowInfo : Ptr32 _s_ThrowInfo //<----

0:000> dt _s_ThrowInfo
TestThrowException!_s_ThrowInfo
+0x000 attributes : Uint4B
+0x004 pmfnUnwind : Ptr32 void
+0x008 pForwardCompat : Ptr32 int
+0x00c pCatchableTypeArray : Ptr32 _s_CatchableTypeArray //<----

0:000> dt _s_CatchableTypeArray
TestThrowException!_s_CatchableTypeArray
+0x000 nCatchableTypes : Int4B
+0x004 arrayOfCatchableTypes : [0] Ptr32 _s_CatchableType //<----

0:000> dt _s_CatchableType
TestThrowException!_s_CatchableType
+0x000 properties : Uint4B
+0x004 pType : Ptr32 TypeDescriptor //<----
+0x008 thisDisplacement : PMD
+0x014 sizeOrOffset : Int4B
+0x018 copyFunction : Ptr32 void

0:000> dt TypeDescriptor
TestThrowException!TypeDescriptor
+0x000 hash : Uint4B
+0x004 spare : Ptr32 Void
+0x008 name : [0] Char //<====


64 位关键结构


0:000> dt EHExceptionRecord
VCRUNTIME140!EHExceptionRecord
+0x000 ExceptionCode : Uint4B
+0x004 ExceptionFlags : Uint4B
+0x008 ExceptionRecord : Ptr64 _EXCEPTION_RECORD
+0x010 ExceptionAddress : Ptr64 Void
+0x018 NumberParameters : Uint4B
+0x020 params : EHExceptionRecord::EHParameters //<----

0:000> dt EHExceptionRecord::EHParameters
VCRUNTIME140!EHExceptionRecord::EHParameters
+0x000 magicNumber : Uint4B
+0x008 pExceptionObject : Ptr64 Void
+0x010 pThrowInfo : Ptr64 _s_ThrowInfo //<----
+0x018 pThrowImageBase : Ptr64 Void

0:000> dt _s_ThrowInfo
VCRUNTIME140!_s_ThrowInfo
+0x000 attributes : Uint4B
+0x004 pmfnUnwind : Int4B
+0x008 pForwardCompat : Int4B
+0x00c pCatchableTypeArray : Int4B //<----

0:000> dt CatchableTypeArray
VCRUNTIME140!CatchableTypeArray
+0x000 nCatchableTypes : Int4B
+0x004 arrayOfCatchableTypes : [0] Int4B //<----

0:000> dt CatchableType
VCRUNTIME140!CatchableType
+0x000 properties : Uint4B
+0x004 pType : Int4B //<----
+0x008 thisDisplacement : PMD
+0x014 sizeOrOffset : Int4B
+0x018 copyFunction : Int4B

0:000> dt TypeDescriptor
VCRUNTIME140!TypeDescriptor
+0x000 pVFTable : Ptr64 Void
+0x008 spare : Ptr64 Void
+0x010 name : [0] Char //<====


接下来,先介绍无调试符号时的定位方法,然后再介绍有调试符号时的定位方法。


无调试符号


如果没有导致异常的模块的调试符号,定位过程会比较复杂,需要根据上一篇文章中总结的方法一步步定位。此方法的坏处是麻烦,好处是比较通用,任何情况下都可以使用。


64 位程序


1.获取EHParameters的地址。_CxxThrowException栈帧的rsp+0x28指向了EHParameters


调试实战 | 从转储文件找出抛出的异常


由上图红色高亮部分可知EHParameters的地址是000000b9f2effba8


1.获取ThrowInfo的地址。EHParameters + 0x10的位置保存了ThrowInfo的地址,EHParameters + 0x18的位置保存了异常模块基址。


调试实战 | 从转储文件找出抛出的异常


由上图红色高亮部分可知ThrowInfo的地址是00007ff71c542a20,异常模块基址是00007ff71c540000

说明:如果有vcruntimexxx.dll的调试符号,可以跳过前两步,直接切换到_CxxThrowException对应的栈帧即可得到ThrowInfo的地址和异常模块基址。


1.获取CatchableTypeArray的地址。ThrowInfo + 0xc保存了CatchableTypeArray的偏移。


调试实战 | 从转储文件找出抛出的异常


由上图红色高亮部分可知CatchableTypeArray的偏移是000029b8

异常模块基址是00007ff71c540000,所以CatchableTypeArray的地址是00007ff71c540000 + 000029b8 = 00007ff71c5429b8


1.获取CatchableType的地址。CatchableTypeArray + 0x04保存了第一个CatchableType对象的偏移CatchableTypeArray + 0x08保存了第二个CatchableType对象的偏移,以此类推。


调试实战 | 从转储文件找出抛出的异常


由上图可知,一共有两个CatchableType类型的对象,第一个偏移是000029d0,第二个偏移是000029f8


异常模块基址是00007ff71c540000,所以第一个CatchableType对象的地址是00007ff71c540000 + 000029d0 = 00007ff71c5429d0


1.获取TypeDescriptor的地址。CatchableType + 0x04保存了TypeDescriptor的偏移。


调试实战 | 从转储文件找出抛出的异常

由上图红色高亮部分可知TypeDescriptor的偏移是00004058

异常模块基址是00007ff71c540000,所以TypeDescriptor的地址是00007ff71c540000 + 00004058 = 00007ff71c544058


1.获取异常类型名。TypeDescriptor + 0x10保存了编码后的异常类型名。


调试实战 | 从转储文件找出抛出的异常


从上图可知,异常类型是.?AVbad_alloc@std@@,也就是std::bad_alloc


32 位程序


32位程序和64位程序定位过程大同小异,只需要把64位程序定位过程中的偏移值当成地址使用即可。这里就不赘述了,参考下图:


调试实战 | 从转储文件找出抛出的异常


其实,对于32位程序,如果有vcruntimexxx.dll对应的符号,还有一种极其简单的方法,在windbg中输入dt -r3 ThrowInfo address,如下图:


调试实战 | 从转储文件找出抛出的异常


有调试符号


对于有调试符号的情况,不仅可以使用无调试符号的定位方法,还可以使用更简单的方法查看——通过查看pExceptionObject对象的虚函数表来推断对应的对象类型。


在测试程序中,pExceptionObject的地址是0x000000b9f2effc00,可以在windbg中执行dps 0x000000b9f2effc00即可查看异常对象对应的虚表,如下图:


调试实战 | 从转储文件找出抛出的异常


从上图可知,异常类型是std::bad_alloc,其虚表内容都是其基类(std::exception)的虚函数,因为std::bad_alloc没重写任何虚函数,也没新增任何虚函数。


为什么没调试符号的时候不能用这个方法呢?因为没有调试符号的情况下,从dps的输出结果中看不到关键的虚表名称,也就不能推断出具体的异常类型了。


调试实战 | 从转储文件找出抛出的异常


亲自动手


对应的程序源码工程文件及对应的转储文件已经上传到我的个人仓库了,感兴趣的小伙伴儿可以从以下链接自行下载:

https://gitee.com/bianchengnan/my-blog-stuff/tree/master/search-throwing-exception-from-dump-file-part2/TestThrowException


还有一个更真实的转储文件,可以实战一把。因为比较大,我传到百度云了,可以到这里下载:

https://pan.baidu.com/s/1K7FzsseMlU6kmrMwm3jn4Q?pwd=8t47


总结


◆获取通过throw抛出的异常的突破点在_CxxThrowException()函数,该函数有源码,涉及到的关键结构体都有源码可以查询。


◆查找throw抛出的异常,关在是掌握对应的数据结构,务必要把关键的数据结构牢记于心。


◆如果有调试符号,还可以直接查看pExceptionObject对象的虚函数表来进行推断。


◆在解析过程中,需要注意的是在64位程序中,很多成员变量都是相对于发生异常模块的偏移,而不是直接可用的地址,需要先把偏移转换成虚拟地址后再使用。


参考资料

vs源码




调试实战 | 从转储文件找出抛出的异常


看雪ID:编程难

https://bbs.kanxue.com/user-home-873494.htm

*本文为看雪论坛优秀文章,由 编程难 原创,转载请注明来自看雪社区

调试实战 | 从转储文件找出抛出的异常

# 往期推荐

1、区块链智能合约逆向-合约创建-调用执行流程分析

2、在Windows平台使用VS2022的MSVC编译LLVM16

3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱

4、为什么在ASLR机制下DLL文件在不同进程中加载的基址相同

5、2022QWB final RDP

6、华为杯研究生国赛 adv_lua


调试实战 | 从转储文件找出抛出的异常


调试实战 | 从转储文件找出抛出的异常

球分享

调试实战 | 从转储文件找出抛出的异常

球点赞

调试实战 | 从转储文件找出抛出的异常

球在看

原文始发于微信公众号(看雪学苑):调试实战 | 从转储文件找出抛出的异常

版权声明:admin 发表于 2024年1月20日 下午6:00。
转载请注明:调试实战 | 从转储文件找出抛出的异常 | CTF导航

相关文章