解决Windows 11 ARM64符号解析问题的技术探索

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

Tyranid’s Lair: Windows 11 ARM64上的符号解析问题

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

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

1
2
3
4
5
6
7
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上会得到不正确的结果。过去我遇到过类似的问题,这些问题是由于解析过程中使用的内部结构的变化引起的。微软为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上一个新特性的产物,该特性旨在简化x64可执行文件的仿真,即ARM64X。这是一个巧妙(或糟糕)的技巧,以避免在系统上需要单独的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公共接口的更好方法,我将不胜感激 :)

发布者:tiraniddo

时间:15:09

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