首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
11
8
调试实战 | 从转储文件找出抛出的异常 —— 理论
发表于: 2024-1-7 18:36
12621
调试实战 | 从转储文件找出抛出的异常 —— 理论
编程难
3
2024-1-7 18:36
12621
## 缘起 最近在分析转储文件时,遇到了一个由 `throw` 抛出的异常。尽管在 `windbg` 中使用 `!analyze -v` 迅速知道了异常码是 `0xe06d7363`(对应的 `ASCII` 码是 `.msc`),但是根据异常码并不能确定具体抛出来的是哪种异常。针对这种情况,确定具体的异常类型才有意义。 本篇文章会简单介绍与抛出异常相关的内容,包括关键的函数及结构体。下一篇文章会通过实例介绍几种典型情况(有调试符号 / 没有调试符号 / `32` 位程序 / `64` 位程序)下的定位方法。 > **说明:** 对源码不感兴趣的小伙伴而可以直接跳到【解析方法小结】查看结论。 ## 突破口 `throw` 关键字编译后对应的函数是 `_CxxThrowException()`,该函数内部会通过 `RaiseException()` 触发异常。`_CxxThrowException()` 是有源码可查的,我们可以从这个函数入手,先来熟悉下这个函数以及相关的结构体。 ## _CxxThrowException 该函数定义在 `vs` 自带的 `throw.cpp` 中,一般在 `crt\src\vcruntime\` 目录下。直接用 `everything` 搜索 `throw.cpp`,然后打开即可。`vs2019` 中的实现代码如下,有删减: ```c++ 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()` 的原型如下: ```c++ 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`。定义如下: ```c++ #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.params`。`ThisException` 的类型是 `EHExceptionRecord`,其定义如下: ## EHExceptionRecord ```c++ 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_TYPEINFO` 为 `0`,则包含 `3` 个成员,否则就会包含第 4 个成员 `pThrowImageBase`。 而 `_EH_RELATIVE_TYPEINFO` 在 `32` 位程序中是 `0`,在 `64` 位程序中是 `1`,定义如下: ```c++ #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` 的值是 `3`,`EHExceptionRecord::EHParameters` 刚好有 `3` 个成员,在 `64` 位程序中 `nNumberOfArguments` 的值是 `4`,`EHExceptionRecord::EHParameters` 刚好有 `4` 个成员。 `EHExceptionRecord::EHParameters` 中的 `pExceptionObject` 和 `pThrowInfo` 是查找异常类型的关键。 其中,`pExceptionObject` 是异常对象的地址,`pThrowInfo` 的类型是 `ThrowInfo`,用来描述异常对象的类型信息。一起来看看 `ThrowInfo` 的定义。 ## ThrowInfo ```c++ 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`。 所以,`pForwardCompat` 和 `pCatchableTypeArray` 在 `32` 位程序中是地址,在 `64` 位程序中是偏移。 还记得 `EHExceptionRecord::EHParameters` 在 `64` 位程序中有 `4` 个成员吗?第 `4` 个成员就是抛出异常对应的模块基址,用这个基址加上这里的偏移就得到了对应成员在内存中的位置。**一定要记住这个结论**,在分析 `64` 位程序的异常对象类型时会用到! 接下来看看关键的 `CatchableTypeArray` 类型的定义,摘录如下: ## CatchableTypeArray ```c++ 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` 的值为 `2`,`arrayOfCatchableTypes[0]` 记录了 `std::bad_alloc` 的类型信息,`arrayOfCatchableTypes[1]` 记录了 `std::exception` 的类型信息。 再来看看结构体 `CatchableType` 的定义,摘录如下: ## CatchableType ```c++ 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 ```c++ 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` 成员是经过名字改编后的异常类型,它是一个以 `\0` 结尾的字符串,可以在 `windbg` 中通过 `da` 查看。 源码有点乱,还是在 `windbg` 中看的直观舒服,还可以看到偏移。以下是 `32` 位和 `64` 位程序中对应的结构体定义: ## 关键结构 ### 32 位关键结构 ```c++ 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 位关键结构 ```c++ 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`。 它是一个以 `\0` 结尾的字符串,可以在 `windbg` 用 `da` 显示其内容。 ## 总结 - `throw` 对应的实现函数是 `_CxxThrowException()` 函数,该函数定义在 `throw.cpp` 中,可以查看源码。 - `_CxxThrowException()` 内部会调用 `RaiseException()`,调用时传递的错误码是 `0xe06d7363`(对应的字符是 `.msc`)。 - 【关键结构】中的结构体是解析时的依据,务必要熟悉。 - 【解析方法小结】中总结的方法是通用方法,适用于任何情况。在实际解析过程中还可以利用虚表等其它相关信息进行解析。 - 在解析过程中,需要注意的是在 `64` 位程序中,很多成员变量都是相对于发生异常模块的偏移,而不是直接可用的地址,需要先把偏移转换成虚拟地址后再使用。 ## 参考资料 `vs` 源码
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#基础知识
#开发技巧
收藏
・
11
点赞
・
8
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
一路南寻
为你点赞!
2025-4-3 02:20
PLEBFE
为你点赞~
2024-4-29 01:00
slcn
为你点赞~
2024-2-26 08:18
Grav1ty
为你点赞~
2024-1-20 22:30
frendguo
为你点赞~
2024-1-10 10:10
hkdong
为你点赞~
2024-1-9 17:48
yp太阳神
为你点赞~
2024-1-8 12:54
zhczf
为你点赞~
2024-1-7 23:30
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
4
)
秋狝
雪 币:
10075
活跃值:
(34136)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
1571
粉丝
39
关注
私信
秋狝
2
楼
感谢分享
2024-1-7 20:18
1
frendguo
雪 币:
771
活跃值:
(1293)
能力值:
( LV2,RANK:10 )
在线值:
发帖
2
回帖
15
粉丝
1
关注
私信
frendguo
3
楼
强啊
2024-1-10 10:11
0
fengyunabc
雪 币:
5357
活跃值:
(6162)
能力值:
( LV4,RANK:50 )
在线值:
发帖
11
回帖
631
粉丝
27
关注
私信
fengyunabc
1
4
楼
感谢分享
2024-1-20 18:09
0
goose007
雪 币:
375
活跃值:
(1579)
能力值:
( LV2,RANK:10 )
在线值:
发帖
11
回帖
27
粉丝
1
关注
私信
goose007
5
楼
6B
2026-1-12 14:22
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
编程难
3
64
发帖
156
回帖
360
RANK
关注
私信
他的文章
[原创]调试技巧 | 从 Win7 到 Win10,DLL引用计数查看全攻略
2816
[原创]调试实战 | 使用 GFlags 与 WinDbg 定位 VS2022 “重复释放” 引发的崩溃
2304
[原创]内存都去哪了?探究 VirtualAlloc 分配背后被“浪费”的 60KB
1779
[原创]调试实战 | 记一次有教益的递归栈查看(续)
11445
[原创]调试实战 | 记一次有教益的递归栈查看
11602
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部