这是Francesco Cagnin有关macOS内核调试的第二篇文章。在上一篇文章中,作者定义了本篇文章中使用的大多数术语,描述了如何为macOS内核实施内核调试,并讨论了可用工具的局限性。本篇文章中,我们就介绍LLDBagility,这是上文中为macOS内核实施内核调试的解决方案,可提供更轻松,更实用的macOS调试体验。
在当前情况下,调试macOS内核(XNU)不太实用,尤其是考虑到诸如必须安装调试内核构建以及无法设置监测点或暂停执行的内核调试器的不便之处。为了改善这种情况,作者开发了LLDBagility ,这是一种借助快速调试协议(FDP,在本文后面进行介绍)调试macOS虚拟机的工具。最重要的是,LLDBagility实现了一组新的LLDB命令,这些命令允许调试器执行以下操作:
1.连接到运行中的macOS VirtualBox虚拟机并秘密地调试其内核,而无需更改客户操作系统(例如,无需开发或调试内核, boot-args修改或禁用SIP),并且只需对虚拟机的配置进行最小的更改;
2.随时中断(然后恢复)客户内核的执行;
3.即使在启动过程开始时,也可以在内核代码中的任何位置设置硬件断点;
4.设置在指定存储位置的读取或写入访问时触发的硬件观察点;
5.保存并在几秒钟内恢复虚拟机的状态。
这些命令旨在与LLDB中已经可用的命令一起使用,例如内存/寄存器读/写、断点(软件),步骤等。此外,如果调试后的内核附带了其内核调试工具包(甚至可能没有,后面会讨论),那么绝大多数lldbmacros都可以正常工作。
下面,我们首先简要介绍什么是FDP,以及它如何增强VirtualBox来实现虚拟机自省。然后,我们研究LLDBagility如何利用这些新功能透明地将LLDB连接到macOS虚拟机,并演示一个简单的内核调试会话。最后,我们提出了一些在缺乏调试工具包的内核构建中使用现有lldbmacros的想法。
通过快速调试协议对虚拟机进行自省
快速调试协议是一个轻量级、快速和高效的调试API,用于虚拟机的自省性检查,使用C语言编写,目前仅作为VirtualBox源代码[FDP]的补丁发布。该API的作用如下:
1.读取和写入虚拟机寄存器和内存;
2.设置和取消设置硬件/软件断点和观察点;
3.暂停、恢复和单步执行虚拟机;
4.保存并恢复虚拟机状态;
FDP的隐身性来自对虚拟机扩展页面表的操作(从客户系统中很难检测到的操作),而速度是FDP和虚拟机之间使用共享内存的结果(在某些情况下达到一百万每秒的内存读取次数)。
除了底层C接口之外,该API还提供了Python绑定(PyFDP),可用于概念的快速证明和LLDBagility等大型项目。下面是一个示例脚本,下面的脚本在每次系统调用时都会中断:
#!/usr/bin/env python2 from PyFDP.FDP import FDP # Open the debuggee VM by its name fdp = FDP("18E226") # Pause the VM to install a breakpoint fdp.Pause() # Install a hardware breakpoint (very fast and stealth) UnixSyscall64 = 0xffffff80115bae84 fdp.dr0 = UnixSyscall64 fdp.dr7 = 0x403 # Resume the execution after the breakpoint installation fdp.Resume() while True: # Wait for a breakpoint hit fdp.WaitForStateChanged() print "%x" % (fdp.rax) # Jump over the breakpoint fdp.SingleStep() # Resume the debuggee fdp.Resume()
更多关于FDP的细节可以在Winbagility [WinbagilitySSTIC2016]的原始文章中找到,这是LLDBagility在Windows和WinDbg上的老版本。
将LLDB附加到虚拟机
如前一篇文章中详细介绍的那样,在常规的两台设备调试期间,LLDB通过向其内部KDP存根发送命令来与被调试对象的macOS内核进行交互,该存根本身是内核的一部分,然后可以检查和更改根据要求确定设备的状态,并传达结果。 LLDBagility背后的关键思想是使用虚拟机管理程序级别的FDP提供的(虚拟)设备自省和修改的类似功能来复制此类代理提供的功能,以便可以用内核外部的等效替代项替换内核的调试存根来调试设备。此外,通过保持与KDP协议的兼容性,这个新的存根可以利用LLDB对macOS内核的现有支持,而无需在任何方面修改调试器,因为与内核中的KDP服务器通信没有区别。在这方面,Ian Beer为他的iOS内核调试器使用了类似的解决方案。
这种方法具有两个显着的优点:消除了在内核中启用KDP进行调试的必要性,以及使用其他功能扩展LLDBagility的存根的可能性,这意味着在第一篇文章中描述的经典调试方法的所有局限性都可以解决。首先,由于不再需要设置XNU进行调试,因此无需修改NVRAM并可能禁用SIP。同样,也无需安装DEBUG或DEVELOPMENT版本。其次,与KDP代码有关的所有系统范围的副作用都消失了,这意味着,例如,rootkit对调试器的检测更加困难。第三,现在可以在内核引导过程中在KDP存根初始化之前开始调试。最后,借助FDP对设备进行完全控制,可以轻松实现硬件断点,观察点以及用于在调试器的命令下暂停内核的机制。
LLDBagility工具的详细概述
如上所述,LLDBagility的前端是一组新的LLDB命令,这些命令在lldbagility.py中实现(来自[LLDBagility100]):
1.fdp-attach或简称为fa,用于将调试器连接到正在运行的macOS VirtualBox虚拟机;
2.fdp-hbreakpoint或fh,用于设置和取消设置读/写/执行硬件断点;
3.fdp-interrupt或fi,以暂停虚拟机的执行并将控制权返回给调试器(相当于已知的sudo dtrace -w -n"BEGIN { breakpoint(); }");
4.fdp-save或fs,保存虚拟机当前状态;
5.fdp-restore或fr,将虚拟机还原到上一次保存的状态。
另一方面,后端由以下部分组成:
1.kdpserver.py,LLDB连接到的新KDP服务器(XNU的替代品),其工作是将调试器收到的KDP请求转换为虚拟机的命令并发送回响应和异常通知(例如断点命中);
2. stubvm.py,用于抽象常见的虚拟机操作并通过FDP实现。
LLDB与要调试的虚拟机之间的连接是在fdp-attach上建立的。当用户执行此命令时,LLDBagility会进行以下操作:
1.实例化用于与虚拟机交互的PyFDP客户端(lldbagility.py#L73);
2.在辅助线程(lldbagility.py#L93)上启动其自己的KDP服务器,等待请求(kdpserver.py#L182);
3.执行LLDB命令kdp-remote将调试器连接到刚刚启动的KDP服务器(lldbagility.py#L99,第一篇博客文章中详细介绍了该过程),从而开始了调试过程。
最后一个有趣的注意事项是,将KDP请求转换为相应的FDP命令不足以使lldbmacros正常工作。事实上,此类宏依赖于使用填充了[xnu49032212]的kdp结构的某些字段(osfmk/kdp/kdp_internal.h#L41/#L55, tools/lldbmacros/core/operating_system.py#L777)的能力。仅在启用其KDP存根时(例如osfmk / kdp / kdp_udp.c#L1349)由内核执行,LLDBagility通过将从调试器读取的内存挂接到kdp结构并返回一个修补的内存块,其中必要的字段(例如kdp_thread)被填满了正确的值(stubvm.py#L242)。
示例演示:该视频演示了使用LLDBagility进行的快速调试会话过程。
综上所述,具体过程如下:
1.启动LLDB并将 fdp-attach附加到后台运行的名为“18E226”的虚拟机中;
2.执行lldbmacros(从.lldbinit自动加载)showcurrentthreads和showrights;
3.使用继续命令恢复虚拟机的执行,然后通过fdp-interrupt快速中断虚拟机;
4.虚拟机的状态首先通过fdp-save保存,然后通过简单地恢复设备执行来进行修改,最后通过fdp-restore恢复。
5.在&(*(boot_args*)PE_state.bootArgs).flags上使用fdp-hbreakpoint设置读取和写入观察点,然后该观察点立即在csr_check()中触发。
在内核之间重用调试信息
在最后一节中,我们探讨了有关使用lldbmacros调试内核的一些想法,这些想法来自为不同内核版本发布的内核调试工具包。这里提出的方法有其明显的局限性,但它们也被证明是有效的,值得一试。有关代码和示例,请参见KDKutils/ 。
如第一篇文章所述,大多数内核构建都缺少KDK,这使得调试更加困难,因为在这种情况下,类型信息和lldbmacros都是不可用的。幸运的是,这些宏正常工作所需的调试信息只包含相对较少的全局变量和结构,例如版本(config/version.c#L40)和kdp,,它们的定义在一个内核版本与另一个内核版本之间变化不大(或者根本没有变化)。在这种情况下,可以尝试重用来自可用KDK的调试信息,这种办法似乎是合理的,以便现有lldbmacros的大多数功能都可以与缺少自己的调试工具包的内核一起使用。例如,假设我们有调试信息(即在编译过程中生成的DWARF文件)和lldbmacros用于内核版本A,但没有用于版本B。目标是加载并使用A的lldbmacros在LLDB中调试B。可以为调试器提供内核B的“伪” DWARF文件,该文件是通过对A进行修补而创建的:其Mach-O UUID匹配B而不是A,并且宏使用的所有符号的加载地址都匹配运行时B的内存中相同符号的虚拟加载地址。最后,这两个小的更改应该足以使大多数宏正常工作。
当lldbmacros所使用的符号在两个内核的代码中具有非常相似的定义时,刚刚讨论的简单方法就可以很好地工作了,当它们是连续的发行版时自然也是如此。如果这种情况没有发生,例如,当在线程结构中添加或删除字段时,则上述解决方法就不灵了,因为还需要修改DWARF文件来更改类型定义;但这并不容易。对于这种情况,就需要从头开始创建伪造的DWARF,方法是首先解析源DWARF文件以提取有关lldbmacros使用的结构的类型信息,然后生成包含类型定义的C源文件。可以手动轻松地编辑这些源代码,根据需要修改类型,然后进行编译,以便创建新的DWARF文件。在将它加载到LLDB中之前,必须如上所述对DWARF文件进行修补,以更改其UUID和加载地址以匹配要调试的内核。
总结
在这篇文章中,我们介绍了LLDBagility,这是基于虚拟机自省的经典macOS内核调试的一个便捷替代方法。虽然还不完美,但我们的解决方案已将标准方法的所有当前限制进行了解除或大大减轻了这些限制,同时还提供了强大的附加功能,使调试更加实用。 LLDBagility在过去的几个月中已在Quarkslab 中得到了积极使用,并且已发布供其他人进行试验。