首页
社区
课程
招聘
[原创]调试实战 | 从转储文件找出抛出的异常 —— 实战
2024-1-11 07:38 5981

[原创]调试实战 | 从转储文件找出抛出的异常 —— 实战

2024-1-11 07:38
5981

缘起

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

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

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

一起来实战吧!

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

32 位关键结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 位关键结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

    _CxxThrowException 对应栈帧的 rsp000000b9f2effb80。在 windbg 中输入 ? 000000b9f2effb80 + 0x28,如下图:

view-address-of-EHParameters

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

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

    windbg 中输入 dq 000000b9f2effba8,如下图:

view-ThrowInfo-imagebase

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

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

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

    windbg 中输入 dd 00007ff71c542a20,如下图:

view-throwinfo

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

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

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

    windbg 中输入 dd 00007ff71c5429b8,如下图:

view-CatchableTypeArray

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

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

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

    windbg 中输入 dd 00007ff71c5429d0,如下图:

view-CatchableType

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

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

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

    windbg 中输入 da 00007ff71c544058 + 0x10,如下图:

view-TypeDescriptor

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

32 位程序

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

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

有调试符号

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

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

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

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

亲自动手

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

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 源码


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞3
打赏
分享
最新回复 (1)
雪    币: 19323
活跃值: (28938)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-1-11 09:33
2
1
感谢分享
游客
登录 | 注册 方可回帖
返回