开启Win10RS4ARM64远程内核调试之旅

author : wup and suezi of IceSword Lab , Qihoo 360


  今年6月,微软联合一线笔记本厂商正式发布了搭载高通骁龙处理器的Windows 10笔记本产品。作为主角的Win10 ARM64,自然亮点无数,对PC设备厂商也是各种利好。实际上,为了与厂商同步发布安全防护产品,IceswordLab的小伙伴早已将底层驱动程序集移植到了Win10 ARM64平台上,笔者也因此积累了一些有趣的内核调试方法。在x86平台使用vmware等虚拟机软件搭建远程内核调试环境是非常方便有效的办法,但目前Win10 ARM64平台没有这样的虚拟机软件,于是笔者利用qemu模拟器DIY一个。

0x0 准备试验环境

物理机系统环境 :Windows10 RS4 x64
虚拟化软件qemu : qemu-w64-setup-20180519.exe
虚拟机系统环境 :Windows10 RS4 ARM64
UEFI 模块 : Linaro 17.08 QEMU_EFI.fd
WINDBG :WDK10 (amd64fre-rs3-16299)附带的WinDBG

0x1 qemu远程内核调试开启失败

  在qemu环境下,我们使用Linaro.org网站提供的针对QEMU(AARCH64)的1708版的UEFI文件QEMU_EFI.fd启动Win10ARM64的系统,并使用bcdedit修改qemu模拟器里的Win10ARM64的启动配置以实现远程内核调试。配置如下图,

我们遇到了两个问题:
(1) 以“-serial pipe:com_1”参数启动qemu模拟器,qemu会被卡住,导致虚拟机系统无法启动;
(2)无论是否开启了基于串口的远程内核调试,系统内核加载的都是kd.dll而非预期的kdcom.dll;

对于问题(1),我们利用qemu串口转发功能,开发一个代理程序:建立一个namedpipe等待windbg的连接,并建立与qemu串口socket服务器的连接,从而实现将pipe上读取(ReadFile)的数据写入(send)到socket、将socket上读取(recv)的数据写入(WriteFile)到pipe。如此我们解决了问题(1)。
至于问题(2),对比VMWare里用UEFI方式部署的Win10RS4x64,不开启内核调试时系统加载的是kd.dll,开启内核调试时系统加载的是kdcom.dll,下面对其进一步分析。

0x2 系统提供的kdcom.dll存在问题

  在Win10RS4ARM64安装镜像的预置驱动里,无法找到serial.sys这个经典的串口驱动;而Win10ARM64笔记本的串口设备是存在的,且串口驱动是高通官方提供的。实际上通过串口远程调试windows,系统正常的启动过程中,调试子系统的初始化是早先于串口驱动程序,调试子系统调用kdcom.dll提供的功能,并不需要串口驱动程序的支持。因此微软没有为Win10RS4ARM64提供串口驱动serial.sys,对我们最终的目标没有影响。

那么问题究竟出在哪里呢?是因为Loader所使用的Qemu中的UEFI有问题吗?

对照qemu的源码可知,qemu为aarch64模拟器环境提供了串口设备PL011。我们研究了Linaro UEFI的源码EDK2并编译了对应的UEFI文件,确保使用的UEFI文件确实提供了串口功能。再用与Win10ARM64模拟器同样的配置安装了Ubuntu for ARM,在这个模拟器里PL011串口通信正常,串口采用MMIO,其映射的基址为0x09000000。但安装Win10后问题依旧:以基于串口的远程内核调试的启动配置来启动Win10RS4ARM64,系统加载的是kd.dll而非期望的kdcom.dll,故而推测是winload 没有识别PL011串口设备、没能去加载kdcom.dll。由此,我们决定直接将kdcom.dll替换kd.dll来使用。不过使用kdcom.dll替换kd.dll后出现了新的问题——系统引导异常,下面进一步分析其原因。

kdcom!KdCompInitialize是串口初始化的关键函数,分析它是如何初始化并使用串口设备的。系统第一次调用kdcom!KdInitialize初始化串口时,传递给KdCompInitialize的第二个参数LoaderBlock是nt!KeLoaderBlock,非NULL,此时kdcom!KdCompInitialize里的关键流程如下:
(1) HalPrivateDispatchTable->KdEnumerateDebuggingDevices已被赋值为hal!HalpKdEnumerateDebuggingDevices,调用返回0xC0000001;
(2) 串口处理器UartHardwareDriver为NULL,没有被赋值;
(3) HalPrivateDispatchTable->KdGetAcpiTablePhase0已被赋值为hal!HalAcpiGetTable,
调用HalAcpiGetTable(loaderBlock, ‘2GBD’)返回NULL,
调用HalAcpiGetTable(loaderBlock, ‘PGBD’)返回NULL,
因此gDebugPortTable为NULL;
(4) 参数LoaderBlocker非NULL且gDebugPortTable为NULL,调用GetDebugAddressFromComPort来配置串口地址;
GetDebugAddressFromComPort调用nt!KeFindConfigurationEntry失败,按照既定策略,基于DebugPortId的值指派串口地址(DebugPort.Address)为0x3F8/0x2F8/0x3E8/0x2E8/0x00五者之一;
(5) 由于gDebugPortTable为NULL,串口处理器UartHardwareDriver赋值为Uart16550HardwareDriver;
由于串口地址(DebugPort.Address)非NULL,调用串口初始化函数UartHardwareDriver->InitializePort初始化串口;
模拟器提供的串口设备为PL011, 串口处理器应被赋值为是PL011HardwareDriver 而非Uart16550HardwareDriver;

至此,我们发现导致异常的原因: 模拟器提供的是PL011串口设备, kdcom.dll虽提供了支持PL011的代码,但未能正确识别适配,依然把它当成了PC的isa-serial串口设备。这应属于kdcom.dll的bug。

0x3 开启qemu远程内核调试

  现在看来,我们需要解决的问题有两个:系统Loader仅加载不支持远程内核调试的kd.dll,系统模块kdcom.dll没能完全支持PL011串口设备。

对于第一个问题,我们简单采取文件替换的办法绕过它。
对于第二个问题,预期可以使用这样的办法解决:开发一个boot类型的驱动,让它能够加载kdcom.dll并主动修正kdcom.dll中所有相关数据,对内核映像Ntoskrnl.exe执行IATHook——把导入地址表中的kd.dll函数地址全部替换成kdcom.dll对应函数地址,最后执行nt!KdInitSystem来初始化调试子系统。这种方案篡改内核数据后,会很快触发PatchGuard蓝屏,因此我们需要设计出一个更可用的方案。

我们可以开发一个能够实现远程内核调试所需的串口通信功能的dll(即没有BUG的kdcom.dll)来替换系统目录下kd.dll,在“禁用驱动程序强制签名”的场景下实现对操作系统初始化流程的劫持。

微软给WINDBG的安装包捆入了一个名为KdSerial的示例项目。这个项目缺少了一些代码,但是关键的部分都在。通过笔者的改造,成功编译得到一个kdserial.dll,它拥有远程内核调试所需的串口通信功能和正确的PL011串口配置,能够替代Win10ARM64RS4系统里的kdcom.dll。将这个kdserial.dll替换系统里的kd.dll,开机时选择“启动设置”菜单里的“禁止驱动程序强制签名”,达成远程内核调试Win10RS4ARM64的目标。

参考文献

[1] Windows Internals 6th
[2] https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/bcdedit--dbgsettings
[3] https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/bcd-boot-options-reference
[4] https://wiki.linaro.org/LEG/UEFIforQEMU
[5] https://blog.csdn.net/iiprogram/article/details/2298550