Windows 11 ARM64符号解析问题深度解析

本文详细探讨了在Windows 11 ARM64系统上使用OleViewDotNet工具时遇到的符号解析问题,分析了ARM64X特性导致的符号重复现象,并提供了通过DBGHELP和DIA库解决符号歧义的完整技术方案。

Windows 11 ARM64符号解析问题

这是一篇关于我在开发OleViewDotNet工具时遇到的一个问题以及如何解决它的简短博客文章。虽然我不确定是否采用了最佳方法,但如果其他人遇到类似问题,这篇文章可能会有所帮助。

OleViewDotNet能够解析进程中的内部COM结构,并显示重要信息,例如进程当前导出的IPID列表和访问安全描述符。

1
2
3
4
5
6
7
8
PS C:\> $p = Get-ComProcess -ProcessId $pid
PS C:\> $p.Ipids

IPID                                   Interface Name  PID    Process Name
----                                   --------------  ---    ------------
00008800-4bd8-0000-c3f9-170a9f197e11  IRundown        19416  powershell.exe
00009401-4bd8-ffff-45b0-a43d5764a731  IRundown        19416  powershell.exe
0000a002-4bd8-5264-7f87-e6cbe82784aa  IRundown        19416  powershell.exe

为了完成此任务,我们需要访问COMBASE DLL的符号,以便解析哈希表和其他运行时工件的各种根指针。解析进程信息的大部分代码都在COMProcessParser类中,该类使用DBGHELP库将符号解析为地址。我的代码还支持一种机制,将解析的指针缓存到文本文件中,随后可以在具有相同COMBASE DLL的其他系统上使用,而无需下载30+ MiB的符号文件。

这在Windows 11 x64上运行良好,但我注意到在ARM64上会得到不正确的结果。过去我遇到过类似的问题,这些问题是由于解析过程中使用的内部结构发生变化所致。Microsoft为COMBASE提供私有符号,因此很容易检查Windows 11的x64和ARM64版本之间的结构是否存在差异。我没有发现任何差异。无论如何,我注意到这也影响了简单的值,例如符号gSecDesc包含指向COM访问安全描述符的指针。然而,在读取该指针时,它始终为NULL,即使它应该已被初始化。

更令人困惑的是,当我在WinDBG中检查该符号时,它显示指针已正确初始化。但是,如果我在WinDBG中使用x命令搜索预期的符号,会发现一些有趣的事情:

1
2
3
0:010> x combase!gSecDesc
00007ffa`d0aecb08 combase!gSecDesc = 0x00000000`00000000
00007ffa`d0aed1c8 combase!gSecDesc = 0x00000180`59fdb750

从输出中我们可以看到有两个gSecDesc符号,而不是一个。第一个值为NULL,第二个具有初始化的值。当我检查我的符号解析器返回的地址时,它是第一个,而WinDBG更智能,返回第二个。这到底是怎么回事?

这是Windows 11在ARM64上的一项新功能ARM64X的产物,旨在简化x64可执行文件的模拟。这是一个巧妙(或糟糕)的技巧,以避免在系统上需要单独的ARM64和x64二进制文件。相反,ARM64和x64兼容的代码(称为ARM64EC)被合并到单个系统二进制文件中。在某些情况下,这意味着全局数据结构需要重复,一次用于ARM64代码,一次用于ARM64EC代码。在这种情况下,似乎不应该有两个独立的全局数据值,因为指针就是指针,但我想可能存在边缘情况,其中并非如此,并且仅复制值以避免冲突更简单。细节非常有趣,有几个地方对此进行了逆向工程,我至少推荐这篇博客文章。

我的代码使用SymFromName API查询符号地址,这只会返回它找到的第一个符号,在这种情况下是ARM64EC的符号,该符号在ARM64进程中未初始化。我不知道这是否是DBGHELP中的一个错误,也许它应该尝试返回与二进制文件机器类型匹配的符号,或者也许我使用不当。无论如何,我需要一种获取正确符号的方法,但在查阅DBGHELP库后,没有明显的方法来区分这两者。然而,显然WinDBG可以做到,所以一定有办法。

经过一番搜索,我发现调试接口访问(DIA)库有一个IDiaSymbol::get_machineType方法,该方法返回符号的机器类型,可以是ARM64(0xAA64)或ARM64EC(0xA641)。不幸的是,我特意使用了DBGHELP,因为它默认安装在Windows上,而DIA需要单独安装。在DBGHELP库中似乎没有等效的方法。

幸运的是,在DBGHELP库中寻找解决方案时,机会出现了。在DBGHELP内部(至少是近期版本),它使用DIA库的私有副本。这本身并没有太大帮助,但该库导出了几个私有API,允许调用者查询当前的DIA状态。例如,有SymGetDiaSession API,它返回IDiaSession接口的实例。从该接口,您可以查询IDiaSymbol接口的实例,然后查询机器类型。我不确定DBGHELP内部的DIA版本与公开发布版本的兼容性如何,但就我的目的而言,它足够兼容。

2024/04/26更新:有人向我指出,机器类型存在于SYMBOL_INFO::Reserved[1]字段中,因此您不需要使用DIA接口进行这整个方法。但重点仍然是,您需要在ARM64平台上枚举符号,因为可能存在多个符号,并且您仍然需要检查机器类型。

为了解决这个问题,OleViewDotNet中的代码在ARM64系统上采取以下步骤:

  • 不调用SymFromName,而是枚举名称的所有符号。
  • 调用SymGetDiaSession获取IDiaSession接口的实例。
  • 调用IDiaSession::findSymbolByVA方法获取符号的IDiaSymbol接口实例。
  • 调用IDiaSymbol::get_machineType方法获取符号的机器类型。
  • 根据上下文过滤符号,例如,如果解析ARM64进程,则使用ARM64符号。

这比我认为需要的要复杂得多,但我还没有找到替代方法。理想情况下,DBGHELP中的SYMBOL_INFO结构应包含机器类型字段,但我想现在很难更改接口。执行机器类型查询的相对简单的代码在这里。如果有人找到了仅使用DBGHELP公共接口的更好方法,我将不胜感激!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计